上周手头一台 2G 内存的云服务器频繁告警,CPU 连续十几分钟维持在 95% 以上,前端页面直接响应超时报 502 Bad Gateway。连上终端用 top 一看,四五个 php-fpm: pool www 进程死死霸占了 CPU。
这种问题在跑 WordPress、Typecho 或一些 PHP 框架的轻量机器上很常见。很多人遇到后第一反应是重启 PHP 甚至重启服务器,但通常治标不治本,几十分钟后又会重演。这里把这次排查的思路和目前调教得比较稳的参数记录下来,留个备忘。
1. 临时恢复与现场定位
如果站点已经无法访问,最快恢复服务的方式是在宝塔面板的「软件商店」-「运行环境」里找到对应的 PHP 版本,点击「重启」。或者在终端执行命令:
bashsystemctl restart php-fpm-74 # 根据实际版本号调整,如 php-fpm-80
但在重启之前,如果还能敲命令,建议先用以下命令确认到底是哪个脚本把进程卡死了:
bash# 查看占用 CPU 最高的 5 个进程及具体执行路径 top -c -b -n 1 | grep php-fpm | head -n 5
同时检查当前 PHP-FPM 的活动进程数:
bashps -ef | grep 'php-fpm: pool' | grep -v grep | wc -l
如果进程数已经达到了配置的 max_children 上限,后续新的 HTTP 请求就只能在队列里排队,队列满了 Nginx 就会直接给客户端吐 502。
2. 关键参数设置参考
很多小内存机器(1G/2G)跑崩,是因为使用了宝塔预设的较高进程数配置。单个 PHP-FPM 进程在处理复杂脚本时,通常会吃掉 30MB 到 60MB 内存。如果进程数设得太大,内存一旦见底,系统会疯狂使用 Swap 交换分区,导致磁盘 IO 飙升、CPU 处于等待状态,最终全部卡死。
在宝塔面板打开对应的 PHP 设置,切换到「性能调整」,以下是我在不同规格机器上长期实测比较稳妥的配置:
| 服务器配置 | 运行模式 | max_children (最大子进程) | start_servers (启动进程) | min_spare_servers (最小空闲) | max_spare_servers (最大空闲) | 建议 Swap 大小 |
|---|---|---|---|---|---|---|
| 1核 1G | ondemand / dynamic | 5 - 8 | 2 | 2 | 5 | 1024 MB |
| 2核 2G | dynamic | 15 - 20 | 5 | 5 | 15 | 2048 MB |
| 2核 4G | dynamic | 30 - 40 | 10 | 10 | 30 | 2048 MB |
| 4核 8G | dynamic | 60 - 80 | 15 | 15 | 50 | 4096 MB |
对于 1G 内存的超小机器,建议将运行模式改为 ondemand(按需分配),有请求时才生成进程,空闲自动释放,能极大降低待机内存开销。
3. 开启慢日志揪出耗时代码
调整进程数量只能保证服务器不被挤爆,但如果 CPU 依然居高不下,核心原因往往是某个 PHP 脚本本身执行极慢(比如死循环、复杂的正则表达式、没加索引的慢数据库查询,或者调用了外部超时的第三方 API)。
抓出这些脚本最有效的工具就是 PHP-FPM 的慢日志(Slowlog)。进入宝塔 PHP 设置里的「配置文件」,找到并修改以下两行(如果被分号注释掉了,把分号删掉):
inirequest_slowlog_timeout = 3 slowlog = /www/server/php/74/var/log/slow.log
request_slowlog_timeout = 3 意味着只要有请求执行时间超过 3 秒,PHP-FPM 就会把完整的调用栈记录下来。修改后在面板点击「重载配置」。
等网站再次出现卡顿或者运行一段时间后,去查看 /www/server/php/74/var/log/slow.log:
bashtail -n 50 /www/server/php/74/var/log/slow.log
日志里会精确显示脚本执行到了哪一个文件、哪一行代码、调用了哪个函数。根据堆栈信息,基本一眼就能看出来是哪个插件在拖后腿,还是某条 SQL 查询耗光了资源。
4. 调整完依然卡死的原因排查
如果代码本身没有大问题,但 PHP-FPM 偶尔还是会瞬间被拉爆,通常要排查以下外部因素:
- 恶意爬虫与扫描行为:大量劣质爬虫高频并发抓取动态页面,或者有扫描器在死刷
/wp-login.php、/xmlrpc.php等接口。每一个请求都会启动一个 PHP 解释器,瞬间把 CPU 占满。可以在 Nginx 访问日志中过滤出请求频次最高的 IP,配合 Nginx 的limit_req模块限流,或者开启面板里的 Nginx WAF 防火墙模块拦截这类无用扫描。 - 外部接口调用挂起:脚本中使用了
file_get_contents()或未设超时的curl去请求外部 API,一旦目标服务器响应缓慢,当前 PHP-FPM 进程就会一直挂起等待,直到达到max_execution_time。排查代码时务必为所有远程请求设置 3-5 秒的硬超时。 - 数据库锁表:MySQL 中有长时间未完成的事务或者写入锁,导致后续 PHP 进程全都在等待数据库返回结果。排查 PHP 问题的同时,最好也检查一下 MySQL 的
slow_query_log或使用SHOW FULL PROCESSLIST;查看是否有阻塞语句。
经过这轮调整,把子进程上限控制在内存允许范围内,并根据慢日志修复了两个超时调用,这台 2G 机器的 CPU 占用基本回落到了日常 15% 左右,没有再出现无故死机的情况。