PHP 站点跑得好不好,很多时候不取决于代码,而取决于 PHP-FPM 的进程池配得对不对。pm.max_children 设小了,请求一多就排队、超时、报 502;设大了,服务器内存被吃光,触发 OOM,进程被系统杀掉,反而全线崩。这两个方向看起来矛盾,但根源是同一个:没有根据服务器实际内存和单个 PHP 进程的开销去算这个数字。
这篇文章要解决的就是"pm.max_children 到底填多少"。我们会先讲清 PHP-FPM 的几种进程管理模式(static、dynamic、ondemand)分别适合什么场景,再给出一个可落地的内存计算公式和实测方法,然后落到 www.conf 的真实配置项,最后讲怎么用 FPM 状态页和日志判断配得够不够。读完你应该能自己算出一个有依据的数值,而不是照抄网上"设成 50 就行"的经验。
文章里的数字都是量级参考,具体以你自己的实测为准。PHP 版本、扩展加载情况、框架大小都会明显影响单进程内存,同样是 PHP-FPM,一个加载了十几 MB 扩展的进程和一个裸进程差距可能有两三倍。
一、先搞懂 PHP-FPM 的三种进程模式
PHP-FPM 是一个进程管理器,它维护一组 PHP worker 子进程来处理请求,每个 worker 同时只处理一个请求。进程池怎么扩怎么缩,由 pm 参数决定,共有三种模式。
static(静态):启动时就固定创建 pm.max_children 个 worker,不增不减。优点是稳定、无额外开销,响应快;缺点是空闲时也占着那么多内存。适合内存充足、流量稳定的场景。
dynamic(动态):按需伸缩,pm.start_servers 是启动时的进程数,空闲多了缩到 pm.min_spare_servers,忙了涨到 pm.max_spare_servers,上限是 pm.max_children。这是默认也是最常用的模式,适合流量有波动的站点。缺点是伸缩判断有一定滞后。
ondemand(按需):平时一个 worker 都不留,来了请求才创建,空闲一段时间后被回收。省内存,适合低流量、内存紧张的机器;但每个新请求都要 fork 进程,首次响应会慢,高并发下反而更吃力。
| 模式 | 内存占用 | 响应速度 | 适用场景 |
|---|---|---|---|
| static | 高(常驻固定) | 最快 | 内存足、流量稳 |
| dynamic | 中(动态伸缩) | 快 | 通用,流量有波动 |
| ondemand | 低(空闲回收) | 首请求慢 | 低流量、小内存机器 |
对大多数中小站点,dynamic 是默认答案;内存特别紧张或访问量很小的后台站点才考虑 ondemand。
二、pm.max_children 的本质:内存 / 单进程开销
pm.max_children 就是"这个池最多能同时有几个 worker 干活"。它的上限不是 CPU、不是并发数,而是内存:每个 worker 都要占一份独立的内存,进程开得越多,总内存越大,超过物理内存就会触发 OOM。
所以计算公式很直接:
pm.max_children ≈ (可用内存 - 其他进程占用) / 单个 PHP 进程平均内存
关键在于两个量要测准:机器给 PHP-FPM 留多少内存,以及单个 PHP 进程实际吃多少。
先看内存盘子。假设一台 4GB 内存的服务器,跑 Nginx、MySQL、Redis、系统本身之外,留给 PHP-FPM 的可能是 2GB。再假设单个 PHP 进程峰值占用 60MB(经验值,装了较多扩展或用了大框架时会更高,裸 PHP-CLI 可能只有 20-30MB)。那么:
pm.max_children ≈ 2GB / 60MB ≈ 34
为避免压到临界值,通常再留出余量,取 30 左右。这个数比"随便设 50"更有依据——设 50 时 50×60MB = 3GB,很可能吃掉别的服务的内存,埋下 OOM 隐患。
三、怎么实测单个 PHP 进程的内存
公式里最容易拍脑袋的是"单进程内存"。实测方法有两种。
第一种,直接看正在跑的 worker 内存占用。
# 看所有 php-fpm 子进程的常驻内存 RSS,按 MB 排序
ps --no-headers -o rss,cmd -C php-fpm | awk '{printf "%.1f MB\t%s\n", $1/1024, $2}' | sort -rn | head
# 汇总所有 php-fpm 进程的内存总和
ps -C php-fpm -o rss= | awk '{s+=$1} END {printf "总 RSS: %.1f MB\n", s/1024}'
注意 RSS 会共享一部分内存(如 opcache 共享段),直接相加会偏大,但用来估上限够用。更重要的是看峰值,不是平均值:跑一段时间、经历高峰后,用 ps 记录最大的那个 worker 的 RSS,才是该用在公式里的数。
第二种,在代码里直接输出峰值内存。
memory_get_peak_usage(true) 返回的是 PHP 向系统申请的真实内存块,比 false 更接近 RSS。多采集几个典型请求(列表页、详情页、导出接口),取最大值作为单进程内存的估计。
如果发现单进程峰值超过 100MB,要警惕:可能是某个接口一次加载了过多数据、或 opcache 配置不当。优化代码往往比一味调大 pm.max_children 更有效。
四、www.conf 关键配置项
配置文件的路径随发行版和安装方式不同:
# RHEL/CentOS/Rocky(remi 源或 epel)
/etc/php-fpm.d/www.conf
# Debian/Ubuntu
/etc/php/8.2/fpm/pool.d/www.conf # 8.2 换成你的版本号
一个 dynamic 模式的推荐配置骨架:
; 进程管理模式
pm = dynamic
; 池能创建的最大 worker 数,按内存公式算出
pm.max_children = 30
; 启动时创建的 worker 数,经验值取 max_children 的 1/4 ~ 1/2
pm.start_servers = 8
; 空闲 worker 少于这个数就扩,通常取 max_children 的 1/4
pm.min_spare_servers = 5
; 空闲 worker 多于这个数就缩,通常取 max_children 的 1/2
pm.max_spare_servers = 15
; 单个 worker 处理多少个请求后重启,防止内存泄漏累积,通常 500~1000
pm.max_requests = 500
; 单个请求最大执行时间(秒),超时会被 kill
request_terminate_timeout = 60s
; 慢日志阈值,超过就记一条,用于找慢请求
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/www-slow.log
几个要点:
pm.max_requests不是越大越好。PHP 扩展和框架偶尔有内存泄漏,设一个 500 到 1000 的重启阈值,能定期释放,避免 worker 内存一路涨到失控。request_terminate_timeout要和 Nginx 的fastcgi_read_timeout协调。如果 Nginx 等 60 秒就断,而 FPM 还在跑 120 秒的脚本,会出现"用户看到超时、后端还在耗资源"的浪费。pm.min_spare_servers不宜设得太小,否则突发流量来时进程来不及创建,请求只能排队。
改完配置后校验并重载:
# 校验语法
php-fpm -t
# 平滑重载(不要 restart)
sudo systemctl reload php-fpm
风险提示:把 pm.max_children 调大是"危险操作"的变体——它不会立刻报错,而是可能在下一次流量高峰时触发 OOM,被系统 OOM Killer 杀掉 MySQL 或 SSH。改之前务必先算内存账,改之后用 dmesg 和内存监控观察一段时间。回滚方式:保留改前的配置文件副本,异常时 cp 回去再 reload。
五、用状态页和日志判断配得够不够
调完不能凭感觉,要有数据支撑。PHP-FPM 内置状态页,能直接看进程池是否打满。
先启用状态页(pool 配置里):
; www.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping
再在 Nginx 里暴露(注意限制访问来源):
location = /fpm-status {
allow 127.0.0.1;
allow 10.0.0.0/8; # 内网监控机
deny all;
fastcgi_pass unix:/run/php-fpm/www.sock;
include fastcgi_params;
}
查询状态:
curl http://127.0.0.1/fpm-status
curl 'http://127.0.0.1/fpm-status?json&full' # JSON 格式,便于监控采集
状态页里几个关键字段:
max children reached:出现大于 0,说明 max_children 被打满过,说明配小了或者业务变重了。active processes:正在干活的 worker 数,长期接近 max_children 就是瓶颈。idle processes:空闲 worker 数,长期为 0 说明没有余量。listen queue/max listen queue:等待被处理的连接数。这个值持续大于 0,是最明确的"进程不够"信号。
同时看错误日志告警:
grep -i 'max_children\|server reached\|pool' /var/log/php-fpm/error.log | tail
如果日志里频繁出现 server reached pm.max_children setting,并且 max listen queue 一直在涨,基本可以判定要扩容——要么加内存、调大 max_children,要么加机器做多实例。
六、压测与容量估算
配置改完最好做一次压测,验证在目标并发下是否稳定。
# 用 ab 或 wrk 压测,注意别打生产
ab -n 2000 -c 50 http://127.0.0.1/ # 50 并发,测 2000 次
# wrk 更接近真实,能持续压
wrk -t4 -c100 -d30s http://www.example.com/
压测时同步观察:
# 实时看 php-fpm 进程数和内存
watch -n1 'ps -C php-fpm -o rss= | awk "{s+=\$1} END {print s/1024\" MB\"}"; ss -lnt | grep 9000'
free -m
一个粗略的容量估算思路:如果单进程平均响应时间 100ms,那么一个 worker 每秒大约能处理 10 个请求;30 个 worker 理论吞吐约 300 QPS(不含数据库等外部瓶颈)。这只是理想上限,真实值通常要打三到五折。所以站点目标 500 QPS,往往不是靠单机调 pm.max_children 硬扛,而是要加机器水平扩展,前面用 Nginx 做负载均衡。
常见问题
Q:pm.max_children 设太大但内存没爆,是不是就没事?
不一定。进程池按需创建,流量没到时不占内存,但一旦同时高并发,几十个进程同时膨胀到峰值内存,就会瞬间吃满触发 OOM。要按"所有 worker 都达到峰值占用"这个最坏情况来算账。
Q:为什么日志里报 reached pm.max_children 但服务器内存还有富余?
可能是 min/max spare 配得不合理,或者 max_children 本身就偏小。也可能是每个请求处理太慢占住了 worker。先看状态页的 active/idle 和慢日志,判断是"进程不够"还是"单个请求太慢"。
Q:static 和 dynamic 哪个生产更稳?
流量平稳、内存充足优先 static,无伸缩开销、响应最稳;流量波动大用 dynamic。两种都不要设 pm.max_children 超过内存能承受的值。
Q:ondemand 省内存,为什么高并发还会变慢?
因为每个新请求都可能要 fork 新进程,fork 是有成本的;高并发下频繁创建销毁进程,反而增加 CPU 开销和延迟。它适合的是低频访问场景。
Q:改了 pm 参数一定要重启吗?
用 systemctl reload php-fpm 平滑重载即可,不必 restart。但要注意 reload 不会杀掉正在处理请求的旧 worker,它们是处理完当前请求后才退出,因此内存占用会短暂叠加,容量紧张时要挑低峰期操作。
总结
pm.max_children 不是拍脑袋填的,核心公式是"留给 PHP-FPM 的可用内存 ÷ 单个 PHP 进程峰值内存",务必按最坏情况(所有 worker 同时达到峰值)算,并留出余量。
单个进程内存要用 ps 看 RSS 峰值,或在代码里用 memory_get_peak_usage(true) 采集,别用平均值估。
模式选择上,通用选 dynamic,内存足且流量稳选 static,低频小内存选 ondemand。
调完后用 PHP-FPM 状态页和错误日志验证:max children reached 大于 0、max listen queue 持续增长,就是进程不够的明确信号。
单机调优有天花板,真正的高并发靠水平扩展。当目标 QPS 超过单机能稳定承载的量级时,加机器 + Nginx 负载均衡比继续加大 pm.max_children 更划算。
企业QQ咨询




