[译]深入 NGINX: 为性能和扩展所做之设计

原文链接:Inside NGINX: How We Designed for Performance & Scale 翻译:丁一

NGINX在web性能上的表现尤为出众,这完全得益于其设计方式,许多web和应用服务器都是基于线程或进程这种简单的架构,NGINX用了一种精妙的事件驱动架构,在现代的硬件上,它可以处理成千上万的并发连接。

Inside NGINX中的信息图对高级别的进程架构和NGINX如何在单个进程中处理多个连接进行了深入探讨。本文更进一步地阐述了NGINX的所有工作原理。

背景——NGINX进程模型

要更好的理解这个设计,需要熟悉NGINX的运行过程。NGINX有一个主进程(该进程执行一些特权操作,例如读取配置以及绑定端口)以及若干worker进程和helper进程。

在这个4核服务器上,NGINX主进程创建了4个worker进程以及一对用来管理磁盘内容缓存的缓存helper进程。

架构为什么重要?

任何Unix应用的基础都是线程或进程。(从Linux操作系统的角度看,线程和进程几乎是一样的;最大的区别是内存共享的度。)一个线程或进程是一组自包含的指令,这些指令可由操作系统调度到某个CPU核心上运行。大部分复杂应用并行运行多个线程或进程一般有两个原因:

  • 可以同时使用多个CPU核心。
  • 线程和进程让并行操作变得简单(例如,同时处理多个连接)。

进程和线程会消耗资源。每个进程或线程都会使用内存以及其他操作系统资源,他们都需要切换CPU(称作上下文切换)。大部分现代的服务器都能同时处理几百个小的、活动的线程或进程,但是,一旦内存耗尽或是遇到高I/O负载导致大量上下文切换时性能就会急剧下降。

常规的网络应用设计都是为每个连接分配一个线程或进程。这种架构简单且容易实现,但是,当应用需要同时处理成千上万的连接时,扩展性就不好了。

NGINX是怎么运行的?

NGINX用了一个可预测的进程模型,支持众多硬件:

  • 主进程执行一些特权操作,比如读取配置以及绑定端口,然后创建少数子进程(下面的三种类型)。
  • 缓存加载进程在启动时运行,用于将磁盘上的缓存加载到内存中,随后退出。对这个进程的调度很保守,所以其资源需求比较低。
  • 缓存管理进程会周期性地运行,从磁盘缓存中删除条目以保证缓存没有超过配置的大小。
  • worker进程做了所有的工作!它们处理网络连接,从磁盘读取内容或往磁盘中写入内容,以及与上游服务器通信。

大部分场景中推荐的NGINX配置是 —— 每个CPU核心运行一个worker进程 —— 以充分利用硬件资源。在配置中加入worker_processes auto指令即可:

worker_processes auto;

当NGINX服务器活动时,只有worker进程是处于繁忙状态的。每个worker进程以非阻塞的方式处理多个连接,这减少了上下文切换的次数。

每个worker进程都是单线程的并且是独立运行的,它们捕获新的连接然后进行处理。进程之间的共享缓存数据、会话持久数据以及其它共享资源的通信通过共享内存实现。

深入理解NGINX 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应用使用的是每个连接分配一个进程或每个连接分配一个线程的模式来处理的。每个进程或线程都包含了从开始到结束需要执行的指令。在服务器运行进程期间,大部分时间都是“阻塞的” —— 等待客户端完成其下一个动作。

  1. web服务器进程在监听套接字上监听新的连接(由客户端初始化的新游戏)。
  2. 当新游戏准备好后,就开始游戏,每个动作之后都会阻塞,等待客户端的响应。
  3. 一旦游戏结束,web服务器进程可能还要等等看是否这个客户端需要发起一轮新的游戏(这相当于keepalive连接)。如果连接被关闭(客户端离开或超时),web服务器进程返回去监听新的游戏请求。

关键的一点在于每个活动的HTTP连接(每盘象棋游戏)都需要一个专门的进程或线程(一个象棋大师)。这种架构在扩展第三方模块(“新的规则”)时非常简单方便。然而,存在一个巨大的失衡问题:相当轻量级的HTTP连接,本由一个文件描述符和少量的内存来表示,却映射到了一个单独的线程或进程这种非常重量级的操作系统对象。编程是便利了,但却是个很大的浪费。

NGINX是真大师

也许你已经听过simultaneous exhibition游戏,一个象棋大师同时与几十个对手对战。

这就是NGINX worker进程下“象棋”的方式。每个worker进程(记住 —— 通常是每个CPU核心一个worker进程)都是一个大师,可以同时处理几百盘(实际上是成千上万)游戏。

  1. worker进程等待监听和连接套接字上的事件。
  2. 套接字上发生事件后,worker进程开始进行处理:

    • 监听套接字上的事件意味着有个客户端发起了一盘新的象棋游戏。worker进程创建出一个新的连接套接字。
    • 连接套接字上的事件意味着客户端开始有新的动作了。worker进程就迅速响应。

worker进程永远不会因为网络拥堵而阻塞来等待“对手”(客户端)的响应。当处理完一个动作,worker进程立即去处理其他游戏中等待处理的动作,或是迎接新玩家的到来。

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

NGINX的可伸缩性非常好,每个worker进程可以支撑成千上万个连接。每个新的连接会创建一个文件描述符以及消耗worker进程中少量的额外内存。每个连接的额外开销极少。NGINX进程可以绑定到CPU。上下文切换是比较罕见的,只有没有任务要处理时才会发生。

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

更多细节解释,看看这篇有关NGINX架构的文章

通过适当的系统调优,NGINX worker进程可以处理成千上万的并发HTTP连接,能够承受流量峰值(新游戏蜂拥而至)还不会错过一个请求。

配置更新与NGINX升级

NGINX这种使用少数worker进程的进程架构,可以非常高效的进行配置更新甚至是更新NGINX介质本身。

更新NGINX配置是个很简单、轻量级且可靠的操作。通常只是意味着去运行一下nginx –s reload命令,这个命令会去检查磁盘上的配置,给主进程发送一个SIGHUP信号。

当主进程收到SIGHUP信号,会做两件事:

  1. 重新加载配置,然后fork出一组新的worker进程。这些新的worker进程会立马开始接受连接并处理(用的是新的配置)。
  2. 发信号让旧的worker进程优雅退出。旧的worker进程停止接受新的连接。一旦每个当前的HTTP请求完成,worker进程将利索地结束连接(也就是,没有lingering keeplive)。一旦所有连接都关闭了,worker进程就可以退出了。

这个配置加载进程会造成CPU和内存使用上的一个小峰值,但相比活动的连接带来的资源负载,这是极其微小的。可以每秒重新加载配置多次(有许多NGINX用户的确是这么干的)。多代NGINX worker进程都在等待连接关闭极少会造成问题,但即便有问题也会很快解决。

NGINX介质升级过程达到了高可用性的标准 —— 可以直接对线上运行的NGINX升级,而不会丢失任何连接,也不会有停机时间与服务中断。

介质升级过程与重新加载配置的过程类似。一个新的NGINX主进程会与旧的主进程并行运行,它们共享监听套接字。两个进程都是活动的,并且各自对应的worker进程都还在处理请求。随后可以发信号让旧的主进程及其worker进程优雅退出。

整个过程在Controlling NGINX中有更详细的描述。

总结

这篇深入NGINX信息图从高层次概述了NGINX的功能,但是在这个简单解释的背后,是十多年的创新与优化,才使NGINX在保证安全和可靠性的同时,在众多硬件上都能发挥出最佳性能。

如果想更多地了解NGINX的优化,下面这些资源不错:

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: [译]深入 NGINX: 为性能和扩展所做之设计

  • Trackback 关闭
  • 评论 (0)
  1. 暂无评论

return top