Nginx高性能架构:进程模型与事件驱动机制深度解析
Executive Summary
核心观点(金字塔原理)
结论先行: Nginx通过Master-Worker进程模型结合异步非阻塞的epoll事件驱动机制,实现了轻量级高并发处理能力,是处理网络I/O密集型应用的理想选择。
支撑论点:
- Master-Worker进程模型实现了管理与处理分离,Worker数量与CPU核数保持一致可避免上下文切换开销
- 异步非阻塞事件处理机制使单个Worker进程能循环处理多个准备好的事件,无需为每个请求创建线程
- epoll模型配合accept_mutex锁机制解决了惊群问题,CPU亲缘性绑定进一步优化了缓存命中率
SWOT 分析
| 维度 | 分析 |
|---|---|
| S 优势 | 单Worker单线程模型避免锁竞争和上下文切换;异步非阻塞机制支持大量并发连接;内存消耗低,性能稳定 |
| W 劣势 | 不适合CPU密集型计算任务;Worker进程数受CPU核数限制;对长连接任务的处理需要特殊优化 |
| O 机会 | 网络I/O密集型的Web服务器、反向代理、负载均衡场景;高并发低延迟的API网关 |
| T 威胁 | 配置不当可能导致Worker进程相互竞争CPU资源;信号处理和定时器管理增加了编程复杂度 |
适用场景
- 高并发Web服务器和反向代理服务
- 需要处理大量并发连接的网络I/O密集型应用
- 静态资源服务、负载均衡、API网关等场景
Nginx-进程模型以及事件模型
- 优势:廉价;可扩展性;增加吞吐量;灵活性;可用性;大量并发请求发散到多台节点分别处理,减少用户等待时间。其次单个负载运算分担到多台节点设备上,每台设备处理结束后,将结果汇总返回,系统的处理能力得到大幅的提高。
- 解决的问题: 如果一个请求产生一个线程来处理,那么线程数就是并发数,那么显而易见的,就是会有很多线程在等待中。等什么?最多的应该是等待网络传输。并且这么多的线程造成CPU的上下文的切换,造成性能瓶颈(比如Tomcat)。而Nginx异步非阻塞,每个请求花费在请求上的时间片不多。这就解决了高并发的问题。异步,非阻塞,使用epoll,和大量细节处的优化。webserver刚好属于网络io密集型应用,不算是计算密集型。
进程模型: Master/Worker
- 进程模型: Master/Worker,一个Master进程生成一个或多个Worker进程。Master进程主要用来管理Worker进程,包含接收外界的信号,向各个Worker发送信号,监控Worker进程的运行状态,当Worker进程退出后(异常情况下)会自动重新启动新的Worker进程,而基本的网络事件都是放在Worker进程中来处理,多个Worker进程之间是对等的,它们同时来竞争来客户端的请求,各个进程之间相互独立,Worker进程个数一般与CPU核数保持一致,Nginx提供了
cpu亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效 ,这些都与Nginx的进程模型与事件模型有关 - 每个Worker都是从Master进程fork产生的,在Master里先建立好了listen的socket,然后做的fork产生多个Worker,这样每个Worker都可以进行accept这个socket。当一个请求过来以后,所有的Worker都会接收到一个信号通知,而只有一个Worker进程能进行处理,其他的Worker则将处理失败,这就是
惊群现象.Nginx在这种情况下,新增了accept_mutex(可控项,默认打开),就是一个accept的共享锁,这样同一时刻就只有一个Worker能处理请求了。当一个Worker进程accept这个请求之后,则进行读取请求-》解析请求-》处理请求-》产生结果并返回给请求方,最后断开。一个请求只有一个Worker进程来进行处理。 - master进程通过socketpair向worker子进程发送命令,终端也可以向master发送各种命令,子进程通过发送信号给master进程的方式与其通信,worker之间通过unix套接口通信。

事件模型
- Web服务器的事件通常有三种机制: 网络事件,信号,定时器
- 事件驱动:通信机制采用epoll模型,支持更大的并发连接。
- Nginx的事件处理模型
while (true) {
for t in run_tasks:
t.handler();
update_time(&now);
timeout = ETERNITY;
for t in wait_tasks:
if (t.time <= now) {
t.timeout_handler();
} else {
timeout = t.time - now;
break;
}
nevents = poll_function(events, timeout);
for i in nevents:
task t;
if (events[i].type == READ) {
t.handler = read_handler;
} else (events[i].type == WRITE) {
t.handler = write_handler;
}
run_tasks_add(t);
}

类比Tomcat:
- 对tomcat来说,每一个进来的请求(request)都需要一个线程,直到该请求结束。如果同时进来的请求多于当前可用的请求处理线程数,额外的线程就会被创建,直到到达配置的最大线程数(maxThreads属性值)。如果仍就同时接收到更多请求,这些来不及处理的请求就会在Connector创建的ServerSocket中堆积起来,直到到达最大的配置值(acceptCount属性值)。至此,任何再来的请求将会收到connection refused错误,直到有可用的资源来处理它们。
- 这里我们关心的是tomcat能同时处理的请求数和请求响应时间,显然Connector元素的maxThreads和acceptCount属性对其有直接的影响。无论acceptCount值为多少,maxThreads直接决定了实际可同时处理的请求数。而不管maxThreads如何,acceptCount则决定了有多少请求可等待处理。然而,不管是可立即处理请求还是需要放入等待区,都需要tomcat先接受该请求(即接受client的连接请求,建立socketchannel),那么tomcat同时可建立的连接数(maxConnections属性值)也会影响可同时处理的请求数。
通过几个问题,进一步理解Nginx的网络事件处理机制
- 问题1: Nginx采用worker进程来处理请求,一个worker进程只有一个主线程,那么有多少个worker子进程就能处理多少个并发,那么能够处理的并发数有限。概括的将,Nginx如何实现高并发?
- 回答1:采用异步非阻塞的事件处理机制。之所以能够并发处理大量的未处理完的请求,是通过异步非阻塞方式,由进程循环处理多个准备好的事件。以epoll为例,为准备好的事件都会放入epoll中,只要有事件准备好,就会进行处理。
- 问题2:何为异步非阻塞方式
- 回答2:见http://blog.csdn.net/yankai0219/article/details/8018232
- 问题3:Nginx与Apache对于高并发处理上的区别。
- 回答3:对于Apache,每个请求都会独占一个工作线程,当并发数到达几千时,就同时有几千的线程在处理请求了。这对于操作系统来说,占用的内存非常大,线程的上下文切换带来的cpu开销也很大,性能就难以上去,同时这些开销是完全没有意义的。 对于Nginx来讲,一个进程只有一个主线程,通过异步非阻塞的事件处理机制,实现了循环处理多个准备好的事件,从而实现轻量级和高并发。
- 问题4:为何推荐worker的个数为cpu的个数?
- 回答4:因为更多的worker书,只会导致进程相互竞争cpu资源,从而带来不必要的上下文切换
- 4如何处理信号和定时器呢?
- 首先,信号的处理。对nginx来说,有一些特定的信号,代码着特定的意义。信号会中断掉程序当前的运行,在改变状态后,继续执行。如果是系统调用,则可能会导致系统调用的失败,需要重入。关于信号的处理,大家可以学习一些专业书籍,这里不多说。对于nginx来说,如果nginx正在等待事件(epoll_wait时),如果程序收到信号,在信号处理函数处理完后,epoll_wait会返回错误,然后程序可再次进入epoll_wait调用。
- 另外,再来看看定时器。由于epoll_wait等函数在调用的时候是可以设置一个超时时间的,所以nginx借助这个超时时间来实现定时器。nginx里面的定时器事件是放在一个最小堆里面,每次在进入epoll_wait前,先从最小堆里面拿到所有定时器事件的最小时间,然后计算出epoll_wait的超时时间,然后进入epoll_wait。所以,当没有事件产生,也没有中断信号时,epoll_wait会超时,也就是说,定时器事件到了。这时,nginx会检查所有的超时事件,将他们的状态设置为超时,然后再去处理网络事件。由此可以看出,当我们写nginx代码时,在处理网络事件的回调函数时,通常做的第一个事情就是判断超时,然后再去处理网络事件。