Nginx 之所以能在单机扛住数十万并发连接,靠的不是堆硬件,而是事件驱动、非阻塞的进程模型。理解这个模型,是性能调优的前提:worker 进程如何分布、事件循环如何工作、连接与缓冲区如何分配、内核如何配合,每一层都有对应的调优旋钮。很多调优文章只给「抄作业」式的参数,本文反过来——先讲清每一层的工作原理,再给出对应的配置与内核参数,最后用压测方法验证每项调整的实际收益,帮助你形成「理解模型 → 定位瓶颈 → 量化验证」的调优闭环。
一句话总结: Nginx 的性能来自事件驱动模型而非线程数,调优的关键是让 worker 与事件循环、连接与缓冲区、内核参数三者匹配业务流量模型。
1. worker 进程模型
一句话总结: Nginx 是「一个 master 管理多个 worker」的进程模型,worker 数量按 CPU 核数设定,配合 CPU 亲和把每个 worker 钉在独立核心上。
Nginx 启动后有一个 master 进程与一组 worker 进程。master 负责任务调度、配置加载、worker 拉起与退出,不处理任何请求;真正干活的是 worker,每个 worker 独立处理自己的连接集合,进程间通过共享内存(如 limit_req_zone、ssl_session_cache)交换少量状态。这种「多进程 + 事件驱动」的设计让 Nginx 避免了多线程模型的锁竞争与上下文切换开销。
# worker 数量:通常等于 CPU 核数
worker_processes auto; # 自动探测核数
# 显式指定核数
# worker_processes 8;
# CPU 亲和:把 worker 绑定到固定核心,减少缓存抖动
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
worker_processes auto 让 Nginx 自动检测 CPU 核数,这是绝大多数场景的正确起点。对纯 I/O 密集(大量静态文件、代理转发)场景,worker 数可以适度超过核数,因为 worker 大部分时间在等待 I/O;对 CPU 密集(大量 Lua 计算、SSL 加解密)场景,worker 数不宜超过核数,否则会引发调度抖动。worker_cpu_affinity 把每个 worker 钉在独立核心上,消除核心间的缓存迁移,在高吞吐场景下通常有 5%~15% 的收益。
每个 worker 能同时处理的连接数由 worker_connections 决定,它是 Nginx 理论并发量的核心:
events {
# 每个 worker 的最大连接数(与文件描述符上限共同决定并发能力)
worker_connections 10240;
# 理论最大并发 = worker_processes × worker_connections
}
理论并发量 = worker_processes × worker_connections。例如 8 worker × 10240 连接 = 81920 并发,但实际还要受内核文件描述符上限(ulimit -n)与内存约束。调大 worker_connections 前必须确认 ulimit -n 足够大,否则 Nginx 会报 worker_connections are more than open file resource limit 并拒绝启动。除了系统级 fd 上限,Nginx 还提供 worker 级别的 worker_rlimit_nofile,直接在配置中声明每个 worker 可打开的文件描述符数,避免依赖外部 ulimit 设置:
worker_rlimit_nofile 20480; # 每个 worker 的文件描述符上限
worker_rlimit_nofile 应在 worker_processes 附近设置,它与 worker_connections 的关系是 worker_connections × 2(每连接约占用 2 个 fd)加上日志、监听 socket 等额外占用。平滑升级与发布也依赖 worker 模型:nginx -s reload 会让新配置的 worker 拉起、旧 worker 处理完存量连接后优雅退出,worker_shutdown_timeout 控制旧 worker 的最长存活时间,避免「旧的干不完、新的上不来」导致的连接堆积。worker 调优要盯「每个 worker 的 CPU 利用率」:如果所有 worker 的 CPU 都接近 100% 而吞吐不再增长,说明 worker 数已到瓶颈;如果 CPU 利用率很低而延迟升高,说明瓶颈在别处(锁、I/O、后端)。
2. epoll 与事件循环
一句话总结: epoll 让单个 worker 用 O(1) 复杂度管理海量连接,multi_accept 与 accept_mutex 控制连接接入的粒度与公平性。
worker 进程内部是事件循环:它向内核询问「哪些连接有事件可处理」,然后逐个处理就绪事件。Linux 上的事件通知机制是 epoll,它比 select/poll 高效得多——就绪连接以 O(1) 复杂度返回,且不会随连接数增长线性变慢。Nginx 默认自动选用最佳的事件机制(Linux 用 epoll),显式配置只是为了让意图清晰:
events {
use epoll;
# 每次事件循环尽量接收所有新连接,而不是逐个接收
multi_accept on;
# 多个 worker 抢连接时的负载均衡开关
accept_mutex on;
accept_mutex_delay 100ms;
}
multi_accept on 让 worker 在一次事件通知中接收多个连接,减少内核往返次数。accept_mutex 解决的是「多个 worker 同时醒来抢一个连接」的惊群问题:开启后同一时刻只有一个 worker 在 accept,其余 worker 继续处理已持有的连接。accept_mutex 在现代内核(EPOLLEXCLUSIVE 支持)下收益变小,但对传统内核仍是有效的公平性开关。
事件循环与「连接饥饿」直接相关。如果某个 worker 持续被大流量连接占满,而其他 worker 空闲,整体延迟会被拖高。定位这类问题要观察 $upstream_addr 与各 worker 的分布:Nginx 的共享内存计数器(如 stub_status 的 accepted)能反映整体,各 worker 的 CPU 与负载分布则反映均衡性。连接接入的粒度还受 backlog 影响:listen 443 ssl backlog=4096; 控制内核 accept 队列长度,队列满时新连接会被内核丢弃或拒绝,客户端表现为连接建立超时。
server {
# backlog 要匹配客户端连接建立速率,通常与 somaxconn 联动
listen 443 ssl backlog=4096 reuseport;
}
reuseport 是另一个值得关注的事件层特性:它让内核把新连接直接分发到不同 worker 的独立 listen socket,替代 accept_mutex 的轮询方式,减少锁竞争。开启 reuseport 后 accept_mutex 的作用被替代,两者不必同时强调。epoll 调优的最终检验是压测:在相同并发下对比开启 multi_accept/reuseport 前后的吞吐与延迟,用数据决定参数取舍。
3. 连接与 keepalive 调优
一句话总结: keepalive 复用连接省掉握手开销,timeout 决定空闲连接驻留时长,连接数与复用率是判断 keepalive 配置是否合适的核心指标。
HTTP 每次新建连接都要经历 TCP 握手(1 次)与 TLS 握手(视协议 1~2 次),对短请求而言握手开销占比极高。keepalive 让连接在请求结束后保持一段时间,后续请求直接复用,省掉握手。Nginx 侧的 keepalive 分两段:客户端与 Nginx 之间的 keepalive_timeout,以及 Nginx 与后端之间的 upstream keepalive。
http {
# 客户端与 Nginx 的 keepalive
keepalive_timeout 65s;
keepalive_requests 1000; # 单条连接最多复用请求数
# 客户端请求头/体缓冲(与连接建立配合)
client_header_buffer_size 4k;
large_client_header_buffers 4 8k;
}
upstream api_cluster {
server 10.0.1.10:8080;
keepalive 256; # Nginx 与后端保持的空闲连接池大小
}
keepalive_requests 控制一条连接上最多处理的请求数,防止单条连接被长期占用的同时防止请求头畸形累积。upstream keepalive 256 是 Nginx 到后端的连接池大小:它让 Nginx 复用与后端的连接,避免每个请求都新建后端连接。连接池的大小要结合 worker 数评估,总连接数 = worker × keepalive,过大会占用后端大量文件描述符。
连接调优的指标是「复用率」。客户端侧的复用率体现在连接的握手次数与请求数之比,后端侧的复用率体现在 $upstream_connect_time 是否频繁出现新连接握手。如果 keepalive_timeout 太短,空闲连接被过早回收,复用率上不去;太长则会占用大量连接资源。对高并发门户站,keepalive_timeout 常被压缩到 15~30s 以控制连接规模;对低并发的 API 网关,65s 默认值更合适。
# 按业务场景调整 keepalive:高并发场景压缩空闲驻留
http {
keepalive_timeout 30s;
keepalive_requests 2000;
}
连接层还有一个容易忽略的点:reset_timedout_connection on。当 keepalive 连接超时被回收时,若客户端还挂在连接上,Nginx 直接发送 RST 断开而非正常 FIN,能更及时地释放连接资源。开启后对「大量僵尸连接拖垮 accept 队列」的场景有明显帮助。连接调优没有普适参数,只有结合压测与线上指标反复校准。
4. 缓冲区与内存调优
一句话总结: 请求头/体缓冲、proxy 缓冲与 sendfile 零拷贝分别作用于数据的不同阶段,缓冲配置要匹配对象大小分布,避免频繁换页与内存浪费。
Nginx 的数据搬运路径上有三组缓冲:请求侧(client)、代理侧(proxy)与发送侧(sendfile)。请求侧缓冲处理客户端上传:client_body_buffer_size 决定请求体先放内存还是落盘(超过即写临时文件);client_max_body_size 决定请求体上限。代理侧缓冲决定 Nginx 是否等后端响应收满再转发(buffering)还是边收边发(streaming)。
http {
# 请求侧缓冲
client_body_buffer_size 16k; # 请求体超过则写临时文件
client_body_timeout 60s;
# 代理侧缓冲:默认 buffering on,可针对流式接口关闭
proxy_buffering on;
proxy_buffer_size 8k; # 响应头缓冲
proxy_buffers 8 8k; # 响应体缓冲块
proxy_busy_buffers_size 16k;
proxy_temp_file_write_size 64k;
}
缓冲配置的坑在于「对象大小分布」。如果大部分响应只有几 KB,proxy_buffers 给得太大就是浪费内存;如果响应普遍较大,缓冲块过小会导致频繁落盘写临时文件,吞吐骤降。合理的做法是先统计线上响应体大小分布,再配置缓冲块。对 SSE(Server-Sent Events)与流式下载,应关闭 proxy_buffering,让响应即时透传:
# 流式接口:关闭缓冲,边收边发
location /stream/ {
proxy_buffering off;
proxy_cache off;
proxy_pass http://stream_cluster;
}
发送侧的核心是 sendfile 与零拷贝。sendfile 让 Nginx 直接把磁盘文件在内核态复制到 socket,绕过用户态缓冲,是静态文件加速的关键。directio 则相反:对超过阈值的大文件关闭 sendfile,改走直接 I/O,避免占用页缓存:
server {
# 静态文件发送:sendfile 零拷贝 + tcp_nopush 合并小包
sendfile on;
sendfile_max_chunk 1m; # 单次 sendfile 上限,防止长时间占 worker
tcp_nopush on; # 与 sendfile 配合,合并数据包
tcp_nodelay on; # 关闭 Nagle,降低小请求延迟
location /download/ {
directio 4m; # 超过 4MB 的大文件走直接 I/O
aio threads; # 大文件异步 I/O,避免阻塞 worker
}
}
sendfile_max_chunk 很重要:不加限制时,一个大文件的 sendfile 会长时间占用 worker,其他连接的请求得不到及时处理。directio 4m 与 aio threads 把大文件从 sendfile 路径挪到异步 I/O 路径,防止单次大传输拖累整体延迟。内存调优的原则是「按对象大小分布给缓冲,按文件大小给发送路径」,先统计线上数据再配置,而不是照抄别人的参数。
5. 内核参数与系统调优
一句话总结: 内核的 socket 队列、文件描述符与 TCP 参数是 Nginx 并发上限的地基,somaxconn、tcp_fastopen 与文件描述符上限是最常用的三个旋钮。
Nginx 跑在 Linux 上,用户态调优的收益受内核参数制约。最典型的例子是 listen backlog:即便 Nginx 侧配置了 backlog=4096,内核的 somaxconn 默认只有 4096,超过即丢弃连接。地基参数集中在 /etc/sysctl.conf:
# /etc/sysctl.conf 常用调优项
net.core.somaxconn = 65535 # 与 Nginx listen backlog 配合
net.ipv4.tcp_max_syn_backlog = 65535 # 半连接队列长度
net.core.netdev_max_backlog = 65535 # 网卡队列长度
fs.file-max = 1048576 # 全局文件描述符上限
net.ipv4.tcp_tw_reuse = 1 # 复用 TIME_WAIT 连接
net.ipv4.tcp_fastopen = 3 # 启用 TCP Fast Open(服务端+客户端)
somaxconn 与 tcp_max_syn_backlog 决定高并发下连接建立的成功率,两者与 Nginx 的 listen backlog 要形成「内核 ≥ Nginx」的关系,否则 Nginx 配置的 backlog 形同虚设。tcp_tw_reuse 复用 TIME_WAIT 状态的连接,缓解短连接场景下端口与连接资源的快速耗尽。tcp_fastopen 允许 TCP 握手携带首包数据,省掉一次 RTT,对 HTTPS 首字节延迟有帮助。
文件描述符上限是另一个常见瓶颈。Nginx 每个连接占用一个文件描述符,worker_connections 调大后必须同步提高进程的 fd 上限:
# 提高进程文件描述符上限(配合 systemd/ulimit)
ulimit -n 655350
# systemd 服务文件中的写法
# LimitNOFILE=655350
内存层面还要关注「连接的内存占用」:每个 HTTP/2 连接、每个 buffer 都占用 RSS。压测时观察 Nginx 的 RSS 与 swap 使用,如果内存持续增长或出现 swap,说明缓冲配置过大。内核参数调整后要验证:sysctl -p 生效后用压测确认连接建立速率与吞吐是否提升。内核调优是「地基工程」,改错影响面极大,建议先在测试环境验证,再逐步推广到生产节点。
6. 日志与 worker I/O 调优
一句话总结: access_log 缓冲把多个日志条目合并写入,降低磁盘 I/O 对 worker 的拖累;日志本身也要做分级与滚动,避免日志 I/O 抢占业务 I/O。
日志是容易被忽视的性能杀手。默认配置下每个请求都同步写一行日志,高并发时磁盘 I/O 会抢占业务 I/O。Nginx 的 access_log 支持缓冲:先攒够缓冲区大小或达到刷新间隔再批量写盘,显著降低系统调用次数。
http {
# 缓冲日志写入:4k 缓冲 + 5 秒刷新
access_log /var/log/nginx/access.log main buffer=4k flush=5s;
# 关闭健康检查与静态资源的日志,减少无效写入
location = /healthz {
access_log off;
}
}
buffer=4k flush=5s 的组合在大多数场景都能把日志 I/O 开销降一个数量级。代价是日志的实时性下降(最多延迟 5 秒),对需要实时日志分析的系统可以在接入侧用更小的 flush 值。日志滚动(logrotate)也要避免「压缩进程与业务抢 I/O」:配置 compress 时放在低峰期执行,并设置合理的滚动周期,防止日志文件无限增长把磁盘写满。
日志量本身也要治理。把访问量最大的静态资源、健康检查、探活请求从 access_log 中排除,能省下大量磁盘与 I/O;错误日志按 error_log 分级,生产环境用 warn 级别避免 debug 日志拖垮性能。日志与业务 I/O 的隔离:如果条件允许,把日志目录放在独立磁盘或 SSD 上,避免与业务数据盘争抢带宽。
# 分级错误日志 + 按 location 关闭访问日志
error_log /var/log/nginx/error.log warn;
日志 I/O 调优的验证同样靠指标:观察磁盘的 iowait 与日志目录的写吞吐。如果 access_log 的写入量占比过高,优先考虑加缓冲、减日志条目,而不是盲目换更强的磁盘。日志是排错的第一手资料,调优要在「完整记录」与「减少开销」之间找到平衡,而不是为了性能牺牲排错能力。
7. 压测与瓶颈定位
一句话总结: 压测要按连接数梯度逼近瓶颈,结合 stub_status 指标与火焰图定位是 worker、锁、内存还是 I/O 在兜底。
调优必须量化验证。压测工具推荐 wrk 或 h2load(HTTP/2),压测命令要覆盖「低并发到高并发」的梯度,逐步逼近瓶颈点。单点压测容易得到「假性峰值」,正确的做法是记录每个并发梯度下的吞吐与延迟曲线,观察拐点在哪:
# wrk 压测示例:100 并发持续 30 秒
wrk -t8 -c100 -d30s --latency https://api.example.com/
# h2load 压测 HTTP/2:并发流与连接数分离
h2load -c 10 -m 200 -n 100000 https://api.example.com/
压测期间盯住 Nginx 的 stub_status 与系统指标。stub_status 的 Active connections、requests/s 反映 Nginx 视角的吞吐;mpstat、iostat、vmstat 反映 CPU、磁盘与内存的真实消耗。如果压测中吞吐不升而延迟陡增,说明进入了瓶颈区,需要判断瓶颈在哪一层:
# 观测 CPU 与中断分布
mpstat -P ALL 1 # 是否有 worker 单核打满
iostat -x 1 # 磁盘是否成为瓶颈
vmstat 1 # 上下文切换与 r 队列
瓶颈定位的常见结论与对应解法:worker 单核打满 → 检查 CPU 亲和与 multi_accept;上下文切换过高 → 检查 accept_mutex 与 reuseport;磁盘 iowait 高 → 检查日志缓冲与临时文件写入;Active connections 触顶 → 检查 worker_connections 与 fd 上限;内存换页 → 压缩缓冲配置。对 CPU 密集的 Lua 逻辑,用火焰图(如 perf + 脚本)定位热点函数:
# 抓取 Nginx worker 的 CPU 火焰图
perf record -F 99 -p $(pgrep -f 'nginx: worker') -g -- sleep 30
perf script > out.perf
# 交给 FlameGraph 脚本生成火焰图
压测的最终目标是找到「当前的瓶颈与下一级瓶颈」。每次调整只改一个变量,压测对比前后吞吐与延迟,确认收益后再动下一项。调优不是把参数调到「网上说的最优值」,而是调到「与你的流量模型匹配的值」——压测方法论与线上监控共同保证这一点。上线前用 1.5~2 倍峰值流量压测,验证系统在峰值下仍有富余,为突发流量留出空间。
8. 总结
| 层次 | 关键旋钮 | 常见瓶颈与解法 |
|---|---|---|
| 进程模型 | worker_processes、worker_cpu_affinity | 单核打满 → 核对核数配置与亲和 |
| 事件循环 | use epoll、multi_accept、reuseport | 惊群/锁竞争 → reuseport + multi_accept |
| 连接 | keepalive_timeout、keepalive_requests | 握手开销高 → 加大复用;空闲占用高 → 压缩 timeout |
| 缓冲内存 | client_body/proxy_buffers、sendfile | 频繁落盘 → 匹配对象大小分布给缓冲 |
| 内核参数 | somaxconn、tcp_fastopen、fd 上限 | 连接建立超时 → 核对内核与 Nginx backlog |
| 日志 I/O | access_log buffer、日志分级 | iowait 高 → 加缓冲、减条目 |
| 压测验证 | wrk/h2load、stub_status、火焰图 | 吞吐触顶 → 定位 worker/锁/内存/I/O 哪层兜底 |
Nginx 性能调优的本质是让「进程模型、事件循环、连接缓冲、内核参数」四层与你的流量模型匹配。调优没有一劳永逸的参数集——业务流量变了,最优参数也会变。可持续的做法是:用压测建立基准线,每次调整只改一个变量并量化对比,把验证过的参数沉淀为基线配置,再用线上监控持续校准。理解事件驱动模型是这一切的前提:Nginx 的高并发来自「少量进程 + 事件驱动」而非堆线程,任何调优动作都要回到这个模型来评估它的合理性。
延伸阅读
- Nginx 日志分析与性能调优 — log_format 与压测定位瓶颈
- Nginx 监控与可观测性 — stub_status 指标与告警
- Nginx 核心配置结构 — 指令上下文与热重载
- Nginx 静态资源与页面加速 — sendfile 与零拷贝实践
- Nginx 反向代理与负载均衡 — keepalive 与连接池配置
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。