第六章 Nginx事件模型

1. WEB请求处理机制

1.1. WEB 服务器常用的处理请求方式

1.1.1. 多进程方式

Apache 的 Prefork 模式就是多进程处理方式,当收到客户端的请求时,由服务器的主进程生成一个子进程来和该客户端进行交互。

该方式的优势是各个进程之间相互隔离,保证的服务器的稳定性。但是缺点很明显,对服务器的资源,尤其是内存的消耗很大,进程之间的上下文切换太重量级了,不适合大并发的场景。

1.1.2. 多线程的方式

Apache 的 worker模式和event模式就是多线程方式,当收到客户端的请求时,由服务器的工作进程生成一个线程来处理。

该方式的优势是大大增强了服务器处理并发请求的能力,但是socket本身是阻塞状态的,当有10万并发连接访问时,就需要10万的线程来维护这些请求,线程的上下文切换非常频繁,会引发很严重的性能问题。

1.1.3. 异步方式

  • 同步和异步

同步是指在发送方发出消息后,需要等待接收到接收方发回的响应,或者通过回调函数来接收到对方响应信息。

异步是指在发送方发出请求后,接收方不返回消息或者不等待返回消息。

  1. 通俗一点解释:
  2. 同步:同步就是我买了一个烧水壶,烧水壶没有提供响应的机制,导致我不知道水啥时候开
  3. 异步:异步就是我买了一个烧水壶,烧水壶提供了响应的机制,导致我可以知道水啥时候开
  • 阻塞和非阻塞

在网络通讯中,阻塞和非阻塞主要是指Socket的阻塞和非阻塞方式,而socket的实质是IO操作。

阻塞是指在IO操作返回结果之前,当前的线程处于被挂起状态,直到调用结果返回后才能处理其它新的请求。

非阻塞指在IO操作返回结果之前,当前的线程会继续处理其它的请求。

  1. 阻塞:和上面一样,我买了一个烧水壶,我要在旁边一直等着烧水壶开,这段时间,一点事情都没法做
  2. 非阻塞:我买了一个烧水壶,在烧水的这段时间,我可以去做其他事情
  • 同步阻塞方式(常用,简单)

发送方向接收方发送请求后,一直处理等待响应状态。接收方处理请求时进行IO操作,如果该操作没有立刻返回结果,将继续等待,直到返回结果后,才响应发送方,期间不能处理其它请求。

  1. 同步阻塞:烧水壶没有提供响应机制,并且我要一直在旁边守着,等待水壶烧开
  2. 同步非阻塞:烧水壶没有提供响应机制,但是我可以在这段期间,去干点别的事情,比如看看电视啊,打打游戏啊,然后每隔一段时间去看一眼水壶是否烧开
  • 异步非阻塞(常用)

发送方向接收方发送请求后,继续进行其它工作。接收方处理请求进行IO操作,如果没有立刻返回结果,将不再等待,而是处理其它请求。

  1. 异步阻塞:烧水壶提供了响应机制,但是我在旁边一直守着,等着水壶开
  2. 异步非阻塞:烧水壶提供了响应机制,并且我可以在这段时间内,去干点别的事情

1.2. Nginx服务器处理请求的方式

1.2.1. Nginx的异步非阻塞

Nginx 在服务启动后会产生一个主进程(master process)、多个工作进程(worker processes)、缓存加载进程(cache load processes)、缓存管理进程(cache manager processes),工作进程用于接收和处理客户端请求。

每个工作进程使用了异步非阻塞的方式,可以处理多个客户端的请求。当某个工作进程接收到客户端的请求后,使用事件驱动方式管理socket,将socket设置为非阻塞方式。

在涉及IO操作时,可以调用Linux文件系统提供的AIO接口,使用异步IO方式完成调用,针对任务繁重的IO操作可以将等待IO的操作卸载给线程池中其它线程处理,从而避免主线程(worker进程的主循环)阻塞。

1.2.2. Nginx处理请求的过程

image-20200824150943087.png

1562763279239-8cf10ff9-e222-409a-acdd-d20fe22dca16.png

2. 事件驱动模型

Nginx管理socket的方式称为事件驱动模型。

2.1. SELECT 库

各个版本的Linux和Windows平台都支持的基本事件驱动模型,并在接口定义上基本相同,使用步骤如下

  • 创建所关注事件的描述符集合,对于一个描述符,可以关注其上面的读(read),写(write),异常(Exception)事件,所以要创建三类文件描述符集合。
  • 调用底层提供的select()函数,等待事件发生。
  • 轮询所有事件描述符集合中的每一个事件描述符,检查是否有相应的事件发生。

Nginx在安装时未指定其它高性能事件驱动模型库,会自动编译安装该库。

2.2. POLL 库

Poll库是Linux平台的事件驱动模型,在Linux 2.1.23中引入。

Poll库与Select类似,区别是select会创建三类事件描述符,而poll只创建一类描述符集合,所以缩减了轮询的时间。

Nginx在安装时未指定其它高性能事件驱动模型库,会自动编译安装该库。

2.3. EPOLL库

无论是SELECT还是POLL事件驱动模型,都是需要使用轮询的方法检查连接是否有新的事件,在连接数非常多的时候,轮询会非常消耗性能。

Epoll库是Linux高性能事件驱动模型库,其效率远超poll和select库。Linux 2.6及以上的版本都可以使用。

  • 首先,当有新的socket连接时,nginx使用epoll库创建与socket相关联的文件描述符,并设置需要关注的事件,并将其添加到内核事件列表中。
  • 然后,epoll库等待内核通知即可。省掉了select和poll库中轮询文件描述符的步骤。

Epoll库支持一个进程打开最大数目的事件描述符,上限是系统可以打开文件的最大数目;同时epoll库的IO效率不随文件描述符的增加而线性下降,因为它只会对内核上报的”活跃的描述符进行操作。

  1. #最大并发连接数,此值和系统内存大小有关,内存多大,那么最大连接数,就可以有多大
  2. [root@nginx-01 ~]# free -h
  3. total used free shared buff/cache available
  4. Mem: 1.8G 124M 403M 8.7M 1.3G 1.4G
  5. Swap: 2.0G 0B 2.0G
  6. [root@nginx-01 ~]# cat /proc/sys/fs/file-max
  7. 181976

3. Nginx的进程

image-20210208102124320.png

3.1 进程的分类

3.1.1. master process

Nginx启动时运行的主要进程,主要功能如下

  • 读取Nginx配置文件并验证其有效性和正确性
  • 建立、绑定、关闭socket连接
  • 按照配置生成、管理进程和结束工作进程
  • 接收外界指令,如重启、退出等
  • 不中断服务,实现平滑升级,重启服务并应用新的配置
  • 编译和处理Perl脚本
  • 开启日志记录

3.1.2. worker processes

  • 接收并处理客户端请求
  • 将请求以此送入各个功能模块进行处理
  • IO调用,获取响应数据
  • 与后端服务器通信,接收后端服务器的处理结果
  • 缓存数据,访问缓存索引,查询和调用缓存数据
  • 发送请求结果,响应客户的请求
  • 接收主程序的指令

3.1.3. cache loader

在开启缓存服务器功能下,在Nginx主进程启动一段时间后(默认1分钟),由主进程生成cache loader,在缓存索引建立完成后将自动退出。

3.1.4. Cache Manager

在开启缓存服务器功能下,在Nginx主进程的整个生命周期内,管理缓存索引,主要对索引是否过期进程判断。

3.2. 进程之间的交互

3.2.1. 主进程与工作进程的交互

工作进程是有主进程生成的,主进程使用fork()函数,在Nginx服务器启动过程中主进程根据配置文件决定启动工作进程的数量,然后建立一张全局的工作表用于存放当前未退出的所有的工作进程,主进程生成工作进程后会将新生成的工作进程加入到工作进程表中,并建立一个单向的管道并将其传递给工作进程,该管道与普通的管道不同,它是由主进程指向工作进程的单项通道,包含了主进程向工作进程发出的指令、工作进程ID、工作进程在工作进程表中的索引和必要的文件描述符等信息。主进程与外界通过信号机制进行通信,当接收到需要处理的信号时,它通过管道向相关的工作进程发送正确的指令,每个工作进程都有能力捕获管道中的可读事件,当管道中有可读事件的时候,工作进程就会从管道中读取并解析指令,然后采取相应的执行动作,这样就完成了主进程与工作进程的交互。

image-20210208102836569.png

3.2.2. 工作进程与工作进程的交互

工作进程之间的通信原理基本上和主进程与工作进程之间的通信是一样的,只要工作进程之间能够取得彼此的信息,建立管道即可通信,但是由于工作进程之间是完全隔离的,因此一个进程想要直到另外一个进程的状态信息就只能通过主进程来设置了。

为了实现工作进程之间的交互,主进程在生成工作进程之后,在工作进程表中进行遍历,将该新进程的ID以及针对该进程建立的管道句柄传递给工作进程中的其他进程,为工作进程之间的通信做准备,当工作进程1向工作进程2发送指令的时候,首先在主进程给它的其他工作进程工作信息中找到2的进程ID,然后将正确的指令写入指向进程2的管道,工作进程2捕获到管道中的事件后,解析指令并进行相关操作,这样就完成了工作进程之间的通信。