PHP-FPM 深度调优指南 告别 502 错误,让你的 PHP 应用飞起来

PHP-FPM 深度调优指南 告别 502 错误,让你的 PHP 应用飞起来

理解 PHP-FPM 请求流程、进程池大小调整,以及防止超时和 502 错误的关键设置 --- 实用规则、实际案例和可直接使用的检查清单。

大多数 PHP 应用出问题,不是因为 Nginx,而是 PHP-FPM(FastCGI 进程管理器)没配好或者进程数设置有问题。结果就是:p95 延迟突然飙高、502 错误随机出现,还有你看不到的请求排队。这篇文章会告诉你请求在 FPM 里是怎么跑的、进程池怎么工作,以及哪些配置能让应用不卡顿 --- 这样流量突然上来时,你的应用还能正常响应。

原文链接-PHP-FPM 深度调优指南 告别 502 错误,让你的 PHP 应用飞起来

你将学到什么

  • Web 请求怎么到 PHP-FPM,又是怎么分配到具体的进程池和工作进程的
  • 控制吞吐量、队列和内存的几个关键进程池配置
  • 怎么根据内存而不是瞎猜来设置 pm.max_children 的靠谱方法
  • 怎么开启和查看 FPM 的状态、慢日志和 ping 来做真正的问题诊断
  • 一个有真实数据的小调优案例,还有马上就能用的调优清单

背景知识与定义

CGI(通用网关接口):旧的模型,Web 服务器为每个请求生成一个进程。简单但很慢。

FastCGI:Web 服务器和应用管理器之间的长连接协议。服务器复用连接,不用每次都新建进程。

PHP-FPM(FastCGI 进程管理器):管理 PHP 工作进程池的后台程序。每个进程池有自己的套接字、用户和配置。Nginx/Apache 把请求扔给套接字,FPM 负责分配给空闲的工作进程,或者排队等待。

进程池(Pool):一组共享配置的工作进程,通常按应用、租户或权限来划分。进程池能隔离故障和资源占用。

工作进程(Worker/Child):执行 PHP 脚本的解释器进程,一次只能处理一个请求。子进程内部不支持并发,每个请求都得有个空闲的子进程来处理。

OPcache:把编译好的 PHP 字节码缓存在共享内存里。每个工作进程都能用,大幅降低每个请求的 CPU 开销。

请求如何在 PHP-FPM 中流转

  1. 客户端 → Web 服务器 。浏览器访问 https://example.com/index.php
  2. 位置匹配 。Nginx 匹配到 .php 文件,转发给 FastCGI,比如:fastcgi_pass unix:/run/php/php-fpm.sock;
  3. 套接字接收。内核把连接放到套接字的 backlog 队列里,FPM 主进程接收
  4. 进程池分发。FPM 根据套接字选择进程池(比如 www),把请求分给空闲的工作进程
  5. 工作进程执行。子进程跑你的 PHP 脚本,用上 OPcache 和各种扩展
  6. 返回响应。工作进程把结果给 Nginx,Nginx 发给客户端,工作进程又变成空闲状态

容易卡住的地方:

  • 没有空闲工作进程 → 请求只能在进程池队列里排队
  • Backlog 满了 → Nginx 拿到 502/504 错误,或者客户端直接超时
  • 工作进程被 I/O 卡住 → CPU 闲着,但是延迟飙升

反常识的规则:按内存而不是 CPU 核数来设置

大多数人设置 pm.max_children 时会想"我们有几个核?"这样想是错的。每个 PHP 工作进程可能要吃掉几十甚至几百 MB 内存,具体看你的代码、扩展和 OPcache。如果开太多工作进程,内核开始换页或者 OOM killer 直接干掉进程 --- 不管哪种情况,延迟都会爆炸。

正确的做法:

  1. 在有负载的情况下测量每个工作进程实际占用的内存
  2. 给进程池划分内存预算
  3. 算出 max_children = floor(进程池内存预算 / 每个子进程RSS)
  4. 然后在真实流量下用 pm.status_path 验证效果

重要的 PHP-FPM 设置(及其原因)

进程管理

  • pm :大部分应用用 dynamic;特别稳定的负载用 static;流量不稳定、访问量小的机器用 ondemand,这时候省内存比避免冷启动更重要
  • pm.max_children:每个进程池能同时处理多少请求的硬上限。这个设对了,决定你是排队等待还是直接挂掉
  • pm.start_servers, pm.min_spare_servers, pm.max_spare_servers:dynamic 模式下,控制保留多少空闲工作进程来应对流量突增
  • pm.process_idle_timeout:什么时候回收空闲工作进程(dynamic/ondemand)。设高点避免正常流量时频繁重启,设低点在流量低谷时释放内存
  • pm.max_requests:处理 N 个请求后重启工作进程,防止内存泄漏。一般设 500-5,000,看你的代码泄漏情况和启动开销

请求生命周期和保护机制

  • request_terminate_timeout:请求最长能跑多久。强制杀掉跑飞的脚本
  • request_slowlog_timeout + slowlog:记录慢请求的调用栈,找出哪里卡住了特别有用
  • rlimit_files 和 rlimit_core:提高文件句柄限制,需要调试的话允许生成 core dump

套接字和 backlog

  • listen:本地 Nginx ↔ FPM 用 Unix 套接字(性能更好)
  • listen.backlog:套接字队列深度。突发流量把队列撑爆了就调大点
  • listen.owner, listen.group, listen.mode:保证 Nginx 能正常访问套接字,不然就 502 了

健康检查和监控

  • pm.status_path:暴露进程池状态(活跃数、空闲数、队列长度、总连接数)
  • ping.path / ping.response:给负载均衡器用的简单健康检查接口

OPcache(相关但很重要)

  • opcache.memory_consumption:字节码共享内存大小。满了的话,缓存命中就变成未命中了
  • opcache.max_accelerated_files 和 revalidate_freq:保证有足够的文件槽位,验证频率也要合理,不然部署时会抖动
  • opcache.preload(PHP 7.4+):框架支持的话,预加载类文件能减少冷启动时间

简单可靠的进程池配置(带注释)

拿这个当模板,根据你的系统调整路径和名称。

ini 复制代码
; /etc/php-fpm.d/app.conf
[app]
user = www-data
group = www-data
listen = /run/php/php-fpm-app.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
listen.backlog = 1024

pm = dynamic
pm.max_children = 32          ; 按内存算出来的 - 看下面的案例
pm.start_servers = 8
pm.min_spare_servers = 8
pm.max_spare_servers = 16
pm.process_idle_timeout = 20s
pm.max_requests = 2000

; 保护机制
request_terminate_timeout = 30s
request_slowlog_timeout = 2s
slowlog = /var/log/php-fpm/app.slow.log

; 健康检查
pm.status_path = /status
ping.path = /ping
ping.response = pong

在 Nginx 里加个 location 来暴露状态(注意控制访问权限):

nginx 复制代码
location ~ ^/(status|ping)$ {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php-fpm-app.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

实际案例:用真实数据来算内存配置

场景:8 GB 内存的虚拟机跑 Laravel 应用。机器上还有 Nginx、Redis 和系统服务,大概要用掉 2 GB。剩下 6 GB 给 PHP-FPM(app 进程池)。

1. 测量每个子进程内存。正常负载下,抽样 50 个工作进程:RSS 在 90-150 MB 之间,中位数 120 MB。

2. 算出 pm.max_childrenfloor(6,000 MB / 120 MB) = 50。保险起见,设成 pm.max_children = 44,给突发流量和 OPcache 碎片留点余量。

3. 应对突发流量。用 dynamic 模式:

ini 复制代码
pm.start_servers = 12
pm.min_spare_servers = 12
pm.max_spare_servers = 24

这样保持 12-24 个空闲工作进程,短时间的流量突增不用排队。

4. 防止内存泄漏 。设置 pm.max_requests = 1500。观察 RSS 变化,如果工作进程一天内内存涨了 30% 以上,就调到 1000。

5. 验证效果

  • 稳定流量:600 RPS 静态文件 + 30 RPS PHP 请求,CPU 占用 ~30%,队列长度 0,p95 延迟 ~120 ms
  • 突发流量:60 秒内 120 RPS PHP 请求,活跃进程数稳定在 40 左右,队列长度 ≤ 5,没有 502 错误

如果队列长度 > pm.max_children/4 持续超过一分钟,要么加大 max_children(内存够的话),要么优化慢接口。

为什么优先考虑内存? 就算你有 16 个 CPU 核心,60 个 PHP 工作进程每个占 150 MB + 共享内存,也可能导致系统抖动。OOM killer 一旦出手,整个分钟的 p95 延迟都完蛋。用内存来控制并发数,避免这种悬崖式的性能下降。

逐步调优现有进程池

开启状态监控和慢日志

  • 启用 pm.status_path = /status,限制只能 localhost 或内网访问
  • 开启 request_slowlog_timeout = 2s,slowlog 路径要能写入
  • 加上 /ping 给健康检查用

常用检查命令:

bash 复制代码
curl -s http://127.0.0.1/status?full  # 查看空闲、活跃、最大子进程数等
tail -f /var/log/php-fpm/app.slow.log  # 负载测试时实时看慢日志

测量每个子进程内存

在真实负载下,用 ps -o pid,rss,cmd -C php-fpmsmem -P php-fpm 采样。

算出中位数 RSS。忽略那些在做文件上传或生成大报告的异常值。

设置 pm 和并发数

  • API 和 Web 应用首选 pm = dynamic;定时任务多、流量小的机器用 ondemand;只有流量特别稳定且内存摸得很清楚时才用 static
  • 按前面说的方法从内存预算算出 pm.max_children,比理论最大值低 10-20% 开始
  • 调整 start_servers 和备用进程数,让平时的空闲进程数保持在备用范围的下半部分

设置合理的超时

  • request_terminate_timeout 要和你的实际 SLO 匹配。客户端 30 秒就超时了,别让 PHP 跑 5 分钟
  • Nginx 里的 fastcgi_read_timeout 要和 PHP 的上限对齐(稍微高一点),避免提前返回 504

调整 backlog(但别掩盖问题)

  • 设置 listen.backlog = 1024 来应对短时间的连接突发
  • 如果持续负载下 backlog 一直不为零,这不是调参能解决的 --- 要么加大 max_children,要么优化代码

负载测试验证

  • 先预热 OPcache
  • 2-5 分钟内逐步提升到预期的峰值 RPS
  • 观察状态:空闲 vs 活跃进程数、队列长度、是否触达最大子进程数
  • 确认 p95 延迟在预期范围内。不行的话,要么优化单个请求的处理时间,要么加内存 → 加子进程

常见陷阱和避免方法

  • 按 CPU 核数而不是内存来设置。结果就是内存不够用,开始换页或者直接 502。应该按内存算,然后确认 CPU 够用
  • 所有功能用一个大进程池。慢的后台任务会拖死前台用户请求。按功能或权限分开进程池
  • 流量大的站点用 pm.ondemand。冷启动会导致延迟忽高忽低。用 dynamic 模式,保持一些预热的备用进程
  • 不管 OPcache 是否满了 。缓存满了就要重新编译,性能直线下降。监控缓存使用率,快满了就加大 opcache.memory_consumption
  • pm.max_requests 设太小。每 50 个请求就重启一次进程会很抖动,除非确实有内存泄漏,否则设大点
  • 不开慢日志。没有调用栈信息,I/O 慢的问题会被误认为是 FPM 的锅。开启慢日志,修复真正的问题
  • 超时时间不匹配。PHP 30 秒就杀进程了,Nginx 还在等 300 秒,或者反过来,都会产生莫名其妙的错误。把它们对齐

什么时候不该用 PHP-FPM

  • 长时间运行的任务(生成报告、数据导入、视频处理):放到队列/后台进程(CLI)里跑,接口直接返回 202 + 轮询/webhook
  • 流式传输或 WebSocket:交给专门的服务处理,或者用支持长连接的 PHP 运行时
  • 超高并发的轻量级请求:考虑其他方案(比如 RoadRunner、Swoole),它们用不同的 I/O 模型在持久进程里跑 PHP。迁移前一定要充分测试

工具和检查清单

快速调优检查清单

  • 状态和 ping 接口已开启且访问受限
  • 慢日志已开启,API 阈值 1-2 秒,SSR 页面可以设高点
  • pm = dynamicpm.max_children 按内存算出来,备用进程保持基线的 10-50% 空闲
  • max_requests 设在 1,000-3,000,确认 24 小时内没有内存泄漏
  • request_terminate_timeout 符合 SLO 要求,Nginx 超时时间对齐
  • OPcache 大小在峰值时还有 >20% 剩余空间
  • Backlog ≥ 512,文件描述符限制远超峰值连接数
  • 前台 Web 和后台管理/定时任务/队列用独立进程池

测量循环

负载测试 → 查看 pm.status 队列和空闲数 → 调整 max_children → 再测试。

观察 p95 延迟和错误率,如果队列一直在增长,先通过慢日志和 APM 找出慢接口,优化后再考虑提高并发数。

关键要点

  • PHP-FPM 一个工作进程同时只能处理一个请求,并发数就等于子进程数
  • 按单个子进程内存占用 × 内存预算来算进程池大小,不要按 CPU 核数
  • 流量有突发的话,保持一些备用工作进程待命;忙站点用 dynamic 比 ondemand 好
  • PHP 和 Nginx 的超时时间要对齐,开慢日志看时间都花在哪了
  • 用状态监控和负载测试来验证效果,别瞎调参数
  • 不同功能用不同进程池,别让后台慢任务影响前台用户
  • OPcache 容量和命中率是影响性能的关键因素
  • 如果业务场景不适合 FPM 模型(长时间运行或流式传输),就改架构,别只是调配置

总结

PHP-FPM 不是什么黑盒子,它就是个小而精确的系统。按内存来算并发数,保持几个预热的备用进程,把超时时间对齐好,像盯着股票一样盯着 OPcache 和队列长度。把进程池当成预算来管理 --- 每个子进程都要吃内存 --- 别照搬其他技术栈的经验值。每次改代码或调基础设施后,跑个简单的负载测试,先看看状态和慢日志,再决定要不要买更多硬件。坚持这个循环,你的 p95 延迟就会一直很稳定 --- 就算流量突然变得很奇怪也不怕。