开源应用架构(第2卷)
nginx

安德鲁·阿列克谢夫

nginx(发音为“engine x”)是由俄罗斯软件工程师 Igor Sysoev 编写的免费开源 Web 服务器。自 2004 年公开发布以来,nginx 一直专注于高性能、高并发和低内存使用。除了 Web 服务器功能之外,还增加了诸如负载均衡、缓存、访问和带宽控制以及能够有效集成各种应用程序的功能,这使得 nginx 成为现代网站架构的理想选择。目前,nginx 是互联网上第二受欢迎的开源 Web 服务器。

14.1. 为什么高并发很重要?

如今,互联网如此普及和无处不在,很难想象十年前它并非我们所知的那样。它已经发生了巨大的变化,从基于 NCSA 然后基于 Apache Web 服务器的简单 HTML 生成可点击文本,到全球超过 20 亿用户使用的始终在线的通信媒介。随着永久连接的 PC、移动设备以及最近平板电脑的激增,互联网格局正在迅速变化,整个经济体都已数字化连接。在线服务变得更加复杂,明显偏向于即时可用的实时信息和娱乐。运营在线业务的安全方面也发生了重大变化。因此,网站现在比以前复杂得多,并且通常需要更多的工程工作才能变得健壮和可扩展。

网站架构师面临的最大挑战之一始终是并发性。从 Web 服务开始,并发级别就一直在持续增长。对于一个受欢迎的网站来说,服务数十万甚至数百万个同时用户并不罕见。十年前,并发的主要原因是客户端速度慢——使用 ADSL 或拨号连接的用户。如今,并发是由移动客户端和基于维护持久连接的新型应用程序架构相结合造成的,这些连接允许客户端更新新闻、推文、朋友动态等。导致并发性增加的另一个重要因素是现代浏览器的行为发生了变化,这些浏览器会打开四个到六个同时连接到网站的连接以提高页面加载速度。

为了说明客户端速度慢的问题,想象一个简单的基于 Apache 的 Web 服务器,它会生成一个相对较短的 100 KB 响应——一个带有文本或图像的网页。生成或检索此页面可能只需一小部分时间,但将其传输到带宽为 80 kbps(10 KB/s)的客户端需要 10 秒。从本质上讲,Web 服务器会相对快速地提取 100 KB 的内容,然后它会忙于 10 秒钟缓慢地将此内容发送到客户端,然后再释放其连接。现在假设您有 1000 个同时连接的客户端请求类似的内容。如果每个客户端仅分配 1 MB 的额外内存,则会导致 1000 MB(约 1 GB)的额外内存用于为仅 1000 个客户端提供 100 KB 的内容。实际上,基于 Apache 的典型 Web 服务器每个连接通常分配超过 1 MB 的额外内存,并且令人遗憾的是,移动通信的有效速度通常仍然是数十 kbps。尽管通过增加操作系统内核套接字缓冲区的大小可以在一定程度上改善向速度慢的客户端发送内容的情况,但这并不是解决问题的通用方法,并且可能产生不良的副作用。

对于持久连接,处理并发的难度更大,因为为了避免与建立新的 HTTP 连接相关的延迟,客户端将保持连接,并且对于每个连接的客户端,Web 服务器都会分配一定数量的内存。

因此,为了处理与受众增长相关的增加的工作负载以及更高的并发级别,并能够持续地做到这一点,网站应该基于许多非常高效的构建块。虽然等式中的其他部分(如硬件(CPU、内存、磁盘)、网络容量、应用程序和数据存储架构)显然很重要,但在 Web 服务器软件中接受并处理客户端连接。因此,Web 服务器应该能够随着同时连接数和每秒请求数的增长而进行非线性扩展。

Apache 不适合吗?

Apache 是如今仍然在很大程度上主导互联网的 Web 服务器软件,其根源可以追溯到 20 世纪 90 年代初。最初,它的架构与当时的现有操作系统和硬件相匹配,但也与互联网的状态相匹配,在互联网中,网站通常是一个运行 Apache 单个实例的独立物理服务器。到 21 世纪初,很明显,独立 Web 服务器模型无法轻松复制以满足不断增长的 Web 服务的需求。尽管 Apache 为未来的发展奠定了坚实的基础,但它的架构是在每个新连接为其自身生成一个副本,这对于网站的非线性可扩展性并不适用。最终,Apache 成为一个通用的 Web 服务器,专注于拥有许多不同的功能、各种第三方扩展以及对几乎任何类型的 Web 应用程序开发的通用适用性。但是,任何事情都有代价,在一个软件中拥有如此丰富和通用的工具组合的缺点是可扩展性降低,因为每个连接的 CPU 和内存使用量增加。

因此,当服务器硬件、操作系统和网络资源不再成为网站增长的主要限制时,全球的 Web 开发人员开始寻找更有效地运行 Web 服务器的方法。大约十年前,著名的软件工程师 Daniel Kegel 宣布“Web 服务器是时候同时处理一万个客户端了”,并预测了我们现在所说的互联网云服务。Kegel 的 C10K 宣言激发了许多尝试解决 Web 服务器优化的难题,以便同时处理大量客户端,而 nginx 证明是最成功的方法之一。

nginx 旨在解决 10,000 个同时连接的 C10K 问题,其编写时考虑了不同的架构——一种更适合于同时连接数和每秒请求数的非线性可扩展性的架构。nginx 是基于事件的,因此它不遵循 Apache 为每个网页请求生成新进程或线程的风格。最终结果是,即使负载增加,内存和 CPU 使用率仍然可以控制。nginx 现在可以在具有典型硬件的服务器上提供数万个并发连接。

当 nginx 的第一个版本发布时,它旨在与 Apache 一起部署,以便 nginx 处理 HTML、CSS、JavaScript 和图像等静态内容,从而减轻 Apache 基于应用程序服务器的并发和延迟处理。在开发过程中,nginx 通过使用 FastCGI、uwsgi 或 SCGI 协议以及与memcached 等分布式内存对象缓存系统集成,增加了与应用程序的集成。还添加了其他有用的功能,如带有负载均衡和缓存的反向代理。这些附加功能已将 nginx 塑造成一个高效的工具组合,可以构建可扩展的 Web 基础设施。

2012 年 2 月,Apache 2.4.x 分支发布给公众。尽管 Apache 的最新版本添加了新的多处理核心模块和新的代理模块,旨在增强可扩展性和性能,但现在就断言其性能、并发性和资源利用率是否与纯事件驱动的 Web 服务器相同或更好还为时过早。不过,看到 Apache 应用程序服务器在新的版本中具有更好的可扩展性会非常棒,因为它可以潜在地缓解后端方面仍然经常在典型的 nginx 加 Apache Web 配置中未解决的瓶颈。

使用 nginx 还有哪些优势?

以高性能和效率处理高并发始终是部署 nginx 的关键优势。但是,现在还有更多有趣的优势。

在过去的几年里,Web 架构师已经接受了将应用程序基础设施与其 Web 服务器分离和解耦的想法。但是,以前以基于 LAMP(Linux、Apache、MySQL、PHP、Python 或 Perl)的网站形式存在的内容,现在可能不仅会变成基于 LEMP 的网站(`E` 代表 `Engine x`),而且越来越多的情况是将 Web 服务器推到基础设施的边缘,并以不同的方式在其周围集成相同或改进的应用程序和数据库工具集。

nginx 非常适合这种情况,因为它提供了方便地卸载并发、延迟处理、SSL(安全套接字层)、静态内容、压缩和缓存、连接和请求节流,甚至将 HTTP 媒体流从应用程序层卸载到更高效的边缘 Web 服务器层的关键功能。它还允许直接与 memcached/Redis 或其他“NoSQL”解决方案集成,以在为大量并发用户提供服务时提高性能。

随着最新版本的开发工具包和编程语言得到广泛使用,越来越多的公司正在改变其应用程序开发和部署习惯。nginx 已成为这些变化范式中最重要的组件之一,并且它已经帮助许多公司快速并在预算范围内启动和开发其 Web 服务。

nginx 的第一行代码编写于 2002 年。2004 年,它在双条款 BSD 许可下发布给公众。从那时起,nginx 用户数量一直在增长,他们贡献想法并提交错误报告、建议和意见,这对整个社区都非常有帮助和益处。

nginx 代码库是原创的,并且完全用 C 编程语言从头编写。nginx 已移植到许多架构和操作系统,包括 Linux、FreeBSD、Solaris、Mac OS X、AIX 和 Microsoft Windows。nginx 拥有自己的库,并且其标准模块除了系统 C 库之外几乎不使用其他库,除了 zlib、PCRE 和 OpenSSL,如果不需要或由于潜在的许可证冲突,可以从构建中选择性地排除它们。

关于 nginx 的 Windows 版本的一些说明。虽然 nginx 在 Windows 环境中工作,但 nginx 的 Windows 版本更像是概念验证,而不是完全功能的移植。nginx 和 Windows 内核架构之间存在某些限制,目前它们无法很好地交互。nginx 的 Windows 版本的已知问题包括并发连接数大大减少、性能下降、无缓存和无带宽控制。nginx 的未来版本将更密切地匹配主流功能。

14.2. nginx 架构概述

处理并发连接的传统进程或线程模型涉及使用单独的进程或线程处理每个连接,并在网络或输入/输出操作上阻塞。根据应用程序的不同,这在内存和 CPU 消耗方面可能非常低效。生成单独的进程或线程需要准备新的运行时环境,包括分配堆和栈内存,以及创建新的执行上下文。额外的 CPU 时间也用于创建这些项目,这最终可能导致性能下降,因为在过度上下文切换时会发生线程争用。所有这些复杂情况都体现在 Apache 等旧版 Web 服务器架构中。这是在提供一套广泛适用的功能和优化服务器资源使用之间进行的权衡。

从一开始,nginx 就旨在成为一种专门的工具,以实现更高的性能、密度和服务器资源的经济使用,同时支持网站的动态增长,因此它遵循了不同的模型。它实际上受到各种操作系统中基于事件的高级机制持续发展的启发。其结果是一个模块化、事件驱动、异步、单线程、非阻塞的架构,它成为 nginx 代码的基础。

nginx 大量使用多路复用和事件通知,并将特定任务分配给单独的进程。连接在称为worker的少量单线程进程中以高效的运行循环进行处理。在每个worker中,nginx 可以处理每秒数千个并发连接和请求。

代码结构

nginx 的worker代码包括核心和功能模块。nginx 的核心负责维护一个紧密的运行循环,并在请求处理的每个阶段执行模块代码的相应部分。模块构成了大部分表示层和应用层功能。模块从网络和存储读取和写入数据,转换内容,执行出站过滤,应用服务器端包含操作,并在激活代理时将请求传递到上游服务器。

nginx 的模块化架构通常允许开发人员扩展 Web 服务器功能集,而无需修改 nginx 核心。nginx 模块以略微不同的形式出现,即核心模块、事件模块、阶段处理程序、协议、变量处理程序、过滤器、上游和负载均衡器。目前,nginx 不支持动态加载模块;即模块在构建阶段与核心一起编译。但是,未来主要版本计划支持可加载模块和 ABI。有关不同模块作用的更多详细信息,请参见第 14.4 节

在处理与接受、处理和管理网络连接以及内容检索相关的各种操作时,nginx 使用事件通知机制和 Linux、Solaris 和基于 BSD 的操作系统中的一些磁盘 I/O 性能增强功能,例如kqueueepoll事件端口。目标是尽可能多地向操作系统提供提示,以便及时获得关于入站和出站流量、磁盘操作、从套接字读取或写入套接字、超时等的异步反馈。针对 nginx 运行的每个基于 Unix 的操作系统,对使用不同方法进行多路复用和高级 I/O 操作进行了大量优化。

nginx 架构的高级概述在图 14.1中给出。

图 14.1:nginx 架构图

Worker 模型

如前所述,nginx 不会为每个连接生成一个进程或线程。相反,worker进程从共享的“监听”套接字接受新请求,并在每个worker内部执行高效的运行循环来处理每个worker数千个连接。nginx 中没有专门的连接到worker的仲裁或分配;此工作由 OS 内核机制完成。启动时,会创建一组初始的监听套接字。然后,worker在处理 HTTP 请求和响应的同时,持续地接受、读取和写入套接字。

运行循环是 nginx worker代码中最复杂的部分。它包含全面的内部调用,并且严重依赖于异步任务处理的概念。异步操作是通过模块化、事件通知、广泛使用回调函数和微调计时器来实现的。总的来说,关键原则是尽可能地非阻塞。nginx 唯一可能仍然阻塞的情况是当worker进程没有足够的磁盘存储性能时。

由于 nginx 不会为每个连接派生进程或线程,因此在绝大多数情况下,内存使用非常保守且极其高效。nginx 也节省了 CPU 周期,因为没有针对进程或线程的持续创建-销毁模式。nginx 所做的是检查网络和存储的状态,初始化新连接,将它们添加到运行循环中,并异步处理直到完成,此时连接将被释放并从运行循环中删除。结合谨慎使用syscall以及对池和slab内存分配器等支持接口的准确实现,nginx 通常即使在极端工作负载下也能实现中等至低的 CPU 使用率。

由于 nginx 生成多个worker来处理连接,因此它可以很好地扩展到多个核心。通常,每个核心一个单独的worker可以充分利用多核架构,并防止线程争用和死锁。没有资源饥饿,并且资源控制机制在单线程worker进程中隔离。此模型还允许跨物理存储设备进行更多扩展,促进更多磁盘利用率,并避免在磁盘 I/O 上阻塞。结果,服务器资源在跨多个 worker 共享工作负载的情况下得到更有效地利用。

对于某些磁盘使用和 CPU 负载模式,应调整 nginx worker的数量。这里的规则有点基本,系统管理员应该尝试几个工作负载的配置。一般建议如下:如果负载模式是 CPU 密集型的——例如,处理大量的 TCP/IP、执行 SSL 或压缩——nginx worker的数量应与 CPU 核心的数量匹配;如果负载主要受磁盘 I/O 限制——例如,从存储中提供不同的内容集或大量的代理——worker的数量可能是核心数量的 1.5 到 2 倍。一些工程师根据单个存储单元的数量来选择worker的数量,尽管这种方法的效率取决于磁盘存储的类型和配置。

nginx 开发人员将在即将发布的版本中解决的一个主要问题是如何避免大部分磁盘 I/O 上的阻塞。目前,如果存储性能不足以服务特定worker生成的磁盘操作,则该worker仍然可能在从磁盘读取/写入时阻塞。存在许多机制和配置文件指令来缓解此类磁盘 I/O 阻塞场景。最值得注意的是,sendfile 和 AIO 等选项的组合通常会为磁盘性能提供很大的空间。应根据数据集、nginx 可用的内存量和底层存储架构来计划 nginx 安装。

现有worker模型的另一个问题与对嵌入式脚本的支持有限有关。一方面,使用标准 nginx 发行版,仅支持嵌入 Perl 脚本。对此有一个简单的解释:关键问题是嵌入式脚本可能在任何操作上阻塞或意外退出。这两种类型的行为都会立即导致 worker 挂起的情况,从而一次影响数千个连接。计划进行更多工作,使 nginx 的嵌入式脚本更简单、更可靠,并适合更广泛的应用程序。

nginx 进程角色

nginx 在内存中运行多个进程;有一个主进程和多个worker进程。还有一些特殊用途的进程,特别是缓存加载器和缓存管理器。在 nginx 1.x 版本中,所有进程都是单线程的。所有进程主要使用共享内存机制进行进程间通信。主进程以root用户身份运行。缓存加载器、缓存管理器和worker以非特权用户身份运行。

主进程负责以下任务

worker进程接受、处理和处理来自客户端的连接,提供反向代理和过滤功能,并执行 nginx 能够执行的几乎所有其他操作。关于监控 nginx 实例的行为,系统管理员应该注意worker,因为它们是反映 Web 服务器实际日常操作的进程。

缓存加载器进程负责检查磁盘上的缓存项,并使用缓存元数据填充 nginx 的内存数据库。从本质上讲,缓存加载器准备 nginx 实例以使用已存储在磁盘上的文件,这些文件位于专门分配的目录结构中。它遍历目录,检查缓存内容元数据,更新共享内存中的相关条目,然后在一切清理并准备好使用时退出。

缓存管理器主要负责缓存过期和失效。它在正常的 nginx 操作期间驻留在内存中,并在发生故障时由主进程重新启动。

nginx 缓存的简要概述

nginx 中的缓存以文件系统上分层数据存储的形式实现。缓存键是可配置的,可以使用不同的请求特定参数来控制哪些内容进入缓存。缓存键和缓存元数据存储在共享内存段中,缓存加载器、缓存管理器和worker可以访问这些段。目前,除了操作系统虚拟文件系统机制隐含的优化之外,没有文件的内存缓存。每个缓存的响应都放置在文件系统上的不同文件中。层次结构(级别和命名详细信息)通过 nginx 配置指令控制。当响应写入缓存目录结构时,文件的路径和名称将从代理 URL 的 MD5 哈希派生。

将内容放入缓存的过程如下:当 nginx 从上游服务器读取响应时,内容首先写入缓存目录结构外部的临时文件。当 nginx 完成请求处理时,它会重命名临时文件并将其移动到缓存目录。如果代理的临时文件目录位于另一个文件系统上,则该文件将被复制,因此建议将临时目录和缓存目录都保留在同一个文件系统上。当需要显式清除文件时,从缓存目录结构中删除文件也相当安全。nginx 有一些第三方扩展可以远程控制缓存内容,并且计划在主发行版中集成此功能。

14.3. nginx 配置

Nginx 的配置系统受到了 Igor Sysoev 在 Apache 上经验的启发。他主要的见解是,一个可扩展的配置系统对于 Web 服务器至关重要。主要的可扩展性问题出现在维护大型复杂配置时,这些配置包含大量的虚拟服务器、目录、位置和数据集。在一个相对较大的 Web 设置中,如果在应用程序级别和系统工程师自身都没有正确处理,这可能会变成一场噩梦。

因此,Nginx 的配置旨在简化日常操作,并提供一种简单的方法来进一步扩展 Web 服务器的配置。

Nginx 的配置保存在多个纯文本文件中,这些文件通常位于 /usr/local/etc/nginx/etc/nginx 中。主配置文件通常称为 nginx.conf。为了保持整洁,配置的一部分可以放在单独的文件中,这些文件可以自动包含在主文件中。但是,这里需要注意的是,Nginx 目前不支持 Apache 风格的分布式配置(例如,.htaccess 文件)。所有与 Nginx Web 服务器行为相关的配置都应该驻留在集中式的配置文件集中。

配置文件最初由主进程读取和验证。Nginx 配置的已编译只读形式可供 worker 进程使用,因为它们是从主进程派生的。配置结构通过通常的虚拟内存管理机制自动共享。

Nginx 配置有几个不同的上下文,用于指令的 mainhttpserverupstreamlocation(以及邮件代理的 mail)块。上下文之间永不重叠。例如,在指令的 main 块中不存在放置 location 块这样的情况。此外,为了避免不必要的歧义,不存在类似“全局 Web 服务器”配置的内容。Nginx 配置旨在简洁和逻辑化,允许用户维护包含数千条指令的复杂配置文件。在一次私下谈话中,Sysoev 说:“全局服务器配置中的位置、目录和其他块是我在 Apache 中从未喜欢的功能,所以这就是它们从未在 Nginx 中实现的原因。”

配置语法、格式和定义遵循所谓的 C 样式约定。这种制作配置文件的特定方法已经被各种开源和商业软件应用程序使用。通过设计,C 样式配置非常适合嵌套描述,逻辑清晰且易于创建、阅读和维护,并且受到许多工程师的喜爱。Nginx 的 C 样式配置也可以轻松实现自动化。

虽然一些 Nginx 指令类似于 Apache 配置的某些部分,但设置 Nginx 实例是一种截然不同的体验。例如,Nginx 支持重写规则,尽管这需要管理员手动调整旧的 Apache 重写配置以匹配 Nginx 样式。重写引擎的实现也不同。

通常,Nginx 设置还提供对几种原始机制的支持,这些机制作为精简 Web 服务器配置的一部分非常有用。有必要简要提及变量和 try_files 指令,它们在某种程度上是 Nginx 独有的。Nginx 中的变量旨在提供一种额外的、更强大的机制来控制 Web 服务器的运行时配置。变量经过优化以进行快速评估,并在内部预编译为索引。评估是按需进行的;也就是说,变量的值通常只计算一次并缓存到特定请求的生命周期。变量可以与不同的配置指令一起使用,为描述条件请求处理行为提供额外的灵活性。

try_files 指令最初旨在以更合适的方式逐步替换条件 if 配置语句,并且它被设计为快速有效地尝试/匹配不同的 URI 到内容的映射。总的来说,try_files 指令运行良好,并且可以非常高效和有用。建议读者仔细检查 try_files 指令,并在适用时采用它。

14.4. Nginx 内部机制

如前所述,Nginx 代码库由核心和许多模块组成。Nginx 的核心负责提供 Web 服务器的基础、Web 和邮件反向代理功能;它支持底层网络协议的使用,构建必要的运行时环境,并确保不同模块之间无缝交互。但是,大多数协议和特定于应用程序的功能是由 Nginx 模块完成的,而不是核心。

在内部,Nginx 通过模块的管道或链来处理连接。换句话说,对于每个操作,都有一个模块执行相关工作;例如,压缩、修改内容、执行服务器端包含、通过 FastCGI 或 uwsgi 协议与上游应用程序服务器通信,或与 Memcached 通信。

有一些 Nginx 模块位于核心和真正的“功能”模块之间。这些模块是 httpmail。这两个模块在核心和更低级别的组件之间提供了额外的抽象层。在这些模块中,实现了与 HTTP、SMTP 或 IMAP 等相应应用层协议相关联的事件序列的处理。与 Nginx 核心结合,这些上层模块负责维护对相应功能模块的正确调用顺序。虽然 HTTP 协议目前作为 http 模块的一部分实现,但由于需要支持其他协议(如 SPDY)(请参阅“SPDY:一种用于更快 Web 的实验性协议”),因此计划将来将其分离成一个功能模块。

功能模块可以分为事件模块、阶段处理程序、输出过滤器、变量处理程序、协议、上游和负载均衡器。这些模块中的大多数补充了 Nginx 的 HTTP 功能,尽管事件模块和协议也用于 mail。事件模块提供特定于操作系统的事件通知机制,例如 kqueueepoll。Nginx 使用的事件模块取决于操作系统的功能和构建配置。协议模块允许 Nginx 通过 HTTPS、TLS/SSL、SMTP、POP3 和 IMAP 进行通信。

典型的 HTTP 请求处理周期如下所示。

  1. 客户端发送 HTTP 请求。
  2. Nginx 核心根据与请求匹配的配置位置选择合适的阶段处理程序。
  3. 如果配置为这样做,负载均衡器会为代理选择一个上游服务器。
  4. 阶段处理程序完成其工作并将每个输出缓冲区传递给第一个过滤器。
  5. 第一个过滤器将输出传递给第二个过滤器。
  6. 第二个过滤器将输出传递给第三个(依此类推)。
  7. 最终响应发送到客户端。

Nginx 模块调用是极其可定制的。它是通过一系列使用指向可执行函数的指针的回调来执行的。但是,这样做的缺点是,它可能会给想要编写自己的模块的程序员带来很大的负担,因为他们必须准确地定义模块应该如何以及何时运行。Nginx API 和开发人员文档都正在改进并提供更多可用性以缓解这种情况。

模块可以附加的一些示例是

worker 内部,导致生成响应的运行循环的一系列操作如下所示

  1. 开始 ngx_worker_process_cycle()
  2. 使用特定于操作系统的机制(例如 epollkqueue)处理事件。
  3. 接受事件并调度相关操作。
  4. 处理/代理请求头和主体。
  5. 生成响应内容(头、主体)并将其流式传输到客户端。
  6. 完成请求。
  7. 重新初始化计时器和事件。

运行循环本身(步骤 5 和 6)确保增量生成响应并将其流式传输到客户端。

处理 HTTP 请求的更详细视图可能如下所示

  1. 初始化请求处理。
  2. 处理标头。
  3. 处理主体。
  4. 调用关联的处理程序。
  5. 运行处理阶段。

这将我们带到阶段。当 Nginx 处理 HTTP 请求时,它会将其通过多个处理阶段。在每个阶段,都有处理程序要调用。通常,阶段处理程序处理请求并生成相关的输出。阶段处理程序附加到配置文件中定义的位置。

阶段处理程序通常执行四件事:获取位置配置、生成适当的响应、发送标头和发送主体。处理程序有一个参数:一个描述请求的特定结构。请求结构包含许多关于客户端请求的有用信息,例如请求方法、URI 和标头。

读取 HTTP 请求标头后,Nginx 会查找关联的虚拟服务器配置。如果找到虚拟服务器,则请求将经过六个阶段

  1. 服务器重写阶段
  2. 位置阶段
  3. 位置重写阶段(这可能会将请求带回上一阶段)
  4. 访问控制阶段
  5. try_files 阶段
  6. 日志阶段

为了尝试生成响应请求所需的必要内容,Nginx 将请求传递给合适的 内容处理程序。根据确切的位置配置,Nginx 可能会首先尝试所谓的无条件处理程序,例如 perlproxy_passflvmp4 等。如果请求与上述任何内容处理程序都不匹配,则它将被以下处理程序之一选择,按此确切顺序:random indexindexautoindexgzip_staticstatic

索引模块详细信息可以在 Nginx 文档中找到,但这些是处理带有尾部斜杠的请求的模块。如果像 mp4autoindex 这样的专用模块不合适,则内容被认为只是磁盘上的文件或目录(即静态),并由 static 内容处理程序提供服务。对于目录,它会自动重写 URI,以便始终存在尾部斜杠(然后发出 HTTP 重定向)。

然后将内容处理程序的内容传递给过滤器。过滤器也附加到位置,并且可以为一个位置配置多个过滤器。过滤器执行处理程序生成的输出的操作任务。过滤器的执行顺序在编译时确定。对于开箱即用的过滤器,它是预定义的,对于第三方过滤器,它可以在构建阶段配置。在现有的 Nginx 实现中,过滤器只能进行出站更改,并且目前没有机制来编写和附加过滤器以进行输入内容转换。输入过滤将在 Nginx 的未来版本中出现。

过滤器遵循特定的设计模式。一个过滤器被调用,开始工作,并调用下一个过滤器,直到链中的最后一个过滤器被调用。之后,Nginx 完成响应。过滤器不必等待前一个过滤器完成。链中的下一个过滤器可以在从前一个过滤器获取到输入后立即开始自己的工作(在功能上非常类似于 Unix 管道)。依次,生成的输出响应可以在从上游服务器接收整个响应之前传递给客户端。

有头部过滤器和主体过滤器;Nginx 分别将响应的头部和主体馈送到关联的过滤器。

头部过滤器包含三个基本步骤

  1. 决定是否对该响应进行操作。
  2. 对响应进行操作。
  3. 调用下一个过滤器。

主体过滤器转换生成的內容。主体过滤器的示例包括

在过滤器链之后,响应被传递给写入器。除了写入器之外,还有几个其他特殊用途的过滤器,即 copy 过滤器和 postpone 过滤器。copy 过滤器负责使用相关的响应内容填充内存缓冲区,这些内容可能存储在代理临时目录中。postpone 过滤器用于子请求。

子请求是请求/响应处理中非常重要的机制。子请求也是 Nginx 最强大的方面之一。使用子请求,Nginx 可以返回与客户端最初请求的 URL 不同的 URL 的结果。一些 Web 框架称之为内部重定向。但是,Nginx 更进一步——过滤器不仅可以执行多个子请求并将输出组合成单个响应,而且子请求还可以嵌套和分层。子请求可以执行自己的子子请求,子子请求可以发起子子子请求。子请求可以映射到硬盘上的文件、其他处理程序或上游服务器。子请求最常用于根据原始响应中的数据插入其他内容。例如,SSI(服务器端包含)模块使用过滤器来解析返回文档的内容,然后将include指令替换为指定 URL 的内容。或者,它可以是一个过滤器的示例,该过滤器将文档的整个内容视为要检索的 URL,然后将新文档附加到 URL 本身。

上游和负载均衡器也值得简要描述。上游用于实现可以识别为内容处理程序的反向代理(proxy_pass 处理程序)。上游模块主要准备要发送到上游服务器(或“后端”)的请求,并接收来自上游服务器的响应。这里没有对输出过滤器的调用。上游模块确切执行的操作是设置回调,以便在上游服务器准备好写入和读取时调用。存在实现以下功能的回调

负载均衡器模块附加到 proxy_pass 处理程序以提供在多个上游服务器符合条件时选择上游服务器的功能。负载均衡器注册启用配置文件指令,提供额外的上游初始化函数(以在 DNS 中解析上游名称等),初始化连接结构,决定将请求路由到哪里,并更新统计信息。目前 Nginx 支持两种用于负载均衡到上游服务器的标准策略:轮循和 IP 哈希。

上游和负载均衡处理机制包括检测失败的上游服务器并将新请求重新路由到剩余服务器的算法——尽管计划进行大量额外工作来增强此功能。总的来说,计划对负载均衡器进行更多工作,在 Nginx 的后续版本中,跨不同上游服务器分配负载的机制以及健康检查将得到极大改进。

还有几个其他有趣的模块,它们提供了一组额外的变量供配置文件使用。虽然 Nginx 中的变量是在不同的模块中创建和更新的,但有两个模块完全专用于变量:geomapgeo 模块用于根据客户端的 IP 地址跟踪客户端。此模块可以创建依赖于客户端 IP 地址的任意变量。另一个模块 map 允许从其他变量创建变量,本质上提供了灵活映射主机名和其他运行时变量的功能。这种类型的模块可以称为变量处理程序。

在单个 Nginx worker 中实现的内存分配机制在某种程度上受到 Apache 的启发。Nginx 内存管理的高级描述如下:对于每个连接,必要的内存缓冲区都是动态分配的,链接的,用于存储和操作请求和响应的头部和主体,然后在连接释放时释放。需要注意的是,Nginx 尽量避免在内存中复制数据,并且大部分数据都是通过指针值传递的,而不是通过调用 memcpy

更深入一点,当响应由模块生成时,检索到的内容将放入内存缓冲区,然后将其添加到缓冲区链链接中。后续处理也使用此缓冲区链链接。缓冲区链在 Nginx 中非常复杂,因为有几种处理场景根据模块类型而有所不同。例如,在实现主体过滤器模块时,精确管理缓冲区可能非常棘手。此类模块一次只能在一个缓冲区(链链接)上操作,并且它必须决定是覆盖输入缓冲区,用新分配的缓冲区替换缓冲区,还是在问题缓冲区之前或之后插入新缓冲区。更复杂的是,有时模块会收到多个缓冲区,因此它拥有必须对其进行操作的不完整缓冲区链。但是,目前 Nginx 仅提供用于操作缓冲区链的低级 API,因此在进行任何实际实现之前,第三方模块开发人员应该真正熟悉 Nginx 的这个神秘部分。

上述方法的一个说明是,为连接的整个生命周期分配了内存缓冲区,因此对于长期连接,会保留一些额外的内存。同时,在空闲的保持活动连接上,Nginx 只消耗 550 字节的内存。未来 Nginx 版本的一个可能的优化是重用和共享长期连接的内存缓冲区。

管理内存分配的任务由 Nginx 池分配器完成。共享内存区域用于接受互斥量、缓存元数据、SSL 会话缓存以及与带宽策略和管理(限制)相关的信息。Nginx 中实现了一个 slab 分配器来管理共享内存分配。为了允许同时安全地使用共享内存,可以使用多种锁定机制(互斥量和信号量)。为了组织复杂的数据结构,Nginx 还提供了一个红黑树实现。红黑树用于在共享内存中保存缓存元数据,跟踪非正则表达式位置定义以及其他一些任务。

不幸的是,以上所有内容从未以一致且简单的方式描述,这使得为 Nginx 开发第三方扩展的工作变得非常复杂。虽然存在一些关于 Nginx 内部结构的优秀文档——例如,Evan Miller 编写的文档——但这些文档需要大量的逆向工程工作,并且 Nginx 模块的实现对许多人来说仍然是一门“黑魔法”。

尽管与第三方模块开发相关的某些困难,但 Nginx 用户社区最近看到了许多有用的第三方模块。例如,有一个用于 Nginx 的嵌入式 Lua 解释器模块、用于负载均衡的其他模块、完整的 WebDAV 支持、高级缓存控制以及其他有趣的第三方工作,本章作者鼓励并将在未来支持这些工作。

14.5. 经验教训

当 Igor Sysoev 开始编写 Nginx 时,大多数支持互联网的软件已经存在,并且此类软件的架构通常遵循传统服务器和网络硬件、操作系统以及总体旧互联网架构的定义。但是,这并没有阻止 Igor 认为他可能能够改进 Web 服务器领域的东西。因此,虽然第一课可能看起来很明显,但它是:总有改进的空间。

怀着改进 Web 软件的想法,Igor 花费了大量时间开发初始代码结构并研究优化各种操作系统的代码的不同方法。十年后,他正在开发 Nginx 2.0 版本的原型,并考虑了多年来对 1.0 版本的积极开发。很明显,新架构的初始原型和初始代码结构对于软件产品的未来至关重要。

另一个值得一提的要点是,开发应该集中。Nginx 的 Windows 版本可能是一个很好的例子,说明如何避免将开发工作分散到既不是开发人员的核心能力也不是目标应用程序的事情上。这也同样适用于在多次尝试使用更多功能增强 Nginx 以向后兼容现有传统设置期间出现的重写引擎。

最后但并非最不重要的一点是,值得一提的是,尽管 Nginx 开发者社区并不庞大,但 Nginx 的第三方模块和扩展一直对其受欢迎程度非常重要。Evan Miller、Piotr Sikora、Valery Kholodkov、张益春(agentzh)和其他才华横溢的软件工程师所做的工作受到了 Nginx 用户社区及其原始开发人员的高度赞赏。