NGINX 在网络性能方面处于领先地位,这完全归功于该软件的设计方式。 尽管许多 Web 服务器和application服务器使用简单的线程或基于进程的架构,但 NGINX 却以复杂的事件驱动架构脱颖而出,使其能够在现代硬件上扩展到数十万个并发连接。
NGINX 内部信息图从高级进程架构深入探讨,以说明 NGINX 如何在单个进程中处理多个连接。 这个博客更详细地解释了它的工作原理。
为了更好地理解这种设计,您需要了解 NGINX 的运行方式。 NGINX 有一个主进程(执行读取配置和绑定到端口等特权操作)和多个工作进程和辅助进程。
# service nginx restart * 重新启动 nginx # ps -ef --forest | grep nginx root 32475 1 0 13:36 ? 00:00:00 nginx:主进程/usr/sbin/nginx -c /etc/nginx/nginx.conf nginx 32476 32475 0 13:36? 00:00:00 _ nginx:工作进程 nginx 32477 32475 0 13:36? 00:00:00 _ nginx:工作进程 nginx 32479 32475 0 13:36? 00:00:00 _ nginx:工作进程 nginx 32480 32475 0 13:36? 00:00:00 _ nginx:工作进程 nginx 32481 32475 0 13:36? 00:00:00 _ nginx:缓存管理器进程 nginx 32482 32475 0 13:36? 00:00:00 _ nginx:缓存加载器进程
在这个四核服务器上,NGINX 主进程创建了四个工作进程和几个缓存辅助进程来管理磁盘上的内容缓存。
任何 Unixapplication的基本基础都是线程或进程。 (从 Linux 操作系统的角度来看,线程和进程大致相同;主要区别在于它们共享内存的程度。) 线程或进程是操作系统可以安排在 CPU 核心上运行的一组独立的指令。 大多数复杂的应用并行运行多个线程或进程,原因有二:
进程和线程消耗资源。 它们各自使用内存和其他操作系统资源,并且需要在核心上和核心外进行交换(这种操作称为上下文切换)。 大多数现代服务器可以同时处理数百个小型活动线程或进程,但一旦内存耗尽或高 I/O 负载导致大量上下文切换,性能就会严重下降。
设计网络应用的常用方法是为每个连接分配一个线程或进程。 这种架构简单且易于实现,但是当application需要处理数千个同时连接时,它无法扩展。
NGINX 使用可预测的进程模型,该模型针对可用的硬件资源进行调整:
大多数情况下推荐的 NGINX 配置是每个 CPU 核心运行一个工作进程,这样可以最有效地利用硬件资源。 您可以通过在worker_processes
指令上设置auto
参数来进行配置:
worker_processes 自动;
当 NGINX 服务器处于活动状态时,只有工作进程处于繁忙状态。 每个工作进程以非阻塞方式处理多个连接,从而减少上下文切换的次数。
每个工作进程都是单线程的,独立运行,抓取新的连接并处理它们。 进程可以使用共享内存进行通信,以获取共享缓存数据、会话持久数据和其他共享资源。
每个 NGINX 工作进程都使用 NGINX 配置进行初始化,并由主进程提供一组监听套接字。
NGINX 工作进程首先等待监听套接字上的事件( accept_mutex和内核套接字分片)。 事件由新传入连接发起。 这些连接被分配给一个状态机- HTTP 状态机是最常用的,但 NGINX 也为流(原始 TCP)流量和许多邮件协议(SMTP、IMAP 和 POP3)实现了状态机。
状态机本质上是告诉 NGINX 如何处理请求的一组指令。 大多数执行与 NGINX 相同功能的 Web 服务器都使用类似的状态机 - 区别在于实现。
可以将状态机想象成国际象棋的规则。 每一个HTTP事务都是一场棋局。 棋盘的一边是网络服务器——一位能够快速做出决策的大师。 另一端是远程客户端——通过相对较慢的网络访问网站或application的网络浏览器。
然而,游戏规则可能非常复杂。 例如,Web 服务器可能需要与其他方通信(代理到上游application)或与身份验证服务器对话。 网络服务器中的第三方模块甚至可以扩展游戏规则。
回想一下,我们对进程或线程的描述是,它是一组独立的指令,操作系统可以安排在 CPU 核心上运行。 大多数 Web 服务器和 Web应用都使用每个连接一个进程或每个连接一个线程的模型来下棋。 每个进程或线程都包含玩一局游戏直至结束的指令。 在服务器运行该进程期间,它大部分时间都处于“阻塞”状态——等待客户端完成下一步操作。
要记住的重点是,每个活动的 HTTP 连接(每场棋局)都需要一个专用的进程或线程(一位大师)。 该架构简单且易于通过第三方模块(“新规则”)进行扩展。 然而,存在着巨大的不平衡:由文件描述符和少量内存表示的相当轻量级的 HTTP 连接映射到单独的线程或进程,这是一个非常重量级的操作系统对象。 这虽然给编程带来了便利,但却造成了极大的浪费。
也许您听说过同步表演赛,即一位国际象棋大师同时与数十名对手比赛?
这就是 NGINX 工作进程下“棋”的方式。 每个工作者(记住——每个 CPU 核心通常有一个工作者)都是一个大师,可以同时玩数百场(实际上是数十万场)游戏。
工作者永远不会阻塞网络流量,等待其“对手”(客户端)响应。 当它完成自己的动作后,工作者立即进入其他等待处理的动作的游戏,或者欢迎新玩家加入。
NGINX 的扩展性非常好,每个工作进程可以支持数十万个连接。 每个新连接都会创建另一个文件描述符,并在工作进程中消耗少量额外的内存。 每个连接的额外开销非常小。 NGINX 进程可以保持固定在 CPU 上。 上下文切换相对不频繁,并且发生在没有工作要做的时候。
在阻塞、每个进程一个连接的方法中,每个连接都需要大量额外的资源和开销,并且上下文切换(从一个进程切换到另一个进程)非常频繁。
有关更详细的解释,请参阅 NGINX 架构的这篇文章,作者是 NGINX 公司企业发展副总裁兼联合创始人 Andrew Alexeev。
通过适当的系统调整,NGINX 可以扩展以处理每个工作进程的数十万个并发 HTTP 连接,并可以吸收流量高峰(新游戏的涌入)而不会错过任何一个节拍。
NGINX 的进程架构具有少量的工作进程,可以非常高效地更新配置甚至 NGINX 二进制文件本身。
更新 NGINX 配置是一个非常简单、轻量且可靠的操作。 它通常只是意味着运行nginx
-s
reload
命令,该命令检查磁盘上的配置并向主进程发送 SIGHUP 信号。
当主进程收到 SIGHUP 时,它会做两件事:
这个重新加载过程可能会导致 CPU 和内存使用率出现小幅飙升,但与活动连接的资源负载相比,这通常是难以察觉的。 您可以每秒重新加载配置多次(许多 NGINX 用户正是这样做的)。 极少数情况下,当有多代 NGINX 工作进程等待连接关闭时,就会出现问题,但即使是这些问题也能很快得到解决。
NGINX 的二进制升级过程实现了高可用性的目标——您可以动态升级软件,而不会出现连接丢失、停机或服务中断的情况。
二进制升级过程在方法上与配置的优雅重新加载类似。 新的 NGINX 主进程与原有的主进程并行运行,并且共享监听套接字。 两个进程都处于活跃状态,并且它们各自的工作进程处理流量。 然后,您可以向旧主人及其工作人员发出信号,让它们正常退出。
整个过程在控制 NGINX中有更详细的描述。
NGINX 内部信息图从高层概述了 NGINX 的功能,但在这个简单的解释背后是十多年的创新和优化,使 NGINX 能够在各种硬件上提供最佳性能,同时保持现代 Web应用所需的安全性和可靠性。
如果您想了解有关 NGINX 优化的更多信息,请查看以下优秀资源:
SO_REUSEPORT
套接字选项)“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”