Nginx并发连接数怎么优化?工作进程、连接数与系统参数配置

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、systemd LimitNOFILE 三者中最小的那个限制。
  • 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 ≈ 并发数 ÷ 平均响应时间。宣传中的"支持十万并发"不等于"每秒能处理十万个请求"。