![Nginx底层设计与源码分析](https://wfqqreader-1252317822.image.myqcloud.com/cover/686/38773686/b_38773686.jpg)
2.1 Nginx进程模型
随着大多数系统需要应对海量的用户流量,人们越来越关注系统的高可用、高吞吐、低延时、低消耗等特性,于是小巧且高效的Nginx走进大家的视野,并很快受到人们的青睐。Nginx的全新进程模型与事件驱动设计使其能轻松应对C10K甚至C100K高并发场景。
Nginx使用了Master管理进程(Master进程)和Worker工作进程(Worker进程)的设计,如图2-1所示。
![](https://epubservercos.yuewen.com/520661/20376623908129206/epubprivate/OEBPS/Images/018-01.jpg?sign=1739982191-H2Ydu7YwGpA0DZJbNEWcr06P2pIRXl92-0-c62ed03e0563b89b8ce09ce3490c9ada)
图2-1 Master-Worker进程模型
Master进程负责管理各个Worker进程,通过信号或管道的方式来控制Worker进程的动作。当某个Worker进程异常退出时,Master进程一般会启动一个新的Worker进程替代它。各Worker进程是平等的,它们通过共享内存、原子操作等一些进程间通信机制实现负载均衡。多进程模型的设计充分利用SMP(Symmetrical Multi-Processing,对称多处理)多核架构的并发处理能力,保障了服务的健壮性。
同样是基于多进程模型,为什么Nginx具备如此强的性能与超高的稳定性,其原因有以下几点。
(1)异步非阻塞
Nginx的Worker进程全程工作在异步非阻塞模式下。从TCP连接的建立到读取内核缓冲区里的请求数据,再到各HTTP模块处理请求,或者反向代理时将请求转发给上游服务器,最后再将响应数据发送给用户,Worker进程几乎不会阻塞。当某个系统调用发生阻塞时(例如进行I/O操作,但是操作系统还没将数据准备好),Worker进程会立即处理下一个请求。当处理条件满足时,操作系统会通知Worker进程继续完成这次操作。一个请求可能需要多个阶段才能完成,但是整体上看每个Worker进程一直处于高效的工作状态,因此Nginx只需要少数Worker进程就能处理大量的并发请求。当然,这些得益于Nginx的全异步非阻塞事件驱动框架,尤其是在Linux 2.5.45之后操作系统的I/O多路复用模型中新增了epoll这款“神器”,让Nginx换上全新的发动机一路狂飙到性能之巅。
(2)CPU绑定
通常,在生产环境中配置Nginx的Worker进程数量等于CPU核心数,同时会通过worker_cpu_affinity将Worker进程绑定到固定的核上,让每个Worker进程独享一个CPU核心,这样既能有效避免CPU频繁地上下文切换,也能大幅提高CPU缓存命中率。
(3)负载均衡
当客户端试图与Nginx服务器建立连接时,操作系统内核将socket对应的fd返回给Nginx,如果每个Worker进程都争抢着去接受(Accept)连接就会造成著名的“惊群”问题,也就是最终只允许有一个Worker进程成功接受连接,其他Worker进程都白白地被操作系统唤醒,这势必会降低系统的整体性能。另外,如果有的Worker进程运气不好,一直接受失败,而有的Worker进程本身已经很忙碌却接受成功,就会造成Worker进程之间负载的不均衡,也会降低Nginx服务器的处理能力与吞吐量。Nginx通过一把全局的accept_mutex锁与一套简单的负载均衡算法就很好地解决了这两个问题。首先每个Worker进程在监听之前都会通过ngx_trylock_accept_mutex无阻塞地获取accept_mutex锁,只有成功抢到锁的Worker进程才会真正监听端口并接受新的连接,而抢锁失败的Worker进程只能继续处理已接受连接的事件。其次,Nginx为每个Worker进程设计了一个全局变量ngx_accept_disabled,并通过如下方式对该值进行初始化:
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n
其中,connection_n表示每个Worker进程可同时接受的连接数,free_connnection_n表示空闲连接数。Worker进程启动时,空闲连接数与可接受连接数相等,也就是ngx_accept_disabled初始值为-7/8×connection_n。当ngx_accept_disabled为正数时,表示空闲连接数已经不足总数的1/8了,说明该Worker进程十分繁忙。于是,它在本次事件循环时放弃争抢accept_mutex锁,专注处理已有的连接,同时将自己的ngx_accept_disabled减一,下次事件循环时继续判断是否进入抢锁环节。下面的代码摘要展示了上述算法逻辑:
if (ngx_use_accept_mutex) { if (ngx_accept_disabled > 0) { ngx_accept_disabled--; } else { if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; } …… }
总体来说,这种设计略显粗糙,但胜在简单实用,一定程度上维护了各Worker进程的负载均衡,避免了单个Worker进程耗尽资源而拒绝服务,提升了Nginx服务器的性能与健壮性。
另外,Nginx也支持单进程模式,但是这种模式不能发挥CPU多核的处理能力,通常只适用于本地调试。