上周维护一台跑着几个业务站点的 2 核 4G 服务器,监测到 CPU 经常在下午或者整点突然飙升到 100%,SSH 登录上去用 top 一看,排在最前面的全是一长串 php-fpm: pool www 进程。
当时第一反应是直接在宝塔面板里点了一下重载 PHP,CPU 瞬间降下去了,但过了不到两个小时又重新打满。重启服务只能治标,显然是有些深层原因没有处理。花了一个下午把整个排查链路理了一遍,顺手把目前调整后稳定运行的参数记下来,方便日后查阅。
1. 紧急处理与即时状态排查
遇到 CPU 打满导致网站打不开时,最快恢复访问的方式是平滑重载 PHP 服务,而不是直接暴力 kill:
bash# 命令行平滑重载(以 PHP 7.4 为例) systemctl reload php-fpm-74
在恢复之后,先不要急着关闭终端,可以通过以下命令看看当前 PHP-FPM 的实际进程数量和内存开销:
bash# 查看当前活跃的 php-fpm 进程数 ps -ef | grep 'php-fpm: pool' | wc -l # 查看每个 php-fpm 进程占用的内存情况 ps --no-headers -o rss,cmd -C php-fpm | awk '{sum+=$1; count++} END {print "平均内存:", sum/count/1024, "MB", "总计:", sum/1024, "MB"}'
正常情况下,每个 PHP-FPM 子进程的内存开销在 30MB 到 60MB 之间。如果进程数被开到了上百个,或者单个进程占了 100MB 以上,多核 CPU 就会频繁在进程上下文切换和 I/O 阻塞中空转,最终把 CPU 资源彻底耗尽。
2. 关键参数设置与机器规格参考
宝塔默认安装的 PHP-FPM 运行模式通常是 dynamic(动态)。如果不根据实际物理内存做限制,突发流量一来就会瞬间爆满。针对不同内存规格的服务器,可以参考以下调优配置。
打开宝塔面板 -> 软件商店 -> 对应 PHP 版本 -> 性能调整,修改几个核心参数:
| 服务器规格 | 运行模式 (pm) | 最大子进程数 (max_children) | 起始进程数 (start_servers) | 最大请求数 (max_requests) | 建议虚拟内存 (Swap) |
|---|---|---|---|---|---|
| 1 核 1G | ondemand / dynamic | 5 - 8 | 2 | 500 | 1024 MB |
| 2 核 2G | dynamic | 15 - 20 | 5 | 1020 | 2048 MB |
| 2 核 4G | dynamic | 30 - 45 | 10 | 2048 | 2048 MB |
| 4 核 8G | dynamic | 60 - 80 | 15 | 3000 | 4096 MB |
这里有两点需要注意:
pm.max_requests必须设置:默认很多环境给的是 0(即永不回收进程)。PHP 脚本多多少少存在微小的内存泄漏,子进程处理久了体积会越来越大。将其设为 1024 或 2048,表示该进程处理完指定次数的请求后会自动退出重建,释放死内存。pm.max_children不要迷信越大越好:算一下公式,可用内存 ÷ 单个进程平均内存。例如 2G 机器留给 PHP 1G 内存,单个进程 40M,那上限设为 25 左右最安全,强行设成 80 只会导致系统频繁发生换页甚至 OOM 杀进程。
3. 开启慢日志抓取卡顿脚本
如果调整了参数,发现 CPU 依然隔三岔五被拉满,那通常是代码里有耗时极长的操作卡住了进程(例如外调第三方超时 API、未加索引的大表查询、死循环逻辑)。
排查这类问题,最有效的工具是 PHP-FPM 自带的慢执行日志(Slow Log)。在 PHP 配置中找到 php-fpm.conf,确认或添加以下配置:
inirequest_slowlog_timeout = 3s slowlog = /www/server/php/74/var/log/slow.log
这行配置的意思是:只要有任何请求执行时间超过 3 秒,PHP 就会把该请求的文件名、行号以及完整的调用栈记录到 slow.log 中。
配置保存并 reload 之后,使用命令行动态监控日志输出:
bashtail -f /www/server/php/74/var/log/slow.log
上次排查时,我就是通过慢日志立刻定位到一个插件在后台向国外已失效的服务器请求推送更新,cURL 默认等待 30 秒才超时。每一个访客进来就挂住一个进程,十几个访客就把所有 PHP 进程全卡死了,修掉这个接口后 CPU 负载立马恢复正常。
4. 为什么改完参数后还是会被打崩?
如果慢日志里空空如也,但 CPU 依然突发 100%,原因往往在服务器外部:遭遇了恶意扫描、暴力破解或高频爬虫。
PHP 脚本本身执行并不慢(可能只需 0.05 秒),但如果外部有一秒钟几十甚至上百个并发打过来,PHP-FPM 依然会被瞬间塞满。检查 Nginx 访问日志,重点排查以下几项:
- 是否有 IP 在大量请求
xmlrpc.php、wp-login.php等接口进行暴力破解? - 是否有无良采集器或搜索引擎爬虫在死循环抓取动态搜索页面?
应对方式:
- 在 Nginx 层面直接拦截:针对特定恶意路径(如 WordPress 的
xmlrpc.php),直接在站点配置里加入规则返回 403,不要让请求透传给 PHP:
nginxlocation = /xmlrpc.php { deny all; }
- 开启访问速率限制:利用 Nginx 的
limit_req模块限制单 IP 的动态请求频率,把恶意并发压下去。 - 日志排查辅助工具:如果不习惯纯手敲命令行分析海量日志,宝塔面板自带的网站监控报表插件或 Nginx 免费防火墙插件也可以拿来直观地看请求 Top IP,找到作恶 IP 后直接在系统防火墙加入黑名单封禁即可。
改完配置后,别忘了在终端执行一次 nginx -t 和 php-fpm -t 检查语法,然后平滑重载服务。挂上监控观察个两三天,CPU 的负载曲线基本就能平稳下来了。