
PHP 跑得慢很多人第一反应是换框架、加缓存、上 OPcache折腾一圈回来发现瓶颈根本不在代码层而在 PHP-FPM 这个看不见的中间层。我见过太多线上环境明明机器 8 核 16GPHP-FPM 却配着pm dynamic加pm.max_children 5高峰期请求排队排到超时日志里全是 502 和 504运维还在那儿怀疑是数据库慢查询。这篇就专门聊透一件事PHP-FPM 的进程管理模式怎么选、进程参数怎么算、配错了会怎样、怎么验证配对了。不管你是刚接手一台云服务器的新手还是已经在调优路上踩过几轮坑的老兵这里面的参数推导过程和实测经验都值得对着自己的php-fpm.conf核一遍。1. 先搞清楚 PHP-FPM 到底在管什么1.1 从一次请求的生命周期说起要理解进程管理得先明白一个 HTTP 请求进到 PHP 这一层之后发生了什么。Web 服务器Nginx 或 Apache收到请求发现是.php后缀通过 FastCGI 协议把请求转给 PHP-FPM。PHP-FPM 是一个常驻的进程管理器它预先维护着一批 PHP 解释器进程worker每个 worker 同一时刻只能处理一个请求。请求来了FPM 从池子里挑一个空闲 worker 接活执行完脚本再把结果回给 Web 服务器。这个模型的关键点在于worker 数量决定了并发处理能力。worker 不够请求就得排队等worker 太多内存被吃光系统开始 swap反而更慢。所以 PHP-FPM 调优的本质就是在并发能力和内存占用之间找一个平衡点而这个平衡点由两样东西决定——管理模式pm和进程数量参数。很多人把 PHP-FPM 当成黑盒装完就用默认配置默认配置是给开发机准备的放到生产环境上要么浪费资源要么直接压垮。我接手过一个项目默认pm.max_children 5日活一上来5 个 worker 全占满后面的请求全部卡在 listen backlog 里用户看到的就是转圈圈然后超时。1.2 三种管理模式的核心差异PHP-FPM 提供三种进程管理模式配置项是pm取值分别是static、dynamic、ondemand。这三个词看着简单背后的行为差异却直接决定了你的服务在流量波动下的表现。static是静态模式启动时就按pm.max_children指定的数量把所有 worker 一次性 fork 出来运行期间数量恒定不变。它的特点是简单粗暴、响应最快因为永远有固定数量的 worker 待命不存在临时创建进程的开销。代价是内存占用是固定的哪怕半夜流量为零这些进程也占着内存不放。dynamic是动态模式启动时先创建pm.start_servers个 worker运行中根据空闲 worker 数量在pm.min_spare_servers和pm.max_spare_servers之间伸缩上限是pm.max_children。流量涨了就多开几个闲了就回收几个。这是最常用的模式兼顾了资源利用和响应速度。ondemand是按需模式平时一个 worker 都不留或者说只留极少数请求来了才现场 fork。它的内存占用最低适合那种流量稀疏、长时间没请求的场景比如内部管理后台、定时任务接口。但代价是每个新请求都可能触发进程创建首请求延迟明显偏高高并发下 fork 的开销会拖垮响应。提示这三种模式没有绝对优劣只有场景匹配。选错的典型症状是——用 ondemand 扛高并发CPU 全耗在 fork 上用 static 跑低频服务内存白白浪费。1.3 为什么默认配置几乎总是错的PHP-FPM 的默认配置pm dynamicpm.max_children 5start_servers 2min_spare 1max_spare 3是十几年前为小内存机器设计的保守值。现在的服务器动辄 8G、16G 内存5 个 worker 连一台普通博客的并发都扛不住更别说业务系统。更麻烦的是这个默认值不会报错不会告警它只是默默地让你的服务在流量稍大时变慢。很多人排查性能问题时会先怀疑代码、怀疑数据库、怀疑网络最后才想到看 FPM 的进程状态。我建议任何新上线的服务第一件事就是把 FPM 配置按实际硬件重算一遍别让默认值坑了你。2. 进程数到底该开多少一套可复现的计算方法2.1 单个 worker 的内存占用怎么测算pm.max_children的前提是知道每个 PHP worker 实际吃多少内存。这个值因项目而异一个纯 API 接口可能只占 20MB一个加载了大量扩展和框架的 Web 应用可能占到 80MB 甚至更多。不能拍脑袋估得实测。最直接的办法是看运行中的 FPM 进程 RSS。在服务器上执行ps --no-headers -o rss,cmd -C php-fpm | awk { sum$1 } END { printf (%d%s\n, sum/NR/1024,M) }这条命令会算出所有 php-fpm 进程的平均 RSS单位 MB。注意要挑业务高峰期测因为这时候 worker 加载的脚本最全内存占用最接近真实峰值。低峰期测出来的值偏小按它算会低估需求。另一个更精确的途径是 FPM 自带的 status 页面。在 pool 配置里开启pm.status_path /fpm-status然后在 Nginx 里把/fpm-status转发到 FPM访问后能看到total processes、active processes、idle processes以及每个进程的内存占用。这个页面的数据比ps更贴近 FPM 内部视角建议长期开着做好访问限制调优时随时能看。实测下来一个典型的 Laravel 或 ThinkPHP 应用单 worker 内存在 40MB 到 70MB 之间浮动取决于加载的扩展数量和框架体量。我一般会在这个实测值上再乘一个 1.2 的安全系数因为内存占用会随请求复杂度波动留点余量避免 OOM。2.2 用内存反推 max_children 的公式拿到单 worker 内存后pm.max_children的计算逻辑就很清晰了pm.max_children (可用内存 - 系统及其他服务预留) / 单 worker 内存举个具体例子。一台 8G 内存的服务器操作系统和 Nginx、MySQL如果同机等基础服务预留 2G留给 PHP-FPM 的可用内存是 6G单 worker 实测 50MB那么max_children 6 * 1024 / 50 ≈ 122但这里有个陷阱不能把算出来的值直接填进去。因为 PHP-FPM 的 worker 是常驻的如果按峰值内存算满一旦某个请求内存暴涨比如处理大文件上传、导出大报表整机就可能 OOM。我的做法是取计算值的 70% 到 80% 作为实际上限剩下的留给突发。上面这个例子实际配max_children 90到100比较稳妥。还有一种情况要特别注意如果 PHP-FPM 和数据库跑在同一台机器上数据库本身也要吃内存MySQL 的 buffer pool 往往要占几个 G这时候留给 FPM 的内存要重新算别把数据库的份额抢了。生产环境我强烈建议 FPM 和数据库分机部署省得互相抢资源。2.3 三种模式下的参数组合怎么配算完max_children不同模式下的其余参数配法不一样下面这张表是我常用的参考组合模式必配参数适用场景内存策略staticmax_children流量稳定、并发高、内存充足固定占用按峰值配dynamicmax_children、start_servers、min_spare_servers、max_spare_servers流量有波动、通用场景弹性占用按峰值配上限ondemandmax_children、process_idle_timeout流量稀疏、低频后台按需占用上限可放宽dynamic模式下几个 spare 参数的经验配法start_servers取max_children的 10% 到 20%min_spare_servers取max_children的 10% 左右max_spare_servers取max_children的 30% 到 50%。比如max_children 100可以配start_servers 15、min_spare 10、max_spare 40。这样平时维持十几个空闲 worker 待命流量上来时快速扩到 40 个空闲超过就继续往上开直到 100。ondemand模式下process_idle_timeout默认是 10 秒意思是 worker 空闲 10 秒就被回收。低频服务可以调大到 60 秒甚至更长减少反复 fork 的次数。但要注意这个值调太大就失去了 ondemand 省内存的意义得根据实际请求间隔来定。3. 配置落地从改文件到生效的完整流程3.1 找到正确的配置文件位置PHP-FPM 的配置文件位置因安装方式而异这是新手最容易卡住的地方。用包管理器装的apt 或 yum主配置通常在/etc/php-fpm.conf或/etc/php/{版本}/fpm/php-fpm.conf而 pool 配置就是写pm那些参数的地方在/etc/php-fpm.d/www.conf或/etc/php/{版本}/fpm/pool.d/www.conf。源码编译安装的配置在编译时--sysconfdir指定的目录下常见是/usr/local/php/etc/php-fpm.conf和/usr/local/php/etc/php-fpm.d/www.conf。不确定的话用这条命令定位php-fpm -t它会做配置语法检查并输出配置文件路径。改之前先跑一遍确认你改的是 FPM 真正在用的那个文件别改了半天发现改的是备份文件。3.2 一份可直接抄的 pool 配置下面是我在一个 8 核 16G、跑中等流量 Web 应用的服务器上用的配置片段放在www.conf的[www]段里[www] user www-data group www-data listen /run/php-fpm/www.sock listen.backlog 511 pm dynamic pm.max_children 120 pm.start_servers 20 pm.min_spare_servers 15 pm.max_spare_servers 50 pm.max_requests 1000 request_terminate_timeout 60s slowlog /var/log/php-fpm/www-slow.log request_slowlog_timeout 5s逐项说一下为什么这么配。listen.backlog 511是等待队列长度默认值偏小高并发时请求在队列里排不上就直接被拒调到 511 能缓冲更多瞬时请求。pm.max_requests 1000让每个 worker 处理 1000 个请求后自动重启这是为了规避 PHP 长期运行可能的内存泄漏——有些扩展或第三方库会慢慢吃内存定期重启 worker 是最省事的兜底手段。request_terminate_timeout 60s是硬超时防止某个卡死的脚本把 worker 永久占住。slowlog那两行是排查利器执行超过 5 秒的请求会被记录到慢日志里附带当时的调用栈。调优时打开它能直接定位到是哪个脚本、哪一行在拖后腿比盲目猜快得多。3.3 平滑重载而不是粗暴重启改完配置后千万别直接kill进程或者systemctl restart。FPM 支持平滑重载用# 方式一发 USR2 信号 kill -USR2 $(cat /run/php-fpm/php-fpm.pid) # 方式二用 systemctl部分发行版支持 reload systemctl reload php-fpmUSR2信号会让 FPM 重新读取配置旧的 worker 处理完手头请求后优雅退出新的 worker 按新配置启动。整个过程用户无感知不会丢请求。而restart会直接杀掉所有 worker正在处理的请求全部中断用户看到的就是报错。注意平滑重载只对pm相关参数和部分 pool 配置生效。像listen地址这种底层变更还是得完整重启。改配置前先想清楚改的是哪一类。重载后立刻验证新配置是否生效ps aux | grep php-fpm | wc -l对比重载前后的进程数如果max_children从 5 改到 120重载后进程数应该明显上升dynamic 模式下会先到 start_servers 的数量。数字对不上说明配置没读到回去检查文件路径和语法。4. 配错了会怎样典型故障与排查实录4.1 症状对照表从现象反推配置问题调优最怕的是不知道问题出在哪。下面这张表是我这些年攒下来的症状—病因对照遇到问题先对号入座现象可能原因排查方向502 Bad Gateway 间歇出现max_children 太小worker 耗尽看 FPM 错误日志有无 pool www seems busy504 Gateway Timeoutrequest_terminate_timeout 太短或脚本卡死查 slowlog 定位慢脚本内存持续上涨直至 OOMmax_children 过大或存在内存泄漏算内存账开 max_requests 兜底首请求延迟高ondemand 模式频繁 fork换 dynamic 或调大 idle_timeoutCPU 飙高但负载不高worker 过多导致上下文切换降 max_children看 CPU 的 sys 占比pool www seems busy 这条日志是 FPM 在告诉你 worker 不够用了它会建议你把pm.max_children调大。但别看到这条就无脑加先确认内存扛不扛得住否则只是把 502 换成了 OOM。4.2 一个真实的 502 排查过程之前有个服务白天偶尔报 502晚上正常。第一反应是看 Nginx 错误日志果然有connect() to unix:/run/php-fpm/www.sock failed (11: Resource temporarily unavailable)。这个 errno 11 就是 EAGAIN意思是 socket 的 backlog 满了连接排不进去。顺着查 FPM 日志发现pool www seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers)。再看 status 页面active processes长期顶在max_children上idle processes是 0。结论很清楚worker 数量不够。但当时那台机器内存只有 4G单 worker 占 45MB理论上限也就 60 个左右。原来的max_children 30确实偏小。我把它调到 50同时把max_spare_servers从 10 提到 25让空闲池更充裕避免流量突增时来不及扩容。改完平滑重载502 消失。这个案例的教训是502 不一定是代码挂了很可能是 FPM 的排队机制在报警。看到 502 先看 Nginx 错误日志的具体 errno再对 FPM 日志比瞎猜高效得多。4.3 内存泄漏的识别与兜底PHP 本身有垃圾回收但某些扩展、长生命周期的单例、或者不当的全局缓存会导致 worker 内存只涨不降。识别方法是隔一段时间采样 worker 的 RSSfor i in 1 2 3; do ps --no-headers -o rss,cmd -C php-fpm | awk {sum$1} END {print sum/NR/1024 MB} sleep 60 done如果三次采样内存持续上涨且不回落基本可以判定有泄漏。彻底解决当然要查代码但线上等不起pm.max_requests就是最实用的兜底——设成 500 到 1000worker 处理够数量就重启内存自然被释放。代价是重启瞬间有个极短的不可用窗口但相比 OOM 导致整机崩溃这点代价完全值得。我一般把max_requests设成 1000配合dynamic模式实测下来内存曲线非常平稳不会出现那种锯齿状的大起大落。5. 调优之后怎么验证和持续观察5.1 用压测验证配置是否合理配置改完不能凭感觉说应该好了得压测。用ab或wrk对核心接口打一轮观察几个指标QPS、平均响应时间、错误率同时盯着 FPM status 页面的active processes和idle processes。合理的状态是压测期间active上升但不到顶max_childrenidle保持一定数量说明还有余量错误率为零。如果active直接顶满、idle归零说明max_children还是不够或者压测强度超过了设计容量如果idle一直很多、active很低说明配大了可以适当回收省内存。压测时别忘了同时看系统层面的vmstat和free确认没有发生 swap。一旦开始 swap所有性能数据都失真了因为瓶颈已经从 PHP 转移到了磁盘 IO。5.2 长期监控的几个关键指标调优不是一锤子买卖流量会变、代码会变、依赖会变配置也得跟着调。我建议至少监控这几个指标FPM 进程数定期采集active和idle画出趋势图看是否长期贴着上限跑。单 worker 内存内存上涨是扩容或查泄漏的信号。FPM 错误日志中的 busy 告警出现频率上升说明容量吃紧。慢日志条数慢请求增多可能是代码退化或数据库变慢的早期信号。这些数据用简单的 shell 脚本定时采集写进时序库就行不需要多复杂的工具。关键是养成看的习惯别等出事了才去翻。5.3 什么时候该考虑横向扩展当单机的max_children已经逼近内存上限压测还是扛不住说明单机容量到顶了这时候该考虑加机器做负载均衡而不是继续在单机上抠参数。硬把max_children往上堆只会让内存和 CPU 的争抢更激烈边际收益递减甚至为负。判断标准很简单如果max_children已经按内存算到 80% 的安全线压测时active仍然顶满、响应时间明显上升那就是该扩容的信号。PHP-FPM 的调优是在单机内找最优解超出单机能力就该换思路了。我个人在实际操作中的体会是PHP-FPM 调优最容易被忽视的不是参数本身而是先测量再配置这个习惯。太多人凭经验拍数字结果要么配小了扛不住要么配大了吃内存。花十分钟测一下单 worker 内存、算一下可用内存比反复试错靠谱得多。另外slowlog和 status 页面这两个东西建议所有生产环境都开着它们是排查问题的眼睛平时不显眼出事时能救命。