一、真实案例:大促当晚FPM进程炸了
2024年双11晚8点,接到报警:线上业务集群A的PHP-FPM进程数从正常80左右飙升到800+,CPU使用率100%,nginx疯狂返回504。业务反馈:商品详情页打不开,下单接口超时。服务器配置:8C16G,PHP 8.3.6,Laravel 11,MySQL 8.0.35,部署在阿里云ECS。
紧急运维操作:重启php-fpm后进程短暂回落到100,但5分钟内再次窜升。需要快速定位根因。
二、问题复现与首次定位
2.1 用top抓现场
ssh登入服务器,执行:
$ top -b -n 1 | head -20
输出:
top - 20:15:30 up 2 days, 3:20, 1 user, load average: 15.20, 8.43, 2.39
%Cpu(s): 98.7 us, 1.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
KiB Mem : 16266156 total, 123456 free, 14876543 used, 1266157 buff/cache
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
30862 www 20 0 450000 89000 45000 R 99.5 0.5 2:30.34 php-fpm
30863 www 20 0 450000 89000 45000 R 99.2 0.5 2:28.12 php-fpm
30864 www 20 0 450000 88000 44000 R 98.9 0.5 2:25.45 php-fpm
...
明显CPU全在php-fpm上,且R状态(正在运行)的进程极多。内存剩余几乎为0。
2.2 查看php-fpm状态(pm.status_path)
开启php-fpm状态页(nginx配置:
location ~ ^/(status|ping)$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $fastcgi_script_name;
include fastcgi_params;
}
访问:
$ curl http://127.0.0.1/status?json
输出(截取关键行):
{
"pool":"www",
"process-manager":"dynamic",
"start time":1700000000,
"start since":7200,
"accepted conn":45000,
"listen queue":230,
"max listen queue":500,
"listen queue len":128,
"idle processes":2,
"active processes":798,
"total processes":800,
"max active processes":800,
"max children reached":3,
"slow requests":654
}
关键指标:active processes 798,max children reached 3(说明配置的pm.max_children=800几乎耗尽),listen queue 230(排队请求多),同时slow requests高达654。
2.3 获取慢请求日志
配置slow log:
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow-www.log
查看最近慢日志:
$ tail -200 /var/log/php-fpm/slow-www.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10
发现大量慢请求集中在 /api/product/detail 和 /cart/add 两个接口。
三、根因排查:两路并行
3.1 方案A:strace追踪单进程
选取一个CPU占用高的php-fpm进程PID(如30862),执行:
$ sudo strace -p 30862 -T -f -o /tmp/strace_30862.log &
sleep 3
kill %1
cat /tmp/strace_30862.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head
输出:
417 mysqld: 10.0.0.1:3306
235 <... recvfrom resumed> ) = 0 ? ERESTARTSYS ...
198 connect(3, ...) = 0
142 poll([{fd=4, events=POLLIN|POLLPRI}], 1, 30000) = 1
...
大量时间花在MySQL网络通信(mysqld),且有大量connect/poll。说明PHP频繁建立数据库连接?
3.2 方案B:开启慢查询日志+性能分析
同时检查MySQL的慢查询:
-- 查看当前慢查询状态
SHOW VARIABLES LIKE 'slow_query_log%';
-- 设置临时开启(生产慎用)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2;
取出最近慢日志:
$ mysqldumpslow -s c -t 10 /var/log/mysql/slow-query.log
输出:
Count: 1523 Time=3.67s (5590s) Lock=0.01s (15s) Rows=1250.0 (1903750), ...
SELECT p.*, c.name as category_name FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.status = 1 AND p.price > N
ORDER BY p.sales DESC LIMIT M;
该查询没有合适索引,而且被频繁执行(1523次)。每次返回平均1250行,但页面只取前10条。MySQL CPU和IO飙升,PHP进程等待结果阻塞。
3.3 对比总结
两种方案互补:strace微观定位单请求行为,慢查询日志宏观确认瓶颈SQL。最终确认:上游流量突增导致慢查询激增,每个PHP请求耗时从0.2s增加到3-5s,FPM进程被占满,新请求排队,进程数收缩及新建频繁,加剧资源消耗。
四、解决方案:三管齐下
4.1 紧急止血:调整pm配置
临时调大pm.max_children并切换pm模式为ondemand来降低峰值内存占用:
; /etc/php-fpm.d/www.conf
pm = ondemand
pm.max_children = 1500
pm.process_idle_timeout = 10s
pm.max_requests = 500
重启php-fpm:
$ systemctl restart php-fpm
ondemand模式不会预派进程,只在请求到达时才fork,减少空闲进程内存。但新请求fork有延迟,不过在线商城可接受。同时调整 request_terminate_timeout = 30s 避免请求死循环。
4.2 根除:优化慢查询+加索引
分析SQL添加复合索引:
ALTER TABLE products ADD INDEX idx_status_price_sales (status, price, sales DESC);
ALTER TABLE categories ADD INDEX idx_id_name (id, name);
然后重构查询只取需要的字段,并分页:
// 原代码
$products = Product::where('status', 1)
->where('price', '>', $minPrice)
->leftJoin('categories', ...)
->orderBy('sales', 'desc')
->take(10)
->get();
// 优化后
$products = Product::select('id', 'name', 'price', 'sales', 'category_id')
->where('status', 1)
->where('price', '>', $minPrice)
->orderBy('sales', 'desc')
->take(10)
->get();
// 类别名再单独缓存或join子查询简化
4.3 架构演进:异步化 + 缓存
将商品详情页的耗时聚合操作(如推荐、价格计算)异步到Redis队列,PHP只返回基础数据,前端用websocket推补全。
引入Laravel Horizon管理队列,设置worker数 = CPU核心数 * 2。
# horizon.yml (示例)
defaults:
supervisor-1:
connection: redis
queue: [high, default, low]
balance: auto
maxProcesses: 16
minProcesses: 4
balanceMaxShift: 1
balanceCooldown: 3
五、效果数据:压测对比
在测试环境(同配置4C8G)使用ab压测,模拟1000并发:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| QPS | 220 | 1050 | 4.77倍 |
| 平均响应时间 | 2.3s | 0.2s | 11.5倍 |
| 99%响应 | 4.1s | 0.5s | 8.2倍 |
| FPM最大活跃进程 | 798 | 32 | ↓96% |
| MySQL CPU% | 85% | 20% | ↓76% |
线上部署后,504告警消失,服务器load从15降至1.2。
六、避坑指南
坑1:直接重启php-fpm解决不了问题
很多同学第一反应重启服务,但几分钟后又会飙高。重启只是临时释放资源,根因未除。必须结合status和slow log定位。
坑2:pm.max_children设置过大导致OOM
每个php-fpm进程平均内存约30-40MB,如果设置max_children=2000,8G内存会直接OOM。我用ondemand模式 + 合理上限(根据内存计算:可用内存/单进程内存 * 0.8)。
坑3:慢日志容易遗漏短平快请求
request_slowlog_timeout如果设置成5s,但有些请求2s完成已经造成压力,不会被记录。建议设成1-2s,或在压测时临时调低。
坑4:strace对高并发进程有性能影响
strace -p会严重拖慢被追踪进程,生产上慎用。可以在低峰时只针对少数进程或者使用perf top等采样工具。
坑5:索引不是万能药,避免过度优化
加的复合索引虽然让这条SQL快了,但可能影响INSERT/UPDATE性能。需要评估写多读少场景。大表加索引最好在低峰执行。
七、自动化排查脚本
以下脚本可一键收集关键信息,适合放在crontab或告警回调:
#!/bin/bash
# fpm_check.sh - 收集php-fpm进程飙高线索
set -e
STATUS_URL="http://127.0.0.1/fpm-status?json"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOGDIR="/tmp/fpm_check_${TIMESTAMP}"
mkdir -p $LOGDIR
# 1. FPM状态
curl -s $STATUS_URL > $LOGDIR/fpm_status.json
echo "[STATUS]" && cat $LOGDIR/fpm_status.json
# 2. top抓取php-fpm进程
ps aux | grep php-fpm | awk '{print $2, $3, $4, $11}' | sort -k2 -rn | head -20 > $LOGDIR/top_php.txt
echo "[TOP PHP-FPM]" && head -10 $LOGDIR/top_php.txt
# 3. 慢日志前10
SLOWLOG=$(grep '^slowlog' /etc/php-fpm.d/www.conf | cut -d= -f2 | xargs)
if [ -f "$SLOWLOG" ]; then
tail -500 $SLOWLOG | php -r 'while($l=fgets(STDIN)){preg_match("/^\[(\d+-\d+-\d+ \d+:\d+:\d+)\]/",$l,$m);if(isset($m[1]))$t[$m[1]]++;} arsort($t); foreach(array_slice($t,0,10) as $k=>$v) echo "$v $k\n";' > $LOGDIR/slow_count.txt
echo "[SLOW COUNT]" && cat $LOGDIR/slow_count.txt
fi
# 4. MySQL慢查询TOP10
mysql -e "SELECT * FROM mysql.slow_log WHERE start_time > NOW() - INTERVAL 10 MINUTE ORDER BY query_time DESC LIMIT 10\G" 2>/dev/null > $LOGDIR/mysql_slow.txt || echo "slow_log not enabled" >> $LOGDIR/mysql_slow.txt
echo "[MYSQL SLOW]" && head -20 $LOGDIR/mysql_slow.txt
# 5. 系统资源
echo "[SYSTEM]" && vmstat 1 2 >> $LOGDIR/vmstat.log && cat $LOGDIR/vmstat.log
echo "Check results in $LOGDIR"
八、总结
PHP-FPM进程飙高不一定是PHP代码问题,往往是外部依赖(DB、API、IO)变慢导致请求堆积。排查路径:top看资源 → status看活跃数 → slow log看哪些请求慢 → 深入分析慢请求根因 → 对症优化。保持监控和自动化脚本,大促前做好压测和预案。