昨晚和一位讀者朋友討論了一個問題:在一台多核 CPU 的 Web 服務器上,存在負載不均衡問題,其中 CPU0 的負載明顯高於其它 CPUx,進一步調查表明 PHP-FPM 的嫌疑很大。話說以前我曾經記錄過軟中斷導致過類似的問題,但是本例中可以排除嫌疑。
讓我們在一台四核服務器上采樣分析一下數據確認看看是否存在負載不均衡問題:
shell> mpstat -P ALL 1 10
CPU %usr %nice %sys %iowait %irq %soft ... %idle
all 17.57 0.03 1.78 0.00 0.35 0.23 ... 80.04
0 43.17 0.00 4.12 0.00 1.41 1.00 ... 50.30
1 9.80 0.00 0.81 0.00 0.00 0.00 ... 89.39
2 9.31 0.00 1.20 0.00 0.00 0.00 ... 89.49
3 7.94 0.10 0.80 0.00 0.00 0.00 ... 91.16
如上命令的含義是每秒運行一次 mpstat,一共采樣 10 次取平均值,可以明顯看出 CPU0 的空閑 idle 明顯小於其它 CPUx,而且大部分都消耗在了用戶態 usr 上面。
再讓我們通過 pidstat 來確認一下是不是 PHP-FPM 導致的 CPU0 負載問題:
shell> pidstat | grep php-fpm | awk '{print $(NF-1)}' | sort | uniq -c
157 0
34 1
34 2
32 3
可見分配給 CPU0 的 PHP-FPM 進程比其他三個 CPUx 總和還要多。為什么大部分進程被分配給了 CPU0?我模模糊糊有一些印象是因為操作系統偏愛使用 CPU0,但我暫時也沒找到實質的線索可以佐證,如果有人知道,麻煩告訴我。
問題總要解決,既然 PHP-FPM 沒有類似 Nginx 那樣 CPU 親緣性(affinity)綁定的指令,那么我們可以使用 taskset 綁定 PHP-FPM 進程到固定的 CPUx 來解決問題:
#!/bin/bash
CPUs=$(grep -c processor /proc/cpuinfo)
PIDs=$(ps aux | grep "php-fpm[:] pool" | awk '{print $2}')
let i=0
for PID in $PIDs; do
CPU=$(echo "$i % $CPUs" | bc)
let i++
taskset -pc $CPU $PID
done
如上腳本運行后,讓我們再來看看各個 CPU 負載分配情況如何:
shell> mpstat -P ALL 1 10
CPU %usr %nice %sys %iowait %irq %soft ... %idle
all 15.73 0.03 1.61 0.00 0.20 0.23 ... 82.20
0 16.28 0.10 1.62 0.10 0.81 0.91 ... 80.18
1 16.16 0.10 1.51 0.00 0.00 0.10 ... 82.13
2 14.46 0.10 1.71 0.00 0.00 0.00 ... 83.73
3 15.95 0.00 1.71 0.00 0.00 0.00 ... 82.35
終於平均了,不過需要提醒的是,一旦 PHP-FPM 處理的請求數超過 max_requests 的設置,那么對應的進程將自動重啟,先前的 taskset 設置也將失效,所以為了一直有效,我們需要把 taskset 腳本添加到 CRON 配置中去,例如每分鍾自動設置一遍!
本文把 PHP-FPM 進程平均分配給了 0,1,2,3 四個 CPU,實際操作的時候可以更靈活一些,比如前文我們提過,操作系統總是偏愛使用 CPU0,如果 CPU0 的負載已經很高了的話,那么我們不妨把 PHP-FPM 進程平均分配給 1,2,3 三個 CPU。