《开源软件架构》中nginx章节

原文链接

nginx

Andrew Alexeev

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

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

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

对于一个网站架构师来说,最大的挑战之一就是并发。自web服务出现以来,并发等级一直在不断提高。一个受欢迎的网站同时为数十万甚至数百万的用户提供服务并不罕见。十年前,并发的主要原因是客户端速度慢——使用ADSL或拨号连接的用户。现在,并发性是由移动客户端和较新的应用程序架构的组合引起的,这些架构通常基于维护长连接来允许客户端实时接收新闻、推文、好友动态等更新。导致并发性增加的另一个重要因素是现代浏览器行为的改变,这些浏览器同时打开4到6个到网站的连接,以提高页面加载速度。

为了说明慢客户端的问题,设想一个简单的基于Apache的web服务器,它产生相对较短的100KB 的响应——一个带有文本或图像的web页面。生成或检索此页面只需零点几秒,但是将其传输到带宽为80kbps (10 KB/s)的客户端需要10秒。本来,web服务器提取100 KB的内容是相对快速地,然后它需要10秒钟缓慢地将这些内容发送到客户端之后才能释放连接 。现在假设你有1,000个同时连接的客户端,它们请求类似的内容。如果每个客户端只额外分配了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实例的独立物理服务器。到2000年代初,很明显,独立的web服务器模型不能通过简单的复制来满足不断增长的web服务的需求。尽管Apache为未来的开发提供了坚实的基础,但它的架构是为每个新连接生成自身的副本,这并不适合网站的非线性可伸缩性。最终,Apache成为了一个通用的web服务器,它专注于拥有许多不同的特性、各种第三方扩展以及普遍适用于几乎任何web应用程序开发。然而,任何东西都是有代价的,在单个软件中拥有如此丰富和通用的组合工具的缺点是可伸缩性较差,因为每个连接的CPU和内存使用量都在增加。

因此,当服务器硬件、操作系统和网络资源不再是网站增长的主要制约因素时,全世界的web开发人员开始寻找一种更有效的运行web服务器的方法。大约十年前,著名软件工程师 Daniel Kegel 宣称,“是时候让web服务器同时处理1万个客户端了”,并预言了我们现在所说的互联网云服务。 Kegel 的 C10K 清单激发了许多解决web服务器优化问题的尝试,以同时处理大量客户端,nginx是其中最成功的一个。

为了解决10,000个并发连接的C10K问题,nginx采用了一种不同的体系结构——这种体系结构更适合于同时连接数和每秒请求数量的非线性扩展。nginx是基于事件的,所以它不遵循Apache为每个web页面请求生成新进程或线程的风格。最终结果是,即使负载增加,内存和CPU使用仍然是可控的。nginx现在可以在使用典型硬件的服务器上处理数万个并发连接。

当nginx的第一个版本发布时,它打算与Apache一起部署,这样nginx就可以处理HTML、CSS、JavaScript和图像等静态内容,从而从基于Apache的应用服务器上卸载并发和延迟处理。在其开发过程中,nginx通过使用FastCGI、uswgi或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已经成为这些不断变化的范例中最重要的组件之一,它已经帮助许多公司在预算内快速启动和开发web服务。

nginx的第一行代码是在2002年写的。2004年,它根据 two-clause BSD 许可协议公开发布。从那以后,nginx用户的数量一直在增长,他们贡献想法,提交bug报告、建议和观察,这些对整个社区都非常有帮助和益处。

nginx代码库是原创的,完全是用C语言从头开始编写的。nginx已经被移植到许多架构和操作系统,包括Linux、FreeBSD、Solaris、Mac OS X、AIX和Microsoft Windows。nginx有它自己的库,它的标准模块除了除了zlib、PCRE和OpenSSL 之外,只使用系统的C库,这几个模块如果不需要或者因为潜在的许可冲突,可以选择将它们在构建中排除。

简单介绍一下nginx的Windows版本。虽然nginx可以在Windows环境中工作,但nginx的Windows版本更像是一个概念验证,而不是一个功能齐全的端口。nginx和Windows内核架构有一定的局限性,它们目前还不能很好地交互。已知的Windows nginx版本的问题包括并发连接数量少得多、性能下降、没有缓存和带宽控制。未来的Windows nginx版本将更接近主流功能。

14.2. nginx架构概述

传统的基于进程或线程的并发连接处理模型涉及到使用单独的进程或线程处理每个连接,以及阻塞网络或输入/输出操作。根据应用程序的不同,它在利用内存和CPU消耗方面可能非常低效。生成单独的进程或线程需要准备新的运行时环境,包括分配堆和堆栈内存,以及创建新的执行上下文。创建这些还会花费额外的CPU时间,这最终会导致性能低下,因为线程会过多的切换上下文。所有这些复杂性都表现在Apache等较老的web服务器体系结构中。这是在提供丰富的通用特性和优化服务器资源使用之间的权衡。

从一开始,nginx就被认为是一种专业的工具,可以在实现网站动态增长的同时,获得更高的性能、更密集和更经济的服务器资源使用,所以它采用了一种不同的模式。它实际上是受到了各种操作系统中正在开发的基于事件的高级机制的启发。其结果是一个模块化的、事件驱动的、异步的、单线程的、非阻塞的体系结构,它成为nginx代码的基础。

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

代码结构

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

nginx的模块化架构通常允许开发人员在不修改nginx核心的情况下扩展web服务器功能集。nginx模块版本略有不同,即核心模块、事件模块、阶段处理程序、协议、变量处理程序、过滤器、上行流和负载均衡器。此时,nginx不支持动态加载模块;也就是说,模块在构建阶段与核心一起编译。但是,计划在未来的主要版本中支持可加载模块和ABI。关于不同模块的角色的更详细信息可以在14.4节中找到。

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

nginx架构的高级概述如图14.1所示。

Figure 14.1: Diagram of nginx’s architecture

Workers 模型

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

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

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

因为nginx生成几个worker来处理连接,所以它可以很好地多核扩展。通常,每个内核使用一个单独的worker能充分利用多核架构,并防止线程抖动和锁定。不存在资源匮乏,资源控制机制被隔离在单线程worker进程中。该模型还允许跨物理存储设备实现更好的可伸缩性,促进更多的磁盘利用率,并避免磁盘I/O上的阻塞。因此,服务器资源在多个worker进程之间共享工作负载的情况下得到了更有效的利用。

对于一些磁盘使用和CPU负载模式,应该调整nginx worker进程的数量。这里的规则有些基础,规则很基础,系统管理员应该针对不同的工作负载尝试一些配置。一般建议如下:如果负载模式是CPU密集型的(例如,处理大量TCP/IP、执行SSL或压缩),那么nginx worker进程的数量应该与CPU内核的数量相匹配;如果负载主要是磁盘I/O限制,(例如,服务于来自存储的不同内容集,或者大量的代理),那么worker的数量可能是CPU内核数量的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进程数量
  • 不中断服务的重新配置
  • 控制不间断的二进制升级(启动新的二进制并在必要时回滚)
  • 重启日志文件
  • 编译嵌入式Perl脚本

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

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

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

nginx缓存简要概述

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

将内容放入缓存的过程如下:当nginx从上游服务器读取响应时,首先将内容写入缓存目录结构之外的临时文件。当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服务器行为相关的配置都应该位于一组集中的配置文件中。

配置文件最初由主进程读取和验证。当从主进程派生worker进程时,worker进程可以使用已编译的只读形式的nginx配置。配置结构由常用的虚拟内存管理机制自动共享。

nginx配置有几个不同的上下文,分别用于main、http、server、upstream 、location  (以及用于邮件代理的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模块位于核心和真正的“功能”模块之间。这些模块是http和mail。这两个模块在核心组件和底层组件之间提供了额外的抽象级别。在这些模块中,实现了与各自的应用层协议(如HTTP、SMTP或IMAP)来处理相关联的事件序列。结合nginx核心,这些上层模块负责维护对各个功能模块的正确调用顺序。虽然HTTP协议目前是作为http模块的一部分实现的,但是由于需要支持SPDY等其他协议(参见“SPDY:用于更快的web的实验性协议”),未来计划将其分离为功能模块。

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

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

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

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

可以附加模块的一些例子是:

  • 在读取和处理配置文件之前
  • location和server出现的每个配置指令
  • 初始化主配置时
  • 初始化服务器(即,主机/端口)时
  • 合并服务器配置和主配置时
  • 初始化location配置或与其父server配置合并时
  • 主进程启动或退出时
  • 当新的worker进程启动或退出时
  • 处理请求时
  • 过滤响应头和响应体时
  • 挑选,初始化并重新初始化对上游服务器的请求时
  • 处理来自上游服务器的响应时
  • 完成与上游服务器的交互时
  • When finishing an interaction with an upstream server

在一个worker进程中,生成响应的运行循环的操作序列如下所示:

  1. 开始ngx_worker_process_cycle()。
  2. 使用特定的OS机制(如epoll 或 kqueue)来处理事件。
  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可能首先尝试所谓的无条件处理程序,如perl、proxy_pass、flv、mp4等。如果请求不匹配上面的任何内容处理程序,则由以下处理程序中的一个按如下顺序选择:random index、index、autoindex、gzip_static、static。

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

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

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

有头过滤器和体过滤器;nginx分别将响应头和响应体提供给关联的过滤器。

头过滤器包括三个基本步骤:

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

体过滤器转换生成的内容。体过滤器的例子包括:

  • 服务器端包含
  • XSLT 过滤
  • 图像过滤(例如,动态调整图像大小)
  • 修改字符集
  • gzip 压缩
  • 分块编码

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

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

upstream 和负载均衡器也值得简要描述。上游用于实现可以定义为反向代理的内容处理程序(proxy_pass处理程序)。upstream 模块主要准备发送到上游服务器(或“后端”)的请求,并从上游服务器接收响应。这里没有对输出过滤器的调用。
upstream 模块所做的事情是设置回调等待上游服务器准备好写入和读取时调用。存在实现以下功能的回调:

  • 制作要发送到上游服务器的请求缓冲区(或缓冲区链)
  • 重新初始化/重置与上游服务器的连接(在再次创建请求之前发生)
  • 处理上游响应的第一位并保存指向从上游服务器接收的有效负载的指针
  • 中止请求(在客户端过早终止时发生)
  • 当nginx从上游服务器读取完成时结束请求
  • 整理响应体(例如移除trailer)

负载均衡模块附加到proxy_pass处理程器,当有多个上游服务器符合条件时,提供选择上游服务器的能力。负载均衡器注册一个启用的配置文件指令,提供额外的上游初始化函数(解析DNS中的上游服务器名称,等等),初始化连接结构,决定请求的路由,并更新统计信息。目前nginx支持两种标准的规则来平衡上游服务器的负载:轮询和ip-hash。

upstream 和负载均衡处理机制包括检测失效的上游服务器和将新请求重新路由到其他服务器的算法——尽管计划进行大量额外工作来增强此功能。一般来说,计划对负载均衡器进行更多的开发,在nginx的下一个版本中,用于在不同上游服务器之间分配负载以及健康检查的机制将得到极大的改进。

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

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

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

关于上述方法的一个注意事项是,存在为连接的整个生命周期分配的内存缓冲区,因此对于长生命周期的连接,会保留一些额外的内存。同时,在空闲的keepalive连接上,nginx只花费550字节的内存。nginx未来版本的一个可能的优化是对长生命周期的连接重用和共享内存缓冲区。

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

不幸的是,上述所有内容从未以一致和简单的方式进行描述,这使得为nginx开发第三方扩展的工作变得非常复杂。尽管存在一些关于nginx内部构件的优秀文档(例如由Evan miller贡献的那些文档),但是这类文档需要大量的逆向工程工作,并且nginx模块的实现对许多人来说仍然是一门神秘的艺术。

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

14.5. 经验总结

当Igor Sysoev开始编写nginx时,大多数支持Internet的软件都已经存在,而此类软件的体系结构通常遵循遗留服务器和网络硬件、操作系统和旧Internet体系结构的定义。然而,这并没有阻止Igor认为他可以改进web服务器领域的东西。因此,第一个经验显而易见,它是:总有改进的空间。

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

另一点值得一提的是,开发应该专注。nginx的Windows版本可能是一个很好的例子,说明了避免开发人员的核心能力或目标应用程序之外的工作耗费太多精力是值得的。它同样适用于重写引擎,出现多次尝试使用更多特性增强nginx,以便与现有遗留设置向后兼容。

最后但同样重要的,值得一提的是,尽管nginx开发人员社区不是很大,但第三方模块和扩展一直是nginx广受欢迎的重要原因。Evan Miller、Piotr Sikora、Valery Kholodkov、Zhang Yichun (agentzh)等优秀软件工程师的工作得到了nginx用户社区及其原始开发人员的高度赞赏。

翻译:xiushao

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 《开源软件架构》中nginx章节

  • Trackback 关闭
  • 评论 (1)
    • manongxiaowen
    • 2021/08/18 3:41下午

    细节满满,赞!推荐博主使用一个好用的接口管理工具-ApiPost,免费下载使用的,谢谢博主啦

return top