Nginx 高性能架构与模块开发
2002 年 Igor Sysoev 动手写 Nginx,就是为了回答 C10K 问题:单机如何扛住一万个并发连接?当时的主流答案 Apache 为每个连接开一个进程/线程,一万连接就是一万个执行体——内存与调度开销先把机器压垮。Nginx 换了一条路:少量 worker 进程 + 每个 worker 一个事件循环 + 操作系统就绪通知(epoll),用几千字节的连接状态取代一整个线程。本文拆解这套架构的每个关键决策,用一段模拟实验把两种模型的差距量化出来,最后讲清楚 Nginx 模块在请求处理流水线中的位置与开发要点。
一、C10K 问题与两种模型的成本结构
并发连接的本质成本有两项:每个连接的内存与每次调度的开销。
- 线程每连接模型:一个连接绑定一个线程。线程默认栈 8MB(虚拟内存),一闲连接也占着;更要命的是调度——操作系统要在所有可运行线程间轮转,大量切换花在『唤醒 → 发现没数据 → 再睡眠』的空转上。连接数翻倍,两项成本都线性翻倍。
- 事件驱动模型:一个 worker 里只有一个事件循环。所有连接注册到 epoll,内核只把已就绪的连接通过一次 epoll_wait 返回;worker 对就绪连接逐个回调处理,不阻塞、不切换。每个连接只占一个连接结构体(约 1KB 级别),调度开销只随活跃连接数增长,与总连接数完全解耦。
二、动手实验:把差距量化出来
下面代码模拟一万并发、1% 活跃度的典型长连接场景,对比两种模型的内存占用与调度行为。零依赖,Python 3 直接运行:
import random
random.seed(21)
CONNS = 10000 # 1 万并发 (C10K)
ACTIVE_RATIO = 0.01 # 任意时刻约 1% 连接活跃
def sim_thread_per_conn():
mem_mb = CONNS * 8 # 每线程 8MB 栈
ctx_switches, useful = 0, 0
for i in range(CONNS):
ctx_switches += 1 # 切到该线程
if random.random() < ACTIVE_RATIO:
useful += 1 # 活跃的才干活
# 否则: 唤醒-空转-再睡眠, 纯浪费
return mem_mb, ctx_switches, useful
def sim_event_driven():
mem_mb = CONNS * 1 / 1024 # 每连接约 1KB 状态
active = [random.random() < ACTIVE_RATIO
for _ in range(CONNS)]
ready = sum(active) # epoll 一次收集全部就绪连接
return mem_mb, ready # 分发次数 = 就绪数
mt, st, ut = sim_thread_per_conn()
me, re_ = sim_event_driven()
print(f'线程模型: {mt:,}MB, {st:,} 次切换')
print(f'事件模型: {me:.1f}MB, {re_} 次分发')
在沙箱(Python 3,无第三方库)上的真实运行输出:
并发连接: 10000, 活跃比例: 1%
== 内存占用 ==
线程每连接: 80,000 MB (1万线程 x 8MB 栈)
事件驱动 : 9.8 MB (1万连接状态 x 1KB)
内存比 : 8,192x
== 调度开销 ==
线程每连接: 10,000 次线程切换 (含大量"唤醒->发现没事->再睡眠"的空转)
事件驱动 : 1 次就绪通知收集 + 109 次事件分发 (epoll 只返回就绪连接)
其中真正干活的连接: 106 个 (两种模型相同)
== 结论 ==
线程模型把 9,894 次切换浪费在不活跃连接上
事件模型的调度开销只随活跃连接数(109)增长, 与总连接数(10000)解耦
校验通过: 事件驱动在万级并发下内存省三个数量级, 调度开销与活跃数成正比
三、结果解读:解耦是全部的关键
- 内存差 8192 倍:线程栈的 8MB 是虚拟内存口径,物理占用虽没这么夸张,但一闲连接绑一个执行体的模型,内存随连接数线性增长的事实不变;事件模型每个连接只是一个状态结构体,一万连接不到 10MB。这决定了单机连接容量的量级差。
- 调度开销的解耦:线程模型一万次切换里只有 106 次干了活,99% 是空转;事件模型的 109 次分发全部对应真实就绪的连接。注意那个关键性质:事件循环的成本曲线跟着活跃数走,不跟总连接数走——这就是『十万连接但每秒只有几百个请求』的长连接场景(推送、IM、物联网接入)几乎只能选事件驱动的原因。
- 事件模型的代价:事件循环里任何一段阻塞代码(同步磁盘读、CPU 密集计算、调用了阻塞库)都会冻结该 worker 上的所有连接。Nginx 的对策是 thread pool(把磁盘 AIO 等卸载到线程池)与多 worker 分摊,这也是写 Nginx 模块时最重要的纪律:handler 里绝不许阻塞。
四、进程架构:master + worker 的分工
- master 进程:不碰流量,只管配置解析、worker 拉起与监控、热重载(reload)与热升级(平滑替换二进制,老 worker 处理完存量连接后退出,新 worker 接管新连接——零停机升级的关键);
- worker 进程:通常设为 CPU 核数(
worker_processes auto),各自独立跑事件循环,进程间无共享内存锁的竞争(少量共享如限速限流用共享内存 + 原子操作)。多进程而非多线程,换来的是故障隔离与免锁设计; - 惊群与负载均衡:多个 worker 监听同一端口,内核新连接到达时历史上存在惊群问题。Nginx 用 accept_mutex 互斥 + 各 worker 的负载自适应(忙者少抢)缓解;Linux 4.5+ 的
SO_REUSEPORT(reuseport 指令)让内核直接按哈希分发到各 worker 的独立监听队列,是当前的推荐配置。
五、请求处理流水线:模块挂在哪
Nginx 把 HTTP 请求处理切成 11 个阶段(phase),模块的本质是往阶段上挂 handler:
NGX_HTTP_POST_READ_PHASE 读请求头后最先进入(如 realip)
NGX_HTTP_SERVER_REWRITE_PHASE server 块级重写(rewrite 指令)
NGX_HTTP_FIND_CONFIG_PHASE 匹配 location(不可挂模块)
NGX_HTTP_REWRITE_PHASE location 级重写
NGX_HTTP_ACCESS_PHASE 访问控制(allow/deny/auth)
NGX_HTTP_CONTENT_PHASE 生成响应内容(核心战场)
NGX_HTTP_LOG_PHASE 记录访问日志
内容生成阶段之外,还有两类重要模块形态:handler 模块(直接产出响应,如静态文件、proxy_pass 反代、FastCGI)与 filter 模块(对响应做流式加工,如 gzip 压缩、sub_filter 内容替换——filter 链式设计,输出依次流经所有启用的 filter)。负载均衡本身是 upstream 框架下的另一类扩展点(本站《负载均衡算法》一篇的 round-robin/least_conn/hash 都在这里实现)。
六、模块开发要点
- 骨架:一个模块 = 指令定义数组(ngx_command_t)+ 模块上下文(创建/合并配置、挂载点)+ 模块结构体(ngx_module_t)+ handler 函数;编译方式是
./configure --add-module=/path/to/module静态编进二进制。 - 内存池:模块内分配一律用请求级内存池(
ngx_palloc(r->pool)),请求结束统一释放——没有 free,也就没有泄漏与悬垂指针,这是 Nginx 内存管理最优雅的设计;长生命周期数据才用 cycle 级池或共享内存。 - 非阻塞纪律:handler 里读写 socket 必须走事件接口注册回调(读事件来再继续),同步阻塞调用会直接拖死整个 worker 的所有连接——这是新手写 Nginx 模块的第一大坑。
- 调试手段:
--with-debug编译 + error_log debug 级别;动态模块(load_module,1.9.11+)可以免重编译加载 .so,适合第三方模块分发。
七、常见误区
- worker 数开成几十上百:worker 绑定事件循环,超过核数只会增加调度竞争;正确值是核数,CPU 亲和用 worker_cpu_affinity 显式绑定。
- 在事件循环里做 CPU 密集活:如对大响应做 CPU 压缩级别过高的 gzip——要么调低级别,要么卸载到 thread pool / 旁路服务。
- 误以为 Nginx 擅长一切:它是顶级的反向代理/静态服务/负载均衡,但复杂业务逻辑应该交给后端应用或 OpenResty(Lua)这类脚本化扩展,而不是硬写 C 模块。
- 忽视 worker_connections 与文件描述符:单机并发上限 = worker_processes × worker_connections,同时受 fs.file-max 与 ulimit -n 约束,三者要一起调。
- 热重载期间重启风暴:reload 会同时存在新老两代 worker,内存瞬时翻倍;配置变更频繁且机器内存紧张时要把这个峰值纳入容量规划。
八、参考文献
- Kegel, D. The C10K Problem. kegel.com/c10k.html, 1999(问题的原始出处).
- Sysoev, I. Nginx Architecture and Internals. nginx.com 官方技术文档.
- Reese, W. Nginx: the High-Performance Web Server and Reverse Proxy. Linux Journal, 2008.
- Gammo, L. et al. Comparing and Evaluating epoll, select, and poll Event Mechanisms. Ottawa Linux Symposium, 2004.
- OpenResty 官方文档: NGINX C Module Development / Lua-nginx-module. openresty.org.