PHP-FPM配置怎么调优才能让你的网站不再“卡脖子”?
你有没有试过:明明服务器监控里CPU才30%、内存也还有空闲,但用户一多,页面就转圈,Nginx直接甩出502 Bad Gateway?别急着怀疑代码——大概率是PHP-FPM在“装死”,它没被你好好调教过。
进程管理模型:static、dynamic还是ondemand?
PHP-FPM有三种进程启动方式:static(固定数量)、dynamic(动态伸缩)、ondemand(有请求才拉起)。
static适合流量稳如老狗、且对延迟极其敏感的服务,比如内部API网关。但它会一直占着内存,哪怕一整天就来了3个请求。dynamic是大多数人的默认选择:设一个最小进程数保底,再给个上限防爆,中间靠负载自动增减。省心,也够用。ondemand则像打车软件——没人叫车,司机就歇着;一单来了,立刻派车。特别适合企业官网、博客这类“常年在线、偶尔热闹”的站点。
我之前接手一个政府单位的展示站,日均访问不到200,结果配置里写着 pm = dynamic,pm.start_servers = 20。你猜怎么着?每天凌晨三点,二十多个php-fpm进程齐刷刷蹲在内存里“值班”,啥也不干。改成 ondemand 后,pm.max_children = 20,pm.process_idle_timeout = 10s,内存占用直接掉了一半,访问速度反而更稳了——毕竟空转的进程,不产生价值,只吃资源。
关键参数:pm.max_children 到底设多少才合理?
这个值不是拍脑袋定的,它直接决定你能同时处理几个PHP请求。设小了,用户排队等;设大了,内存告急,系统开始杀进程。
最实在的算法就一条:pm.max_children ≈ (可用内存 ÷ 单个PHP进程平均内存)
怎么知道单个进程吃多少内存?别猜,实测:
ps --no-headers -o "rss,cmd" -C php-fpm | awk '{ sum += $1 } END { printf "%.0fMB\n", sum/NR/1024 }'
然后留足余量:系统本身要吃点内存,MySQL、Redis、Nginx也得吃饭。8G机器别把6G全分给PHP,留1.5~2G缓冲很必要。
我优化过一个本地生活类站点,老板说“服务器挺新,8G内存随便用”。结果一查,单个php-fpm进程平均占75MB,他却设了max_children = 100……内存天天飙到95%,swap狂抖。我把值压到65,配合pm = dynamic和合理的min/max,502错误明显减少,后台任务也不再莫名中断。
请求处理与超时:如何防止“慢请求”拖死整个池子?
一个请求卡住3分钟,不是它一个人的事——它占着一个worker,后面几十个请求全得排队等着。这种“雪球效应”,比并发高更致命。
关键就两个参数:
request_terminate_timeout:超时就砍,别惯着。设30秒不难,但得比你95%的正常接口耗时略长一点。别设成5秒,否则支付回调、导出Excel这类合理长耗时操作全被误杀。request_slowlog_timeout:不终止,先记账。设成10秒,慢日志里立刻能看到是哪个URL、哪行代码在拖后腿。
真实例子:某社区网站每逢发帖高峰就挂,查日志全是502。开了慢日志才发现,用户头像上传后调用第三方图床接口,对方偶尔卡顿到90秒。加上request_terminate_timeout = 30s后,异常请求被快速释放,其他用户照常发帖;我们再拿着慢日志找图床方优化,问题根除。这就像在每条流水线上装了个熔断器,坏一个零件,不影响整条线运转。
进程回收与状态检查:怎样保持进程池健康?
PHP扩展或老旧代码有时会有轻微内存泄漏——单次看不出来,跑上几百个请求,进程RSS悄悄涨了20MB。时间一长,worker越跑越重,响应越来越慢。
pm.max_requests 就是你的“定期换班指令”:每个进程干满一定数量请求后,自动优雅退出,由master拉起新进程接替。
- 稳定项目可以设500~1000;
- 用了很多C扩展或已知有泄漏风险的,干脆设成200~500。
另一个实用功能是 pm.status_path。在配置里打开它(比如设为 /status),再配好Nginx转发,就能随时用浏览器打开 https://yoursite.com/status?full 查看实时状态:当前几个进程忙、几个闲、队列里压了多少请求、谁卡在哪儿……比翻日志快十倍。
有个老CMS站点,上线半年后发现内存缓慢爬升,重启FPM能缓两天。加了 pm.max_requests = 500 后,内存曲线变得平直,再也不用半夜起来手动reload了——进程自己知道什么时候该退休。
高级调优:这些容易被忽略的参数也影响性能
有些参数不起眼,但真到了流量尖峰,它们就是最后一道防线:
rlimit_files:控制PHP-FPM能打开多少文件句柄。默认可能只有1024,而一个HTTP请求+数据库连接+日志写入,轻松干掉上百个fd。建议设成65535或和系统ulimit -n一致。listen.backlog:当Nginx疯狂往FPM socket扔连接时,这个队列就是缓冲带。默认-1(依赖系统),高并发下容易丢连接。显式设成65535更稳妥。clear_env = no:别清空环境变量!很多扩展(比如APCu、OPcache)依赖PATH或LD_LIBRARY_PATH,清空了反而报错。
实战场景:某电商做周年庆预热,凌晨流量突增,部分用户提示“Connection refused”。排查发现listen.backlog还是-1,系统默认队列太小,瞬间涌入的连接被内核直接丢弃。补上listen.backlog = 65535并同步调大rlimit_files,第二天同样峰值下,连接全部接得住。
今天下班前就能执行的一个具体操作
别收藏吃灰,现在就动手改一个地方:重新算一遍你的 pm.max_children。
- 登录服务器,打开PHP-FPM主配置(通常是
/etc/php-fpm.d/www.conf或/usr/local/etc/php-fpm.d/www.conf); - 找到
pm =和pm.max_children =这两行,记下当前值; - 复制粘贴这条命令,回车运行:
ps --no-headers -o "rss,cmd" -C php-fpm | awk '{ sum += $1 } END { printf "%.0fMB\n", sum/NR/1024 }' - 算一笔账:假设服务器总内存8G,系统+MySQL+Nginx保守预留2.5G,剩下5.5G;测出来单进程占72MB,那么
5500 ÷ 72 ≈ 76,建议先设成70(留足缓冲); - 修改配置文件里的
pm.max_children值,保存; - 执行
sudo systemctl reload php-fpm—— 配置热更新,无需重启服务。
改完盯10分钟:tail -f /var/log/php-fpm/www-error.log 看有没有新报错,htop 观察内存是否平稳。这一步不做大手术,只拧紧一颗关键螺丝,但很可能就是你网站从“偶发卡顿”到“始终在线”的分水岭。