nginx(发音为“engine x”)是一个由俄罗斯软件工程师Igor Sysoev编写的免费开源web服务器。自2004年发布以来,nginx一直专注于高性能、高并发和低内存使用。在web服务器功能之上的其他特性,如负载平衡、缓存、访问和带宽控制,以及高效地与各种应用程序集成的能力,都有助于使nginx成为现代网站架构的良好选择。目前,nginx是互联网上第二受欢迎的开源web服务器。
为什么高并发很重要?
如今,互联网是如此广泛和无处不在,以至于我们很难想象十年前没有它。它已经从简单的HTML生成可点击的文本,基于NCSA,然后在Apache web服务器上,发展成为全球超过20亿用户使用的始终在线的通信媒体。随着永久连接的个人电脑、移动设备和最近的平板电脑的普及,互联网的格局正在迅速改变,整个经济已经数字化连接。在线服务变得更加精致,明显偏向于即时获得的实时信息和娱乐。运行在线业务的安全方面也发生了重大变化。因此,现在的网站比以前复杂得多,通常需要更多的工程努力来保持健壮和可扩展。
对网站架构师来说,最大的挑战之一一直是并发。自web服务诞生以来,并发性的水平一直在不断提高。对于一个热门网站来说,同时为数十万甚至数百万用户提供服务并不罕见。十年前,并发的主要原因是客户端速度慢——即使用ADSL或拨号连接的用户。现在,并发是由移动客户端和较新的应用程序架构的组合引起的,这些架构通常基于维护持久连接,允许客户端更新新闻、推文、好友提要等。另一个提高并发性的重要因素是现代浏览器的行为发生了变化,浏览器会同时打开4到6个连接来提高页面加载速度。
为了说明客户端速度慢的问题,想象一个简单的基于apache的web服务器,它产生一个相对较短的100kb响应——一个包含文本或图像的web页面。生成或检索此页面可能只需要几分之一秒,但在带宽为80 kbps (10 KB/s)的情况下,将其传输到客户端却需要10秒。本质上,web服务器会相对较快地拉取100 KB的内容,然后在释放连接之前将这些内容发送给客户端,并需要忙碌10秒。现在假设您同时有1000个请求相似内容的连接客户端。如果为每个客户端只分配1 MB的额外内存,那么仅为1000个客户端提供100 KB的内容就需要额外分配1000 MB(约1 GB)的内存。实际上,一个典型的基于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服务器同时处理1万个客户端了”,并预言了我们现在所说的互联网云服务。Kegel的C10K方案激发了许多解决web服务器优化问题的尝试,以解决同时处理大量客户端的问题,nginx被证明是最成功的一个。
为了解决C10K的10,000个同时连接问题,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-plus-Apache web配置中仍然经常得不到解决。
使用nginx还有其他优势吗?
高性能高效地处理高并发一直是部署nginx的主要好处。然而,现在还有更有趣的好处。
在过去的几年中,web架构师已经接受了将应用程序基础设施与web服务器分离的想法。然而,以前以LAMP (Linux、Apache、MySQL、PHP、Python或Perl)形式存在的网站,现在可能不仅仅是基于lemp的网站(“E”代表“Engine x”),而是越来越多地将web服务器推向基础设施的边缘,并以不同的方式集成相同的应用程序和数据库工具。
nginx非常适合这样做,因为它提供了必要的关键特性,可以方便地卸载并发、延迟处理、SSL(安全套接字层)、静态内容、压缩和缓存、连接和请求限制,甚至从应用层到更高效的边缘web服务器层的HTTP流媒体。它还允许直接与memcached/Redis或其他“NoSQL”解决方案集成,以提高服务大量并发用户时的性能。
随着最新风格的开发工具包和编程语言的广泛使用,越来越多的公司正在改变他们的应用程序开发和部署习惯。Nginx已经成为这些不断变化的范式中最重要的组成部分之一,它已经帮助许多公司在预算范围内快速启动和开发他们的web服务。
nginx的第一行代码写于2002年。2004年,它在两条款BSD许可下向公众发布。自那以后,nginx用户的数量一直在增长,他们贡献了想法,提交了bug报告、建议和观察结果,这对整个社区非常有帮助和有益。
nginx的代码库是原始的,完全用C编程语言从头编写。nginx已经被移植到许多架构和操作系统上,包括Linux, FreeBSD, Solaris, Mac OS X, AIX和Microsoft Windows。nginx有自己的库,除了zlib, PCRE和OpenSSL之外,它的标准模块不使用系统的C库。如果不需要,或者因为潜在的许可证冲突,可以选择将它们排除在构建之外。
简单介绍一下Windows版本的nginx。虽然nginx工作在Windows环境中,但Windows版本的nginx更像是一个概念验证,而不是一个功能齐全的端口。目前,nginx和Windows内核架构的某些限制不能很好地交互。已知的Windows版本的nginx存在的问题包括并发连接数量少、性能下降、没有缓存和没有带宽管制。未来版本的Windows nginx将更接近主流功能。
nginx架构概述
传统的基于进程或线程的并发连接处理模型涉及用单独的进程或线程处理每个连接,并阻塞网络或输入/输出操作。根据应用程序的不同,它在内存和CPU消耗方面可能非常低效。派生一个单独的进程或线程需要准备一个新的运行时环境,包括分配堆和堆栈内存,以及创建一个新的执行上下文。创建这些项目也花费了额外的CPU时间,由于过度的上下文切换导致线程抖动,最终可能导致性能低下。所有这些复杂的问题都体现在像Apache这样的旧web服务器架构中。这是在提供丰富的通用特性和优化服务器资源使用之间的一种权衡。
从一开始,nginx就被认为是一个专门的工具,可以实现更高的性能、密度和服务器资源的经济使用,同时支持网站的动态增长,因此它遵循了不同的模型。实际上,它的灵感来自于各种操作系统中不断发展的基于事件的高级机制。由此产生了模块化、事件驱动、异步、单线程、非阻塞的架构,这成为了nginx代码的基础。
Nginx大量使用多路复用和事件通知,并将特定的任务分配给单独的进程。连接在数量有限的单线程进程(称为worker)中高效的运行循环中处理。在每个worker中,nginx每秒可以处理数千个并发连接和请求。
代码结构
nginx worker的代码包括核心模块和功能模块。nginx的核心负责维护一个紧密的运行循环,并在请求处理的每个阶段执行模块中适当的代码段。模块构成了表示层和应用层的大部分功能。模块从网络和存储读取和写入,转换内容,进行出站过滤,应用服务器端包括动作,并在代理激活时将请求传递给上游服务器。
Nginx的模块化架构通常允许开发人员在不修改Nginx核心的情况下扩展web服务器功能集。Nginx模块有一些不同的版本,即核心模块、事件模块、阶段处理程序、协议、变量处理程序、过滤器、upstream和负载均衡器。目前,nginx不支持动态加载模块;例如,模块在构建阶段与核心一起编译。但是,未来的主要版本计划支持可加载模块和ABI。有关不同模块角色的更详细信息,可以在14.4节找到。
在处理与接受、处理和管理网络连接和内容检索相关的各种操作时,nginx使用事件通知机制,以及在Linux、Solaris和基于bsd的操作系统(如kqueue、epoll和事件端口)中的许多磁盘I/O性能增强。其目标是向操作系统提供尽可能多的提示,以便获得及时的异步反馈,包括入站和出站通信、磁盘操作、从套接字读取或写入、超时等。对于nginx运行的每个基于unix的操作系统,对多路复用和高级I/O操作的不同方法的使用进行了大量优化。
nginx架构的高层架构
Workers Model
如前所述,nginx不会为每个连接生成一个进程或线程。相反,worker进程接受来自共享的“listen”套接字的新请求,并在每个worker内部执行高效的run-loop,以处理每个worker的数千个连接。在nginx中没有专门的仲裁或分配到worker的连接;这项工作是由操作系统的内核机制完成的。启动后,会创建一组初始的监听套接字。worker在处理HTTP请求和响应的同时,持续地接受、读取和写入套接字。
run-loop是nginx worker代码中最复杂的部分。它包含了全面的内部调用,并且严重依赖于异步任务处理的思想。异步操作是通过模块化、事件通知、大量使用回调函数和微调过的定时器实现的。总的来说,关键原则是尽可能的非阻塞。nginx仍然阻塞的唯一情况是工作进程没有足够的磁盘存储性能。
因为nginx不会为每个连接派生一个进程或线程,所以内存使用非常保守,在大多数情况下非常高效。nginx也节省了CPU周期,因为进程或线程没有持续的创建-销毁模式。nginx所做的是检查网络和存储的状态,初始化新连接,将它们添加到run-loop中,并异步处理直到完成,此时连接被释放并从run-loop中删除。结合谨慎使用系统调用和对支持接口(如pool和slab内存分配器)的精确实现,即使在极端工作负载下,nginx通常也能实现中等到低的CPU使用率。
因为nginx生成了多个worker来处理连接,所以它可以很好地跨多个内核扩展。通常,每个核单独的工作线程可以充分利用多核架构,并防止线程抖动和锁。没有资源饥饿,资源控制机制在单线程工作进程中是隔离的。该模型还允许跨物理存储设备具有更好的可扩展性,有利于提高磁盘利用率,避免磁盘I/O阻塞。因此,在多个worker共享工作负载的情况下,服务器资源得到了更有效的利用。
对于某些磁盘使用和CPU负载模式,应该调整nginx worker的数量。这里的规则有些基础,系统管理员应该针对他们的工作负载尝试一些配置。一般建议如下:如果负载模式是CPU密集型的——例如,处理大量TCP/IP,进行SSL或压缩——nginx worker的数量应该与CPU内核的数量相匹配;如果负载主要是磁盘I/O,例如,从存储中提供不同的内容集,或者繁重的代理,那么工作节点的数量可能是内核数量的1.5到2倍。一些工程师根据单个存储单元的数量来选择worker的数量,尽管这种方法的效率取决于磁盘存储的类型和配置。
nginx的开发人员将在即将发布的版本中解决的一个主要问题是如何避免磁盘I/O上的大部分阻塞。目前,如果没有足够的存储性能来服务于特定worker生成的磁盘操作,该worker仍然可能阻塞从磁盘的读/写。存在许多机制和配置文件指令来减轻这种磁盘I/O阻塞场景。最值得注意的是,像sendfile和AIO这样的选项组合通常会产生大量磁盘性能空间。应该根据数据集、nginx可用的内存量和底层存储架构来规划nginx的安装。
现有worker模型的另一个问题是对嵌入式脚本的支持有限。首先,使用标准的nginx发行版,只支持嵌入Perl脚本。对此有一个简单的解释:关键问题是嵌入式脚本阻塞任何操作或意外退出的可能性。这两种行为都会立即导致worker处于挂起状态,同时影响成千上万个连接。计划进行更多工作,使nginx的嵌入式脚本更简单,更可靠,适用于更广泛的应用程序。
nginx进程角色
Nginx在内存中运行多个进程;有一个主进程和几个工作进程。还有一些特殊用途的进程,特别是缓存加载器和缓存管理器。版本1.x中的所有进程都是单线程的。所有进程主要使用共享内存机制进行进程间通信。master进程以root用户运行。缓存加载器、缓存管理器和工作程序以非特权用户身份运行。
master进程负责以下任务:
- reading and validating configuration
- creating, binding and closing sockets
- starting, terminating and maintaining the configured number of
worker
processes - reconfiguring without service interruption
- controlling non-stop binary upgrades (starting new binary and rolling back if necessary)
- re-opening log files
- compiling embedded Perl scripts
worker进程接受、处理和处理来自客户端的连接,提供反向代理和过滤功能,并完成nginx能做的几乎所有其他事情。关于监控nginx实例的行为,系统管理员应该关注worker,因为它们是反映web服务器实际日常操作的进程。
缓存加载器进程负责检查磁盘上的缓存项,并使用缓存元数据填充nginx的内存数据库。本质上,缓存加载器准备nginx实例来处理已经存储在磁盘上特定分配的目录结构中的文件。它遍历目录,检查缓存内容元数据,更新共享内存中的相关条目,然后在一切都干净并可以使用时退出。
缓存管理器主要负责缓存过期和失效。它在正常的nginx操作期间保持在内存中,在失败的情况下由主进程重新启动。
nginx缓存简要概述
nginx中的缓存以文件系统中分层数据存储的形式实现。缓存键是可配置的,不同的请求-特定参数可以用来控制进入缓存的内容。缓存键和缓存元数据存储在共享内存段中,其中缓存加载器、缓存管理器和工作人员可以访问。目前没有文件的任何内存缓存,除了操作系统的虚拟文件系统机制所暗示的优化之外。每个缓存响应都放置在文件系统上的不同文件中。层次结构(级别和命名细节)通过nginx配置指令来控制。当将响应写入到缓存目录结构时,该文件的路径和名称从代理URL的MD5哈希派生出来。
将内容放置在缓存中的过程如下:当nginx从上游服务器读取响应时,内容首先写入缓存目录结构之外的临时文件。当nginx完成处理请求时,它会重命名临时文件并将其移动到缓存目录。如果用于代理的临时文件目录在另一个文件系统上,该文件将被复制,因此建议将临时目录和缓存目录保存在同一个文件系统上。当需要显式清除文件时,从缓存目录结构中删除文件也是相当安全的。nginx有第三方扩展,可以远程控制缓存的内容,并且计划进行更多工作来将此功能集成到主发行版中。
nginx配置
nginx的配置系统受到Igor Sysoev使用Apache经验的启发。他的主要见解是可伸缩的配置系统对web服务器至关重要。当维护包含大量虚拟服务器、目录、位置和数据集的大型复杂配置时,会遇到主要的扩展问题。在一个相对较大的web设置中,如果应用程序级别和系统工程师自己没有正确地完成,可能会成为一场噩梦。
因此,nginx配置旨在简化日常操作,并为进一步扩展web服务器配置提供一种简单的方法。
nginx配置保存在一些普通文本文件中,这些文件通常驻留在/ usrc / local/es/etc/nginx或/ etc / nginx。主配置文件通常称为nginx.conf。为了保持它的整洁,配置的部分可以放置在单独的文件中,可以自动包含在主文件中。然而,应该指出的是,nginx目前不支持apache风格的分布式配置(即htaccess文件)。所有与nginx web服务器行为相关的配置都应该驻留在一组集中的配置文件中。
配置文件最初由master进程读取和验证。当worker进程从master进程派生出来时,它们可以使用编译后只读形式的nginx配置。配置结构由通常的虚拟内存管理机制自动共享。
Nginx配置有几个不同的上下文,用于main、http、server、upstream、location(以及邮件代理的mail)指令块。上下文永远不会重叠。例如,没有将location块放在指令的主块中这样的事情。此外,为了避免不必要的歧义,没有任何类似于“全局web服务器”配置的东西。Nginx的配置应该是干净且符合逻辑的,允许用户维护包含数千个指令的复杂配置文件。在一次私人谈话中,Sysoev说:“全局服务器配置中的位置、目录和其他块是我在Apache中不喜欢的功能,所以这就是为什么它们从未在nginx中实现的原因。”
配置语法、格式和定义遵循所谓的c风格约定。这种制作配置文件的特殊方法已经被各种开源和商业软件应用程序所使用。从设计上讲,c风格的配置非常适合嵌套描述,具有逻辑性,易于创建、阅读和维护,受到许多工程师的喜欢。nginx的c风格配置也可以很容易地自动化。
虽然一些nginx指令类似于Apache配置的某些部分,但设置nginx实例是完全不同的体验。例如,nginx支持重写规则,尽管它需要管理员手动调整遗留Apache重写配置以匹配nginx风格。重写引擎的实现也有所不同。
通常,nginx设置还提供了一些原始机制的支持,这些机制可以非常有用,作为敏捷web服务器配置的一部分。简单地提到变量和try_file指令是有意义的,这对nginx来说是独一无二的。开发了nginx中的变量,以提供一个更强大的机制来控制web服务器的运行时配置。变量对快速评估进行了优化,并在内部预编译到索引。评估是根据需求进行的;即变量的值通常只计算一次,并缓存一个特定请求的生命周期。变量可以使用不同的配置指令,为描述条件请求处理行为提供额外的灵活性。
try_files指令最初旨在以一种更适当的方式逐步取代有条件的if配置语句,它旨在快速有效地尝试/匹配不同的uri到内容的映射。总的来说,try_files指令工作得很好,非常高效和有用。建议读者彻底检查try_files指令,并在适当的时候使用它。
nginx Internals
如前所述,nginx代码库由一个核心和许多模块组成。nginx的核心负责提供web服务器的基础,web和邮件反向代理功能;它支持使用底层网络协议,构建必要的运行时环境,并确保不同模块之间的无缝交互。然而,大多数特定于协议和应用程序的功能是由nginx模块完成的,而不是核心。
在内部,nginx通过管道或模块链处理连接。换句话说,对于每个操作,都有一个模块在做相关的工作;例如,压缩,修改内容,执行服务器端,通过FastCGI或uwsgi协议与上游应用服务器通信,或与memcached通信。
有一些nginx模块位于核心和真正的“功能”模块之间。这些模块是http和mail。这两个模块在核心组件和较低级别组件之间提供了额外的抽象级别。在这些模块中,实现了与相应的应用层协议(如HTTP、SMTP或IMAP)相关联的事件序列的处理。结合nginx核心,这些上层模块负责维护对各自功能模块的正确调用顺序。虽然HTTP协议目前是作为HTTP模块的一部分实现的,但由于需要支持其他协议,如SPDY(请参阅“SPDY: An experimental protocol for a faster web”),未来计划将其分离为一个功能模块。
功能模块可分为事件模块、阶段处理程序、输出过滤器、变量处理程序、协议、上游和负载均衡器。这些模块中的大多数补充了nginx的HTTP功能,尽管事件模块和协议也用于邮件。事件模块提供了一种特定的依赖于操作系统的事件通知机制,如kqueue或epoll。nginx使用的事件模块取决于操作系统的功能和构建配置。协议模块允许nginx通过HTTPS, TLS/SSL, SMTP, POP3和IMAP进行通信。
典型的HTTP请求处理周期如下。
- Client sends HTTP request.
- nginx core chooses the appropriate phase handler based on the configured location matching the request.
- If configured to do so, a load balancer picks an upstream server for proxying.
- Phase handler does its job and passes each output buffer to the first filter.
- First filter passes the output to the second filter.
- Second filter passes the output to third (and so on).
- Final response is sent to the client.
Nginx模块调用是非常可定制的。它通过使用指向可执行函数的指针的一系列回调来执行。然而,这样做的缺点是,它可能会给想要编写自己的模块的程序员带来很大的负担,因为他们必须准确定义模块应该如何以及何时运行。nginx API和开发者文档都在改进中,并提供更多可用性来缓解这一问题。
下面是一些可以附加模块的例子:
- Before the configuration file is read and processed
- For each configuration directive for the location and the server where it appears
- When the main configuration is initialized
- When the server (i.e., host/port) is initialized
- When the server configuration is merged with the main configuration
- When the location configuration is initialized or merged with its parent server configuration
- When the master process starts or exits
- When a new worker process starts or exits
- When handling a request
- When filtering the response header and the body
- When picking, initiating and re-initiating a request to an upstream server
- When processing the response from an upstream server
- When finishing an interaction with an upstream server
在worker内部,导致生成响应的运行循环的操作序列如下所示:
- Begin
ngx_worker_process_cycle()
. - Process events with OS specific mechanisms (such as
epoll
orkqueue
). - Accept events and dispatch the relevant actions.
- Process/proxy request header and body.
- Generate response content (header, body) and stream it to the client.
- Finalize request.
- Re-initialize timers and events.
运行循环本身(步骤5和6)确保增量生成响应并将其流式传输到客户端。
处理HTTP请求的更详细视图可能如下所示:
- Initialize request processing.
- Process header.
- Process body.
- Call the associated handler.
- Run through the processing phases.
这就把我们带到了各个阶段。当nginx处理HTTP请求时,它会将请求传递给多个处理阶段。在每个阶段都有处理程序可以调用。一般来说,阶段处理程序处理请求并产生相关的输出。阶段处理程序附加到配置文件中定义的位置。
阶段处理程序通常做四件事:获取位置配置、生成适当的响应、发送header和发送body。处理程序有一个参数:描述请求的特定结构。请求结构包含许多关于客户端请求的有用信息,例如请求方法、URI和首部。
当读取HTTP请求头时,nginx会查找相关的虚拟服务器配置。如果找到虚拟服务器,请求将经历6个阶段:
- server rewrite phase
- location phase
- location rewrite phase (which can bring the request back to the previous phase)
- access control phase
- try_files phase
- log phase
为了响应请求生成必要的内容,nginx将请求传递给适当的内容处理程序。根据具体的位置配置,nginx可能会首先尝试所谓的无条件处理程序,如perl、proxy_pass、flv、mp4等。如果请求与上述任何一个内容处理程序都不匹配,则由下列处理程序之一选择,顺序如下:random index、index、autoindex、gzip_static、static。
索引模块的详细信息可以在nginx文档中找到,但这些模块处理以斜杠结尾的请求。如果像mp4或autoindex这样的专用模块不合适,则内容被认为只是磁盘上的文件或目录(即静态),并由静态内容处理程序提供服务。对于目录,它将自动重写URI,以便始终保留结尾的斜杠(然后发出HTTP重定向)。
然后,内容处理程序的内容被传递给过滤器。过滤器也附加到位置,并且可以为一个位置配置多个过滤器。过滤器执行操作处理程序产生的输出的任务。过滤器的执行顺序在编译时确定。对于开箱即用的过滤器,它是预定义的,对于第三方过滤器,它可以在构建阶段进行配置。在现有的nginx实现中,过滤器只能进行出站更改,目前没有编写和附加过滤器以进行输入内容转换的机制。输入过滤将出现在nginx的未来版本中。
过滤器遵循特定的设计模式。一个过滤器被调用,开始工作,并调用下一个过滤器,直到链中的最后一个过滤器被调用。之后,nginx完成响应。滤镜不必等待前一个滤镜完成。链中的下一个过滤器可以在前一个过滤器的输入可用时立即开始它自己的工作(功能上很像Unix管道)。反过来,生成的输出响应可以在接收来自上游服务器的整个响应之前传递给客户端。
有header过滤器和body过滤器;Nginx将响应头和响应体分别提供给相关的过滤器。
报文头过滤器由三个基本步骤组成。
- Decide whether to operate on this response.
- Operate on the response.
- Call the next filter.
Body过滤器转换生成的内容。body过滤器的例子包括:
- server-side includes
- XSLT filtering
- image filtering (for instance, resizing images on the fly)
- charset modification
gzip
compression- chunked encoding
在过滤器链之后,响应被传递给写入器。除了写入器,还有两个额外的特殊用途过滤器,即复制过滤器和延迟过滤器。复制过滤器负责用相关的响应内容填充内存缓冲区,这些内容可能存储在代理临时目录中。延后过滤器用于子请求。
子请求是处理请求/响应的一个非常重要的机制。子请求也是nginx最强大的功能之一。对于子请求,nginx可以从与客户端最初请求的URL不同的URL返回结果。有些web框架称之为内部重定向(internal redirect)。然而,nginx更进一步——过滤器不仅可以执行多个子请求并将输出组合为单个响应,而且子请求也可以嵌套和分层。子请求可以执行自己的子请求,子请求可以发起子请求。子请求可以映射到硬盘上的文件、其他处理程序或上游服务器。子请求对于根据原始响应中的数据插入额外的内容非常有用。例如,SSI(服务器端include)模块使用过滤器来解析返回文档的内容,然后用指定url的内容替换include指令。或者,它可以是一个创建过滤器的例子,将文档的整个内容作为要检索的URL,然后将新文档附加到URL本身。
Upstream和负载均衡器也值得简要介绍。upstream用于实现内容处理程序,它是一个反向代理(proxy_pass处理程序)。Upstream模块主要是准备要发送到上游服务器(或“后端”)的请求,并从上游服务器接收响应。这里没有调用输出过滤器。upstream模块所做的就是设置回调函数,当upstream服务器准备写入和读取时调用。存在实现下列功能的回调函数:
- Crafting a request buffer (or a chain of them) to be sent to the upstream server
- Re-initializing/resetting the connection to the upstream server (which happens right before creating the request again)
- Processing the first bits of an upstream response and saving pointers to the payload received from the upstream server
- Aborting requests (which happens when the client terminates prematurely)
- Finalizing the request when nginx finishes reading from the upstream server
- Trimming the response body (e.g. removing a trailer)
负载均衡器模块附加到proxy_pass处理程序,以提供在超过一个上游服务器合格时选择上游服务器的能力。负载平衡器注册一个启用配置文件指令,提供额外的上游初始化函数(用于解析DNS等的上游名称),初始化连接结构,决定在哪里路由请求,并更新统计信息。目前nginx支持两种标准规程,用于在上游服务器上进行负载平衡:循环和ip-hash。
Upstream和负载均衡处理机制包括检测失败的Upstream服务器和将新请求重新路由到剩余服务器的算法——尽管计划进行大量额外的工作来增强这一功能。总的来说,nginx计划在负载均衡器上进行更多的工作,在nginx的下一个版本中,跨不同上游服务器分配负载的机制以及健康检查将得到极大改进。
还有一些其他有趣的模块提供了一组额外的变量,供在配置文件中使用。虽然nginx中的变量是在不同的模块中创建和更新的,但有两个模块完全专用于变量:geo和map。geo模块用于基于IP地址跟踪客户端。这个模块可以创建依赖于客户端的IP地址的任意变量。另一个模块map允许从其他变量创建变量,本质上提供了对主机名和其他运行时变量进行灵活映射的能力。这种模块可以称为变量处理程序。
在单个nginx worker中实现的内存分配机制在某种程度上受到了Apache的启发。nginx内存管理的高级描述如下:对于每个连接,必要的内存缓冲区是动态分配、链接的,用于存储和操作请求和响应的头、体,然后在连接释放时释放。值得注意的是,nginx尽量避免在内存中复制数据,并且大多数数据都是通过指针值传递的,而不是通过调用memcpy。
更深入地说,当模块生成响应时,检索到的内容将放入内存缓冲区,然后添加到缓冲区链链接。后续处理也可以使用这个缓冲链链接。缓冲链在nginx中是相当复杂的,因为有几种不同的处理方案,它们取决于模块类型。例如,在实现body filter模块时,精确管理缓冲区可能非常棘手。这样的模块一次只能操作一个缓冲区(链),它必须决定是覆盖输入缓冲区,用新分配的缓冲区替换该缓冲区,还是在该缓冲区之前或之后插入一个新的缓冲区。更复杂的是,有时一个模块会接收多个缓冲区,因此它必须对一个不完整的缓冲区链进行操作。然而,此时nginx只提供了用于操作缓冲链的底层API,因此在进行任何实际实现之前,第三方模块开发人员应该对nginx的这个神秘部分非常熟悉。
关于上述方法的注意事项是,在连接的整个生命周期中都分配了内存缓冲区,因此对于长连接,需要保留一些额外的内存。同时,在空闲的keepalive连接上,nginx只花费550字节的内存。对nginx未来版本的一个可能的优化是为长连接重用和共享内存缓冲区。
管理内存分配的任务由nginx池分配器完成。共享内存区域用于接受互斥量、缓存元数据、SSL会话缓存以及与带宽管制和管理(限制)相关的信息。nginx中实现了一个slab分配器来管理共享内存分配。为了允许同时安全地使用共享内存,有许多可用的锁机制(互斥量和信号量)。为了组织复杂的数据结构,nginx还提供了一个红黑树实现。红黑树用于将缓存元数据保存在共享内存中,跟踪非正则表达式位置定义以及其他一些任务。
不幸的是,上述所有内容从未以一致和简单的方式描述过,这使得为nginx开发第三方扩展的工作相当复杂。尽管存在一些关于nginx内部的优秀文档(例如,由Evan miller生成的文档),但这些文档需要大量的逆向工程工作,而且nginx模块的实现对许多人来说仍然是一门黑魔法。
尽管与第三方模块开发相关的某些困难,nginx用户社区最近看到了许多有用的第三方模块。例如,有一个用于nginx的嵌入式Lua解释器模块,用于负载均衡的附加模块,完全支持WebDAV,高级缓存控制以及其他有趣的第三方工作,这些都是本章作者鼓励并将在未来支持的。
经验教训
当Igor Sysoev开始编写nginx时,大多数支持互联网的软件已经存在,这些软件的架构通常遵循传统服务器和网络硬件、操作系统和旧的互联网架构的定义。然而,这并没有阻止Igor认为他可以改进web服务器领域的东西。因此,虽然第一个教训可能看起来很明显,但它是:总是有改进的空间。
在考虑更好的web软件的想法时,Igor花了很多时间开发初始代码结构,并研究了不同的方法来优化各种操作系统的代码。10年后,他正在开发nginx 2.0版本的原型,考虑到第1版的积极开发年。很明显,新架构的初始原型和初始代码结构对软件产品的未来至关重要。
值得一提的另一点是,发展应有重点。Windows版本的nginx可能是一个很好的例子,它说明了如何避免在既不是开发人员的核心竞争力也不是目标应用程序的东西上稀释开发精力。它同样适用于在多次尝试用更多特性增强nginx以与现有遗留设置向后兼容期间出现的重写引擎。
最后值得一提的是,尽管nginx开发者社区不是很大,但nginx的第三方模块和扩展一直是其受欢迎程度的重要组成部分。Evan Miller, Piotr Sikora, Valery Kholodkov, Zhang Yichun (agentzh)和其他才华横溢的软件工程师所做的工作受到了nginx用户社区及其原始开发人员的赞赏。