译荐|Nginx: 高性能与规模化的设计逻辑

本文首发于公众号:ReadingPython

译荐系列主要翻译推荐一些我觉得写得很不错的英文内容。也欢迎大家推荐优秀内容,我将选取一些译成中文,降低中文读者的阅读门槛。

原文链接:https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/

web 服务器中,NGINX 是高性能的代表,而其性能表现主要来自其独特的设计。大多数 web 服务器和应用服务器采用简单的、基于线程或进程的架构,而 Nginx 采用的是成熟的、事件驱动的架构,从而能在主流硬件上处理数十万以上的并发规模。

这张Nginx 设计一览图简要展示了 Nginx 在单个进程中处理众多连接的方式,本文是其详细介绍。


1. 基本图景:Nginx 的进程模型

Nginx 主进程会生成 3 种类型的子进程:工作进程、缓存管理器、缓存加载器。子进程之间通过共享内存来实现缓存管理、session 保持、速度限制以及日志等功能。

为了更好地理解这种设计,你需要理解 Nginx 的工作方式。Nginx 有一个主进程(负责高权限操作,如读取配置,绑定端口等),以及多个工作与辅助进程。

# service nginx restart
* Restarting nginx
# ps -ef --forest | grep nginx
root     32475     1  0 13:36 ?        00:00:00 nginx: master process /usr/sbin/nginx 
                                                -c /etc/nginx/nginx.conf
nginx    32476 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32477 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32479 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32480 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32481 32475  0 13:36 ?        00:00:00  _ nginx: cache manager process
nginx    32482 32475  0 13:36 ?        00:00:00  _ nginx: cache loader process

在这个 4 核服务器上,Nginx 主进程创建了 4 个工作进程、以及一些处理硬盘上的缓存内容的辅助进程。


2. 为什么架构很重要?

Unix 应用的基础是线程或进程。(就 Linux 系统而言,线程与进程非常相似,主要区别在于共享内存的程度。)一个线程或进程就是一个指令系列,操作系统可以在 CPU 中独立地运行这个指令系列。

大多数复杂应用使用多线程或多进程的原因有两个:

  • 可以同时使用多个 CPU 核心;
  • 可以轻易地实现并行处理;(比如说,可以同时处理多个连接)

线程与进程都会消耗资源。每个线程或进程都需要内存空间及其它系统资源,并且还要在 CPU 中互相切换(即所谓的“切换上下文”)。大多数现代服务器都能同时处理数百个小的活跃线程或进程,但一旦内存耗尽,或高 I/O 操作导致大量的上下文切换,就会严重影响服务器的性能表现。

网络应用通常为每个连接分配一个新的线程或进程,这种架构比较简单,容易实现。但当并发连接达数千个以上时,就不太适用了。


3. Nginx 是如何工作的?

Nginx 通过一个可预测的进程模型实现与硬件资源的和谐工作:

  • 主进程负责高权限操作,如读取配置,绑定端口,创建子进程(即之后所述的三种进程);
  • 缓存加载进程负责在应用启动时将保存在硬盘上的缓存内容加载到内存中,加载完成即退出。这个进程的资源需求很小;
  • 缓存管理进程会定期运行,负责将缓存规模控制在用户配置的容量之内;
  • 工作进程基本干了所有的活,如处理网络连接、读写磁盘内容、转发请求到上游服务器等;

大多数情况下,Nginx 的推荐配置是,有几个 CPU 核心,就配置几个工作进程,从而最大效率地使用硬件资源。用户将 worker_processes 设为 auto 即可:

worker_processes auto;

在 Nginx 服务器启动后,只有工作进程是时刻忙碌的。每个工作进程都以一种非阻塞的方式同时处理多个连接,从而减少了上下文切换的操作。

每个工作进程都独立地,以单线程的方式运行,接收并处理新的连接。工作进程之间以共享内存的方式进行通信,因而可以共享缓存数据、session 以及其它资源。


4. Nginx 的工作进程

Nginx 的工作进程一个非阻塞的、事件驱动的请求处理引擎。

Nginx 的每个工作进程在初始化时会带上用户的配置信息,以及由主进程提供的可监听 socket 集合。

工作进程启动后,会监听 socket 事件(参考 accept_mutexkernel socket sharding)。当出现新的连接请求,就会初始化一个事件,这些事件会由状态机进行管理——最常见的是 HTTP 状态机。不过 Nginx 也为一些其它协议实现了状态机,如原生 TCP 协议及一系列邮件协议(SMTP、IMAP、POP3)等。

处理客户端请求时,Nginx 会读取 HTTP 请求头,根据用户配置执行限制条件,进行内部重定向,执行子请求,执行过滤器并记录日志等。

状态机本质上就是关于 Nginx 如何处理请求的一系列指令。大多数 web 服务器都使用相似的状态机——只是具体实现上有所不同。


5. 状态机的调度

我们可以把状态机看作一种下棋的规则,每个 HTTP 连接都是一个棋局。在棋盘的一边,坐着 web 服务器——一位下棋大师,可以快速做出决策;而在坐在另一边的,则是远方的客户端——通过相对缓慢的网络与服务器进行通信的 web 浏览器。

当然,棋局的规则有时会非常复杂。例如,web 服务器可能需要与其它第三方进行通信(代理转发至上游服务器),或与授权服务器进行交流。而用户还可能通过一些第三方插件来扩展这个棋局的规则。

5.1 阻塞式状态机

前文曾提到,一个进程或线程就是一个操作系统可以独立地在 CPU 中运行的指令系列。大多数 web 服务器和应用服务器会给每个网络连接分配一个进程或线程,这个进程或线程中包含了完成棋局所需的完整指令。在处理棋局的过程中,这个进程或线程的大部分时间都是“阻塞的”——它在等待客户端执行下一步动作。

阻塞式棋局

  1. web 服务器进程监听新的 socket 连接(有客户端发起的新棋局);
  2. 出现新的棋局时,根据规则进行响应,每次动作执行完成之后,等待客户端的回复;
  3. 棋局完成后,web 服务器进程可能会等待一段时间,以防客户端发起新的棋局(指 keepalive 连接)。如果连接关闭(客户端关闭或等待超时),web 服务器进程会监听新的棋局;

在这里我们看到,每个活跃的 HTTP 连接(每个棋局)都需要一个单独的进程或线程(一位下棋大师)进行处理。这种架构比较简单,也易于使用第三方插件实现新的功能(“新的规则”),但存在一个巨大的不平衡:一个非常轻量的 HTTP 连接,即一个文件描述符及少量内存空间,对应着一个单独的线程或进程——一个重量级的操作系统对象。

也就是说,为了编程的便利,产生了巨大的浪费。

5.2 NGINX 是真正的大师

你或许听过下棋中的车轮战,即一位大师同时与众多对手下棋。

Kiril Georgiev

Kiril Georgiev 同时与 360 位选手对弈 他的最终成绩是 284 胜,70 平,6负。

这正是 Nginx 的工作进程所采取的“下棋”方式。每个工作进程(一般来说,对应于一个 CPU 核心)都是一位能同时与数百名(事实上,是数十万名)选手进行交战的下棋大师。

Nginx 通过事件驱动、非 I/O 阻塞式架构,实现同时处理数十万条连接的能力。

  1. 工作进程等待 socket 连接事件;
  2. 事件发生,开始处理:
    • 新的事件表示客户端发起了一个新的棋局,工作进程会创建一个新的 socket 连接;
    • socket 连接上的新事件表示客户端执行了一个新的动作,工作进程会进行相应的处理;

工作进程永远不会被网络通信所阻塞,不会等待它的“对手”(客户端)的响应。当它完成自己的操作后,会立即开始处理下一个等待它处理的棋局,或者迎接新的对手。

5.3 为什么这种架构比阻塞式的,多进程的架构更快?

Nginx 的每个工作进程可以轻松应对数十万条连接。每个新连接会创建一个新的文件描述符,也会占用一点内存,但相对来说资源增量非常有限。并且 Nginx 的进程会与 CPU 核心保持绑定关系,也就是说,只有在所有任务都处理完成之后,才会出现切换上下文的情况。

而在阻塞式的、连接与进程绑定的架构中,每个新连接都需要占用大量系统资源,上下文切换也非常频繁(从一个进程切换到另一个进程)。

如果想了解更多细节,可以参考 NGINX 公司联合创始人,企业发展 VP Andrew Alexeev 写的另一篇文章

通过与操作系统的协调配置,每个 Nginx 工作进程可以轻松处理数十万条 HTTP 连接,也能毫无压力地应对流量峰值。


6. 更新配置与升级版本

通过几个工作进程间的切换,Nginx 可以高效地更新服务器配置,甚至更新 Nginx 软件本身。

Nginx 可以在重载配置时保持服务不间断

更新 Nginx 的配置是一个非常简单、轻量、可靠的操作。用户只需执行 nginx -s reload命令,系统就会重新检查配置文件,并给 Nginx 的主进程发出 SIGHUP 信号。

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

  1. 重载配置,并 fork 出一系列新的工作进程。这些新的工作进程会立即开始接收并处理新的网络连接(使用新的配置信息);
  2. 通知原有工作进程优雅退出。原有工作进程随即停止接收新的连接请求,并在处理完本次请求后关闭当前 HTTP 连接(也就是说,停止所有的 keepalive 连接),等所有处理中的连接都关闭后,原有工作进程自动退出;

重载过程会导致 CPU 和内存占用的短暂上升,但一般来说,与处理活跃连接所用的资源相比,上升的资源可以忽略不计。用户可以在一秒钟内多次重载配置(事实上,确实有很多用户这么干)。当然,偶尔也会出现许多工作进程都在等待连接关闭的状态,即便如此,这种等待也是非常短暂的。

Nginx 程序的升级过程简直是高可用的典范——用户可以在没有任何服务中断的情况下实现热升级。

Nginx 升级时不会导致服务中断。

程序升级过程与配置重载过程极为相似。新的 Nginx 主进程会与原有主进程同时运行,共享 sockets,并通过各自的子进程处理网络连接。之后再给原有主进程发送信号,优雅退出。

关于这个过程的更多细节,可以参考 NGINX 管理


7. 总结

Nginx 设计一览图看似简单,其背后却是十数年的持续创新与优化,从而能在安全可靠的基础上,在现代 web 服务器所需的各类硬件设施上实现最佳的性能表现。


发表评论

评论列表,共 0 条评论