Nginx高性能大规模并发的实现原理

Nginx能够在主流硬件上把并发连接扩展到数十万。Nginx之所以能够在网络性能方面处于领先地位,完全归功于它突破性的事件驱动架构设计。

nginx architecture

Nginx在每颗内核上都有一个工作进程,这样可以有效利用硬件资源,能够在单个工作进程内交替多个连接,从而能够应对突如其来的网络流量,Nginx使用这个魔法,实现大规模可扩展的HTTP应用程序引擎,这就是NGINX。

Nginx资源管理

Nginx使用状态机来管理流量。

创新的非阻塞,事件驱动架构,允许Nginx同时调度多个状态机。

Nginx架构支持优雅的更新,在不停机的情况下进行升级。

NGINX进程模型

Nginx运行模式

为了更好地理解Nginx的架构设计,要从Nginx的运行模式入手。Nginx有一个主进程(用于执行特权操作,如读取配置和绑定端口)和多个工作进程以及辅助进程。

在四核服务器上,Nginx主进程创建四个工作进程,和若干缓存辅助进程,用于管理磁盘上的内容缓存。

任何Unix应用程序的基础都是线程或进程。(在Linux上线程和进程大致相同,主要区别在于它们共享内存的程度。)线程或进程是一组独立的指令,操作系统可以用一颗CPU内核来运行它。

大多数复杂的应用程序,并行执行多个线程或进程有两个原因:

1、充分利用多核计算,让并行执行操作变得非常容易(例如,同时处理多个连接)。

2、充分利用多颗cpu内核进行计算,能够轻松实现并行操作(例如,同时处理多个连接)。

进程和线程对资源的消耗

不管是进程还线程都要使用内存和操作系统的其它资源,并且需要在核心上进行交换(称为上下文切换操作)。大多数现代服务器,可以同时处理数百个小的活动线程或进程,但是,一旦内存耗尽或高I/O负载,导致产生大量的上下文切换,应用的性能会严重下降。

网络应用程序设计的常规手段

在做网络应用程序设计时,通常会为每个连接分配一个线程或进程,这种体系结构简单并易于实现,但当应用程序需要同时处理数千个连接时,将无法进行扩展。

NGINX如何工作?

Nginx使用可预测的进程模型调用可用的硬件资源。

可预测的进程模型

主进程执行特权操作(如读取配置、绑定端口),然后创建少量子进程(接下来的三种类型)。缓存加载器进程在Nginx启动时运行,把基于磁盘的缓存内容加载到内存中,然后退出。 它是一个保守地调度,因此它消耗的资源很低。工作进程负责处理网络连接。

缓存管理进程

管理缓存的进程,会定期地处理来自磁盘缓存中的条目数量,使缓存的条目数量,保持在配置指定的范围内。

工作进程

工作进程处理网络连接,完成从磁盘上读取内容,以及把内容写入到磁盘的工作,并与后端服务进行通信。

Nginx推荐配置

在大多数情况下建议使用的NGINX配置如下:

每颗CPU核心运行一个工作进程,如此可以最大化地利用硬件资源。可以在worker_processes指令上设置auto参数来达到这样的效果。

worker_processes auto;

在Nginx服务器处于活动状态时,其实只有工作进程处于繁忙状态。每个工作进程以非阻塞的方式处理多个连接,以减少上下文切换。

每个工作进程都是单线程的,能够独立运行,以获取新连接并进行处理。进程可以使用共享内存进行通信,共享内容包括:缓存数据、持久性会话数据和其他共享资源的。

NGINX工作进程

 

每个Nginx工作进程都使用Nginx配置进行初始化,并由主进程提供一组监听的socket。Nginx工作进程首先会等待监听上的socket事件(accept_mutex和内核socket分片),事件会由新到达的连接启动,这些连接会被分配给状态机(状态机应用于HTTP是比较常见的(http状态机),但Nginx还为流(原始TCP)交换,和一些邮件协议(SMTP,IMAP和POP3)实现了状态机)。

Nginx状态机本质上是一组告诉Nginx如何处理请求的指令。大多数Web服务器执行这样的功能时,都会采用类似Nginx这样的状态机 - 不同之处在于实现。 

NGINX调度状态机

把Nginx调度状态机想像成国际象棋的规则,每个HTTP事务都是国际象棋游戏。

在棋盘的一侧是web服务器,一个能够快速做出决定的大师。

另一侧是远程客户端,Web浏览器通过相对较慢的网络,访问一个站点或应用程序。

但是,游戏规则可能非常复杂。如,Web服务器可能需要与其他部分(另一个后端服务),或与身份验证服务器通信。Web服务器中的第三方模块甚至可以扩展游戏规则。 

Nginx阻塞状态机

回想一下对进程或线程的描述,它是一组独立的指令,操作系统可以把它安排在CPU内核上运行。大多数Web服务器和Web应用程序,使用一个进程或一个线程的连接模型来玩象棋游戏。每个进程或线程都玩到游戏结束为止,在服务器运行该过程期间,它将大部分时间处于“阻塞”状态 - 等待客户端完成下一步操作。

Web服务器进程在被监听的socket上等待新连接(由客户端发起的新游戏)。当获得一个新游戏时,它会开始该游戏,在每次移动后会被阻塞,以等待客户端的响应。游戏完成后,Web服务器进程可能会等待客户端是否要启动新游戏(这对应于keepalive连接)。如果连接已关闭(客户端消失或发生超时),则Web服务器进程将返回并监听新游戏。

需要关注到的重点是,每个活动的HTTP连接(每个国际象棋游戏),都需要一个专用的进程或线程(一个特级大师)。这种架构很简单,易于使用第三方模块(“新规则”)进行扩展。

然而,存在巨大的不对称:把相当轻量级的HTTP连接(由文件描述符和少量内存表示),映射到单独的线程或进程(一个非常重量级的操作系统对象)上,这种编程方式很方便,但却非常浪费资源。 

NGINX是真正的大师

也许你听说过simultaneous exhibition游戏,一位国际象棋大师同时有几十个对手?Kiril Georgiev在保加利亚索非亚同时对弈360个人,他的最终得分是284胜,70平6负。Nginx工作进程就是这样的大师,与多人同时对弈。

每个工作进程(记住 - 每颗CPU上通常只有一个工作进程),是一个可以同时玩数百个(实际上是数十万个)游戏的大师。

工作进程等待侦听事件,然后建立sockets连接,事件发生,工作进程处理它们。

监听上的事件,意味着客户端已经开始了新一轮的象棋游戏,工作进程会创建一个新的sockets连接。

sockets连接上的一个事件意味着,客户端已经进行了新的移动,工作进程会迅速作出反应。

工作进程永远不会被阻塞,以便等待其“对手”(客户端)做出响应,当客户端移动时,工作进程会前往其他等待移动的游戏中,或者欢迎新的玩家入场。 

为什么比阻塞的多进程架构更快?

Nginx可以很好地扩展,以完成每个工作进程处理数十万个连接。每个新连接都会另创建一个文件描述符,并在工作进程中消耗少量内存。

每个连接几乎没有额外的开销。Nginx进程会持续待在CPU时间片上,上下文切换相对而言就会减少很多,只在没有工作要做时才发生切换。

在阻塞进程处理连接的方法中,每个连接都需要大量额外资源和开销,并且上下文切换(从一个进程切换到另一个进程)非常频繁。

通过适当的系统调整,扩展Nginx的每个工作进程,可以并发处理数十万个HTTP连接,并且可以吸收流量峰值(新游戏的涌入),而不会错过任何一个beat。 

更新配置和升级NGINX

Nginx的进程架构只有少量的工作进程,可以非常有效地更新配置甚至Nginx本身。

更新NGINX配置是一个非常简单、轻便、可靠的操作。它通常意味着只需运行nginx -s reload命令,该命令检查磁盘上的配置并向主进程发送SIGHUP信号。

当主进程收到SIGHUP时会做两件事:

1.重新加载配置,并创建一组新的工作进程。这些新的工作进程立即开始接受连接和处理流量(使用Nginx更新后的配置项所创建的工作进程)。

2.发信号通知旧工作进程正常退出。工作进程停止接受新连接。一旦每个处于连接中的HTTP请求完成,工作进程就会干净地关闭连接(也就是说,没有延迟的keepalive),关闭所有连接后,旧工作进程退出。

重新加载的过程,可能会导致CPU和内存使用量出现小幅增长,但与活动连接的资源负载相比,它通常难以察觉。可以每秒多次重新加载配置(许多NGINX用户都这样做),很少有NGINX工作进程在等待连接关闭时出现问题,即使有这样的问题也会很快得到解决。

Nginx的升级过程实现了高可用性的Holy Grail  - 可以即时升级软件,不会出现任何连接丢失,停机或服务中断。

Nginx升级过程与正常重新加载配置的方法类似,新的Nginx主进程与原始主进程并行运行,它们共享侦听。两个进程都处于活动状态,它们各自的工作进程都会处理流量。然后,通过发信号通知原始主进程,及其工作进程优雅的退出。

Nginx借助操作系统实现IO多路复用(I/O multiplexing)的效果,所以Nginx的性能与所运行的平台有莫大的关联。