这几天在维护一台 2 核 2G 的小云服务器,跑了两个 WordPress 和一个自建的 API 站点。平时负载都在 0.3 左右,但最近只要遇到蜘蛛抓取或者后台更新,CPU 经常瞬间冲到 100%,接着 Nginx 就开始吐 502 Bad Gateway。通过 top 命令一看,全是一堆 php-fpm 的工作进程在抢占计算资源。
顺手把这次的排查逻辑、修改后的配置参数以及后续的排错方式记下来,给有类似困扰的朋友做个参考。
1. 临时应急与排查现状
当 CPU 已经被 php-fpm 打满、网站打不开时,最快恢复服务的方式是在宝塔面板的【软件商店 -> 运行环境 -> PHP 设置】中点击【重载配置】或者【重启】。
如果连宝塔后台都卡死进不去,直接通过 SSH 连上服务器执行重启:
bashsystemctl restart php-fpm-74 # 根据实际 PHP 版本替换数字
恢复后先别急着走开,通过 SSH 查看当前系统中 PHP 进程的平均内存占用:
bashps --no-headers -o rss -C php-fpm | awk '{ sum+=$1 } END { printf "平均内存: %.2f MB\n", sum/NR/1024 }'
以常见的 WordPress 站点为例,一个装了几个常规插件的 PHP 7.4/8.0 进程,单进程内存通常在 30MB 到 60MB 之间。如果你的配置允许派生过多进程,内存耗尽后系统会频繁读写 Swap(甚至直接 OOM 杀进程),此时 CPU 就会因为大量的上下文切换和内存换页直接飙到 100%。
2. 关键运行参数调整参考
宝塔默认安装 PHP 时给的性能配置相对比较激进,特别是对于 1G/2G 的小机器,默认的 max_children 很容易设得太高。建议在 PHP 设置中的【性能调整】里改成以下数值:
| 服务器配置 | 运行模式 | max_children | start_servers | min_spare_servers | max_spare_servers | max_requests | 建议 Swap 大小 |
|---|---|---|---|---|---|---|---|
| 1核 1G | ondemand / dynamic | 10 | 2 | 2 | 5 | 500 | 1024 MB |
| 2核 2G | dynamic | 20 | 4 | 3 | 10 | 1000 | 2048 MB |
| 2核 4G | dynamic | 40 | 8 | 5 | 20 | 1500 | 2048 MB |
| 4核 8G | dynamic | 80 | 15 | 10 | 40 | 2000 | 4096 MB |
这里重点解释两个关键参数:
- max_children(最大子进程数):这是硬上限。公式一般是
(空闲物理内存 - 系统及 MySQL 保留内存) / 单个 PHP 进程内存。宁可设得稍微保守一点让请求排队,也千万不要让它无节制派生把系统内存吃光。 - max_requests(单个进程最大处理请求数):默认可能是 0(不限制)或者几千。强烈建议改成 500 到 1000。PHP 代码或插件或多或少存在内存泄漏问题,让进程在处理完一定请求数后自动自杀并释放内存,是防止 CPU 和内存慢性跑满最有效的方法。
3. 定位具体是哪个请求拖慢了进程
参数改完后,如果某个时间段 CPU 依然会突然升高,说明根源在具体某段执行超时的代码或请求上。此时需要打开 PHP 的慢日志(slow log)。
在宝塔【PHP 设置】的【配置文件】中,搜索并确认以下两项开启:
inirequest_slowlog_timeout = 3 slowlog = /www/server/php/74/var/log/slow.log
这里把慢查询超时设为 3 秒(即任何执行时间超过 3 秒的脚本都会被记录下调用堆栈)。改完保存并重载 PHP 配置。
当再次发生高负载时,直接查看该日志:
bashtail -n 50 /www/server/php/74/var/log/slow.log
日志会精确指出具体是哪个 PHP 文件的哪一行(例如某个主题的远程 API 请求超时、某个复杂的数据库查询、或者某个恶意抓取的 POST 接口),根据堆栈去修复代码或直接禁用异常插件即可。
4. 为什么调完参数还是会被打崩?
如果配置改小了,日志里也没有明显的死循环代码,但 CPU 依然间歇性被拉满,通常是以下外部原因造成的:
- 恶意扫描与垃圾蜘蛛疯狂抓取:海外部分采集程序会高频扫你的搜索页(如
/search?s=xxx)或后台登录页,每次搜索都会调用一次完整的 PHP 渲染和数据库查询。检查 Nginx 访问日志,如果看到某些特定 IP 或 User-Agent 几秒钟发出几十次动态请求,直接在 Nginx 层面用limit_req模块限流,或者用防火墙规则拦截,不要让这类请求穿透到 PHP-FPM 层。 - 缺失对象缓存与静态缓存:动态页面每次都让 PHP 去计算是非常昂贵的开销。可以在宝塔里给 PHP 装上
Redis或APCu扩展,并为博客配置静态页面缓存(例如 WP Super Cache 或 Nginx fastcgi_cache)。让 90% 的普通访客直接由 Nginx 返回静态 HTML,PHP-FPM 的负载自然就会断崖式下跌。