Nginx学习笔记:工作原理

Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单,仅仅通过查找配置文件将客户端请求映射到一个location block进行工作,而在这个location中所配置的每个指令将会启动不同的模块去完成相应的工作。

工作模块的分类可以从结构和功能上各分三类,从结构上大体分为如下几部分:

工作模块

Nginx的工作流程就是基于按功能划分的三类模块运行的,Nginx本身做的工作实际很少,因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块(多个location可以复用同一个模块)。handler模块负责处理请求,完成响应内容的生成,而filter模块对响应内容进行处理。

工作流程:(图摘自网络)

工作流程

Nginx的进程模型

Nginx有单工作进程和多工作进程两种模式。

  • 单工作进程指除主进程外还有一个工作进程,且该工作进程是单线程的。
  • 多工作进程模式下,每个工作进程包含多个线程。

默认为单工作进程模式,正常情况下,Nginx启动后,会有一个master进程和多个worker进程(worker进程非之前提到的工作进程)。

master进程

主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。master进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。

worker进程

worker进程处理基本的网络事件。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致。

worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接。如何做到只有一个worker进程处理这个连接呢?

  • 首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。
  • 所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。
  • 当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,完成请求与响应。

Nginx进程模型如下所示:(图摘自21CTO)

进程模型

Nginx为什么性能好?

说到了工作方式,那么一定要讨论性能的问题。因为,性能的好坏一定是工作方式直接造成的,相较于Apache,Nginx的直接优势在于处理高并发的性能好于前者。

回忆一下Apache的工作方式。每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销很大,自然性能就上不去了。

Nginx采用的是进程的工作方式,连接由每个worker进程处理,独立的进程不需要加锁,省去了大量的锁开销。就算一个进程因异常而终止也不会影响其他进程。但问题又来了,之前提到worker进程的数量一般等于CPU核心数量,一般地电脑CPU核心数量也就几个,每个worker进程能只有一个主线程,这又如何处理高并发呢?

Nginx的另一个高明之处就是采用了异步非阻塞方式,该方式可以同时处理成千上万个请求。虽然Apache也有异步非阻塞版本,但与其自身的一些基础模块冲突,并不常用。流行的Apache大多都是一步阻塞的工作方式。

异步阻塞和异步非阻塞

一个完整的请求响应流程:首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。

具体到系统底层,就是读写事件,而当读写事件没有准备好时,如果采用阻塞调用,那就只能等了,等事件准备好了再继续。阻塞调用会进入内核等待,cpu就会让出去给别人用了,对单线程的worker来说,显然不合适,当网络事件越多时,大家都在等待呢,cpu空闲下来没人用,cpu利用率自然上不去了,更别谈高并发了。

而如果采用非阻塞调用,事件没有准备好,马上返回EAGAIN,告诉你,事件还没准备好,过会再来吧。过一会,再来检查一下事件,直到事件准备好了为止。在这期间,可以先去做其它事情,然后再来看看事件好了没。虽然不阻塞了,但不时地过来检查一下事件的状态,带来的开销也是不小的。所以,才会有了异步非阻塞的事件处理机制。

异步非阻塞机制具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。它们提供了一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。以epoll为例,当事件没准备好时,放到epoll里面,事件准备好了,我们就去读写,当读写返回EAGAIN时,我们将它再次加入到epoll里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll里面等着。这里的并发请求,是指未处理完的请求,线程只有一个,所以只能同时处理一个请求,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,可以理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。

标签: Nginx, 笔记, 开发, 工作原理, 模块

添加新评论

已有 2 条评论

  1. ⑨BIE ⑨BIE

    哇说的好清楚,感谢解惑

    1. Syc Syc

      有收获在、很开心。欢迎在网上分享自己的经验,庆祝开源精神。