1. 先搞明白 PHP-FPM 到底在解决什么问题1.1 从一次网页请求的完整链路说起很多人配置PHP-FPM上来就抄pm.max_children 50之类的参数抄完发现流量一上来还是502、504、CPU飙满。问题不在于某个参数写得不对而在于你根本没搞懂PHP-FPM在整个请求链路里扮演什么角色。一次典型的请求长这样浏览器发起请求Nginx收到后看到是以.php结尾的URI就把请求转交给PHP-FPM处理PHP-FPM的某个Worker进程执行PHP脚本把结果返回给NginxNginx再返回给浏览器。这中间任何一个环节出了问题表现到前端就是各种网关错误。但很多运维新手容易忽略一个关键事实PHP-FPM不是“跑PHP脚本”这么简单它是一个常驻内存的进程管理器。它要干的活是保持一定数量的Worker进程待命、接收Nginx转交来的请求、分配给空闲Worker执行、处理完以后把结果回写、还要处理Worker崩了以后的重启和回收。这个机制本身就决定了你的配置本质上是在管理「资源分配」。所有参数加在一起回答的就一个问题你这台机器同一时刻能有多少PHP请求同时在跑死了以后多久能补回来。1.2 三种模式你调的到底是哪个旋钮PHP-FPM提供了三种进程管理模式这是所有配置的地基不理解透这三种模式后面调什么都白搭。第一种是static固定进程数。进程数从头到尾不变不受流量波动影响。优势在于没有动态创建进程的开销请求来了必定有Worker候着时延低劣势在于流量低谷时白白占用内存高峰期又没法自动扩容。第二种是dynamic动态模式。有最少进程数、最大进程数、初始进程数这些参数。实际运行的Worker数会随着请求量增减但幅度受限于你设定的最大值。这是绝大多数人会选择的模式兼顾了低峰期的资源占用和高峰期的承载能力。第三种是ondemand按需模式PHP-FPM 7.x以后才有的老版本别用。平时只保留一个主力进程来了请求才拉新的Worker出来处理完空闲一会儿就杀掉。内存占用极低但代价是请求突增时会有一段时间的进程创建开销表现为响应变慢。适合那种内存机器极小、请求量又很稀疏的场景。这里我给个特别明确的建议如果你对业务流量完全没把握先上ondemand保命同时预留充足资源如果你已经有压测数据或者对业务峰值有大致估算就上dynamic如果机器资源非常富余或者追求最稳定的低延迟static反而最省心。我见过不少团队从头到尾就只用dynamic然后只调一个pm.max_children其实是在拿最简单的模式应付复杂流量这种思路一直不纠正后续问题会接连不断。1.3 选模式之前先认清自己的业务画像模式选择的本质是匹配你的业务请求特征。有些业务是高吞吐的API服务每个请求本身很轻几毫秒就完成了但请求每秒几千上万有些业务是CMS内容站点页面生成要跑几十个SQL、渲染模板慢是慢但并发量并不高还有一批业务是低频管理后台平时根本没几个人访问月初月末突然一堆人进来导出报表。不同业务画像对PHP-FPM的需求完全相反。轻量高并发场景怕的是进程频繁创建销毁适合static维持稳定Worker池重量低并发场景怕的是内存不够导致频繁OOM和swap适合ondemand按需拉起混合型场景才轮到dynamic发挥价值。所以在动手配置之前先别急着问别人拿配置模板先回答自己三个问题单请求平均耗时多少毫秒高峰期QPS大约多少机器可用内存有多少这三个答案直接决定了后面所有参数的具体数值。2. 核心参数逐一解剖每个数字背后都是资源2.1 pm.max_children 的计算方法先说最关键的pm.max_children它的含义是允许创建的最大Worker进程数。每个Worker在同一时刻只能处理一个请求所以这个数字直接决定了PHP的并发处理能力。这个参数最核心的计算逻辑就一条进程数乘以单个进程内存占用不能超过系统可用内存。公式是pm.max_children 可用内存 - 系统服务占用内存 / 单个PHP进程平均内存占用单个PHP进程平均内存占用怎么拿用这条命令实测ps aux | grep php-fpm | grep -v grep输出里每一行代表一个进程RSS列就是实际物理内存占用。把多个空闲Worker的RSS加起来取个平均值再拿一个满载Worker的RSS作为峰值参考。我见过不少简单项目的PHP进程平均占用在40~70MB之间复杂框架加各种组件库的能到150MB甚至更高。这是一个必须拿实测数据说话的地方拍脑袋很容易出事。举个例子一台2G内存的云服务器系统加Nginx加MySQL已经吃掉大概600MB剩约1.4G假设单个PHP Worker平均吃60MB那么理论上限是23个进程。但我不建议直接贴23这个上限而是预留20%的内存给突发流量和系统缓存所以最终配置在17~19之间比较稳妥。这个计算方式在线下算明白上线才不心虚不然哪天流量稍微高一点或者业务代码改版、内存上涨就会连着宕机。提示计算的时候还要考虑zend opcache、PHP扩展、业务框架各自的内存开销。上线后要观察实际负载超过80%内存占用就该降进程数或者加机器了。2.2 池配置里的其他关键参数围绕模式选择还有几个配套参数很多人根本不知道它们之间存在联动关系。pm.start_serversdynamic模式下启动时创建的进程数。设置范围必须在pm.min_spare_servers和pm.max_spare_servers之间否则PHP-FPM拒绝启动。这个数字取多少合适通常建议压测后按平均空闲进程的中间值取。pm.min_spare_servers和pm.max_spare_servers空闲进程数的下限和上限。管理器会定期检查空闲进程数低于下限就补高于上限就杀。这两个参数决定了系统的伸缩灵敏度。如果业务请求波动剧烈差距拉大一点让FPM有更宽的调节空间如果业务稳定差距拉小一点。pm.max_requests每个Worker进程处理多少请求后自动回收重建。这个参数特别容易被忽略但作用极其重要。PHP脚本跑久了没法完全避免内存泄漏哪怕每次泄漏几个KB上万请求之后也会累积成几十MB让整个进程越来越缓慢地变臃肿。设置一个上限让进程定期自动重启是成本最低的防泄漏手段。我通常给后端API服务和定时任务场景设pm.max_requests 1000~2000给后台管理系统或者常驻运营脚本相关接口设3000~5000。设得太低进程重启过于频繁会白白损失性能设得太高内存泄漏兜不住底。这个数值的合理区间压测时看着内存曲线就能快速校准。listen.backlog连接排队队列长度。Nginx和一个Worker都在忙时新请求会临时排队队一长就丢包。默认511压力大的时候可以调大到1024甚至2048但对应的也要调大系统层面的net.core.somaxconn不然内核层面的队列先满了应用层的等待队列再长也是白搭。2.3 请求相关参数不该随手改的默认值除了进程池配置FPM的请求级参数经常被忽略但恰恰是它们决定了用户体验的边界。request_terminate_timeout单个请求的执行超时时间到点直接杀掉进程返回502。这个参数一定要和Nginx的fastcgi_read_timeout配合设置两者相差过大就会碰到一层已经超时、一层还在傻等的尴尬局。一般建议PHP侧的值略大于Nginx侧避免Nginx先断导致PHP写一半被中断产生脏数据。request_slowlog_timeout慢请求记录阈值。设5秒或者10秒超过就写入慢日志。开启这个不会对性能产生实际开销但对排查线上问题帮助巨大它记录了请求耗时、调用文件、函数栈。生产环境不开慢日志的PHP-FPM我基本可以断定他们的性能问题是靠猜来解决的。catch_workers_output默认值是no意味着PHP的error_log输出会被丢弃。设为yesPHP错误、警告会进到FPM的error_log排查诡异问题或框架起了但看不到任何报错时会有很大帮助。前提是error_log路径写对日志轮转配好不然会迅速写满磁盘。max_input_time解析请求体数据的最长时间。大部分业务根本不需要关掉但如果是长轮询或者视频上传类接口这个参数如果不够请求还没到PHP代码层就被掐断了。遇到这种情况的典型表现是日志里一切正常但接口就是超时报错而且往往发生在请求体特别大的时候。3. 实操从一台2G内存服务器出发的完整配置过程3.1 采集现状数据别拍脑袋我见过太多人配置全靠抄同一套配置从轻量服务器搬到物理机、从开发环境搬到生产环境不报错就万事大吉。这种思路要不得。正确做法是先花十分钟采集机器现状数据再决定参数。首先是内存现状用free -h看总内存、已用内存、可用内存注意available这一列才是系统真正可分配给新进程的内存而不是简单的total减used。然后是CPU核数用lscpu看Worker进程总数在CPU密集型业务上建议不要超过核数的两倍不然上下文切换本身就吃掉不少CPU。再观察一下你服务的平均负载uptime里有一分钟、五分钟、十五分钟的负载值别拿某一个时刻的峰值来定所有参数。关键的一步是拿到PHP进程真实的RSS数据。新起一个干净的PHP-FPM在低峰期找几个空闲进程看看平均RSS再踩一个压测高峰期看满载进程的RSS。这两个值分别用来算从容配置和极限上限。3.2 调整PHP-FPM的完整配置示例以一台2G内存、2核CPU的服务器为例假设单Worker平均60MB。实际配置我建议这样写[www] user www-data group www-data listen /run/php/php-fpm.sock listen.owner www-data listen.group www-data listen.mode 0660 pm dynamic pm.max_children 18 pm.start_servers 6 pm.min_spare_servers 4 pm.max_spare_servers 12 pm.max_requests 1500 request_slowlog_timeout 5s request_slowlog_log /var/log/php/slow.log request_terminate_timeout 30s catch_workers_output yes php_admin_value[memory_limit] 128M这里有几个细节值得注意。先看listen我强烈建议同一台机器上用Unix域名套接字因为省掉了TCP握手开销性能稍胜一筹。套接字文件的属主和属组必须和Nginx的worker进程运行用户一致否则Nginx没权限访问套接字马上就会出现“Permission denied”的502。我踩过无数次这种坑每次都是权限没对齐。memory_limit建议显式设置不让PHP脚本随意消费内存。注意PHP配置里用php_admin_value设置的值是脚本内无法使用ini_set()覆盖的这有助于防止业务代码自己把内存限制改高造成连锁故障。注意pm.max_spare_servers别设得和max_children太接近否则动态模式几乎会一直按上限运行和 static 就没区别了。另外请求超时参数如果设得太短正常的大批量导出请求全会失败设得太长慢请求又把所有Worker占满后面的请求排不上队。我一般先从30秒起加根据业务实际再调整。3.3 多站点场景下的池隔离配置一台机器上跑多套业务是常态。有人图省事一个站点对应一个Nginx server但PHP-FPM还是用同一个[www]池。这种做法在流量不大时看似没问题一旦某个站点被刷或者代码出问题就会把池内所有Worker进程占满连累其他站一起502。正确的思路是按业务域建多个FPM池各配各的进程数上限。FPM池的创建方式是在/etc/php/8.x/fpm/pool.d/下新增独立的配置文件比如site-a.conf、site-b.conf每个文件里有各自的[池名]、listen套接字路径、user、pm参数。然后Nginx配置里通过fastcgi_pass unix:/run/php/site-a.sock;指定转发到对应池。这种隔离的价值是双向的流量大的一方最多吃掉自己池里的Worker影响不到另一方另一方也能按自己的业务特征独立调配进程数。代价是多占用一点内存因为每个池至少保有各自的最小空闲进程。有个想法可以省内存把低频后台业务池复杂度调低一点用ondemand模式平时基本不占资源核心业务池用dynamic模式保证响应速度这样资源配置利用率会高出很多。如果是同机多个站点但业务模式完全不同我建议不只是建池隔离连用户也隔开。每个池配不同的user/group这样即使某个站点的PHP脚本被入侵出现漏洞权限边界也被限制在一个池里不至于直接碰到其他站点的文件。这套方案的成本很低但安全性提升一个层次值得所有多站点服务器照做。4. 性能调优中你容易忽略的几个点4.1 Unix Socket vs TCP连接方式的真实差异不少人在配置FPM的listen参数时习惯写成listen 127.0.0.1:9000理由是方便记忆也不容易出错。但这是Nginx和FPM在同一台机器上的连接方式TCP意味着每次请求都有完整的连接建立和挥手断开流程内核里多走一遍网络协议栈。换成Unix套接字数据不走网络栈直接通过套接字文件交换性能在小包高频请求场景下有明显提升。直观感受来说两者差异在压测数据上大概是几个毫秒到几十个毫秒的差距视并发量而定。并发越高TCP方式的额外开销越明显因为它还要处理端口占用和连接文件描述符。如果必须跨机器部署Nginx和FPM那就只能走TCP此时要注意listen.allowed_clients参数默认所有客户端都能连上来必须限制为Nginx所在机器的IP否则等于把PHP执行能力暴露给了网络里任何人这是非常危险的配置疏漏。另外补充一个细节使用Unix套接字时如果Nginx和FPM运行用户不同记得把listen.owner、listen.group、listen.mode都设置好。否则就会出现Nginx无法连接Unix套接字的错误错误日志里会写connect() to unix:/run/php/php-fpm.sock failed (13: Permission denied)。4.2 slow log 怎么用才有价值很多人开了request_slowlog_timeout但从来不看日志开了等于白开。慢日志真正值钱的地方是它能告诉你性能瓶颈到底卡在哪个函数调用里。默认配置下慢日志写的是PHP调用栈。你可以看到一条请求慢是因为某个数据库查询函数耗时过长、某个外部HTTP调用迟迟不返回、还是某个正则表达式处理了大文本导致CPU耗时飙升。有了这些信息才能做针对性优化比如加索引、改缓存、调整超时时间等。我来举个实际例子。有一次某系统接口平时耗时100毫秒某天突然飙到3秒开启慢日志后发现90%的耗时都集中在某个Excel导出函数里进一步排查发现这个函数调用了数百次file_get_contents每次都在拉远端资源。改成本地缓存后耗时降到200毫秒以内。没有慢日志这种问题定位起来如同大海捞针。慢日志的读取建议用这样一条命令grep -A 20 pool www /var/log/php/slow.log | tail -100-A后面的数字表示取匹配行之后的多少行把整个调用栈都带出来方便快速定位。定期观察慢日志的耗时分布如果出现大量超过5秒的条目就该考虑是不是代码逻辑有阻塞点、连接池满、或者底层依赖服务变慢。4.3 pm.status_path 与状态页解析FPM自带一个状态页功能平时根本没人注意但在扩容决策时比什么监控工具都直观。开启方式是在池配置里加一行pm.status_path /status然后在Nginx配置里单独把/status这个路径转发给FPM并限定来源IP避免公网直接暴露。比如location ~ ^/(status|ping)$ { fastcgi_pass unix:/run/php/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; allow 127.0.0.1; deny all; }访问http://你的服务器地址/status?full能看到一组非常有价值的实时指标accepted conn是累计接收的连接数listen queue是当前等待队列长度max listen queue是队列长度的历史峰值idle processes是空闲进程数active processes是活跃进程数total processes是总进程数。最有参考价值的两个数字是listen queue和max listen queue。如果listen queue经常维持在一个较大的数值说明Worker数量不够请求在排队如果max listen queue已经接近listen.backlog的设定值说明队列曾被打满过业务出现了请求丢失风险这时候必须扩容或优化单请求耗时。这个状态页还有一个隐藏用途配合定时抓取能画出一整天的FPM负载曲线未来的扩容依据就有了数据支撑。尤其要强调的是很多云服务器开通时的机型和内存配置选择都是根据这类性能指标确定而不是仅看流量多寡。5. 常见问题与排查实操5.1 502 Bad Gateway 排查502可能是所有人遇到最多的FPM错误但它的成因却五花八门很多人一个固定的答案应对所有502这是不对的。我建议按照以下顺序排查通常能很快定位。第一步看FPM进程到底活着没有。ps aux | grep php-fpm | grep -v grep | wc -l如果结果是0直接看FPM的错误日志多半是启动失败。启动失败的常见原因有配置语法错误、某个扩展加载不上、监听端口或套接字文件被占用。第二步如果进程活着就看FPM错误日志和Nginx错误日志。Nginx错误日志里能看到connect() failed (111: Connection refused)说明FPM没在监听或者监听端口被别的进程占用了如果是connect() failed (13: Permission denied)基本就是套接字文件权限问题。检查Nginx的运行用户和FPM套接字文件的属主属组是否一致。第三步如果所有日志都没有明显错误看FPM状态页。我发现一种场景进程还活着但所有Worker都卡在一个慢请求或者僵死请求上新请求排队排不进去Nginx连接被拒最终表现为502。这种情况下看看状态页的active processes是否长时间等于max_children如果是就是进程池被打满。提示动态模式下如果pm.max_children设置得太小那么实际进程数很容易打满上限。遇到这种情况别只想到扩容先想办法优化慢请求否则就算扩到200个Worker慢请求一样能全部塞满而且内存开销更大。5.2 504 Gateway Timeout504是“Nginx等待FPM响应超时”跟502完全两回事。502是连接建立失败504是连接建立了但对方迟迟不返回结果。通常先看Nginx的fastcgi_read_timeout默认一般是60秒如果业务接口确实需要跑更久比如报表导出、批量数据处理就要把这个超时调大。但注意Nginx的fastcgi_read_timeout调整之后PHP-FPM侧的request_terminate_timeout也必须同步否则FPM比Nginx先杀掉请求同样会导致504。504还有一种隐蔽成因MySQL连接打满业务代码等待数据库连接时一直不返回。这时PHP-FPM侧看什么日志都正常没有报错就是请求一直卡在数据库连接等待上。排查方式可以打开慢日志看看调用栈是不是长时间停在某个数据库驱动函数上如果是多半要查数据库连接数、慢查询和死锁。还有一类场景常出现在API接口的并发控制上业务代码里有锁机制比如用户级互斥锁、队列锁某个请求持锁时间过长其他请求全部排队等锁。它们等锁的过程中FPM Worker全部占着不干活表现为“活跃进程数高但CPU占用很低”。遇到这类问题优先考虑锁粒度是否过大或者引入异步任务机制。5.3 CPU飙高、内存溢出、FPM假死CPU飙高先别急着怪FPM。top进去看一下是php-fpm进程占CPU还是MySQL进程占CPU。如果是php-fpm占CPU高排查方向主要是某个函数有无限循环或死循环、正则表达式对超长文本灾难性回溯、图片处理函数的并发过大。如果只有个别Worker进程CPU高那是业务代码问题如果所有Worker都高那可能是攻击流量或者压测流量打进来了。内存溢出常见表现是系统可用内存持续下降直到触发OOM Killer某个php-fpm进程被杀然后Nginx间歇性报502。问题根源往往是php_admin_value[memory_limit]设得太大或者某个脚本申请了超大数组、超大字符串。优化方法有三层第一层是检查业务代码里有没有一次性加载全表数据、一次性拼接超大字符串这类行为第二层是调小memory_limit让进程早死早超生不要拖累整机第三层是配合pm.max_requests定期回收进程。FPM假死比崩溃更难搞。表象是状态页能打开、Nginx也能连上FPM但所有请求都不响应。最常见的原因是所有Worker都在等一个永远等不到的IO比如远程服务器不返回、MySQL死锁、某个服务连接数打满。这种问题排查有个实用技巧在Shell里执行kill -USR1 php-fpm主进程PID让它把当前所有Worker的执行栈写进日志然后看日志里每个Worker卡在哪一行代码上基本能揪出共同等待的那个资源。5.4 常见误区与配置清单针对长期调FPM踩坑的经验我整理出了几个几乎可以称为“普遍雷区”的误判在这里逐一说明。第一个误区把pm.max_children当成唯一需要调的参数其他参数统统默认。这样做的后果是你在动态模式下调整了进程数上限但因为pm.max_spare_servers默认值太小进程池根本无法扩张到你设置的上限瓶颈依然存在你却以为是配置没生效。第二个误区pm.max_requests从来不设。有些人担心进程经常重启会影响性能实际完全不用担心FPM的重建是平滑接力式的旧进程处理完当前请求后退出新进程已经顶上不会出现服务中断。反而是不设max_requests内存泄漏迟早把机器拖垮。第三个误区所有站点共用一个池不做隔离。前面已经说过一个池里某个站点的慢请求能把整台机器拖到停机状态而多个池的方案隔离的是故障边界这是保命手段而不是什么优化技巧。我也梳理了一份简化版的上线前配置自查清单检查项推荐做法进程模式依据业务流量选择核心业务推荐dynamicmax_children按实测RSS和可用内存计算预留20%缓冲max_requests1000~5000之间按业务稳定性调整request_terminate_timeout与Nginx fastcgi读超时配合从30秒起步listen方式同机用Unix套接字跨机才用TCP慢日志开启并设5秒阈值定期读调用栈状态页开启仅限内网访问用于扩缩容决策多站点隔离一站点一池按业务特征独立配置这套清单不一定保证你一次就调出“完美”配置但能确保你不在基础层面犯低级错误。配置完成后建议按照业务峰值压力的1.2倍做一次压测观察状态页的队列长度、平均内存、CPU占用这三个指标是否都处于合理区间。如果压测期间出现502或504优先回看排查记录再调参数。写在最后的一点经验这几年调过的FPM进程池各种模式都反复验证过我最大的体会是配置本身不是目标让请求在一个可控的延迟范围内稳定完成才是目标所以数值要随业务和代码质量动态调整。任何一套配置模板在换业务、换框架、换流量场景后都必须重新校验否则当时调好的参数会变成后面出故障的定时炸弹。如果你刚开始优化PHP-FPM别急着上复杂方案先把pm.max_children、pm.max_requests、request_slowlog_timeout这三个参数配好跑一周观察状态页和慢日志再决定下一步改哪里。如果遇到某个参数调了以后没有明显效果先回过头看看是不是别的参数把它的作用空间给限制了。进程管理这种联动系统很多问题不是单点参数能解释的。最后再分享一个习惯每次调整完FPM配置我都会在运维记录里留一笔把当时的业务状态、压测数据、调整原因写清楚几个月后回看这些记录比任何理论文章都更能帮自己形成对FPM的判断力。这套配置优化过程本身其实就是一趟很值得记录的成长路线。
阅读完成 · 觉得有帮助?