用户登录

技术文章

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

Nginx 与 PHP-FPM 性能调优实战:从压测到容量配置

一句话回答

Nginx 与 PHP-FPM 调优的关键不是照抄参数表,而是先建立可复现的压测基线,用实测内存算出 pm.max_children,再优先处理日志缓冲、FastCGI 缓冲与 OPcache,最后用 slowlog 和状态页形成观测闭环。

核心要点 TL;DR
  • 先建立压测基线,看 p95/p99 与错误率,别只看平均值
  • access_log 加 buffer、调整连接数与 FastCGI 缓冲,收益最直接
  • pm.max_children 按可用内存除以实测单进程 RSS,宁可偏小也不要 swap
  • opcache.validate_timestamps=0 后必须把 reload FPM 写进发布流程
  • 用 slowlog、FPM 状态页与 stub_status 构成观测闭环,每次只改一个变量

为什么按参数表抄一遍,性能往往没有变化#

大多数 Nginx + PHP-FPM 调优文章会给出一张“最佳参数表”,但直接抄下来的结果常常是:QPS(Queries Per Second,每秒请求数) 没涨,CPU 反而更高,或者压测时偶发 502。原因并不复杂——这些参数互相耦合,而且强依赖机器规格、PHP 进程的真实内存占用,以及业务到底卡在 CPU 还是卡在 IO。你改的是参数,瓶颈往往在队列和内存上。

下面这条路径是实战中比较可靠的:先建立可复现的压测基线,再改配置;一次只动一个变量;用实测数据决定 pm.max_children 这类关键数字;最后把观测手段固化下来,让下一次调优有据可依。示例基于 PHP 8.3 与 Nginx 1.26,其他版本思路一致。

第一步:建立可复现的压测基线#

没有基线的调优等于赌博。开始前先把环境“冻结”:压测机与被测机分开,关闭 Nginx 的 access_log 或改为带缓冲写入,确认 OPcache 已生效,并且在正式计时前先跑一轮预热请求,避免把首次编译的时间算进结果。

示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。

# 并发梯度压测,观察延迟分布而不是只看平均值
for c in 50 100 200 400; do
  echo "=== concurrency=$c ==="
  wrk -t4 -c$c -d60s --latency http://127.0.0.1/api/get
done

每组数据至少记录五项:吞吐(Requests/sec)、p95 与 p99(99 分位延迟) 延迟、错误数、Nginx worker 的 CPU 使用率、以及 FPM 进程的常驻内存总量。平均值会骗人,一个 3 秒的尾延迟就能让平均值看起来还行,但用户已经流失了。

如果你暂时没有压测目标,可以先用 POST 示例接口 在测试环境模拟写请求,用 IP 查询接口 模拟读请求,先把工具链跑通,再换成自己的业务接口。

Nginx 侧:连接数、缓冲与日志开销#

进程与连接:别把两个数字混为一谈

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 8192;
    multi_accept on;
    use epoll;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    keepalive_timeout 30s;
    keepalive_requests 1000;

    client_header_timeout 10s;
    client_body_timeout 10s;
    client_max_body_size 8m;

    open_file_cache max=10000 inactive=30s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;

    gzip on;
    gzip_comp_level 5;
    gzip_min_length 1024;
    gzip_vary on;
    gzip_types text/plain text/css application/json application/javascript;

    access_log /var/log/nginx/access.log combined buffer=32k flush=5s;
    error_log  /var/log/nginx/error.log warn;
}

几个容易踩的点:worker_connections 是每个 worker 的上限,而且一条反向代理请求会同时占用“客户端连接”和“上游连接”两个槽位,所以实际可服务的并发大约是总量的一半;worker_rlimit_nofile 必须大于 worker_connections,否则会出现 accept 失败的报错;access_log 加 buffer=32k 在高并发下能明显减少磁盘写入,这是最省事的一次优化。

FastCGI 缓冲:502 的真正来源之一

upstream php_backend {
    server unix:/run/php/php8.3-fpm.sock;
    keepalive 64;
}

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass php_backend;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    fastcgi_keep_conn on;
    fastcgi_connect_timeout 2s;
    fastcgi_send_timeout 30s;
    fastcgi_read_timeout 30s;
    fastcgi_buffer_size 32k;
    fastcgi_buffers 16 16k;
    fastcgi_busy_buffers_size 64k;
}

如果你的响应头里塞了大量 Cookie 或较长的缓存控制字段,默认的 4k 响应头缓冲会被撑爆,于是出现“upstream sent too big header”并返回 502——很多人第一反应是去加 pm.max_children,其实完全改错了地方。另外,upstream 里写 keepalive 之后必须配合 fastcgi_keep_conn on,否则连接复用不会生效。

PHP-FPM 侧:进程模型比单个参数更重要#

pm.max_children 必须算出来,不能猜

公式很简单:可用于 FPM 的内存 ÷ 单个 PHP 进程的平均常驻内存。难点在第二个数字,必须实测。

# 统计 FPM 子进程数量与平均 RSS(进程名按实际情况调整)
ps -o rss= -C php-fpm8.3 | awk '{s+=$1; n++} END {printf "procs=%d avg=%.1fMB total=%.1fMB\n", n, s/n/1024, s/1024/1024}'

假设 8GB 的机器,留给 FPM 6GB,单进程平均 55MB,那么上限大约 110。但这里有个反直觉的事实:pm.max_children 设得过大,比设小了更糟。一旦超出物理内存,系统进入 swap,所有请求一起变慢,表现为 CPU 不高但延迟飙升。宁可让 FPM 在极端流量下排队甚至短暂拒绝,也不要让整台机器 swap。

[www]
user = www-data
group = www-data

listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
listen.backlog = 4096

pm = dynamic
pm.max_children = 96
pm.start_servers = 16
pm.min_spare_servers = 12
pm.max_spare_servers = 32
pm.max_requests = 800

request_terminate_timeout = 30s
request_slowlog_timeout = 3s
slowlog = /var/log/php-fpm/slow.log
pm.status_path = /fpm-status

进程模型的选择:流量长期高位且稳定,用 static,省掉 fork 开销,延迟更平稳;有明显波峰波谷,用 dynamic;低频内部服务用 ondemand。pm.max_requests 是防御性参数,用来隔离第三方库的轻微内存泄漏,不要设得太小,否则频繁重建进程反而增加开销。

慢日志与状态页:把问题定位到函数

location = /fpm-status {
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_param SCRIPT_NAME /fpm-status;
    allow 127.0.0.1;
    deny all;
}

location = /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
}

FPM 状态页里 listen queue 持续大于 0,说明子进程不够或请求变慢了;Nginx 的 stub_status 里 Waiting 偏高在开启 keepalive 时是正常的,不必惊慌。真正值得追的是 slowlog——它会直接打印出卡住的 PHP 调用栈,比任何猜测都有价值。

通信层与 OPcache:两个被低估的开关#

Nginx 与 FPM 在同一台机器上时,优先用 Unix Socket,并注意 backlog 是否溢出:

ss -xl | grep php   # Send-Q 列可反映监听队列排队情况
ss -lnt | head

跨机部署才用 TCP,并显式开启 listen = 0.0.0.0:9000 配合内网防火墙,不要暴露在公网。OPcache 的收益通常比调 FPM 参数更大:

opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.jit=tracing
opcache.jit_buffer_size=64M

realpath_cache_size=4096k
realpath_cache_ttl=600

两个细节值得强调:validate_timestamps=0 意味着发布代码后必须 reload FPM,否则改动不生效,请把这一步写进发布脚本;save_comments=1 不能关,关掉后依赖注解的框架(Doctrine、Symfony 路由等)会直接报错。至于 JIT(Just-In-Time,即时编译),在典型的 Web 请求场景里收益有限,更多是给 CPU 密集型计算带来提升,建议开启后用自己的基线压测验证,别指望它把 QPS(Queries Per Second,每秒请求数) 翻倍。

用观测数据闭环,而不是靠感觉#

现象常见的错误归因更可能的原因
突发 502FPM 进程数不够响应头超过 FastCGI 缓冲,或 backlog 溢出
504 增多Nginx 性能问题业务代码慢查询、外部接口阻塞,应看 slowlog
CPU 打满但 QPS(Queries Per Second,每秒请求数) 很低PHP 版本太老OPcache 未命中、磁盘 IO 等待、频繁创建连接
内存高但吞吐不变需要更多进程max_children 过大导致 swap

把上述指标接入 Prometheus 或至少写进每日巡检脚本,每次调整只改一项并立刻复测。没有回归验证的调优,本质上是把风险留给下一次流量高峰。

AI 在这个流程里能做什么,不能做什么#

2026 年,Amazon 在其工程师角色指南更新中提到一个观点:AI 并不定义开发者这个角色,真正被看重的是把工具、判断力和领域知识结合起来的能力。放到性能调优上,这句话非常具体。

可以让 AI 帮你做的事:把 slowlog 按调用栈聚合后再让它解读、根据你的接口语义生成 k6 压测脚本、审阅两份 nginx.conf 的差异、解释 ss 输出里某个字段的含义。不该让 AI 替你做的事:直接给你一个 pm.max_children 的数字。任何没有结合你机器实际内存与进程 RSS 的容量建议,都只是巧合。同理,不要让模型直接改生产配置,正确姿势是产出 diff、人工 review、灰度发布、观察指标再全量。

如果你的服务包含图片型接口(例如 随机动漫壁纸接口 这类返回图片地址的场景),那么 CDN(Content Delivery Network,内容分发网络) 与缓存头的收益往往远大于 FPM 调参——先确认瓶颈在哪一层,再决定投入方向,这也是判断力的一部分。

总结#

把整套流程压缩成一份清单:

  1. 先压测出基线,记录 p95、p99(99 分位延迟) 与错误率,而不是只看平均值;
  2. Nginx 侧优先做日志缓冲、连接数与 FastCGI 缓冲,这三项收益最直接;
  3. pm.max_children 用内存实测值计算,宁可略小也不要让机器 swap;
  4. 开启 OPcache 并处理发布流程中的 reload,JIT 用数据验证是否值得;
  5. 用 slowlog、FPM 状态页、stub_status 形成观测闭环,每次只改一个变量。

调优的本质不是堆参数,而是把系统里看不见的队列和内存变成可观测的数字。做完这些之后,如果你需要一个稳定的外部接口来做链路回归或补充业务能力,可以在 API.PLUS 注册获取免费 API Key,先跑通 IP 查询接口 与 POST 示例接口,更多接入方式可浏览 API 文章中心,遇到问题也可以通过 技术支持 反馈。

常见问题(FAQ)

pm.max_children 到底该设多大?
用可分配给 FPM 的内存除以单个 PHP 进程的平均常驻内存(ps 实测 RSS)得出上限。设得过大一旦越过物理内存就会进入 swap,所有请求一起变慢,比设小更糟。
压测时偶发 502,是不是进程数不够?
不一定。更常见的原因是响应头超过 FastCGI 默认 4k 缓冲,报 upstream sent too big header,可调大 fastcgi_buffer_size 与 fastcgi_buffers;也可能是 listen.backlog 溢出。
worker_connections 该设多少,和 worker_rlimit_nofile 什么关系?
worker_connections 是每个 worker 的上限,反向代理请求会同时占用客户端与上游两个槽位,实际并发约为总量一半;worker_rlimit_nofile 必须大于它,否则会 accept 失败。
开启 OPcache 的 JIT 能显著提升 QPS 吗?
在典型 Web 请求场景收益有限,JIT 更多帮助 CPU 密集型计算。建议开启后用自己的压测基线验证,不要指望它把 QPS 翻倍。
调优时能让 AI 直接给出配置数字吗?
不能。脱离你机器实际内存与进程 RSS 的容量建议只是巧合。AI 更适合解读 slowlog、生成压测脚本、审阅配置差异;改生产应产出 diff、人工 review、灰度发布。

作者:沐小悠

本文约 15 分钟读完 · 关键词:Nginx 调优、PHP-FPM、pm.max_children、OPcache、性能压测

更多文章 GET 示例接口

「」标签下的其他文章