i007.cc

i007.cc

优先队列-降维打击

05.价值资料

Nginx 网络模型与基本原理

Nginx 和 Redis 在网络层的设计哲学很像,都是事件驱动 + IO 多路复用,但因为定位不同(Redis 是单机内存数据库,Nginx 是要吃满多核 CPU 的反向代理/Web 服务器),两者在进程/线程模型上走了不同路线:Redis 主要靠单线程,Nginx 走的是多进程 + 每进程单线程事件循环

整体架构:Master-Worker 多进程模型

Nginx 启动后会有一个 Master 进程和多个 Worker 进程。Master 进程不处理任何实际的网络请求,它的职责是:读取和校验配置文件、创建监听 socket、管理 Worker 进程的生命周期(fork、监控、重启崩溃的 worker)、接收信号完成平滑重启和热更新配置(比如 nginx -s reload 不会中断现有连接)、处理日志切割等运维类信号。

真正处理请求的是 Worker 进程,数量通常配置成和 CPU 核数一致(worker_processes auto),每个 Worker 是完全独立的进程,彼此之间不共享内存(除了一些只读的共享内存区,比如 upstream 的健康检查状态、限流计数器这类需要跨 worker 共享的数据,会显式用共享内存 + 锁机制),互不影响,一个 Worker 崩溃不会波及其他 Worker,也不需要处理复杂的多线程同步问题。这是 Nginx 选择多进程而不是多线程的核心原因之一:进程级隔离,故障域更小,而且避免了多线程共享状态带来的锁竞争和数据竞争问题。

每个 Worker 内部:单线程事件循环 + IO 多路复用

关键点在于,每个 Worker 进程内部,处理网络 IO 的模型和 Redis 的思路几乎一模一样:单线程跑一个事件循环,底层用 epoll(Linux)/kqueue(BSD)这类多路复用机制,一个 Worker 就能同时管理成千上万个连接,不需要一个连接一个线程/进程。

这也是为什么 Nginx 能用很少的 Worker 数量(通常等于 CPU 核数,而不是像 Apache 传统的 prefork/worker 模式那样为每个连接分配线程)扛住海量并发连接,这就是常说的 C10K 问题的经典解法:用异步非阻塞 + 事件驱动取代”一个连接一个线程”的同步阻塞模型,省掉了海量线程切换和内存开销。

具体到一个请求的生命周期,Worker 的事件循环大致是:epoll_wait 拿到就绪的文件描述符(新连接到达、某个连接可读、可写),分发给对应的事件处理器,处理器以非阻塞方式尽量少地占用 CPU(比如读到数据但还不完整,就注册好回调直接返回,不会阻塞等待),然后回到 epoll_wait 继续处理下一批就绪事件。整个处理过程被拆成很多小的状态机步骤,而不是一次性同步跑完,这样单线程也不会被某个慢连接卡住。

多个 Worker 抢同一个监听端口:惊群问题

所有 Worker 进程会监听同一个端口(同一个 socket 描述符是 fork 时从 Master 继承下来的),当一个新连接到达时,理论上所有 Worker 的 epoll 都会被唤醒去竞争 accept,但最终只有一个能抢到,其余的白白被唤醒又睡回去,这就是惊群问题(thundering herd),会造成不必要的上下文切换开销。

Nginx 早期版本用 accept_mutex 这个应用层锁来解决:同一时刻只让一个 Worker 去监听 accept 事件,其他 Worker 不去竞争,规避惊群,但这个方案本质是把并发 accept 串行化,牺牲了一部分接受连接的效率。较新的 Linux 内核提供了 EPOLLEXCLUSIVE 标志,让内核只唤醒一个(或少数几个)等待中的 epoll 实例,从内核层面解决惊群,不需要应用层加锁,现在的 Nginx 会优先使用这种机制。

高性能的其他关键点

除了事件驱动模型本身,Nginx 还用了几个系统层面的优化来榨取性能。零拷贝技术方面,静态文件传输大量使用 sendfile 系统调用,数据直接在内核空间从磁盘文件描述符拷贝到 socket 描述符,不需要经过用户态,省掉了传统 read+write 方式的多次内存拷贝和上下文切换,这在做静态资源服务器或者反向代理时对吞吐量提升明显。

内存管理方面,Nginx 用内存池(memory pool)机制,每个请求生命周期内的内存分配都从预先申请好的内存池里拿,请求结束时整体释放,避免频繁调用 malloc/free 带来的开销和内存碎片。

模块化架构方面,Nginx 的功能通过模块组合实现,核心只做事件循环和请求调度,具体功能(HTTP 处理、反向代理、负载均衡、gzip 压缩、SSL 终结等)都是模块,请求会依次经过一条处理阶段链(Nginx HTTP 请求处理分成多个 phase,比如 post-read、rewrite、access、content 等阶段),每个阶段挂载相应的模块处理器,这种设计让 Nginx 的核心非常精简,扩展性也好(虽然传统 Nginx 模块需要编译时静态链接,不像 OpenResty/Nginx 商业版那样支持动态模块热加载,但也有 --add-dynamic-module 支持部分动态加载能力)。

和 Redis 网络模型的类比与差异

如果把两者放一起对比会更容易记:相同点是核心都基于 Reactor 模式,都用 IO 多路复用(epoll)做事件分发,都追求异步非阻塞、避免线程切换开销。不同点在于并发单元的选择,Redis 是单机内存数据库,操作本身是纯内存计算,瓶颈更多在网络 IO 和内存访问,所以选择单线程执行 + (6.0 后)多线程分担网络 IO 这种相对轻量的方案;Nginx 要处理的是大量并发连接下的网络转发、静态文件传输、反向代理这类天然可以并行、且要吃满多核 CPU 的场景,所以选择多进程,每个进程独立跑一份事件循环,天然利用多核,同时用进程隔离规避了多线程共享状态的复杂性。

一句话总结

Nginx 靠 Master-Worker 多进程架构保证进程级隔离和多核利用,每个 Worker 内部靠单线程 + epoll 事件循环支撑海量并发连接,再叠加 sendfile 零拷贝、内存池、EPOLLEXCLUSIVE 解决惊群等一系列系统级优化,共同撑起了它在高并发场景下的高吞吐、低延迟表现。

如果想继续深入,可以聊聊 HTTP 请求处理的 11 个 phase 具体是怎么串起来的、Nginx 反向代理时和 upstream 之间的连接复用(keepalive connection pool)、或者 OpenResty/Lua 是怎么在这套事件模型上做非阻塞扩展的。

发表回复