Nginx 与 PHP-FPM 调优实战:从瓶颈判定到超时顺序的可验证做法
调优应作为受控实验:先用 $upstream_response_time、FPM slowlog 与 listen queue 判定瓶颈在 Nginx、FPM 还是下游,再单变量改配置并回归验证。pm.max_children 由可用内存除以实测单进程 RSS 估算上限而非目标值;超时需让 PHP 层先于 FPM request_terminate_timeout 触发、Nginx fastcgi_read_timeout 兜底;连接复用要与进程数匹配。瓶颈在数据库时增加进程只会转移排队。
把 Nginx 与 PHP-FPM 调优当成“改几个参数”,通常得不到可复现的结果。更可靠的做法是把它当成一次受控实验:先固定观测口径,判断延迟发生在 Nginx、FPM 还是下游(数据库、外部接口);再单变量修改配置,用同一份负载做回归验证。本文只讲这个流程与几处参数耦合关系,不提供性能数字——本次没有可核验的测试记录,具体默认值和版本行为请对照所用版本的官方文档核实。
先确认瓶颈在哪一层#
- Nginx 侧:
$request_time与$upstream_response_time差距大,说明时间耗在 Nginx 自身或客户端链路;两者接近,说明时间耗在 FPM。 - FPM 侧:开启
slowlog与request_slowlog_timeout,可定位请求卡在哪个函数;pm.status_path可观察 listen queue 长度与 active/idle 进程分布。 - 系统侧:CPU、内存、磁盘 IO 与上下文切换,用来判断是否已到资源上限。
listen queue 持续非零,是进程不足的直接证据;但 CPU 已饱和时继续加进程,只会加重切换开销。这一步决定后面该调进程数,还是回头改代码和 SQL。
进程数:算上限,而不是抄数字#
pm.max_children 是硬上限,超过后请求进入 listen queue 排队。可用内存除以单个 worker 的常驻内存(RSS)可估算理论上限,但这不是目标值:还要给系统进程和其他服务留余量,且单进程 RSS 必须在真实负载下测量,空载值明显偏低。
pm = static 适合内存固定、流量平稳的场景,省去调度开销;pm = dynamic 适合波动较大的场景,但要同时设置 pm.max_requests,避免第三方扩展的内存泄漏持续累积。进程回收本身有成本,周期不宜设得过短。
超时是有先后顺序的#
超时配置最常见的问题是各层顺序颠倒,导致错误出现在不期望的层。一种通行约定是:让 PHP 层(max_execution_time)先于 FPM 的 request_terminate_timeout 触发,再由 Nginx 的 fastcgi_read_timeout 兜底。这样超时能以应用可捕获的形式暴露;若 FPM 先杀 worker,客户端往往只看到一个 504,排查线索更少。
另外要区分两个语义不同的参数:fastcgi_connect_timeout 是连上 FPM 的等待时间,fastcgi_read_timeout 是等待响应的时间,后者通常应显著大于前者。
连接复用要与进程数匹配#
# 未执行示例,仅示意参数关系,取值需按实际环境核实
upstream php_fpm {
server 127.0.0.1:9000;
keepalive 16;
}
server {
location ~ \.php$ {
fastcgi_pass php_fpm;
fastcgi_keep_conn on; # 需与 upstream 中的 keepalive 配合
include fastcgi_params;
}
}
连接复用减少每次请求的建连开销,但如果上游连接池超出 worker 的承受能力,收益会被排队抵消。上述指令的版本要求与 FPM 侧的配合方式待核实。
变更纪律#
- 一次只改一个参数,改动前后对比同一指标。
- 用固定脚本或压测工具复现同一负载,不要拿线上流量做对照。
- 改完先用
php-fpm -t做语法检查,再做重载;保留可回滚的旧配置。
如果瓶颈在慢查询或外部接口,调 FPM 进程数只是把排队从 FPM 转移到数据库——这是这类调优里最常见的误判。