Nginx 与 PHP-FPM 调优:先建容量模型,再改参数
Nginx 与 PHP-FPM 调优的起点不是参数表,而是判断请求在哪一层排队。本文说明连接数与进程池各自的容量约束、FastCGI 连接复用与缓冲的作用,并给出一个有先后顺序的改动清单;所有配置仅为示意,未在真实环境执行。
- 调参前先分层定位:连接层、进程池、PHP 执行、下游依赖,四类瓶颈的处置方式不同。
- PHP-FPM 的 pm.max_children 受内存约束,不是越大越好;Nginx 的 worker_connec
- 开启 FastCGI 长连接与 slowlog 的收益通常先于调大缓冲区。
- 本文配置为示意,未执行,默认值与版本行为请以所用版本文档为准。
Nginx 与 PHP-FPM 的性能问题,多数不是"某个参数没调对",而是两层之间的容量模型没有对齐:Nginx 按连接数接纳请求,PHP-FPM 按进程数执行请求,两边额度不匹配时,排队就会从一个位置挪到另一个位置。所以调优的起点是定位瓶颈层,而不是抄一份参数表。
下面涉及参数语义的部分属于通用说明;具体默认值、版本差异与上限行为,请以所用 Nginx 与 PHP-FPM 版本文档为准,本次未联网核验。
先判断请求停在哪一层#
同一组现象可能对应完全不同的原因,先分层再动手:
- 连接层:Nginx 无法接纳更多连接,表现为新连接被拒或握手变慢。关注 worker 进程数与每个 worker 的连接额度。
- 进程池层:请求已到 PHP-FPM,但没有空闲进程执行,表现为请求在队列里等待、耗时整体被拉长。
- PHP 执行层:进程拿到了,但单个请求执行很久。看慢日志与分段耗时。
- 下游依赖:数据库、缓存、外部接口慢。这一层无论怎么调 Nginx 都不会变快。
区分方法是看耗时分布的形状:整体等比例变长更像排队,少数请求极慢更像执行层或依赖层(这是推断性的判断方法,不是实测结论)。
Nginx 侧:连接额度要留出上游份额#
worker_connections 是单个 worker 进程可同时打开的最大连接数。关键点在于:与上游(这里是 PHP-FPM)的连接通常也占用这个额度。若请求路径是"客户端 → Nginx → PHP-FPM",一个正在处理的请求会同时占用一条客户端连接和一条上游连接。只按客户端数量估算额度,会在压力上来时提前耗尽。
因此 worker_processes 与 worker_connections 要一起看,并与 PHP-FPM 能同时处理的请求数大致对齐。上游侧超出 PHP-FPM 处理能力的部分,留在 Nginx 里排队并不会更快。
FastCGI 连接复用
默认情况下 Nginx 与 PHP-FPM 之间往往是短连接,每个请求都要建立一次连接。若使用 TCP,这会带来握手开销;使用 Unix socket 时开销较小,但仍非零。在 FPM 侧开启连接保持、并在 Nginx 侧对应配置后,连接可以复用:
# 示意配置,未执行;参数名与可用性请对照所用版本文档
upstream php_fpm {
server unix:/run/php-fpm.sock;
keepalive 32; # 保留的空闲上游连接数
}
location ~ \.php$ {
fastcgi_keep_conn on;
fastcgi_pass php_fpm;
include fastcgi_params;
}
注意 keepalive 的值是保留的空闲连接数,不等于并发上限;设得比实际并发大很多并不会带来额外收益。
缓冲与响应体积
fastcgi_buffer_size、fastcgi_buffers 决定响应头与响应体的缓冲能力。响应体超过缓冲总额时会被写入临时文件,这在返回大 JSON 或文件时可能明显增加磁盘 I/O。判断依据是响应体积的实际分布,而不是凭感觉调大——把缓冲调到远超实际响应大小,只会增加每个连接的内存占用。
PHP-FPM 侧:进程池是一个内存预算问题#
PHP-FPM 的进程池有三种调度模式:static 固定进程数、dynamic 按空闲情况伸缩、ondemand 按需创建。pm.max_children 是上限,也是最容易被盲目调大的参数。
它的真实约束来自内存:每个 PHP 进程常驻内存乘以进程数,是这台机器必须付得起的账。可用的估算方法是:用单进程常驻内存(可从进程监控或 FPM 状态页获得)乘以预期的 max_children,再加上 Nginx、系统缓存与其他服务的占用,与可用内存比较。这是估算方法,具体数值需在目标环境测量,本文不提供任何实测数字。
; 示意配置,未执行
[www]
pm = dynamic
pm.max_children = 40 ; 由内存预算反推,而非照抄
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 12
pm.max_requests = 500 ; 让进程周期性退出,回收缓慢增长的内存
request_terminate_timeout = 30s
request_slowlog_timeout = 2s
slowlog = /var/log/php-fpm/slow.log
几个容易忽略的约束:
pm.start_servers、pm.min_spare_servers、pm.max_spare_servers必须落在max_children之内,否则配置不生效或无法启动。pm.max_requests是缓解内存缓慢增长的常用手段,代价是进程重建;设为 0 表示不回收。- 慢日志比任何参数猜测都更有信息量:它直接给出哪些请求慢、慢在调用的哪一段。
request_terminate_timeout与 Nginx 的 FastCGI 超时要放在一起看,两者的先后顺序决定客户端最终看到的是超时还是 502。
参数之间的约束关系#
| 参数 | 作用 | 与谁相互约束 |
|---|---|---|
worker_connections | 单 worker 的连接上限 | 客户端连接 + 上游连接的总和 |
keepalive(upstream) | 空闲上游连接保留数 | 实际并发量与 FPM 空闲进程数 |
pm.max_children | FPM 并发执行上限 | 可用内存、Nginx 侧排队策略 |
fastcgi_buffers | 响应体缓冲 | 实际响应体积分布 |
这张表想说明的是:这些参数不是各自独立的最优值,改一个就会改变另一个的实际含义。
一个有顺序的改动清单#
- 先开慢日志与 FPM 状态页,拿到耗时分布与进程池占用情况。
- 确认瓶颈层:连接、进程池、执行还是依赖。
- 若在进程池,按内存预算定
max_children,再让 min/max spare 落在其内。 - 若连接层紧张,先核对 worker 数与连接额度是否为上游留了份额,再考虑调大。
- 开启 FastCGI 长连接,观察是否减少了连接建立开销。
- 最后才调整缓冲区,且以实际响应体积分布为依据。
边界与不确定性#
本文给出的是一套判断顺序与参数之间的约束关系,不构成任何环境下的推荐数值。默认值、参数是否仍受支持、以及长连接在特定 FPM 配置下的行为,都随版本变化,需以实际部署版本的文档与目标环境测量为准。此外,当瓶颈位于应用代码或下游依赖时,调整 Nginx 与 PHP-FPM 参数通常只能改变排队位置,不会改变总处理能力。