本文共 3720 字,大约阅读时间需要 12 分钟。
NGINX在web性能上的表现尤为出众,这完全得益于其设计方式,许多web和应用服务器都是基于线程或进程这种简单的架构,NGINX用了一种精妙的事件驱动架构,在现代的硬件上,它可以处理成千上万的并发连接。
中的信息图对高级别的进程架构和NGINX如何在单个进程中处理多个连接进行了深入探讨。本文更进一步地阐述了NGINX的所有工作原理。
要更好的理解这个设计,需要熟悉NGINX的运行过程。NGINX有一个主进程(该进程执行一些特权操作,例如读取配置以及绑定端口)以及若干worker进程和helper进程。
在这个4核服务器上,NGINX主进程创建了4个worker进程以及一对用来管理磁盘内容缓存的缓存helper进程。
任何Unix应用的基础都是线程或进程。(从Linux操作系统的角度看,线程和进程几乎是一样的;最大的区别是内存共享的度。)一个线程或进程是一组自包含的指令,这些指令可由操作系统调度到某个CPU核心上运行。大部分复杂应用并行运行多个线程或进程一般有两个原因:
进程和线程会消耗资源。每个进程或线程都会使用内存以及其他操作系统资源,他们都需要切换CPU(称作上下文切换)。大部分现代的服务器都能同时处理几百个小的、活动的线程或进程,但是,一旦内存耗尽或是遇到高I/O负载导致大量上下文切换时性能就会急剧下降。
常规的网络应用设计都是为每个连接分配一个线程或进程。这种架构简单且容易实现,但是,当应用需要同时处理成千上万的连接时,扩展性就不好了。
NGINX用了一个可预测的进程模型,支持众多硬件:
大部分场景中推荐的NGINX配置是 —— 每个CPU核心运行一个worker进程 —— 以充分利用硬件资源。在配置中加入指令即可:
worker_processes auto;
当NGINX服务器活动时,只有worker进程是处于繁忙状态的。每个worker进程以非阻塞的方式处理多个连接,这减少了上下文切换的次数。
每个worker进程都是单线程的并且是独立运行的,它们捕获新的连接然后进行处理。进程之间的共享缓存数据、会话持久数据以及其它共享资源的通信通过共享内存实现。
每个worker进程都是用NGINX配置进行初始化的,并且由主进程提供了一组监听套接字。
NGINX worker进程从等待监听套接字上的事件开始(accept_mutex和内核套接字切分(kernel socket sharding))。事件由新进来的连接进行初始化。这些连接被分配给一个状态机 —— HTTP状态机是最常用的,但NGINX也为流(原始TCP)流量以及一些邮件协议(SMTP,IMAP和POP3)实现了状态机。
状态机本质上是一组指令,由它们告诉NGINX如何处理请求。大部分执行与NGINX相同方法的web服务器也用的类似的状态机 —— 区别在于实现。
将状态机想象成象棋规则。每个HTTP事务就是一盘象棋游戏。棋盘的一侧是web服务器 —— 一个可以快速做决定的象棋大师。另一侧是远程客户端 —— 正在相对较慢的网络中访问站点或应用的web浏览器。
然而,游戏的规则可能会非常复杂。比如,web服务器也许要与其它方(代理到上游应用)进行交流或是要与认证服务器对话。web服务器中的第三方模块甚至还可能扩展游戏规则。
前面我们提到,一个线程或进程是一组自包含的指令,这些指令可由操作系统调度到某个CPU核心上运行。大部分web服务器以及web应用使用的是每个连接分配一个进程或每个连接分配一个线程的模式来处理的。每个进程或线程都包含了从开始到结束需要执行的指令。在服务器运行进程期间,大部分时间都是“阻塞的” —— 等待客户端完成其下一个动作。
关键的一点在于每个活动的HTTP连接(每盘象棋游戏)都需要一个专门的进程或线程(一个象棋大师)。这种架构在扩展第三方模块(“新的规则”)时非常简单方便。然而,存在一个巨大的失衡问题:相当轻量级的HTTP连接,本由一个文件描述符和少量的内存来表示,却映射到了一个单独的线程或进程这种非常重量级的操作系统对象。编程是便利了,但却是个很大的浪费。
也许你已经听过游戏,一个象棋大师同时与几十个对手对战。
这就是NGINX worker进程下“象棋”的方式。每个worker进程(记住 —— 通常是每个CPU核心一个worker进程)都是一个大师,可以同时处理几百盘(实际上是成千上万)游戏。
worker进程永远不会因为网络拥堵而阻塞来等待“对手”(客户端)的响应。当处理完一个动作,worker进程立即去处理其他游戏中等待处理的动作,或是迎接新玩家的到来。
NGINX的可伸缩性非常好,每个worker进程可以支撑成千上万个连接。每个新的连接会创建一个文件描述符以及消耗worker进程中少量的额外内存。每个连接的额外开销极少。NGINX进程可以绑定到CPU。上下文切换是比较罕见的,只有没有任务要处理时才会发生。
在阻塞的、每个连接一个进程的方式下,每个连接都需要大量的额外资源与开销,且上下文切换(从一个进程切换到另一个)非常频繁。
更多细节解释,看看这篇有关NGINX架构的。
通过适当的系统调优,NGINX worker进程可以处理成千上万的并发HTTP连接,能够承受流量峰值(新游戏蜂拥而至)还不会错过一个请求。
NGINX这种使用少数worker进程的进程架构,可以非常高效的进行配置更新甚至是更新NGINX介质本身。
更新NGINX配置是个很简单、轻量级且可靠的操作。通常只是意味着去运行一下nginx –s reload
命令,这个命令会去检查磁盘上的配置,给主进程发送一个SIGHUP信号。
当主进程收到SIGHUP信号,会做两件事:
这个配置加载进程会造成CPU和内存使用上的一个小峰值,但相比活动的连接带来的资源负载,这是极其微小的。可以每秒重新加载配置多次(有许多NGINX用户的确是这么干的)。多代NGINX worker进程都在等待连接关闭极少会造成问题,但即便有问题也会很快解决。
NGINX介质升级过程达到了高可用性的标准 —— 可以直接对线上运行的NGINX升级,而不会丢失任何连接,也不会有停机时间与服务中断。
介质升级过程与重新加载配置的过程类似。一个新的NGINX主进程会与旧的主进程并行运行,它们共享监听套接字。两个进程都是活动的,并且各自对应的worker进程都还在处理请求。随后可以发信号让旧的主进程及其worker进程优雅退出。
整个过程在中有更详细的描述。
这篇深入NGINX信息图从高层次概述了NGINX的功能,但是在这个简单解释的背后,是十多年的创新与优化,才使NGINX在保证安全和可靠性的同时,在众多硬件上都能发挥出最佳性能。
如果想更多地了解NGINX的优化,下面这些资源不错:
转载地址:http://nquio.baihongyu.com/