Nginx worker_processes 与 worker_connections 容量计算实战:从并发目标反推参数与 Too many open files 排查

「我的服务器 4 核 8G,Nginx 该配几个 worker?」——这是被问得最多、也被答得最糊的问题。网上流传的答案从「worker_processes 1; 最省内存」到「配 64,越多越快」都有,彼此矛盾却都言之凿凿。更麻烦的是,很多人调完 worker_processes 和 worker_connections 之后,发现页面该慢还是慢,于是断定「这些参数没用」。

问题不在参数,在于没人把这两个数字背后的容量模型讲清楚。本文就把模型拆开:worker_connections 到底数的是什么、为什么它不等于最大并发用户数、worker_processes 什么时候该等于核数什么时候不该、以及如何用压测和二进制的 worker_connections are not enough 告警反推真实容量。

先纠一个常见错误:worker_connections 不是并发用户数

Nginx 文档里对 worker_connections 的描述是「设置单个 worker 进程允许同时打开的最大连接数」。这里的关键词是连接,不是用户,也不是请求。

一个浏览器访问一个页面,通常要建立 1 到 6 个 TCP 连接(HTTP/1.1 每域名并行上限),如果页面走 HTTPS 且有 HTTP/2,通常只有 1 个连接承载几十个请求。但 Nginx 这一侧还要额外算上反向代理时的上游连接。

这就是最容易被忽略的一点:当 Nginx 是反向代理时,一个用户请求会同时占用两个连接:

  • 一个客户端连接(浏览器 ↔ Nginx)
  • 一个上游连接(Nginx ↔ PHP-FPM / Node / 后端服务)

所以粗略的换算关系是:

单个 worker 能承载的并发请求数 ≈ worker_connections / 2   (反向代理场景)
单个 worker 能承载的并发请求数 ≈ worker_connections / 1   (纯静态文件场景)

如果还启用了 HTTP/2 并同时保持 HTTP/1.1 兼容、或者做了 proxy_cache 之外还要回源,实际系数会更偏向 2。厂值 worker_connections 512; 意味着反代场景下单个 worker 只够约 256 个并发请求。这就是为什么很多「按默认配置跑得好好的」小站,一上活动流量就冒出大量 502 和 worker_connections are not enough。

容量公式:从并发目标反推算参数

不要先问「配多少」,先问「我要扛多少并发」。设:

  • C = 目标同时在线请求数(connection-level concurrency,不是 UV)
  • k = 每请求的连接系数,纯静态 1,反代 2
  • N = worker_processes
  • W = worker_connections

要求 N × W ≥ C × k,并且留 20%~30% 余量。反推 W:

W ≥ ceil(C × k × 1.25 / N)

举个具体例子。一台 4 核 8G 的 VPS,跑 Nginx + PHP-FPM 的 WordPress 站,目标是扛住 800 个并发请求:

C = 800, k = 2, N = 4
W ≥ ceil(800 × 2 × 1.25 / 4) = ceil(500) = 500
取整到 1024(便于记忆,且留足余量)

于是 worker_processes 4; worker_connections 1024; 就够。理论上限是 4 × 1024 / 2 = 2048 并发请求,是目标的 2.5 倍,安全。

反过来看一个常见的错误配置:8 核机器配 worker_connections 512,反代场景理论上限 8 × 512 / 2 = 2048 并发。看起来不少,但这是绝对上限,一旦触达,Nginx 会直接拒绝新连接(返回 503 或直接断开),而不是排队。所以容量规划必须留余量,不能把理论上限当实际能力。

worker_processes:auto 还是手工指定

worker_processes auto; 是现在绝大多数场景的正确答案,Nginx 会自动读取 CPU 核数(通过 sysconf(_SC_NPROCESSORS_ONLN))。它等价于「让 Nginx 自己数核」。

但有几个场景不该用 auto:

场景一:容器里共享宿主 CPU

Docker / Kubernetes 里,auto 读到的可能是宿主机的全部核数,而不是容器的 CPU quota。一个 --cpus=1 的容器跑在一台 16 核宿主机上,auto 会起 16 个 worker,每个都在抢那 1 核的时间片,上下文切换成本反而上升。正确做法是显式写数字,或者让 Nginx 读不到宿主拓扑:

# 容器 CPU 限制为 1 核时,显式指定
worker_processes 1;

# 若使用 cgroup v2 且 Nginx 版本较新(1.23+),auto 已能正确读取 cpu.max,
# 但仍建议核对:
nproc              # 容器内看到的核数
cat /sys/fs/cgroup/cpu.max    # cgroup v2: "100000 100000" 表示 1 核

场景二:和 PHP-FPM 抢同一批核

这是个人站长最常踩的坑。4 核机器,Nginx 配 worker_processes auto(4 个),PHP-FPM 配 pm.max_children = 20,MySQL 又是单进程多线程。结果 4 个 Nginx worker 加 20 个 PHP 子进程一起抢 4 个核,load average 冲到 20 以上,每个请求的排队时间比处理时间还长。

这时正确做法不是「把 Nginx worker 减到 1」,而是认识到 Nginx 是事件驱动的、几乎不吃 CPU,真正吃核的是 PHP-FPM 子进程。所以调整方向应该是压缩 pm.max_children,让 PHP 的并发数与核数匹配,而不是牺牲 Nginx 的并发能力。

一个实用的经验区间(4 核 8G 内容站):

组件参数建议值理由
Nginxworker_processes4等于核数,事件驱动开销极小
Nginxworker_connections2048反代场景上限 4096 并发
PHP-FPMpm.max_children8~12按单进程内存约 60MB 计算,留出系统余量
PHP-FPMpmstatic 或 dynamic内存够用优先 static,避免频繁 fork
MySQLinnodb_buffer_pool_size2~3G剩余内存的 50%~60%

注意 worker_connections 写 2048 并不会真的占 2048 份内存——它只是一个上限计数器,每连接的实际内存开销在几 KB 到几十 KB 之间(受 client_header_buffer_size、large_client_header_buffers 等影响),连接空闲时才真正分配读写缓冲。所以调大 worker_connections 本身几乎无成本,真正需要节制的是 PHP-FPM 子进程数。

worker_rlimit_nofile:被忘记的另一半

调大了 worker_connections 却依然报 worker_connections are not enough,或者 error.log 里出现 Too many open files,那问题多半不在 Nginx 配置,而在文件描述符上限。

Nginx 每个连接至少要占用 1 个文件描述符,反代场景是 2 个。所以 worker_connections 2048 实际需要至少 4096 个 FD,加上监听套接字、日志文件、缓存文件等,要 5000 以上。worker_rlimit_nofile 是 Nginx 主动去调用 setrlimit() 提升自身进程上限的指令:

worker_rlimit_nofile 65535;

但它不能超过系统级上限。完整链条是三层:

# 1) 系统级
sysctl fs.file-max                 # 全局上限,通常几十万,一般不是瓶颈
sysctl fs.nr_open                  # 单进程硬上限

# 2) systemd 服务级(Nginx 由 systemd 启动时,这个才生效)
systemctl show nginx -p LimitNOFILE
# 若输出 1024,需要在 override 里提高:
mkdir -p /etc/systemd/system/nginx.service.d
cat > /etc/systemd/system/nginx.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=65535
EOF
systemctl daemon-reload && systemctl restart nginx

# 3) Nginx 进程级
# worker_rlimit_nofile 65535;  写在主配置顶层

# 验证:看实际进程的 limits
grep "Max open files" /proc/$(cat /run/nginx.pid)/limits

这三层任意一层卡住,都会导致调大 worker_connections 无效。systemd 这一层是最常见的漏网之鱼,因为很多人改完 /etc/security/limits.conf 就去测试,却发现对 systemd 管理的服务完全不起作用——那个文件只对 PAM 登录会话生效。

怎么确认「已经到瓶颈了」

调参数之前先确认瓶颈在哪,否则就是瞎调。三个观察点:

观察点一:error.log 里的连接拒绝

Nginx 在 worker_connections 耗尽时会写:

2026/10/17 09:12:44 [alert] 12345#0: *998877
worker_connections are not enough while connecting to upstream,
client: 203.0.113.5, server: example.com, request: "GET / HTTP/1.1"

看到 [alert] 级别加上这句,就是百分之百的容量不足,直接调大 worker_connections。反过来,如果没有这条而用户仍然抱怨慢,那瓶颈不在连接数——去查 PHP-FPM 的 listen queue 或 MySQL 慢查询。

观察点二:当前活跃连接数

stub_status 模块给的状态页是最直接的工具:

# nginx.conf 中
location = /nginx_status {
    stub_status;
    access_log off;
    allow 127.0.0.1;
    deny all;
}

# 查看
curl -s http://127.0.0.1/nginx_status
# Active connections: 291
# server accepts handled requests
#  16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106

关键看 Active connections 和 Waiting。Waiting 是已建立但无请求的 keepalive 连接,它同样占用 worker_connections 名额。这就是纯静态站点也容易被 keepalive 淹没的原因:默认 keepalive_timeout 75s;,一个用户开着标签页挂一分钟就占一个名额。对静态站可以把超时降到 15~20 秒,显著降低空闲连接占用。

观察点三:worker 之间的负载是否均衡

如果多个 worker 中有一个 CPU 跑满、其余空闲,说明 accept_mutex 或连接分配有问题。现代 Nginx 默认 accept_mutex off;,靠内核的 SO_REUSEPORT 分发,一般不需要动。但在某些虚拟化环境(尤其是老内核或 KVM 上网络中断绑定不佳时)会出现明显偏斜。检查方法:

# 看每个 worker 的 CPU 时间是否相近
top -H -p $(pgrep -d, -o nginx) | head -20
# 或者直接看所有 nginx 进程
ps -o pid,pcpu,etime,cmd -C nginx

若确实偏斜严重且内核较老,可以显式开启互斥:accept_mutex on;,代价是有一点点唤醒延迟。

压测验证:别用 ab,用 wrk

调完参数一定要验证。ab 是单线程的,压不满多核,测出来的数字会严重低估真实能力。用 wrk,它支持多线程和 keepalive,更接近真实浏览器行为:

# 4 线程、100 并发、持续 30 秒、保持 keepalive
wrk -t4 -c100 -d30s --latency https://example.com/

# 输出解读
# Requests/sec:   1823.45      <- 实际 QPS
# Latency  P50      48.20ms
#          P99     210.55ms    <- 尾部延迟,比平均值重要得多

压测的核心不是看最大 QPS 有多漂亮,而是找拐点:并发从 50 加到 100 时 QPS 线性上升,加到 200 时 QPS 不升反降、P99 暴涨,说明 200 就是这台机器的饱和点。此后所有优化(缓存、静态化、数据库索引)都是在把这个拐点往右推。

还有一个必须在压测中做的动作:同时开一个新终端跑 vmstat 1 和 ss -s,观察 r 列(运行队列)、si/so(swap 活动)、以及 TCP 连接总数。如果 si/so 非零,说明已经在用 swap,此时任何 Nginx 参数调优都是徒劳——先把内存问题解决。

小结与推荐配置

把上面所有内容收敛成一份可以直接用的模板(4 核 8G,Nginx + PHP-FPM 反代,目标 800 并发):

worker_processes 4;
worker_rlimit_nofile 65535;

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

http {
    keepalive_timeout 20s;
    keepalive_requests 200;
    # ... 其余配置
}

配套的 systemd override 记得加上 LimitNOFILE=65535,否则前两行是白写的。

最后说一句方法论上的话:worker_processes 和 worker_connections 是天花板参数,不是性能参数。调大它们不会让站点变快,只会让站点在流量高峰时不那么容易崩。真正决定速度的是缓存命中率、PHP 执行时间、数据库查询效率这些实打实的东西。搞混了这两类参数,就会出现「参数调到极致,页面还是三秒白屏」的困惑——那时候该看的是 TTFB 落在哪一段,而不是继续加 worker。

Last modification:October 17th, 2026 at 09:25 pm

Leave a Comment