1.2.1 多进程模型
进程是操作系统资源分配的最小单位,由于CPU数量有限,多个进程间通过被分配的时间片来获得CPU的使用权,系统在进行内核管理和进程调度时,要执行保存当前进程上下文、更新控制信息、选择另一就绪进程、恢复就绪进程上下文等一系列操作,而频繁切换进程会造成资源消耗。
Nginx采用的是固定数量的多进程模型(见图1-2),由一个主进程(Master Process)和数量与主机CPU核数相同的工作进程协同处理各种事件。主管理进程负责工作进程的配置加载、启停等操作,工作进程负责处理具体请求。进程间的资源都是独立的,每个工作进程处理多个连接,每个连接由一个工作进程全权处理,不需要进行进程切换,也就不会产生由进程切换引起的资源消耗问题。默认配置下,工作进程的数量与主机CPU核数相同,充分利用CPU和进程的亲缘性(affinity)将工作进程与CPU绑定,从而最大限度地发挥多核CPU的处理能力。
图1-2 多进程模型
Nginx主进程负责监听外部控制信号,通过频道机制将相关信号操作传递给工作进程,多个工作进程间通过共享内存来共享数据和信息。
1.信号
信号(signal)又称软中断信号,可通过调用系统命令kill来发送信号实现进程通信。在Nginx系统中,主进程负责监听外部信号,实现对进程的热加载、平滑重启及安全关闭等操作的响应。Nginx支持的信号如表1-1所示。
表1-1 Nginx支持的信号
(1)在Linux系统下可以通过kill命令向Nginx进程发送信号指令,代码如下:
kill -HUP 'cat nginx.pid'
(2)在Linux系统下也可以通过nginx -s命令行参数实现信号指令的发送,代码如下:
nginx -s reload
2.频道
频道(channel)是Nginx主进程向工作进程传递信号操作的通信方式,用于将控制工作进程的信号操作传递给工作进程。通信频道的原理是应用socketpair方法使用本机的socket方式实现进程间的通信。主进程发送频道消息,工作进程接收频道消息并执行相应操作,如工作进程的创建与停止等。创建工作进程时会将接收频道消息的套接字注册到对应的事件引擎(如epoll)中,当事件引擎监听到主进程发送的频道消息时,就会触发回调函数通知工作进程执行响应操作。
3.共享内存
共享内存是Linux操作系统下进程间的一种简单、高效的通信方式,其允许多个进程访问同一个内存地址,一个进程改变了内存中的内容后,其他进程都可以使用变更后的内容。Nginx的多个进程间就是通过共享内存的方式共享数据的,主进程启动时创建共享内存,工作进程创建(fork方式)完成后,所有的进程都开始使用共享内存。用户可以在配置文件中配置共享内存名称和大小,定义不同的共享内存块供Nginx不同的功能使用,Nginx解析完配置文件后,会将定义的共享内存通过slab机制进行内部统一划分和管理。
4.进程调度
当工作进程被创建时,每个工作进程都继承了主进程的监听套接字(socket),所以所有工作进程的事件监听列表中会共享相同的监听套接字。但是多个工作进程间同一时间内只能由一个工作进程接收网络连接,为使多个工作进程间能够协调工作,Nginx的工作进程有如下几种调度方式。
(1)无调度模式
所有工作进程都会在连接事件被触发时争相与客户端建立连接,建立连接成功则开始处理客户端请求。无调度模式下所有进程都会争抢资源,但最终只有一个进程可以与客户端建立连接,对于系统而言这将在瞬间产生大量的资源消耗,这就是所谓的惊群现象。
(2)互斥锁模式(accept_mutex)
互斥锁是一种声明机制,每个工作进程都会周期性地争抢互斥锁,一旦某个工作进程抢到互斥锁,就表示其拥有接收HTTP建立连接事件的处理权,并将当前进程的socket监听注入事件引擎(如epoll)中,接收外部的连接事件。其他工作进程只能继续处理已经建立连接的读写事件,并周期性地轮询查看互斥锁的状态,只有互斥锁被释放后工作进程才可以抢占互斥锁,获取HTTP建立连接事件的处理权。当工作进程最大连接数的1/8与该进程可用连接(free_connection)的差大于或等于1时,则放弃本轮争抢互斥锁的机会,不再接收新的连接请求,只处理已建立连接的读写事件。互斥锁模式有效地避免了惊群现象,对于大量HTTP的短连接,该机制有效避免了因工作进程争抢事件处理权而产生的资源消耗。但对于大量启用长连接方式的HTTP连接,互斥锁模式会将压力集中在少数工作进程上,进而因工作进程负载不均而导致QPS下降。
(3)套接字分片(Socket Sharding)
套接字分片是由内核提供的一种分配机制,该机制允许每个工作进程都有一组相同的监听套接字。当有外部连接请求时,由内核决定哪个工作进程的套接字监听可以接收连接。这有效避免了惊群现象的发生,相比互斥锁机制提高了多核系统的性能。该功能需要在配置listen指令时启用reuseport参数。
Nginx 1.11.3以后的版本中互斥锁模式默认是关闭的,由于Nginx的工作进程数量有限,且Nginx通常会在高并发场景下应用,很少有空闲的工作进程,所以惊群现象的影响不大。无调度模式因少了争抢互斥锁的处理,在高并发场景下可提高系统的响应能力。套接字分片模式则因为由Linux内核提供进程的调度机制,所以性能最好。
5.事件驱动
事件驱动程序设计(Event-Driven Programming)是一种程序设计模型,这种模型的程序流程是由外部操作或消息交互事件触发的。其代码架构通常是预先设计一个事件循环方法,再由这个事件循环方法不断地检查当前要处理的信息,并根据相应的信息触发事件函数进行事件处理。通常未被处理的事件会放在事件队列中等待处理,而被事件函数处理的事件也会形成一个事件串,因此事件驱动模型的重点就在于事件处理的弹性和异步化。
为了确保操作系统运行的稳定性,Linux系统将用于寻址操作的虚拟存储器分为内核空间和用户空间,所有硬件设备的操作都是在内核空间中实现的。当应用程序监听的网络接口接收到网络数据时,内核会先把数据保存在内核空间的缓冲区中,然后再由应用程序复制到用户空间进行处理。Linux操作系统下所有的设备都被看作文件来操作,所有的文件都通过文件描述符(File Descriptor,FD)集合进行映射管理。套接字是应用程序与TCP/IP协议通信的中间抽象层,也是一种特殊的文件,应用程序以文件描述符的方式对其进行读/写(I/O)、打开或关闭操作。每次对socket进行读操作都需要等待数据准备(数据被读取到内核缓冲区),然后再将数据从内核缓冲区复制到用户空间。
为了提高网络I/O操作的性能,操作系统设计了多种I/O网络模型。在Linux系统下,网络并发应用处理最常用的就是I/O多路复用模型,该模型是一种一个进程可以监视多个文件描述符的机制,一旦某个文件描述符就绪(数据准备就绪),进程就可以进行相应的读写操作。epoll模型是Linux系统下I/O多路复用模型里最高效的I/O事件处理模型,其最大并发连接数仅受内核的最大打开文件数限制,在1GB内存下可以监听10万个端口。epoll模型监听的所有连接中,只有数据就绪的文件描述符才会调用应用进程、触发响应事件,从而提升数据处理效率。epoll模型利用mmap映射内存加速与内核空间的消息传递,从而减少复制消耗。
作为Web服务器,Nginx的基本功能是处理网络事件,快速从网络接口读写数据。Nginx结合操作系统的特点,基于I/O多路复用模型的事件驱动程序设计,采用了异步非阻塞的事件循环方法响应处理套接字上的accept事件,使其在调用accept时不会长时间占用进程的CPU时间片,从而能够及时处理其他工作。通过事件驱动的异步非阻塞机制(见图1-3),使大量任务可以在工作进程中得到高效处理,以应对高并发的连接和请求。