
网站突然卡死,CPU 被 PHP-FPM 占满怎么办
下午三点,外贸站订单高峰,客户反馈页面转圈打不开。你登录服务器敲下 top,发现 CPU 已经 100%,进程列表里一排 php-fpm: pool www 全部处于运行状态,重启服务后好十分钟,问题又回来了。这种情况说明不是单次故障,而是请求处理能力被耗尽了:进来的请求排着队,每一个都在等前一个释放进程。本文会帮助你解决这类问题——先确认进程现场,再算清 pm.max_children 这道容量题,最后用慢日志把真正拖慢请求的那行代码找出来。整个排查过程不需要更换服务器,多数情况下改几行配置就能恢复响应速度。
先看现场:三个命令确认 PHP-FPM 是不是真的满载
动手改配置之前,先花两分钟确认瓶颈确实在 PHP-FPM,而不是 MySQL 或磁盘 IO:
# 查看 PHP-FPM 进程数量与 CPU 占用
ps aux --sort=-%cpu | grep php-fpm | head -15
# 统计当前 php-fpm 工作进程总数(对比 pm.max_children 上限)
ps aux | grep -c "[p]hp-fpm: pool"
# 查看监听队列是否有堆积
netstat -anp | grep 9000 | grep -c WAIT
判断标准很直接:如果工作进程数已经等于 pm.max_children,并且每个进程 CPU 接近 100%,说明进程池被占满,新请求只能排队。此时再去看 /var/log/php*-fpm.log,如果出现 server reached pm.max_children setting (50) 这样的警告,就基本可以锁定问题——这个警告表示队列曾经满过,后面到达的请求被迫等待甚至被丢弃,用户看到的就是长时间转圈。
还有一种情况:进程数没到上限但 CPU 依然很高,这时卡顿来自某个脚本在死循环或执行重型查询,靠加进程解决不了,必须靠慢日志定位。
算清容量题:pm.max_children 到底该设多少
pm.max_children(进程管理器允许的最大子进程数)决定了 PHP-FPM 同一时刻最多能并行处理多少个请求。设小了请求排队、高峰卡顿;设大了每个进程 30-80MB 的内存会撑爆物理上限,服务器反而更慢。很多人卡顿后第一反应是把这个值翻倍,结果第二天内存爆了。
正确的算法只有一条公式:
pm.max_children = 可分配给 PHP 的总内存 ÷ 单个进程平均内存
举个例子:一台 4GB 内存的 VPS 上跑着 WordPress 站,系统和 MySQL 固定占用 1.5GB,留给 PHP 约 2.5GB。先在高峰期执行下面的命令算单进程平均内存:
ps --no-headers -o rss -C php-fpm | awk '{sum+=$1; n++} END {printf "平均单进程内存: %.0f MB\n", sum/n/1024}'
假设测出单进程平均 60MB,那么 2.5GB × 1024 ÷ 60 ≈ 42,实际配置建议留 20% 余量,写成 34 左右。对应的进程池配置可以这样写:
[www]
pm = dynamic
pm.max_children = 34
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
三个 spare 参数控制闲置进程的伸缩区间:start_servers 是启动进程数,min_spare_servers 保证突发流量有空闲进程接单,max_spare_servers 防止低峰期白占内存。再配合 pm.max_requests = 500 定期重启进程,避免内存泄漏累积。
内存充足的大型站点可以考虑 pm = static,把进程数固定在算好的值上。固定进程池省去了动态fork的开销,响应延迟更平稳,代价是低峰期也占着同样的内存——对于流量稳定的电商站,这是值得的交换。

打开慢日志:把拖慢请求的那行代码揪出来
调好进程数只是治标。如果某个插件里的 SQL 查询要跑 8 秒,34 个进程也会被它逐个拖住,最终照样满载。慢日志(slowlog)的作用就是在这种情况下替你”抓现行”:任何执行时间超过阈值的请求,它都会把当时的调用栈完整记下来,你不用守在屏幕前,第二天翻日志就能看到卡顿源头。
配置只需两行:
request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 3s 表示超过 3 秒的请求会被记录。阈值要结合业务定:普通内容页应在 1 秒内完成,结账流程可放宽到 5 秒。设成 0 会记录所有请求,日志量极大,只适合临时排查。改完执行 systemctl reload php-fpm 生效。
跑一天之后打开日志,典型的记录长这样:
[20-Oct-2026 14:32:07] [pool www] pid 12345
script_filename = /home/site/public_html/index.php
[0x00007f3a2c1e0f30] query() /home/site/public_html/wp-includes/class-wpdb.php:2345
[0x00007f3a2c1e0e40] get_results() /home/site/public_html/wp-content/plugins/xxx-report/report.php:88
解读方法是从上往下找第一个属于 wp-content/plugins/ 或业务代码的行。上面的例子说明:卡顿不是资源不足,而是某个报表插件第 88 行的慢查询每次要跑 3 秒以上,高峰期几个用户同时触发就把进程池占满。处理方式随之明确——给字段补索引、加缓存或停用插件,CPU 占用通常当天就降下来。

参数怎么配:dynamic 与 static 的真实取舍
同一个 pm.max_children 数值,配在不同模式下表现差别很大。下面这张对比表来自我们处理过的两类典型客户场景,你可以对照自己的业务选型:
| 对比维度 | pm = dynamic | pm = static |
|---|---|---|
| 适用流量 | 波动大,白天高峰、夜里空闲 | 全天平稳,订单持续进入 |
| 内存占用 | 低峰期自动收缩,闲置内存少 | 始终占满固定内存 |
| 响应延迟 | 突发流量来时需 fork 进程,首请求略慢 | 无 fork 开销,延迟平稳 |
| 典型场景 | 企业官网、博客、季节性外贸站 | 电商平台、API 服务 |
一个实际案例可以说明选错模式的代价。某外贸独立站在共享主机上配置了 pm = dynamic、pm.max_children = 5,黑五促销时并发请求一度冲到 40 个,超出上限的 35 个请求全部排队,页面响应时间从 800 毫秒涨到 20 秒以上,当小时订单转化率掉了一半。迁到带独立资源的方案后按 2GB 可用内存重新计算,pm.max_children 提到 25,同一时段的响应时间稳定在 900 毫秒以内。这个案例说明两件事:一是参数必须跟着流量走,抄别人的配置没有意义;二是当算出的进程数已经撑不起业务峰值时,单靠调参解决不了,需要升级到资源更充裕的独立服务器或 VPS(虚拟专用服务器)。
还有一个中间路线 pm = ondemand:平时不启动工作进程,请求来了才 fork,内存最省,但首个请求要承担完整启动耗时,只适合测试环境,生产站会出现”第一下特别慢”。
改完之后:用监控和状态页验证效果
配置改完还需要一套验证闭环,确认调整生效并在流量增长前提前预警。分三层来做:
第一层是即时验证。重启或 reload PHP-FPM 后压测一下首页,观察响应时间是否回到正常区间:
ab -n 200 -c 20 https://你的域名/ | grep -E "Time per request|Failed requests"
压测结果之外,也可以对照我们之前整理的 TTFB 与主机优化方法 检查首字节时间是否回到正常区间,那篇文章里的优化手段和本文的进程池调优可以配合使用。
第二层是开启状态页持续观察。在进程池配置中加入 pm.status_path = /fpm-status,配合 Nginx 的 location 放行,就能实时看到 active processes(活跃进程数)、listen queue(排队数)和 max children reached(是否触顶)三个关键指标。判断标准:listen queue 应长期为 0,max children reached 出现非零值就说明上限还需要上调或存在慢请求。
第三层是留下证据链。把慢日志接入 logrotate 防止单文件膨胀,每周导出一次状态页快照对比进程数趋势:活跃进程数连续两周爬升而流量没变,大概率是新插件在泄漏内存。服务器层面的其他调优项,可以参考 服务器优化分类 下的系列文章。
当这套闭环跑起来之后,绝大多数”突然卡死”的事故都会变成”提前一周看到的曲线”。如果你的站点目前还在共享环境里、连 SSH 权限和慢日志都没法配置,建议考虑迁移到资源独享的方案——Hostease 的 VPS 主机 和 独立服务器 都提供完整的 PHP-FPM 配置权限和中文技术支持,可以按上文公式自行调优,也可以让支持团队协助核查配置。
总结与行动建议
回顾整条排查路线:先用 ps 和 PHP-FPM 日志确认进程池是否触顶,再用”可分配内存 ÷ 单进程平均内存”算出合理的 pm.max_children,然后打开慢日志把拖慢请求的具体代码行揪出来,最后通过状态页持续验证。这四步走完,卡顿源头基本都能定位到插件级别。
给正在处理同类问题的你三条建议:第一,今天就检查一遍 PHP-FPM 日志里有没有 reached pm.max_children 警告,它出现过的次数就是你正在流失的请求量;第二,把慢日志阈值设在 3 秒并保留一周日志,用真实调用栈代替猜测;第三,如果你需要托管环境下开箱即用的 PHP 调优支持,可以考虑 Hostease 共享主机,预置的 PHP 运行环境与进程池配置能覆盖大多数中小站点,遇到问题也有中文工单随时响应。服务器性能问题从来不是一次性修好的,但只要监控在、日志在,每次卡顿都会变成下一次优化的输入。