用户登录

技术文章

外部链接将在新标签页打开。

Nginx 与 PHP-FPM 性能调优实战:定位、配置与压测

一句话回答

Nginx 与 PHP-FPM 变慢多数不是 Nginx 本身,而是 PHP-FPM 进程池打满与 fastcgi 超时、缓冲配置不匹配;先看状态页和慢日志定位瓶颈,再按内存反推 pm.max_children,配合 upstream keepalive 可显著提升吞吐。

核心要点 TL;DR
  • 先定位瓶颈再调参,盲目改 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 outFPM 进程排队,或脚本执行超过超时看 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风险提示
30MB4GB96适合轻量 JSON 接口
45MB6GB96需开启 OPcache
80MB8GB72大框架项目,注意 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 上下文切换在拖后腿。

上线前的检查清单#

  1. PHP-FPM 状态页与慢日志已开启,且仅本机可访问。
  2. pm.max_children 由内存反推得出,并留出 25% 安全水位。
  3. Nginx upstream 已启用 keepalive,fastcgi_keep_conn on 已生效。
  4. fastcgi_read_timeout 与 request_terminate_timeout 成对设置且顺序合理。
  5. OPcache 已开启,且 opcache.validate_timestamps 策略与发布流程匹配。
  6. fastcgi_buffers 覆盖典型响应体大小,大响应有 gzip 兜底。
  7. 压测数据留档,作为下一次扩容的基线。

总结#

Nginx 与 PHP-FPM 的调优并不神秘,关键顺序是先观测、再改参数、最后压测验证。状态页告诉你进程是否排队,慢日志告诉你时间花在哪,内存公式告诉你进程数上限,keepalive 与缓冲区负责减少无效开销。把这四件事做到位,大多数“莫名其妙变慢”的问题都能在半小时内定位。

需要持续联调接口的团队,可以在 API.PLUS(沐小悠API)注册获取免费 API Key,用 GET 示例接口 与 随机动漫壁纸接口 做真实请求验证;更多实践记录见 API 文章中心,遇到接入问题可直接走 技术支持。

常见问题(FAQ)

Nginx 报 upstream timed out 是 Nginx 的问题吗?
通常不是。该错误表示 Nginx 等待 PHP-FPM 返回超时,根因多为进程池耗尽排队、脚本本身慢或 request_terminate_timeout 小于业务耗时。应先看 PHP-FPM 慢日志和 active processes 再调整超时。
pm.max_children 设得越大越好吗?
不是。进程数超过内存承载会触发 OOM Killer,反而导致 502。正确做法是用可用内存乘以 0.7 再除以单个 PHP 进程的平均 RSS,并预留 Nginx、Redis、数据库的内存。
调优后如何验证是否真的生效?
用 wrk 或 ab 在固定并发下压测本地服务,对比吞吐、P99 延迟与 5xx 比例,同时观察 PHP-FPM 状态页的 listen queue 是否长期大于 0,只有排队消失、错误率下降才算生效。

作者:沐小悠

本文约 13 分钟读完 · 关键词:Nginx、PHP-FPM、性能调优、fastcgi、进程池

更多文章 GET 示例接口

「」标签下的其他文章