Nginx 与 PHP-FPM 性能调优实战指南
先铺开观测点定位瓶颈,再依次调整 Nginx 连接层、PHP-FPM 进程池与 OPcache,最后用压测和慢日志验证回归,并保证超时链层层递进、保持一致。
- 调优前先开启 stub_status、pm.status_path 与慢日志定位瓶颈
- worker_connections 是单个 worker 的连接上限,反向代理场景的实际并发要减半
- max_children 按可用内存除以单进程常驻内存估算,8GB 机器约取 100
- request_terminate_timeout 必须小于 fastcgi_read_timeout,避免僵尸请求
- OPcache 关掉 validate_timestamps 后需发布即 reload,并查 get_status 看命
为什么现在还要认真调 Nginx 与 PHP-FPM#
2026 年的开发节奏和几年前完全不同:AI 辅助编码让功能上线的速度快了一个量级,但运行时的瓶颈不会因为代码写得快就消失。很多线上事故并不是业务逻辑写错,而是默认配置在流量上来的那一刻暴露了问题——PHP-FPM 的子进程池被打满,Nginx 拿着 60 秒的 FastCGI 读超时一直等,用户端看到的就是 502 和 504。
近期社区里出现了不少实时对抗、在线观战类的应用(比如实时 PvP 编程对战平台),这类产品对流量的突发性极其敏感:一个房间开赛,几十上百个连接同时涌入。传统 PHP 业务虽然没有那么极端,但秒杀、活动页、定时任务触发的批量请求,本质上是同一类问题。这篇文章按“观测 → 连接层 → 进程池 → 执行缓存 → 压测回归”的顺序,给出一套可以直接抄到生产环境的调优路径。
第一步:先定位瓶颈,再动配置#
调优最常见的错误是凭感觉改参数。Nginx 和 PHP-FPM 之间的调用链只有一层,但影响性能的层次有三层:接入层(Nginx 的 worker 与连接数)、执行层(PHP-FPM 的进程池)、数据层(数据库、Redis、外部接口)。动手之前先把观测点铺好:
- Nginx 状态:开启
stub_status,重点看 Active connections 与 Reading/Writing/Waiting 的比例。 - PHP-FPM 状态:开启
pm.status_path,观察active processes与listen queue。listen queue 长期大于 0,说明进程池不够或单个请求太慢。 - 慢日志:
request_slowlog_timeout打出来的 PHP 调用栈,是定位“哪个函数把进程占住了”的最直接证据。 - 系统层:
ss -lnt看监听队列的 Recv-Q 是否堆积,pidstat看 CPU 是否被用户态打满,iostat排查磁盘 IO。
这套观测方法对任何 HTTP 服务都适用。平时做接口联调时,可以拿站内的 IP 查询接口 这类轻量接口作为基准目标,先摸清自己机器在无业务逻辑干扰下的极限 QPS(Queries Per Second,每秒请求数),再逐步叠加真实业务,对比才有意义。更多关于请求构造与排查思路的整理,可以翻一翻 API 文章中心。
Nginx 侧:worker 数量、连接数与内核协同#
Nginx 的默认配置是“能跑起来”,不是“跑得好”。下面这份是通用性较强的基础版:
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 8192;
multi_accept on;
use epoll;
}
http {
keepalive_timeout 30;
keepalive_requests 1000;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 10s;
}
几个容易理解错的点:
worker_processes auto会按 CPU 核数设置。如果 PHP-FPM 和 Nginx 同机部署,可以把 Nginx 的 worker 数设为核数的一半左右,把 CPU 留给 PHP;如果 Nginx 单独做接入层,用 auto 即可。worker_connections是单个 worker 能同时处理的最大连接数,总能力约为 worker 数乘以该值。注意反向代理场景下,一个客户端请求会额外占用一个到上游的连接,实际可服务的并发客户端数要打对折。send_timeout、client_body_timeout这类参数不要留默认的 60s,慢客户端会长期占住连接,白白消耗 worker 配额。- 系统层的
net.core.somaxconn和fs.file-max要与worker_rlimit_nofile对齐,否则连接数上限会被内核提前截断。
PHP-FPM 进程池:pm 策略与 max_children 的算法#
进程池是整套链路里最容易成为瓶颈的一环。pm.max_children 不能拍脑袋,用一个简单的容量公式:先看单个 PHP 进程的常驻内存(ps -o rss 对某个 worker 取样,典型值是 30 到 80MB,取决于加载的扩展),再算可用内存。
max_children ≈(总内存 - 系统与数据库预留 - 文件缓存预留)/ 单进程常驻内存
例如 8GB 内存的机器,留 2GB 给系统和数据库,单进程 50MB,则理论上限约 120,实际取 100 左右更稳妥。配置示例:
; /etc/php-fpm.d/www.conf
pm = dynamic
pm.max_children = 100
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 800
request_terminate_timeout = 25s
request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/slow.log
listen.backlog = 4096
| 模式 | 特点 | 适用场景 |
|---|---|---|
| static | 进程数固定,无 fork 开销 | 流量稳定、内存可预测的核心业务 |
| dynamic | 按空闲进程数伸缩 | 大多数 Web 业务,波峰波谷明显 |
| ondemand | 有请求才创建进程 | 低频后台服务、内存极度受限的容器 |
另外三个细节值得单独说:pm.max_requests 建议设 500 到 1000,让 worker 定期回收,避免第三方扩展的内存泄漏累积;request_terminate_timeout 必须小于 Nginx 侧的 fastcgi_read_timeout,否则 Nginx 已经放弃等待,PHP 进程还在跑,形成“僵尸请求”持续占用池子;监听方式上,同机部署用 Unix socket 通常略快于 TCP,但在容器或跨机场景下 TCP 更可控,此时 listen.backlog 要与内核 somaxconn 匹配。
FastCGI 层:缓冲、超时与连接复用#
请求从 Nginx 交到 PHP 的过程也有可调空间。开启缓冲可以让 Nginx 先收下 FastCGI 响应再发给客户端,避免慢客户端拖住 PHP 进程;超时参数则决定了错误以多快的速度暴露出来。
location ~ [.]php$ {
fastcgi_pass unix:/run/php-fpm/www.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
fastcgi_busy_buffers_size 64k;
fastcgi_connect_timeout 3s;
fastcgi_send_timeout 20s;
fastcgi_read_timeout 20s;
fastcgi_keep_conn on;
fastcgi_next_upstream error timeout http_503;
}
如果响应体普遍偏大(比如导出文件、图片代理),fastcgi_buffers 的数量要相应提高,否则多余的响应会落盘到临时目录,把磁盘 IO 变成新的瓶颈。反过来,如果大部分接口返回 JSON,缓冲区保持小一点更节省内存。测试大响应体时可以借助 随机动漫壁纸接口 这类返回图片的场景,观察临时文件写入与响应耗时是否出现异常抖动。
OPcache 与真实执行效率#
PHP 调优里性价比最高的一步永远是 OPcache。生产环境建议这样配置:
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=0
realpath_cache_size=4096K
realpath_cache_ttl=600
validate_timestamps=0 表示不再检查文件变更,代价是每次发布代码后必须 reload PHP-FPM。这是标准做法,把它写进发布脚本即可。max_accelerated_files 如果设得太小,缓存会被频繁淘汰,命中率掉下来之后性能比没开还糟,用 opcache_get_status() 查看 cache_full 和 oom_restarts 是否为 0。realpath_cache_size 对使用大量 include 的框架影响明显,默认 16K 基本不够。
至于 JIT(Just-In-Time,即时编译),在典型的 IO 密集 Web 请求里收益非常有限,不要指望它解决架构层面的问题;如果业务里有大量纯数学计算,再针对性开启。
压测与慢日志形成闭环#
改完配置一定要回归验证。慢日志先开起来,阈值设得比平均响应时间高两三倍:
request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/slow.log
压测用 wrk 就够,重点看长尾延迟而不是只看平均 QPS(Queries Per Second,每秒请求数):
wrk -t8 -c400 -d30s --latency http://127.0.0.1/api/bench
wrk -t8 -c400 -d30s -s post.lua http://127.0.0.1/api/submit
压测目标可以是自己的业务接口,也可以先用站内的公共接口打底,比如用 POST 示例接口 验证带请求体场景下的 Nginx 缓冲与超时表现。压测过程中同时盯三个数字:Nginx 的 Active connections、FPM 的 listen queue、系统的 load。如果 listen queue 持续增长而 CPU 没打满,多半是下游数据库或外部 HTTP 调用在拖时间,此时加 max_children 只是把问题延后。生产环境的部署细节或联调问题,也可以通过 技术支持 进一步沟通。
常见故障对照表#
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| 502 Bad Gateway | FPM 进程池打满或 socket 不可用 | 调大 max_children,检查 listen 与权限 |
| 504 Gateway Timeout | fastcgi_read_timeout 小于实际执行时间 | 优化慢查询,统一超时链,避免无脑调大 |
| 响应时快时慢 | 动态进程池频繁 fork,或缓存被淘汰 | 改用 static 或调大 min_spare_servers |
| CPU 高但 QPS(Queries Per Second,每秒请求数) 低 | OPcache 未生效或命中率低 | 检查缓存容量与 validate_timestamps |
总结#
Nginx 与 PHP-FPM 的调优并不神秘,关键顺序是:先把观测点铺好,再从连接层(worker、keepalive、内核参数)到进程池(max_children、超时链、max_requests),最后到执行缓存(OPcache、realpath cache),每一步都用压测数据验证,而不是一次性把所有参数改满。超时链的一致性尤其重要,request_terminate_timeout、fastcgi_read_timeout 与业务自身的超时设置必须形成递进关系,否则故障现场会非常难排查。
调优之外,选一个稳定的接口来源同样能省下不少时间。如果你正在搭建监控探针、压测基准或业务里的辅助数据接口,可以在 API.PLUS 注册获取免费 API Key,直接调用 IP 查询、随机图片等现成接口,把精力集中在自己的核心链路上。