Nginx 与 PHP-FPM 性能调优:超时与进程模型
先观测请求时间与FPM队列建立基线,再按内存预算确定max_children,设置分层超时与缓冲,优化连接复用和内核参数,最后分层压测回归并补上限流、缓存与降级。
- 先观测请求时间与FPM队列,建立调优基线
- max_children按可用内存除以单进程RSS估算,不是并发口号
- 超时预算由外向内递减,最内层先超时并留2到5秒余量
- 同机优先unix socket,HTTP上游开启keepalive复用连接
- 分层压测每轮只改一个变量,并补限流、缓存与降级
引言:从 2026 年 Q3 的网络波动说起#
2026 年第三季度,Cloudflare Radar 记录的全球网络中断再次提醒我们:电力故障、自然灾害和区域性断网都会让“看起来高可用”的服务瞬间降级。对 API 提供方而言,真正的考验不是峰值 QPS(Queries Per Second,每秒请求数),而是上游抖动、回源超时、进程排队时,入口层能否把故障隔离在可控范围内。
Nginx + PHP-FPM 仍是大量 API 网关和 PHP 业务的生产组合。本文不堆参数,而是按“观测—容量—超时—连接—压测—弹性”的顺序,给出一套可复现的调优路径。文中示例基于 Linux + Nginx 1.26 + PHP-FPM 8.3,其他版本思路一致。
一、先观测:把请求时间拆开看#
调优最怕“凭感觉改 max_children”。先让 Nginx 访问日志记录完整时间分布:$request_time 是 Nginx 从收到请求到发送完响应的总时间,$upstream_response_time 是等待 FPM 的时间,$upstream_connect_time 和 $upstream_header_time 能进一步区分连接慢还是应用首字节慢。
示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。
log_format apifmt '$remote_addr - $request_id [$time_local] '
'$request_method $uri $status '
'rt=$request_time urt=$upstream_response_time '
'uct=$upstream_connect_time uht=$upstream_header_time '
'bytes=$body_bytes_sent ua=$http_user_agent';
access_log /var/log/nginx/api_access.log apifmt;
如果 rt 高但 urt 低,瓶颈在客户端网络或 Nginx 自身发送;如果 urt 高且 uct 低、uht 高,说明连接 FPM 很快,但 PHP 处理慢;如果 uct 高,则要关注 FPM 的 listen backlog 和进程是否打满。
FPM 侧打开 status 页,至少观察 active processes、idle processes、listen queue、max listen queue 和 slow requests。listen queue 长期大于 0,说明请求已经在排队,继续加 Nginx worker 没有意义。
; php-fpm.d/www.conf
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong
; nginx.conf
location = /fpm-status {
allow 127.0.0.1;
deny all;
include fastcgi_params;
fastcgi_param SCRIPT_NAME /fpm-status;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
把这些指标接入 Prometheus 或至少定时 curl 落盘,才有调优基线。没有基线的参数调整,只是把问题挪到下一次故障。
二、进程模型:max_children 是内存预算,不是并发口号#
PHP-FPM 的 pm.max_children 决定同时能有多少个 PHP 进程处理请求。设得过大,内存超卖会触发 swap 甚至 OOM Killer;设得过小,请求在 listen queue 里排队,表现为 502/504 或客户端超时。
一个可用的起点是:max_children ≈(可用内存 - 系统与 Nginx 预留)/ 单进程常驻内存。例如 8 核 16GB 机器,预留 4GB,单进程 RSS 约 60MB,则上限约 200;再结合压测观察 CPU 和 P99(99 分位延迟),通常不需要跑满。I/O 密集型 API 可以超过核数,但不要让内存成为短板。
; 估算 php-fpm 平均 RSS(MB)
ps --no-headers -o rss -C php-fpm8.3 | awk '{sum+=$1; n++} END {print sum/n/1024}'
动态进程池的参数关系要满足 min_spare_servers ≤ start_servers ≤ max_spare_servers ≤ max_children。max_requests 用于定期回收进程,缓解扩展内存泄漏,API 场景可设在 500 到 2000 之间;request_terminate_timeout 必须大于业务 P99(99 分位延迟),否则正常慢请求会被误杀。
; php-fpm.d/www.conf
pm = dynamic
pm.max_children = 120
pm.start_servers = 12
pm.min_spare_servers = 8
pm.max_spare_servers = 24
pm.max_requests = 1000
request_terminate_timeout = 20s
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log
listen.backlog = 4096
listen.backlog 要和内核 net.core.somaxconn 一起看。backlog 再大,内核队列上限小也会丢连接。Nginx 的 worker_connections、worker_rlimit_nofile 以及 systemd 的 LimitNOFILE 也要同步放大,否则高并发时先撞到文件描述符上限。
三、超时与缓冲:让 504 有边界、可解释#
超时预算要从客户端往内层递减。客户端等 30 秒,Nginx 等 FPM 20 秒,FPM 等下游依赖 15 秒,数据库再短一些。最内层先超时,外层留 2 到 5 秒余量,这样 PHP 进程能先释放,不会出现 Nginx 已断开而 FPM 还在跑的情况。
fastcgi_connect_timeout 对同机 unix socket 可以设 1 到 2 秒;fastcgi_read_timeout 按业务 P99(99 分位延迟) 设置,API 常见 10 到 30 秒。不要用 600 秒掩盖慢查询,那只会让进程池更快耗尽。缓冲方面,API 返回的 JSON 通常不大,fastcgi_buffer_size 16k、fastcgi_buffers 16 16k 足够;只有大响应且不能落盘时才需要调大,否则临时文件会带来额外 I/O。
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_connect_timeout 2s;
fastcgi_send_timeout 20s;
fastcgi_read_timeout 20s;
fastcgi_buffer_size 16k;
fastcgi_buffers 16 16k;
fastcgi_busy_buffers_size 32k;
fastcgi_temp_path /var/cache/nginx/fastcgi_temp 1 2;
fastcgi_intercept_errors on;
error_page 502 504 = @fallback;
}
location @fallback {
default_type application/json;
add_header Retry-After 3 always;
return 503 '{"code":503,"message":"service busy"}';
}
把 502 和 504 统一映射成带 Retry-After 的 503,客户端才能做退避重试。注意只对幂等请求自动重试;POST 创建类接口如果要做重试,必须配合幂等键,否则会在故障时制造重复数据。
四、连接复用与内核参数:别让握手吃掉吞吐#
同机部署优先用 unix socket,减少一次 TCP 栈;跨机再用 TCP,并设置合理的 keepalive。如果 Nginx 反向代理的是 HTTP 上游,务必开启 upstream keepalive,否则每个请求都新建连接,TIME_WAIT 和握手成本会迅速上升。
upstream api_backend {
server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
server 127.0.0.1:8081 max_fails=3 fail_timeout=10s;
keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}
location / {
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_pass http://api_backend;
}
内核侧重点看 somaxconn、syn backlog、本地端口范围和文件描述符。tcp_tw_recycle 在新内核已移除,不要再照抄旧文章。调整前先用 ss -s、netstat -s 观察重传、丢包和 TIME_WAIT 分布,避免把应用问题误判为内核问题。
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 10000 65000
net.ipv4.tcp_tw_reuse = 1
fs.file-max = 1000000
五、压测与回归:用分层压测定位,而不是只看 QPS(Queries Per Second,每秒请求数)#
压测要分层:第一层压 Nginx 直接 return 200,确认入口和 TLS 不是瓶颈;第二层压 FPM 的 ping 路径,确认进程池和 socket 正常;第三层再压真实业务。如果第一层很快、第二层很慢,问题在 FPM;如果第二层正常、第三层慢,问题在应用或下游依赖。
# 接口连通性与首字节时间检查
curl -sS -o /dev/null -w '%{http_code} %{time_total}' 'https://www.api.plus/api/dongman?type=json'
# 使用 wrk 做压测时,替换为你的自建服务地址
wrk -t8 -c400 -d120s --latency 'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8'
每轮只改一个变量,记录 QPS(Queries Per Second,每秒请求数)、P50/P95/P99(99 分位延迟)、错误率、FPM active/idle、listen queue 和单进程 RSS。很多“调优”最后发现是慢 SQL 或外部 API 超时,加进程只是把内存吃满。对 API.PLUS 这类外部接口做联调时,也可以用 IP 查询接口 和 随机动漫壁纸接口 验证网络与 JSON 解析链路,再回到自建服务做容量测试。
六、基础设施波动下的弹性:限流、缓存与降级#
回到 2026 年 Q3 的中断事件:当某个区域网络或电力异常时,流量会突然迁移,重试会放大故障。Nginx 层应配置 limit_req 和 limit_conn 保护 FPM,避免雪崩;对可缓存接口使用 proxy_cache,减少回源;对非核心接口准备静态降级响应。
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;
location /api/ {
limit_req zone=api burst=40 nodelay;
limit_conn addr 20;
proxy_connect_timeout 2s;
proxy_read_timeout 10s;
proxy_pass http://api_backend;
}
缓存要区分接口语义:随机动漫壁纸可以按短 TTL 缓存,IP 查询可以按 IP 做分钟级缓存,写接口不能缓存。可以用 POST 示例接口 验证写路径不被缓存误伤;更多接口说明在 API 文章中心,遇到配置问题也可以通过 技术支持 排查。
最后,健康检查不要只检查端口。至少检查 FPM ping、数据库连接和关键下游;被动健康检查用 max_fails 和 fail_timeout 摘除异常节点,重试只对幂等请求开启。故障时返回明确的 503 和 Retry-After,比让客户端长时间挂起更可控。
七、总结:调优是一套闭环,不是一组魔法参数#
Nginx 与 PHP-FPM 的调优顺序应该是:先观测请求时间和 FPM 队列,再做内存预算确定 max_children,接着设置分层超时和缓冲,然后优化连接复用与内核参数,最后用分层压测回归,并补上限流、缓存和降级。每次只改一个变量,保留基线,才能在下一次网络波动时有据可查。
如果你正在搭建 API 联调环境,可以在 API.PLUS 注册获取免费 API Key,先用 IP 查询和随机动漫壁纸接口跑通调用链,再把这些接口作为健康检查或压测对照。入口层稳定了,后面的业务优化才有意义。