Nginx 的默认配置在一台 2 核 4G 的机器上能跑出一个还不错的成绩,但换成 32 核的时候,它可能还在用两个 worker 慢悠悠地处理请求。反过来说,把 worker_processes 开到 64 也不会让一台 4 核机器变快,只会让上下文切换变得更糟。
这篇文章讲的是怎么把 Nginx 的并发能力调到位:worker 进程数与连接数怎么算、为什么反向代理场景要按一半来估算、文件描述符限制在 systemd 时代改哪里才生效、epoll 与 multi_accept 的作用、keepalive 对并发的巨大影响、以及 Linux 内核侧必须配套调整的一组参数。最后给出一套完整的配置示例和验证方法,以及三个流传很广但没用的误区。
一、Nginx 的并发模型:为什么它这么能撑
Nginx 采用的是事件驱动 + 异步非阻塞的模型,和 Apache httpd 传统的 prefork/MPM worker(一个连接一个进程/线程)完全不同。
核心机制是这样的:每个 worker 进程是一个单线程的事件循环,通过 epoll(Linux 上的高效 IO 多路复用机制)同时监视成千上万个连接。当某个连接有数据可读时,epoll 把它返回给 worker,worker 处理完这一段再去看下一个。整个过程里,worker 从不"等着某个连接",所以单个 worker 就能维持大量连接。
对比一下两种模型的资源开销:
| 对比项 | 传统多进程/多线程模型 | Nginx 事件驱动模型 |
|---|---|---|
| 单连接成本 | 一个进程/线程,含独立栈空间(通常几 MB) | 一个 fd + 少量内存(几 KB) |
| 内存随连接数增长 | 线性且很快 | 缓慢 |
| 上下文切换 | 连接越多切换越频繁 | 恒定,与连接数无关 |
| CPU 亲和性 | 差(进程被随机调度) | 好(worker 数量固定,可绑核) |
| 适合场景 | 计算密集型、需要线程隔离 | IO 密集型、高并发长连接 |
这也是为什么 Nginx 能在普通硬件上轻松支撑数万并发连接(C10K 问题在它这里基本不存在)。
但这个模型有一个重要的推论:请求处理必须是非阻塞的。如果在 worker 里做阻塞操作(同步查数据库的 Lua 代码、磁盘上等待慢速 IO),整个 worker 上的所有连接都会被拖慢。所以 Nginx 侧要避免任何阻塞式操作,这也是 openresty 里强调"避免阻塞 API"的原因。
还有一条实用推论:worker 数量建议等于 CPU 核数。少则浪费并行能力,多则产生不必要的上下文切换(多个 worker 抢同一个核)。Nginx 官方推荐的做法就是 worker_processes auto;。
二、四个核心参数的含义与算法
worker_processes
worker_processes auto; # 自动设为 CPU 逻辑核数
worker_cpu_affinity auto; # 自动绑核,减少跨核调度开销
auto 会读取 sysconf(_SC_NPROCESSORS_ONLN),等于逻辑 CPU 数(nproc 的输出)。
手动指定的场景不多,但在容器里值得注意:如果容器只分配了 2 个核的 CPU quota,但宿主机有 64 核,auto 会读到 64(因为 CPU 亲和性信息不总是受限),结果起 64 个 worker 反而效果更差。容器场景建议显式设置为实际配额,或者确保 cpuset 被正确限制。
worker_cpu_affinity auto 让每个 worker 绑定到固定的 CPU,好处是提升 CPU 缓存命中率、减少跨 NUMA 访问。它是可选优化,不要指望带来数量级提升。
worker_connections
events {
use epoll;
worker_connections 10240;
multi_accept on;
}
这是每个 worker 进程能同时打开的最大连接数,不是整机的。整机的理论上限是:
最大并发连接数 = worker_processes × worker_connections
注意这只是 Nginx 自己维护的连接上限,实际可用的并发还要受制于:
- 文件描述符限制(
worker_rlimit_nofile与系统限制) - 内存(每个连接几 KB 到几十 KB,10 万连接大约几百 MB 到 1GB)
- 内核参数(端口范围、backlog、TCP 内存)
反向代理场景要除以 2,这是最容易被遗漏的一点:当 Nginx 作为反向代理时,一个客户端请求会占用两个连接(两个 fd)——一个是面向客户端的连接,一个是 Nginx 到上游服务器的连接。所以:
反向代理时单机最大并发请求数 ≈ (worker_processes × worker_connections) / 2
举例:8 核机器,worker_processes 8、worker_connections 10240,理论最大连接数是 81920,作为反向代理时大约能同时处理 4 万个请求。作为静态服务器(不经上游)时是 8 万。
use epoll
epoll 是 Linux 下最好的 IO 多路复用机制,Nginx 在 Linux 上默认就会选它。显式写出来是为了明确意图,也便于在不同平台间做对比。可用的方案对比:
| 机制 | 平台 | 说明 |
|---|---|---|
| epoll | Linux | 首选,O(1) 复杂度,无连接数限制 |
| kqueue | FreeBSD、macOS | BSD 系的首选 |
| select / poll | 通用 | 低效,O(n) 遍历,且有 fd 数量硬限制(1024) |
如果你的 Nginx 意外地用了 select/poll(nginx -V 或错误日志里能看到),性能会很差。现代系统上不会发生,但如果是自行编译的非常规版本,值得确认。
multi_accept
设为 on 时,一个 worker 在一次事件循环里会尽可能多地接受新连接,而不是只接受一个。在高并发短连接场景下能提升接受新连接的效率,但也可能导致某个 worker 一下子拿走太多连接,造成负载不均。
一般建议 on,但如果发现各 worker 连接数分布差异很大(可以看 stub_status 或各 worker 的 fd 数),可以关掉观察一下。
worker_rlimit_nofile
worker_rlimit_nofile 65535; # 写在 main 上下文(events 之外,最顶层)
这个指令设置 worker 进程能打开的最大文件描述符数,直接限制了 worker_connections 的实际上限。如果它比 worker_processes × worker_connections 小,那么实际可用的连接数会被它卡住,Nginx 日志里会出现 "Too many open files"。
它必须大于等于单 worker 需要的 fd 数,而且要注意:一个 worker 除了用户连接还要用 fd(日志文件、到上游的连接、监听 socket、缓存文件等),所以留出余量,通常设置为期望值再乘 1.2 左右。
关键点:这个值不能超过系统的硬限制(hard limit),否则无效。所以系统侧也要改,见下一节。
三、系统侧必须配套改的限制
很多"改了 worker_connections 却没效果"的案例,根源都在系统限制上。需要改三处:
第一处:系统级总限制 fs.file-max
cat /proc/sys/fs/file-max # 查看
cat /proc/sys/fs/file-nr # 已分配/已使用/上限
# 持久化设置
echo 'fs.file-max = 200000' | sudo tee /etc/sysctl.d/99-app-tuning.conf
sudo sysctl --system
第二处:systemd 服务的 LimitNOFILE(最关键)
这是现代 Linux 上最容易踩坑的一步。 如果你的 Nginx 是通过 systemd 启动的(绝大多数情况),那么 /etc/security/limits.conf 里的设置对 Nginx 不生效——systemd 不会读取 PAM 的 limits。
正确做法是修改 service 单元:
sudo systemctl edit nginx
在打开的编辑器中写入(这会生成 override 文件,不改动原始 unit):
[Service]
LimitNOFILE=655350
LimitNPROC=65535
然后:
sudo systemctl daemon-reload
sudo systemctl restart nginx
# 验证:查看实际生效的限制
cat /proc/$(cat /var/run/nginx.pid 2>/dev/null | head -1)/limits | grep -i 'open files'
systemctl show nginx | grep -i limitnofile
/etc/security/limits.conf 的场景:只对通过 PAM 登录的会话有效(SSH 登录的用户启动的进程)。如果你的服务用 su - user -c "cmd" 方式启动,它才起作用。配置方式:
# /etc/security/limits.conf
nginx soft nofile 65535
nginx hard nofile 65535
* soft nofile 65535
* hard nofile 65535
发行版默认 systemd 的 DefaultLimitNOFILE 可能只有 1024 或 4096,这是最常见的原因。
第三处:Nginx 自身的 worker_rlimit_nofile
已在前一节说明,必须 ≤ 上面两关。三者取最小值才是真正生效的上限。可以用下面这条一眼验证:
cat /proc/$(pgrep -f 'nginx: worker' | head -1)/limits | grep -i 'Max open files'
还要确认让 Nginx 以哪个用户跑
user nginx; # Debian/Ubuntu 通常是 www-data
用户级别的限制也会继承给 worker,改完 limits.conf 要确认 nginx 用户也在覆盖范围内。
四、内核网络参数
连接能建立起来之前,还要过内核这一关。下面是与高并发最相关的几项:
# /etc/sysctl.d/99-app-tuning.conf
# 监听队列上限(listen backlog),默认 128 太小
net.core.somaxconn = 65535
# 未被应用程序 accept 的连接请求队列最大值
net.core.netdev_max_backlog = 65535
# 本地可用端口范围(作为客户端连上游时的关键限制)
net.ipv4.ip_local_port_range = 1024 65535
# 典型默认值是 32768 60999,约 28000 个端口,反向代理高并发时会耗尽
# TIME_WAIT 状态的 socket 最大数量
net.ipv4.tcp_max_tw_buckets = 262144
# 允许复用处于 TIME_WAIT 的 socket 用于新连接(仅对客户端连出有效)
net.ipv4.tcp_tw_reuse = 1
# SYN 队列长度(半连接队列)
net.ipv4.tcp_max_syn_backlog = 65535
# TCP 读写缓冲区
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
应用并验证:
sudo sysctl --system
sudo sysctl -a | grep -E 'somaxconn|ip_local_port_range|tw_reuse'
关于 tcp_tw_recycle:不要开启。 它在 Linux 4.12 之后已被彻底移除,原因是在 NAT 环境下会破坏连接的可靠性(时间戳机制导致来自同一 NAT 后不同客户端的连接被误判)。网上很多老教程还在推荐它,照抄会导致诡异的断连。同时,tcp_tw_reuse 只在作为客户端有大量连出时才有意义。
确认 somaxconn 与 Nginx 的配合:Nginx 的 listen 指令支持指定 backlog 参数,它会取与 somaxconn 的较小值:
server {
listen 80 backlog=65535;
...
}
确认 ip_local_port_range 是否真的够用:作为反向代理时,Nginx 到上游的每条连接都占一个本地端口,处于 TIME_WAIT 的端口在 60 秒内不能复用(除非开启 reuse)。假设代理场景 QPS 20000,那么理论上 60 秒内会消耗超过百万个四元组,端口很可能成为瓶颈——这就是为什么 upstream keepalive 如此重要(见下节)。
五、keepalive:影响最大的一个杠杆
如果说本文只保留一条优化建议,那就是好好用 keepalive。
客户端侧 keepalive
http {
keepalive_timeout 65s;
keepalive_requests 1000;
}
keepalive_timeout:一个长连接在空闲多久后关闭。默认 75s。keepalive_requests:一个长连接最多处理多少个请求后关闭,防止单连接占用过久、也避免内存碎片累积。
HTTP/1.1 默认开启长连接。开启后,客户端在同一个连接上顺序发送多个请求,省掉了反复的 TCP 三次握手和 TLS 握手。对 HTTPS 站点,握手成本尤其高(RSA/ECDHE 交换需要 CPU),长连接的收益非常明显。
设置太长的代价:空闲连接会占用 worker_connections 配额和内存。如果客户端数量极大且请求稀疏,可以适当缩短(比如 15~30s);内网、移动端可以考虑更长。
上游侧 keepalive(反向代理必配)
upstream backend {
server 10.0.1.11:8080;
server 10.0.1.12:8080;
keepalive 64; # 每个 worker 与上游保持的空闲长连接数
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # 必须,1.0 不支持长连接
proxy_set_header Connection ""; # 必须,清掉默认的 close
}
}
这是最容易被漏掉的两个配置。Nginx 默认用 HTTP/1.0 连上游,并且会发送 Connection: close,所以如果不显式设置这两行,即使配了 keepalive 也完全不起作用。
keepalive 64 的含义是每个 worker 与整个 upstream 组保持 64 条空闲长连接(不是每台服务器,是整个组)。它的合理值大致等于"同时能并发打到上游的请求数",太小了发挥不了作用,太大了会占用上游的连接资源。
配好之后的效果:端口不再快速耗尽、TIME_WAIT 数量大幅下降、延迟也因为省掉握手而降低。用这条命令感受差别:
ss -ant | awk '{print $1}' | sort | uniq -c | sort -rn
# 观察 ESTABLISHED 与 TIME-WAIT 的数量
六、一份完整的优化配置示例
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 655350;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
use epoll;
worker_connections 16384;
multi_accept on;
accept_mutex off; # 高负载下建议关闭,减少锁竞争
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on; # sendfile 时最大化单个包的数据量
tcp_nodelay on; # keepalive 连接上禁用 Nagle,降低延迟
keepalive_timeout 65s;
keepalive_requests 1000;
client_max_body_size 20m;
client_body_buffer_size 128k;
# 打开的文件描述符缓存(静态文件多时有效)
open_file_cache max=100000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# 日志格式与缓冲,减少写 IO
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $request_time $upstream_response_time';
access_log /var/log/nginx/access.log main buffer=32k flush=5s;
gzip on;
gzip_comp_level 4;
gzip_types text/plain text/css application/json application/javascript text/xml;
upstream backend {
server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
keepalive 128;
}
server {
listen 80 backlog=65535;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
location /status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
}
}
accept_mutex off 值得说明:老版本 Nginx 用它来保证多个 worker 之间均衡地接受新连接(EPOLLEXCLUSIVE 之前),但在高并发下会造成额外延迟。现代内核(Linux 4.5+,glibc 支持 EPOLLEXCLUSIVE)建议关闭。
改完配置的标准动作:
sudo nginx -t # 语法校验,必须通过
sudo nginx -T | head -50 # 输出合并后的完整配置,检查有没有重复
sudo systemctl reload nginx # 平滑重载,不断连接
风险提示:reload 会创建新 worker 并让旧 worker 处理完已有连接后退出,通常是安全的。但如果 worker_connections 调大到超过文件描述符限制,reload 过程中新 worker 可能启动失败或报 "Too many open files",此时 Nginx 仍保留旧 worker 提供服务,但状态不一致。所以变更后务必检查 error_log 并确认 worker 数量与限制:
ps aux | grep -c '[n]ginx: worker'
tail -20 /var/log/nginx/error.log
七、怎么验证真的提升了
调参之后必须有验证,不能凭感觉。
第一步:确认实际生效的限制。
NGINX_PID=$(pgrep -f 'nginx: worker' | head -1)
cat /proc/$NGINX_PID/limits | grep -i 'Max open files'
ls /proc/$NGINX_PID/fd | wc -l # 当前打开的 fd 数
cat /proc/sys/net/core/somaxconn
第二步:用 stub_status 看实时连接数。
curl -s http://127.0.0.1/status
输出:
Active connections: 291
server accepts handled requests
16630948 16630948 31070465
Reading: 6 Writing: 179 Waiting: 106
含义:Active connections 是当前活跃连接数(含 Waiting);三个累计值分别是接受的连接、处理的连接、处理的请求;Reading 是正在读取请求头的连接数,Writing 是正在向客户端写响应的连接数,Waiting 是空闲的 keepalive 连接数。正常情况下 Waiting 占大头,说明长连接配置生效了。
第三步:压测。 用 wrk 之类的工具逐步加压:
wrk -t4 -c1000 -d60s --latency http://example.com/
压测要在与生产一致的独立环境进行,不要直接压生产服务。 逐步提高 -c(并发连接数),观察什么时候出现错误率上升或延迟陡增,那个拐点就是当前的真实承载能力。
第四步:记住并发数不等于 QPS。 这是一个重要的概念区分,下一节详述。
常见问题
把 worker_connections 调到 65535 就能支持 6 万并发了吗?
不一定,有三个卡点:一是 worker_rlimit_nofile 和系统限制(systemd 的 LimitNOFILE)必须同步提高,取三者最小值才生效;二是反向代理场景每请求占 2 个 fd,实际请求处理量要除以 2;三是内存要够——十万级别的连接通常需要额外的几百 MB 到 1GB 内存。改完必须看 error_log 有没有 "too many open files"。
为什么我改了 worker_connections,连接数上不去?
最常见的原因是 systemd 的 LimitNOFILE 还是默认的 1024。systemctl show nginx | grep LimitNOFILE 可以确认。第二个原因是 fs.file-max。第三是 nginx.conf 里忘了配 worker_rlimit_nofile(它默认继承系统限制)。记住 /etc/security/limits.conf 对 systemd 启动的服务无效。
我该把 worker_processes 设成 CPU 核数的几倍?
通常是 1 倍(auto)。worker 是单线程事件循环,一个核跑一个 worker 就够把这个核用满。设多会引入上下文切换反而更慢。只有一种例外:worker 里确实有少量阻塞操作(比如磁盘读静态大文件占比很高),可以适当加倍(如 1.5~2 倍)来掩盖阻塞时间,但这种情况更应该去解决阻塞本身。
并发连接数和 QPS 是一回事吗?
完全不是。并发连接数是"同一时刻系统维持了多少条连接",包含大量空闲的 keepalive 连接;QPS 是"每秒处理了多少请求"。两者的关系是:QPS ≈ 并发数 / 平均响应时间(Little's Law)。举例:并发 10000、平均响应时间 200ms,则 QPS = 10000 / 0.2 = 50000。反过来,如果 QPS 是 1000、响应时间是 50ms,则平均并发只有 50。看到"支持 10 万并发"的宣传,要问清楚是指哪种。
keepalive_timeout 设得越长越好吗?
不是。空闲连接会一直占用 worker_connections 的配额,而且每 Nginx 侧每条 keepalive 连接大约消耗几 KB 内存。在客户端数量极大、请求很稀疏的场景下,长连接会占满配额导致新连接进不来。一般 15~75 秒是合理区间,移动端可以适当延长,配合 keepalive_requests 一起用更好。
容器里跑 Nginx,worker_processes auto 准不准?
可能不准。auto 读的是宿主机可见的 CPU 数量,如果容器是通过 CPU quota(如 --cpus=2)限制而不是 cpuset 限制的,Nginx 看到的仍是宿主机核数,会起过多 worker。容器场景建议显式设置 worker_processes,或者用 cpuset 明确限制可见 CPU。
总结
- Nginx 的事件驱动模型使单 worker 能维持大量连接:每个 worker 是一个单线程的 epoll 事件循环。因此 worker_processes 应等于 CPU 逻辑核数(
auto),多了只会增加上下文切换。容器里用 CPU quota 限制时auto可能读到宿主机核数,要显式指定。 - 理论最大连接数 = worker_processes × worker_connections,反向代理场景要除以 2(每个请求占用面向客户端和面向上游两个 fd)。真正的上限还受
worker_rlimit_nofile、系统fs.file-max、systemdLimitNOFILE三者中最小的那个限制。 - systemd 时代必须用
systemctl edit设置 LimitNOFILE,/etc/security/limits.conf只对 PAM 登录会话生效,对 systemd 启动的服务无效。这是绝大多数"改了 worker_connections 没用"的原因。改完用/proc/验证真实上限。/limits - 内核侧要配套调整:
net.core.somaxconn(默认 128 太小)、net.ipv4.ip_local_port_range(反向代理连出时的端口瓶颈)、net.ipv4.tcp_max_syn_backlog、tcp_tw_reuse=1。tcp_tw_recycle已被内核移除且有害,不要开启。 - keepalive 是收益最大的杠杆:客户端侧配
keepalive_timeout与keepalive_requests;反向代理必须配upstream的keepalive并在 location 里加proxy_http_version 1.1和proxy_set_header Connection "",否则 Nginx 默认用 HTTP/1.0 加 Connection: close,keepalive 完全不生效。 - 验证方式:
ngx_http_stub_status_module的/status页面看 Active connections 与 Waiting(Waiting 占大头说明长连接生效);用ss -ant统计 TCP 状态分布;用 wrk 逐步加压找拐点。任何改动都要先nginx -t校验再用systemctl reload平滑生效,并检查 error_log 有无 "too many open files"。 - 记住并发连接数与 QPS 是两个概念,关系由 Little's Law 决定:QPS ≈ 并发数 ÷ 平均响应时间。宣传中的"支持十万并发"不等于"每秒能处理十万个请求"。
企业QQ咨询




