PHP-FPM进程飙高排查实战:从崩溃到稳定
发布日期: 2026/07/30 阅读总量: 0

一、真实案例:大促当晚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并发:

指标优化前优化后提升
QPS22010504.77倍
平均响应时间2.3s0.2s11.5倍
99%响应4.1s0.5s8.2倍
FPM最大活跃进程79832↓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看哪些请求慢 → 深入分析慢请求根因 → 对症优化。保持监控和自动化脚本,大促前做好压测和预案。