Nginx 与 PHP-FPM 性能调优实战:定位、配置与压测
Nginx 与 PHP-FPM 变慢多数不是 Nginx 本身,而是 PHP-FPM 进程池打满与 fastcgi 超时、缓冲配置不匹配;先看状态页和慢日志定位瓶颈,再按内存反推 pm.max_children,配合 upstream keepalive 可显著提升吞吐。
- 先定位瓶颈再调参,盲目改 pm.max_children 会引发 OOM
- pm.max_children 由可用内存除以单进程常驻内存反推得出
- upstream keepalive 配合 fastcgi_keep_conn 可降低连接开销
- 读超时与 request_terminate_timeout 必须成对设置
结论先行:多数 Nginx + PHP-FPM 变慢并非 Nginx 本身的问题,而是 PHP-FPM 进程池打满、fastcgi 超时与缓冲配置不匹配。先定位瓶颈再改参数,吞吐量通常能提升数倍。
为什么 Nginx + PHP-FPM 在 2026 年仍然会突然变慢?#
过去两年,线上流量形态发生了一个明显变化:长连接的大流量攻击变少了,短时高频的突发请求变多了。Cloudflare 在 2026 Q3 DDoS 威胁报告 中提到,其平均每小时缓解超过 8200 次攻击,攻击者明显转向更短、更高频的打击方式。这种模式对 PHP-FPM 这类“进程池模型”的服务尤其不友好——平时 CPU 只有 10%,但十几秒的并发尖峰就足以把 pm.max_children 打满,请求开始排队,Nginx 侧随之出现 502 与 upstream timed out。
更隐蔽的是,很多团队把这类现象误判为“服务器配置太低”,直接加机器或把进程数翻倍,结果内存被吃光触发 OOM Killer,故障从偶发变成周期性。真实瓶颈往往藏在下面几处:
| 现象 | 常见根因 | 优先动作 |
|---|---|---|
| Nginx 日志大量 upstream timed out | FPM 进程排队,或脚本执行超过超时 | 看 FPM 状态页 listen queue 与慢日志 |
| 间歇性 502 Bad Gateway | 进程被 OOM Killer 杀掉,或 socket backlog 溢出 | 查 dmesg 与 pm.max_children 是否超内存 |
| CPU 不高但 RT 很高 | 磁盘 IO 等待、外部 API 阻塞、未开 OPcache | 抓 slowlog,检查 opcache.enable |
| 大响应体传输慢 | fastcgi_buffers 过小触发磁盘临时文件 | 调大 buffers 并开启 gzip |
如何判断瓶颈在 Nginx 还是在 PHP-FPM?#
判断方法很朴素:让两个组件各自把状态暴露出来,用数据说话。先把 PHP-FPM 的状态页和慢日志打开,这两个开关在生产环境也能安全使用。
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
; /etc/php/8.3/fpm/pool.d/www.conf
[www]
pm = dynamic
pm.max_children = 64
pm.start_servers = 16
pm.min_spare_servers = 8
pm.max_spare_servers = 24
pm.max_requests = 800
request_terminate_timeout = 15s
request_slowlog_timeout = 2s
slowlog = /var/log/php-fpm/slow.log
然后在 Nginx 中挂一个只允许本机访问的状态入口:
location = /fpm-status {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_NAME /status;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
执行下面这条命令,重点看 listen queue、active processes 和 max active processes 三个字段:
curl -s http://127.0.0.1/fpm-status?full | grep -E "listen queue|active processes|max active|slow requests"
- listen queue 长期大于 0:请求在排队,说明进程数不够或单个请求太慢。
- max active processes 长期等于 pm.max_children:进程池已触顶,加机器或降单请求耗时。
- slow requests 持续增长:去看
/var/log/php-fpm/slow.log里的调用栈,通常是数据库或外部 HTTP 调用。 - queue 为 0 但 Nginx 仍超时:瓶颈不在 FPM 容量,而在读超时或网络链路。
如果状态页一切正常,而 Nginx 错误日志里出现 no live upstreams 或连接被拒,问题就在 socket 路径、权限或 FPM 进程已崩溃,与性能参数无关。
Nginx 侧:upstream keepalive 与 fastcgi 参数怎么配?#
默认配置下,Nginx 每次转发 FastCGI 请求都会重新建立到 PHP-FPM 的连接。在高并发短请求场景里,这部分开销相当可观。启用 upstream keepalive 并配合 fastcgi_keep_conn on,可以复用连接池,降低握手与 fork 压力。
upstream php_backend {
server unix:/run/php/php8.3-fpm.sock;
keepalive 64;
}
server {
listen 80;
server_name api.plus;
root /var/www/api/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass php_backend;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_keep_conn on;
fastcgi_connect_timeout 2s;
fastcgi_send_timeout 10s;
fastcgi_read_timeout 10s;
fastcgi_buffers 16 16k;
fastcgi_buffer_size 32k;
fastcgi_busy_buffers_size 32k;
}
}
几个容易被忽略的细节:fastcgi_read_timeout 必须大于业务最长耗时,但小于 request_terminate_timeout 会更安全,否则 Nginx 已经断开、PHP 还在跑,白白占用进程。缓冲区方面,如果接口返回较大的 JSON,比如壁纸列表类响应,默认 4k/8k 的 buffer 会频繁写临时文件,改成 16k 级别能明显降低 IO。
PHP-FPM 侧:pm.max_children 到底该设多少?#
这是被问得最多、也最容易拍脑袋决定的一个参数。正确的算法是用可用内存反推进程数,而不是用 CPU 核数:先测出单个 PHP 进程的常驻内存,再用可用内存乘以安全系数除以它。
# 统计当前 php-fpm 进程数量与平均常驻内存
ps -eo rss,comm | awk '$2 ~ /php-fpm/ {sum+=$1; n++} END {printf "进程数=%d 平均RSS=%.1fMB 合计=%.0fMB\n", n, sum/n/1024, sum/1024}'
假设测得平均 RSS 为 45MB,机器 8GB 内存,Nginx、Redis 与系统本身预留 2GB,那么留给 FPM 的是 6GB,按 75% 安全水位计算:6 × 1024 × 0.75 ÷ 45 ≈ 102。此时把 pm.max_children 设为 96 比设成 200 更稳——后者在流量尖峰时一定 OOM。
| 单进程 RSS | 可分配内存 | 建议 max_children | 风险提示 |
|---|---|---|---|
| 30MB | 4GB | 96 | 适合轻量 JSON 接口 |
| 45MB | 6GB | 96 | 需开启 OPcache |
| 80MB | 8GB | 72 | 大框架项目,注意 GC 开销 |
另外,pm.max_requests 建议设成 500~1000,让进程定期回收,避免长时间运行后的内存碎片膨胀;opcache.enable=1 与合理的 opcache.memory_consumption 在生产环境几乎是必选项,收益远大于任何 Nginx 参数微调。
改完参数后,如何验证调优真的生效?#
调优不做验证等于没做。建议分两步:先用真实接口确认功能正常,再用压测对比改前改后的数字。功能验证可以直接调用 API.PLUS 的公开示例,例如 GET 示例接口,配合 curl 看状态码与耗时:
curl -s -o /dev/null -w "http=%{http_code} time_total=%{time_total}s\n" \
"https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8"
如果你正在优化的是图片或大响应场景,可以顺带用 随机动漫壁纸接口 观察 fastcgi_buffers 调整前后的传输差异;表单类业务则参考 POST 示例接口 的请求格式自查。注意压测一定要针对自己的服务,本站接口只适合做连通性与响应结构验证。
本地压测推荐 wrk,固定并发对比 P99(99 分位延迟) 与 5xx 比例:
wrk -t4 -c200 -d30s --latency "http://127.0.0.1/api/get?ip=8.8.8.8"
判断是否真的变好,只看三个指标:吞吐量上升、P99(99 分位延迟) 下降、5xx 归零。如果吞吐上去但 P99(99 分位延迟) 反而变差,说明进程数加过头,CPU 上下文切换在拖后腿。
上线前的检查清单#
- PHP-FPM 状态页与慢日志已开启,且仅本机可访问。
pm.max_children由内存反推得出,并留出 25% 安全水位。- Nginx upstream 已启用 keepalive,
fastcgi_keep_conn on已生效。 fastcgi_read_timeout与request_terminate_timeout成对设置且顺序合理。- OPcache 已开启,且
opcache.validate_timestamps策略与发布流程匹配。 - fastcgi_buffers 覆盖典型响应体大小,大响应有 gzip 兜底。
- 压测数据留档,作为下一次扩容的基线。
总结#
Nginx 与 PHP-FPM 的调优并不神秘,关键顺序是先观测、再改参数、最后压测验证。状态页告诉你进程是否排队,慢日志告诉你时间花在哪,内存公式告诉你进程数上限,keepalive 与缓冲区负责减少无效开销。把这四件事做到位,大多数“莫名其妙变慢”的问题都能在半小时内定位。
需要持续联调接口的团队,可以在 API.PLUS(沐小悠API)注册获取免费 API Key,用 GET 示例接口 与 随机动漫壁纸接口 做真实请求验证;更多实践记录见 API 文章中心,遇到接入问题可直接走 技术支持。