深入 NGINX:面向性能与规模的架构设计

NGINX 在 Web 性能方面表现出色,关键在于软件的设计方式。许多 Web 服务器和应用服务器采用简单的线程式或进程式架构;NGINX 则采用复杂的事件驱动架构,使其能在现代硬件上扩展到数十万条并发连接。

NGINX 架构信息图从高层进程架构逐步深入,展示 NGINX 如何在单个进程中处理多个连接。本文进一步解释它的运作方式。

先看整体:NGINX 的进程模型

The NGINX (and NGINX Plus) master process spawns three types of child process: worker, cache manage, and cache loader. They used shared memory for caching, session persistence, rate limits, and logging.

要理解这个设计,首先需要知道 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

在这台四核服务器上,NGINX 主进程创建了四个工作进程,以及两个管理磁盘内容缓存的辅助进程。

为什么架构很重要?

Unix 应用的基本执行单元是线程或进程。从 Linux 操作系统的角度看,二者大体相似,主要差别是共享内存的程度。线程或进程包含一组独立指令,操作系统可以把它调度到某个 CPU 核心上执行。多数复杂应用同时运行多个线程或进程,原因有两个:

  • 能够同时利用更多计算核心。
  • 线程和进程让并行操作很容易实现,例如同时处理多个连接。

进程和线程会消耗资源:各自需要内存和其他操作系统资源,还要被换入、换出 CPU 核心,这一操作称为上下文切换。多数现代服务器可以同时处理数百个小型活跃线程或进程,但内存耗尽,或者高 I/O 负载导致大量上下文切换时,性能就会严重下降。

设计网络应用的一种常见方式,是为每个连接分配一个线程或进程。这种架构简单、容易实现,但应用需要处理数千条同时连接时,便难以扩展。

NGINX 怎样工作?

NGINX 使用可预测的进程模型,并根据可用硬件资源进行调优:

  • 主进程(master)负责读取配置、绑定端口等特权操作,然后创建少量子进程,即下面三类进程。
  • 缓存加载进程(cache loader)在启动时运行,把磁盘缓存的信息加载到内存,然后退出。它采用保守的调度方式,资源需求较低。
  • 缓存管理进程(cache manager)定期运行,清理磁盘缓存条目,让缓存保持在配置的大小范围内。
  • 工作进程(worker)承担实际工作:处理网络连接、读写磁盘内容、与上游服务器通信。

多数场景下建议每个 CPU 核心运行一个工作进程,以高效利用硬件。可以把 worker_processes 指令设置为 auto:

worker_processes auto;

NGINX 服务活跃时,忙碌的主要是工作进程。每个工作进程以非阻塞方式处理多个连接,减少上下文切换次数。

每个工作进程都是单线程、独立运行的,接收并处理新连接。进程之间可以通过共享内存交换缓存数据、会话保持数据和其他共享资源。

深入 NGINX 工作进程

The NGINX worker process is a nonblocking, event-driven engine for processing requests from web clients.

每个 NGINX 工作进程都使用 NGINX 配置进行初始化,并由主进程提供一组监听套接字。

工作进程首先等待监听套接字上的事件(相关机制包括 accept_mutex 和内核套接字分片)。新连接进入时会产生事件。这些连接会被交给状态机:HTTP 状态机最常用,NGINX 也为 stream(原始 TCP)流量以及 SMTP、IMAP、POP3 等邮件协议实现了状态机。

To process and incoming client request, NGINX reads the HTTP headers, applies limits if configured, makes internal redirects and subrequests as required, forwards to backend services, applies filters, and logs its actions.

状态机本质上是一组告诉 NGINX 如何处理请求的指令。多数提供类似功能的 Web 服务器也使用相似的状态机,区别在于实现方式。

调度状态机

可以把状态机想象成国际象棋的规则,每个 HTTP 事务是一盘棋。棋盘的一边是 Web 服务器:一位能迅速作出决策的特级大师;另一边是远程客户端,也就是通过相对缓慢的网络访问网站或应用的浏览器。

不过,规则可能相当复杂。例如,Web 服务器可能需要与其他参与者通信,代理请求到上游应用,或与认证服务器交互。服务器里的第三方模块甚至能够扩展这盘棋的规则。

阻塞式状态机

前面把进程或线程描述为一组独立指令,操作系统能将它调度到 CPU 核心上执行。多数 Web 服务器和 Web 应用使用“一连接一进程”或“一连接一线程”的模型下棋。每个进程或线程包含把一盘棋下到结束的指令。在服务器执行它的时间里,它大部分时候处于阻塞状态,等待客户端走出下一步。

Most web application platforms use blocking I/O, meaning each worker (thread or process) can handle only one active connection at a time.
  1. Web 服务器进程在监听套接字上等待新连接,也就是客户端发起新棋局。
  2. 收到新棋局后,它开始下棋,每走一步就阻塞,等待客户端回应。
  3. 一盘棋结束后,进程可能等待客户端是否要开始新棋局,这相当于 keepalive 连接。若连接关闭(客户端离开或发生超时),服务器进程重新回去等待新棋局。

关键在于,每条活跃 HTTP 连接,也就是每盘棋,都需要一个专用进程或线程,也就是一位特级大师。架构简单,容易通过第三方模块增加“新规则”。但它极不均衡:仅由文件描述符和少量内存表示的轻量 HTTP 连接,被映射成一个独立线程或进程——非常重量级的操作系统对象。它方便编程,却浪费大量资源。

NGINX 是真正的特级大师

你可能听过同时对弈:一位国际象棋特级大师同时与几十位对手下棋。

Kiril Georgiev
Kiril Georgiev 在保加利亚索非亚同时与360人对弈。最终取得284胜、70平、6负。

NGINX 工作进程就是这样“下棋”的。每个工作进程——记住,通常每个 CPU 核心对应一个——都像一位特级大师,可以同时进行数百盘、实际上数十万盘棋。

NGINX uses an event-driven architecture with nonblocking I/O, so it can handle hundreds of thousands of simultaneous connections.
  1. 工作进程等待监听套接字和连接套接字上的事件。
  2. Events occur on the sockets and the worker handles them:
    • 监听套接字上的事件意味着客户端开始了新棋局,工作进程会创建新的连接套接字。
    • 连接套接字上的事件意味着客户端走了一步,工作进程迅速回应。

工作进程不会因为网络流量而阻塞,等待“对手”——客户端——回应。走完自己的这一步,它立即去处理其他已等待的棋步,或者迎接新参与者。

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

NGINX 的扩展能力很好,每个工作进程可支撑数十万条连接。每条新连接只增加一个文件描述符,并在工作进程中消耗少量额外内存,因此连接的额外开销很低。NGINX 进程可以固定在 CPU 上,上下文切换相对少见,通常发生在没有工作可做时。

在阻塞式“一连接一进程”方法中,每条连接都需要大量额外资源和开销,进程之间的上下文切换非常频繁。

想看更详细的解释,可阅读 NGINX, Inc. 联合创始人、企业发展副总裁 Andrew Alexeev 撰写的 NGINX 架构文章。

经过适当的系统调优,NGINX 每个工作进程能够处理数十万条并发 HTTP 连接,并从容吸收流量突增,也就是大量新棋局涌入。

更新配置与升级 NGINX

NGINX 的进程架构只使用少量工作进程,使配置更新乃至 NGINX 二进制程序的升级都很高效。

NGINX reloads its configuration without any downtime (interruption of request processing).

更新 NGINX 配置是一项简单、轻量、可靠的操作。通常只需执行 nginx -s reload,检查磁盘上的配置,并向主进程发送 SIGHUP 信号。

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

  1. 重新加载配置,并派生一组新的工作进程。新进程立即使用新配置接收连接、处理流量。
  2. 通知旧工作进程优雅退出。旧进程停止接收新连接;当前每个 HTTP 请求完成后,干净地关闭连接,不保留空闲 keepalive。所有连接关闭后,工作进程退出。

重载可能短暂增加 CPU 和内存使用,但相对于活跃连接的资源负载,通常不明显。配置可以每秒重载多次,很多 NGINX 用户正是这样做的。极少数情况下,多代工作进程等待连接关闭会造成问题,但通常也能很快解决。

NGINX 的二进制升级过程实现了高可用的理想目标:可以在线升级软件,而不丢失连接、不停机、不打断服务。

NGINX reloads its binary without any downtime (interruption of request processing).

二进制升级的方法类似于配置的优雅重载。一个新的 NGINX 主进程与原主进程并行运行,共享监听套接字。两个主进程都活跃,各自的工作进程处理流量,然后可以通知旧主进程及其工作进程优雅退出。

完整过程见控制 NGINX文档。

结论

NGINX 架构信息图展示了 NGINX 工作方式的高层概览。但这份简单解释背后,是十多年的创新与优化:让 NGINX 在各种硬件上提供尽可能好的性能,同时维持现代 Web 应用所要求的安全性和可靠性。

想进一步了解 NGINX 的优化,可以阅读以下资源:


来源与版权

原文作者:Owen Garrett,NGINX Community Blog,2015-06-10。依用户已声明的转载授权译载。架构图、Kiril Georgiev 配图与原图说明保留,原文关联的纽约时报链接保留。未另行推定第三方图片为公共领域。 阅读原文。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容