宝塔 PHP-FPM 调优:pm.max_children 到底该给多少-第三维度MokIII

先说结论:按内存算,别按感觉调

pm.max_children 是 PHP-FPM 池能同时处理的请求数上限。给太小 → 高峰期报错、请求排队甚至 502;给太大 → 内存吃光触发 OOM Kill,整台机器上的站一起挂。

正确算法就一句话:单个 PHP 进程的实际内存 × max_children < 可用内存,同时给系统和缓存留出余量。

一、报错长什么样

php-fpm.log 里会出现这样一行:

WARNING: [pool www] server reached pm.max_children setting (8), consider raising it

它不代表已经挂了,而是说明已经顶到上限,请求开始排队——对 WordPress 来说,下一步就是响应变慢、然后 502。

二、先量一个 PHP 进程到底吃多少

不要拿 ps 的 RSS 直接相加(共享内存会被重复计算),看 PSS:

for p in $(pgrep -f 'php-fpm: pool www'); do
  awk '/^Pss:/{s+=$2} END{print s}' /proc/$p/smaps_rollup 2>/dev/null
done | sort -n | awk '{a[NR]=$1; s+=$1} END{print "最小",a[1],"最大",a[NR],"平均",int(s/NR)}'

我实测一组 WordPress worker:私有内存 11~33 MB,PSS 40~57 MB(Redis 扩展开着、opcache 开着、主题不算重)。

注意区分两个数:私有内存(Private_Dirty,真正独占的)和 PSS(把共享内存按比例摊回来)。ps 的 RSS 会把 opcache、扩展这些共享页在每个进程里各算一遍,所以用它估总量会偏大。取偏保守的 55 MB 来算上限即可。

三、再算可用内存:要看 MemAvailable

free -m
grep -E 'MemAvailable|SwapTotal' /proc/meminfo

关键是 MemAvailable,不是 free 那一列。我调优时实测 ≈ 2.4 GB(这是一台 4 GB 的机器,系统和面板已经先占掉一部分):

2400 MB ÷ 55 MB ≈ 43

理论上能开到 43,但不要按理论值给满——还要留 Redis、nginx、系统进程和突发峰值的余量。

我实际的调整过程是这样两步:

时间max_children依据
最初8面板默认,两次触发 reached pm.max_children setting (8)
第一次2020 × 55 MB ≈ 1.1 GB,留出约 1.3 GB 余量
第二次24实测打满时是 6~8 请求/秒 × 2~3 秒/请求 ≈ 20 并发,小幅上调

第二次没有直接跳到 28:因为站群里会用 Imagick 处理大图,单个请求就可能吃掉几百 MB,留点余量比多开几个 worker 划算。

四、完整参数

pm = dynamic
pm.max_children = 24
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 8
pm.max_requests = 500
request_terminate_timeout = 120
request_slowlog_timeout = 5
slowlog = /www/server/php/82/var/log/slow.log
listen.backlog = 8192
  • pm.max_requests = 500:worker 处理 500 个请求后自我重启,缓解慢性内存泄漏;
  • request_terminate_timeout:单请求超 120 秒直接杀掉,避免 worker 被卡死;
  • request_slowlog_timeout = 5 + slowlog:超过 5 秒的请求会记录 PHP 调用栈,是抓"到底哪个插件慢"最有效的工具,别关。

五、一个容易踩的坑:池里的 memory_limit 覆盖

我一开始在 FPM 池里单独写了 php_admin_value[memory_limit] = 256M,结果用 Imagick 处理大图时反复报:

Allowed memory size of 268435456 bytes exhausted

原因是池里的覆盖会盖掉 php.ini。后来把这行删掉,统一用 php.ini 的 1024M,内存风险改由 max_children 控制——因为真正决定内存占用的是「进程数 × 单进程」,不是把单进程限额调大就会用满。

六、改完怎么验证

# reload(宝塔下服务名以实际为准)
systemctl reload php-fpm-82

# 之后不再出现 reached
grep -c 'reached pm.max_children' /www/server/php/82/var/log/php-fpm.log

# 看实时并发
pgrep -c -f 'php-fpm: pool www'

# 重点看 swap 有没有继续涨(快涨说明给多了,该降回去)
swapon --show

调整之后要盯两个信号:reached 告警是否消失、swap 的 used 是否继续增长。前者还出现说明给少了,后者持续上涨说明给多了。

七、回滚

把 pm.max_children / pm.start_servers / pm.min_spare_servers / pm.max_spare_servers 改回原值(这条从 20 调到 24,回滚就是改回 20)再 reload。


一句话总结:max_children 不是越大越好,用「PSS × 并发数 < MemAvailable」来算;调之前先量 PSS,调之后盯 reached 告警和 swap 增长,并且把 slowlog 打开——它比任何猜测都更快指出真正的瓶颈。