2026/10/26 21:47:13

PHP-FPM进程管理调优:模式选择与pm.max_children计算指南

PHP-FPM进程管理调优:模式选择与pm.max_children计算指南 PHP 跑得慢很多人第一反应是换框架、加缓存、上 OPcache折腾一圈发现响应时间还是忽高忽低。我遇到过好几次这种情况最后把问题定位到一个特别不起眼的地方PHP-FPM 的进程管理模式和参数配置。这套东西默认值往往是为“能跑起来”设计的不是为“跑得好”设计的。pm 选错模式、pm.max_children 拍脑袋填个数轻则请求排队、CPU 空转重则内存被打爆、整台机器上的服务一起遭殃。这篇内容围绕 PHP-FPM 的进程管理展开讲清楚三件事static、dynamic、ondemand 三种模式各自适合什么场景pm.max_children 这类参数到底该怎么算而不是怎么猜以及配置改完之后怎么验证、怎么根据监控数据回头调。适合已经能把 PHP 项目跑起来、但还没认真调过 FPM 参数的后端同学也适合那些线上偶发 502、响应抖动却一直找不到根因的人。下面全部是我在实际项目里踩过、调过、验证过的东西参数计算过程会写全你可以直接照着套。1. 先搞清楚 PHP-FPM 到底在管什么1.1 master 与 worker 的分工PHP-FPM 是一个常驻的进程管理器它自己master 进程不处理请求只负责监听端口、接收连接、fork 出子进程、回收挂掉的子进程。真正干活的是 worker 进程每个 worker 在同一时刻只处理一个请求。这一点非常关键并发能力约等于 worker 数量一个 worker 忙的时候新来的请求要么排队要么被拒绝要么由 master 再拉一个新 worker 出来取决于模式。理解了这个模型很多现象就顺了。比如你发现 QPS 上不去但 CPU 只用了 30%大概率是 worker 数量不够请求在 listen backlog 里排队反过来你发现机器内存吃紧、swap 飙升多半是 worker 开太多每个 worker 都占着一份 PHP 进程内存。所以调 FPM 的本质是在“并发能力”和“内存占用”之间找一个平衡点而这个平衡点由模式选择和进程数共同决定。还有一个容易被忽略的点worker 处理完请求后不会退出而是回到空闲池等待下一个请求。这意味着 worker 占用的内存是常驻的不是用完就还。你按“峰值并发 × 单进程内存”去估算内存占用基本是准的但如果按“平均并发”去算线上高峰一定会打脸。1.2 请求在 FPM 内部的流转路径一个 HTTP 请求进来大致走这么几步Web 服务器Nginx 之类把请求通过 FastCGI 协议转给 FPM 的监听 socketmaster 收到连接后从空闲 worker 池里挑一个可用的worker 执行 PHP 脚本把结果回写worker 处理完回到空闲状态。整条链路里worker 的“可用性”是瓶颈所在。这里有个细节值得说FPM 的监听队列backlog是有限的默认 511。当所有 worker 都在忙、队列也满了新连接会被直接拒绝表现出来就是 502 或者连接重置。所以你在日志里看到大量 502先别急着怀疑代码去看看 FPM 的慢日志和进程池状态很可能就是 worker 不够、队列溢出。另外worker 从空闲到接活的切换是有微小开销的但真正的大头开销在 worker 的“创建”上——fork 一个进程、加载 PHP 扩展、初始化运行时这一套下来几十毫秒起步。这就引出了三种管理模式的核心差异worker 是提前创建好、按需创建、还是用完就销毁。2. 三种管理模式不是选最好的是选最合适的2.1 static固定进程数简单粗暴但可控static 模式下FPM 启动时就一次性 fork 出 pm.max_children 个 worker之后数量不变。请求来了就从这固定的一批里挑空闲的没有空闲就排队。它的优点是行为完全可预测内存占用是固定的worker 数 × 单进程内存没有 fork 抖动响应时间稳定。缺点是不弹性流量低谷时这些 worker 白占着内存流量高峰时又顶不上去。所以 static 适合两类场景一是流量非常平稳的服务比如内部定时任务接口、稳定的 API 网关二是对延迟极其敏感、不能容忍 fork 开销的场景。我一般会在内存充足、流量可预测的核心服务上用 static把 pm.max_children 设成“能扛住日常峰值”的数量宁可平时浪费一点内存也不要在高峰期出现 fork 抖动。这里有个经验值如果单进程内存是 40MB机器给 PHP 分配 4GB那 pm.max_children 最多 100实际配置留 20% 余量设 80 左右比较稳。2.2 dynamic按需伸缩大多数业务的首选dynamic 模式维护一个 worker 池池的大小在 pm.min_spare_servers 和 pm.max_spare_servers 之间浮动。空闲 worker 少于下限就 fork 新的多于上限就杀掉多余的同时总数不超过 pm.max_children。这是绝大多数 Web 业务该用的模式。它兼顾了弹性和开销平时维持少量空闲 worker 应对突发流量上来时快速扩容流量下去后回收。关键在于三个参数要配合好——min_spare_servers 保证低谷期有足够的“预备队”max_spare_servers 防止空闲 worker 占太多内存max_children 是硬上限防止无限扩容把机器撑爆。举个我调过的例子某业务日常并发 20 左右峰值能到 150。单进程内存约 50MB机器给 PHP 留了 6GB。按内存算 max_children 上限是 120但峰值需要 150 并发说明内存不够得先扩内存或者优化单进程内存而不是硬把 max_children 设到 150 然后等 OOM。最后我们把单进程内存压到 35MB关掉不必要的扩展、调小 OPcache 内存max_children 设到 150min_spare 设 20max_spare 设 40跑下来峰值稳定低谷也不浪费。2.3 ondemand用完就回收省内存但牺牲延迟ondemand 模式下FPM 平时不保留空闲 worker请求来了才 fork处理完空闲超过 pm.process_idle_timeout 就销毁。内存占用极低适合流量稀疏、对延迟不敏感的场景比如低频的后台接口、内部管理工具、定时触发的脚本入口。但它的代价很直接每个请求都可能触发 fork冷启动延迟明显。如果你的接口本身执行就几十毫秒fork 再花几十毫秒用户体感就是“这系统怎么这么卡”。所以 ondemand 千万别用在面向用户的、有并发要求的接口上。我见过有人图省内存把主站 API 设成 ondemand结果高峰期响应时间翻倍排查半天才发现是 fork 开销。三种模式的对比我整理成一张表选型时直接对照模式进程创建时机内存占用延迟表现适用场景static启动时一次性创建固定偏高最稳定无 fork 抖动流量平稳、延迟敏感的核心服务dynamic按空闲数动态增减弹性中等较稳定扩容时有轻微抖动大多数 Web 业务ondemand请求到来时创建最低冷启动延迟明显低频接口、后台工具选型逻辑其实一句话流量可预测且要稳定延迟选 static流量有波动选 dynamic流量稀疏且不在乎延迟选 ondemand。绝大多数人应该落在 dynamic 上然后重点调参数。3. pm.max_children 到底怎么算而不是怎么猜3.1 从内存反推的硬上限pm.max_children 是 worker 总数的天花板设大了内存爆设小了并发上不去。正确的算法是从可用内存反推pm.max_children 分配给 PHP 的内存 / 单个 worker 峰值内存单个 worker 峰值内存怎么来不是看 php -i 里的 memory_limit那个是单请求的内存上限不是进程实际占用。准确的做法是在压测或线上高峰期用ps或top看实际 RSSps --no-headers -o rss -C php-fpm | awk {sum$1} END {print sum/NR/1024 MB per worker}这条命令把所有 php-fpm 进程的 RSS 加起来除以进程数得到平均单进程内存单位 MB。注意要看峰值时段的数据低谷期的数字会偏小按低谷算会高估容量。假设算出来单进程 45MB机器给 PHP 留 5GB留出系统和其他服务的余量那硬上限就是 5120 / 45 ≈ 113。实际配置别贴着上限设留 15% 到 20% 的缓冲应对内存波动设 90 到 95 比较稳妥。贴着上限设的后果是某个请求内存突然飙高整台机器开始 swap然后雪崩。3.2 从并发需求反推的下限光有内存上限还不够还得确认这个上限能不能扛住业务并发。并发需求怎么估看两个数日常峰值 QPS 和单个请求的平均处理时间。用利特尔法则估算所需 worker 数 ≈ 峰值 QPS × 平均请求处理时间秒比如峰值 QPS 是 200平均请求处理 80ms0.08 秒那所需 worker 数约 200 × 0.08 16。这个数字看起来很小但它算的是“平均情况”。实际要按 P95 甚至 P99 的处理时间来算因为慢请求会长时间占着 worker。如果 P99 是 500ms那 200 × 0.5 100这才是更接近真实的并发占用。把内存上限和并发下限放在一起看如果并发下限小于内存上限说明内存够用按并发需求往上留余量配置即可如果并发下限大于内存上限说明内存是瓶颈要么加内存要么优化单请求内存占用和处理时间硬调 max_children 只会把问题推迟到线上爆发。3.3 一个完整的计算实例拿一个具体场景走一遍。某 API 服务压测数据峰值 QPS 300P95 处理时间 120msP99 处理时间 400ms。线上高峰期实测单 worker RSS 约 55MB。机器 16GB操作系统和其他服务占 4GB给 PHP 留 10GB。先算并发需求按 P95 算300 × 0.12 36按 P99 算300 × 0.4 120。取 P99 的 120 作为并发下限因为慢请求才是真正占着 worker 不放的。再算内存上限10240 / 55 ≈ 186。并发下限 120 小于内存上限 186说明内存能支撑配置时取并发需求往上留 30% 余量120 × 1.3 ≈ 156取整设 150。min_spare_servers 设成日常并发的量级比如 30max_spare_servers 设成 min 的 2 倍左右60。这样低谷期维持 30 个空闲 worker 应对突发高峰期最多扩到 150内存占用峰值约 150 × 55 8250MB在 10GB 预算内安全。如果算出来并发下限超过内存上限比如并发要 200 但内存只够 150那就得做取舍优化单请求内存关扩展、调小 buffer、优化慢请求加缓存、拆查询或者加机器做水平扩展。这时候调 FPM 参数已经不是解法了它只是把瓶颈暴露出来。4. 那些参数表里不会写的配套细节4.1 pm.max_requests 与内存泄漏的博弈pm.max_requests 控制一个 worker 处理多少个请求后自动重启。设成 0 表示永不重启设成具体数字则到量就销毁重建。这个参数的存在意义是兜底内存泄漏PHP 应用里第三方扩展、长生命周期对象、静态变量缓存都可能让 worker 内存缓慢增长重启是最简单粗暴的解法。但设太小有代价频繁重启意味着频繁 forkfork 开销会吃掉性能。我一般设 500 到 1000 之间具体看单请求内存增长曲线——如果压测 1000 个请求后单进程内存涨了不到 10%设 1000 足够如果涨得厉害先查泄漏别指望靠重启掩盖。提示pm.max_requests 是兜底手段不是优化手段。如果发现必须设得很小比如 100才能控制内存说明应用有明确的内存泄漏应该去定位代码或扩展问题而不是把重启频率调高。4.2 request_terminate_timeout 与慢请求治理request_terminate_timeout 是单个请求的最长执行时间超时后 FPM 会杀掉这个 worker。它和 PHP 的 max_execution_time 不是一回事max_execution_time 只算 CPU 时间不算 IO 等待request_terminate_timeout 是墙钟时间IO 阻塞也算。所以对付“卡在数据库查询或外部调用上”的慢请求这个参数更管用。设多少合适看业务的最长合理处理时间。一般接口 30 秒、报表类 300 秒。设太短会误杀正常的长任务设太长会让卡死的请求长期占着 worker。我习惯设成 P99.9 处理时间的 2 到 3 倍既能兜住异常又不至于让僵尸请求赖着不走。配套还要开慢日志request_slowlog_timeout设成比正常处理时间略高的值比如 2 秒slowlog指向一个独立文件。这样超过阈值的请求会被记录下完整调用栈排查慢查询、死循环特别有用。这个日志平时没人看但出问题时是救命稻草。4.3 listen backlog 与 502 的关系前面提过 backlog 默认 511这个值在高并发下可能不够。当所有 worker 忙、队列也满新连接被拒Nginx 侧就是 502。调大 backlog 能缓解瞬时突刺但它治标不治本——队列再长worker 不够还是会排队只是把“拒绝”变成了“等待”。我的做法是backlog 设成 max_children 的 2 到 3 倍比如 max_children 150 就设 400 左右。同时监控 FPM 的listen queue指标如果这个值持续大于 0说明 worker 长期不够该扩容或优化了而不是继续调大 backlog。; 进程池配置片段示例 pm dynamic pm.max_children 150 pm.start_servers 30 pm.min_spare_servers 30 pm.max_spare_servers 60 pm.max_requests 800 request_terminate_timeout 60s request_slowlog_timeout 2s slowlog /var/log/php-fpm/slow.log listen.backlog 400这段配置可以直接作为 dynamic 模式的起点再根据实际监控微调。注意 start_servers 要落在 min 和 max spare 之间否则 FPM 启动时会告警。5. 改完怎么验证别信感觉看数据5.1 上线前用压测确认拐点参数改完不能直接上生产先在预发环境压测。压测的目的不是看“能扛多少 QPS”而是找拐点QPS 随并发上升到某个点后响应时间突然飙升那个点就是当前配置的容量上限。如果拐点远低于业务峰值说明参数还不够如果拐点很高但内存先爆了说明 max_children 设大了。压测时重点看三个指标QPS、P95/P99 响应时间、单进程内存。三者一起看才能判断配置是否合理。我习惯用阶梯加压并发从 10 开始每轮加 20每轮跑 2 分钟记录三个指标的变化曲线。曲线在某个并发点突然翘起就是拐点。5.2 线上监控 FPM 状态页FPM 自带一个状态页开启后能实时看到进程池的健康状况。配置里加pm.status_path /fpm-status然后在 Nginx 里把这个路径暴露给内网千万别对公网开放访问后能看到这些关键字段字段含义健康判断active processes正在处理请求的 worker 数持续接近 max_children 说明不够用idle processes空闲 worker 数长期为 0 说明没有预备队listen queue排队等待的连接数持续大于 0 说明 worker 不足max children reached达到上限的次数大于 0 说明曾被限流这几个指标里listen queue和max children reached最能说明问题。前者持续大于 0说明请求在排队worker 不够后者大于 0说明曾经有请求因为达到 max_children 被拒。两个都为 0 且 active 稳定在 max_children 的 60% 到 70%说明配置比较健康。5.3 根据监控数据回头调参监控数据不是用来看的是用来调的。我总结了一个简单的调参逻辑listen queue持续大于 0且内存还有余量调大 max_children同时按比例调大 max_spare_servers。listen queue持续大于 0但内存已接近上限说明内存是瓶颈优化单进程内存或加机器别硬调。idle processes长期为 0 但listen queue也为 0说明刚好够用没有余量建议把 min_spare_servers 调大一点留缓冲。max children reached大于 0说明曾经触顶结合当时的流量判断是偶发突刺还是常态不足偶发可以调大 backlog 缓解常态则要调大 max_children。调参是个迭代过程一次只改一个参数改完观察至少一个完整的流量周期比如一整天确认稳定再改下一个。一次改一堆参数出问题根本不知道是哪个引起的。6. 几个真实踩过的坑6.1 把 max_children 当 QPS 指标设早期我犯过一个错看到业务 QPS 是 500就把 max_children 设成 500觉得“一个 worker 扛一个 QPS 总够了吧”。结果内存直接爆掉机器开始 swap所有服务一起卡。问题在于 QPS 和并发不是一回事——500 QPS 如果每个请求 20ms 处理完实际并发只需要 10 个 worker。max_children 对应的是同时正在处理的请求数不是每秒请求数。这个换算关系一定要用利特尔法则算清楚别拍脑袋。6.2 dynamic 模式下 min_spare 设成 1有次接手一个配置dynamic 模式min_spare_servers 设成 1max_spare_servers 设成 2。表面看很省内存实际问题是流量稍微一波动空闲 worker 瞬间被吃光然后 FPM 开始疯狂 fork 新进程fork 本身又消耗 CPU形成恶性循环响应时间剧烈抖动。min_spare_servers 的意义是“随时有预备队”设成 1 等于没有预备队。后来改成 min 20、max 40抖动立刻消失。这个参数省不得它是用少量常驻内存换稳定性的。6.3 忽略 OPcache 与 FPM 的联动调 FPM 的时候很容易忽略 OPcache。worker 重启pm.max_requests 到量后如果 OPcache 是每个进程独立的默认共享内存模式其实不是但配置不当会退化成独立新 worker 要重新编译脚本冷启动变慢。正确做法是确认 OPcache 用的是共享内存opcache.enable1 且 opcache.memory_consumption 设置合理这样 worker 重启后能直接复用已编译的字节码fork 开销大幅降低。另外 opcache.max_accelerated_files 要设得比项目文件数大否则缓存命中率上不去等于白开。我一般设 20000 起步大项目设 50000。这个值和 FPM 参数没有直接关系但它决定了 worker 重启后的“热身”速度间接影响 FPM 调优的效果。6.4 状态页对公网开放这个不算性能坑算安全坑但值得提。有人为了图方便把 pm.status_path 直接暴露在公网 Nginx 上结果进程池的活跃数、队列长度全被外面看到了属于信息泄露。状态页一定要限制内网访问或者用独立的监听端口只绑 127.0.0.1。配置安全这事省一步后面可能要花十步补。7. 不同规模下的配置思路差异小项目单机、日请求百万级以下不用太纠结dynamic 模式max_children 按内存算出来设个 30 到 50min_spare 设 5 到 10基本够用。这个量级下瓶颈通常不在 FPM在数据库或代码逻辑FPM 参数调到“不拖后腿”就行。中等规模多机、日请求千万级要认真算按前面的方法把内存上限和并发下限都算清楚配好监控建立调参的反馈闭环。这个阶段 FPM 配置不当会直接体现在成本和稳定性上值得花时间。大规模集群、日请求亿级以上FPM 参数往往不是主要矛盾了架构层面的水平扩展、读写分离、缓存分层才是重点。但即便如此单机的 FPM 配置也要标准化避免个别机器成为短板。这时候更适合用配置管理工具把参数模板化按机器规格自动计算 max_children而不是每台手配。我个人在实际操作中的体会是FPM 调优这件事八成的收益来自“把 max_children 算对”和“选对模式”剩下两成来自细节参数和监控迭代。很多人一上来就研究各种高级参数却连自己机器的单进程内存都没测过这是本末倒置。先把ps那条命令跑一遍把单进程内存和峰值并发这两个数拿到手后面的计算才有意义。另外任何参数改动都要有监控数据支撑改完观察一个完整流量周期再下结论别凭一次压测或者一时感觉就定稿——线上的流量形态比压测复杂得多只有真实数据才靠得住。