NginX

nginx(读作”engine x”)是俄罗斯软件工程师Igor Sysoev开发的免费开源web服务器软件。nginx在2004年发布后,就一直专注于高性能,高并发和低内存消耗问题。在基本的web服务器功能的基础上,nginx还具有一些额外的特性,比如负载均衡,缓存,访问控制,带宽控制,以及高效整合各种应用的能力,这些特性使nginx成为构架现代网站架构一个不错的选择。目前,nginx是互联网上第二流行的开源web服务器软件。

14.1.为什么高并发重要

现在互联网是如此的广泛和普及以至于我们很难想象十年前没有网络的时候是什么样子的,它已经从用NCSA后来用Apache的web服务器提供的可点击的文本HTML已然进化成超过20亿人在线的通信媒介。随着永久在线的个人电脑,移动终端以及平板电脑的增多,互联网在快速变化,经济系统也变得数字有线化。在线服务日益精化并且明显的偏向于提供实时可用信息和娱乐,而且运行在线服务的安全需求也有了显著变化。现在的网站比从前更为复杂,需要在工程上做的更具有健壮性和可伸缩性。

并发总是网站架构最大的挑战之一。由于web服务的兴起,并发的数量级在不断增长。热门网站为几十万甚至几百万的同时在线用户提供服务也不稀罕。十年前,并发的主要原因是由于客户端接入速度慢–用户使用ADSL或者拨号商务。现在,并发是由移动终端和新应用架构所的合并所带来的,这些应用通常基于持久连接来为客户端提供新闻,微博,通知等服务。另一个重要的因素就是现代浏览器行为变了,它们浏览网站的时候会同时打开4到6个连接来加快页面加载速度。

为了说明客户端接入速度慢的问题,我们可以想象一个Apache网站要产生小于100KB的响应–包含文本或图片的网页。产生或者恢复这个页面可能仅仅只需要1秒钟,但是如果带宽只有80kbps(10KB/s),需要花10秒才能把这个页面发送到客户端。基本上,web服务器相对快速的推送100KB数据,然后需要等待10秒发送数据之后才能关闭连接。那么现在如果有1000个同时连接的客户端请求相同的页面,那么如果为每个客户端分配1MB内存,就需要1000MB内存来为这1000个客户端提供这个页面。实际上,一个典型的基于Apache的web服务器通常为每个连接分配1MB内存,而移动通信的有效速度也通常是几十kbps。虽然借助于增加操作系统内核socket缓冲区大小,可以优化发送数据给慢客户端的场景,但是这并不是一个常规的解决方案,并且会带来无法预料的副作用。

随着持久连接的需要,处理并发行的问题更加明显,因为为了避免新建HTTP连接所带来的延时,客户端需要保持连接,那么web服务器就需要为每个连接上的客户端分配一定数量的内存。

所以,为了处理持续增长的用户所带来的负载和高水平的并发性要求(并且能够持续处理),网站需要建立在高效的构建上面。然而在与其所对应的另一面,例如硬件(CPU、内存、硬盘)、网络容量、应用和数据存储结构就变得尤为重要,毕竟客户端连接还是在web服务器软件上处理和接受的。因而,随着同时在线数和每秒请求数的增长,web服务器性能也应该能够非线性扩展。

Apache就不适用吗?

Apache,一个产生于二十世纪九十年代的web服务器,在现代的互联网市场上仍占据极大地份额。起初,它的架构符合当时的硬件和操作系统,当然也符合当时的互联网的状况,那时一个网站通常是运行着一个Apache实例的独立物理服务器。到了二十一世纪初,通过对独立的物理服务器模型进行复制来满足日益增长的web服务需求,显然是不能了。虽然Apache为未来的发展打下了坚实的基础,但是它已经被架构成对每一个新的连接请求就把自身复制一份,而这种架构并不适用于网站的非线性扩展。最终Apache成为了一个通用的web服务器,聚焦于多特性、多种第三方扩展、对任何种类的web应用开发的普遍适用性。然而,由于每次连接所消耗的CPU和内存不断增加,把大量的功能集中在单个软件上而不降价,这种做法已经不具有可扩展性了。

因此,当服务器硬件、操作系统和网络资源不再是网站增长的主要限制因素时,网站开发者们开始满世界的寻找高效搭建web服务器的方法。大概十年前,Daniel Kegel,一个卓越的软件工程师,宣布说:“是时候让web服务器同时处理10000个客户端了。”并且他还预言了现在称为云服务的技术。Kegel的C10K问题很明显激励着一大批人去尝试解决这个问题,他们试图通过优化web服务器软件来支持大规模客户端连接的并发处理,而结果nginx是其中最好的一个。

(注:C10K问题的详细介绍:http://www.kegel.com/c10k.html)

为了解决C10K问题,也就是10000个并发连接问题,nginx基于一个不同的架构,一个在处理并发连接和每秒请求的非线性扩展性上更合适的架构。nginx是事件驱动的,所以它没有按照Apache那样对每一个网页请求都创建一个新的进程或线程。最后的结果就是,即使负载增加了,cpu和内存的使用仍然保持可控。现在nginx可以在一个普通硬件构建的服务器上同时处理10000个并发请求

当第一版nginx发布的时候,它是为了部署在Apache旁边以便nginx处理像Html、CSS、JS和图片等静态的内容来为基于Apache应用搭建的服务器分担并发的和潜在的进程。后来随着nginx的演化,它通过支持FastCGI,uswgi和SCGI协议增加了应用程序的集成,以及对分布式内存对象缓存系统如memcached的支持。其他的很有用的功能比如带有缓存和负载均衡的反向代理也被加了进来。这些新加进来的功能把nginx塑造成了一个工具的整合包,这些工具可以高效的搭建一个可扩展的网络基础设施。

在2012年2月,Apache的2.4.x版本发布了。虽然最新发布的Apache版本增加了新的并发处理核心模块和新的代理模块来优化可扩展性和性能,但要说性能、并发能力和资源利用率是否能赶上或超过纯事件驱动模型的web服务器还为时尚早。Apache新版本具有了更好的性能值得高兴,因为对于nginx+Apache的web网站架构,新的Apache虽然不能解决全部问题,但是够缓解后端潜在的瓶颈。

使用nginx还有更多优点吗?

高效的处理高并发性一直是部署nginx的最关键的好处。但是除此以外部署nginx还有很多很有意思的优势。

最近几年,web架构一直在拥抱去耦的理念并且从web服务器中分离出它们的应用设施。虽然现在仅仅是将原先基于LAMP(Linux, Apache, MySQL, PHP, Python or Perl)所构建的网站,变为基于LEMP(E表示Engine x)的。但是,越来越多的实践是将web服务器推入基础设施的边缘,并且用不同的方法整合这些相同或更新的应用和数据库工具集。

nginx很适合做这样的事情,因为它提供了关键性的特性和功能,可以很方便地把并发性、延时处理、SSL、缓存和压缩、连接和请求限制甚至是HTTP媒体流从应用层剥离下来,转移到一个更加高效的边缘的服务器层上。同时nginx还允许直接集成memcached/Redis或者是NoSQL的其他解决方案,以在处理大量并发用户的时候提升性能。

随着现代开发包和程序设计语言的普及,越来越多的公司开始改变他们应用开发和部署的方式。nginx已经成为这些正在改变的范式当中最重要的组件之一,而且它已经帮助许多公司在预算内快速开发出他们的web应用。

nginx的开发始于2002年,在2004年基于2-clause BSD协议发布。自此以后,nginx的用户数量不断增加,他们贡献想法和创意,提交错误报告、建议和观察报告,为整个nginx社区提供了极大的帮助。

nginx的基本代码是完全由C语言从头编写的,现在它已经移植到了许多架构和操作系统中,包括Linux, FreeBSD, Solaris, Mac OS X, AIX和Microsoft Windows。Nginx有自己的函数库,并且除了zlib、PCRE和OpenSSL之外,标准模块只使用系统C库函数。而且如果不需要的话或者为了避免潜在的证书上的矛盾,我们还可以有选择的去掉一些模块。

下面是关于Windows版nginx的一些话。当在Windows环境下运行nginx,Windows版的nginx更像是一个概念验证的版本而不是全部功能移植的版本。这是因为在现阶段nginx与Windows的内核架构有限制所导致的。我们目前已知的Windows版本的nginx存在以下问题:并发连接数低、效率下降、没有缓存和宽带管理。未来Windows版本的nginx会更加贴近于主流版本。

14.2.nginx架构总览

传统的基于进程/线程模型来解决并发连接的问题通过对每一个连接都创造一个单独的进程或线程来处理,从而陷入了网络或者是I/O操作的阻塞。根据应用的不同,就内存和CPU的消耗而言,这种方式是毫无效率的。产生一个单独的进程或线程需要准备一个新的运行时环境,包括分配栈区和堆区的内存和产生一个新的执行上下文。创建这些事情还需要额外的CPU时间,并且由于CPU在线程可执行上下文之间大连切换,最终导致效率低下。所有这些并发症在像Apache一样的旧web服务器架构上凸显出来。这是一个在提供丰富的通用应用功能和优化服务器资源使用之间的权衡。

从一开始的时候,nginx就意在成为一个专门的软件,在允许网站可以动态增长的同时,提高服务器性能和资源的密集高效的使用效率,所以它使用了不同的模型。这个模型的灵感来源于日益发展的在不同操作系统上的事件驱动的开发技术,并最终形成了一个模块化、事件驱动的、异步的、单线程的、非阻塞的架构,这就是nginx代码的基础。

nginx大量使用多路传输和事件通知机制,并且致力于为不同的进程分配不同的任务。连接请求是由有限的单线程进程组成的高效的回环处理的,这些进程被称作’’worker’’.每个’’worker’’每秒可以处理成千上万的并发请求和连接。

代码结构

nginx’’worker’’的代码包含核心和功能模块。nginx的核心负责维持一个紧致的事件处理循环并且在请求处理的每个阶段执行恰当的代码模块。模块组成了大部分的展示和应用层的功能。模块会从网络和存储设备上读取和写入信息、转换内容、做输出过滤、SSI(server-side include)处理或者在启用代理的时候给上游服务器发送请求。

nginx模块化的架构通常允许开发者在不修改nginx核心的情况下扩展web服务器功能。nginx的模块有以下这些略有不同的形态,叫做核心模块、事件模块、阶段处理器、协议、变量处理器、过滤器、上游和负载均衡器等。目前,nginx不支持动态的加载模块,例如(可以这样理解),模块代码和核心代码是一起编译的。然而,支持动态加载模块和ABI已经计划在将来的某个版本开发。关于不同模块角色的详细信息可以参见14.4节。

(注:在最近发布的nginx1.10.0中已经开始加入动态模块加载)

在处理各种各样的跟接受、处理、管理网络连接和内容检索的操作时,nginx使用事件通知机制和大量Linux、Solaris和BSD系统上的增强硬件IO性能的技术,比如’’kqueue’’、’’epoll’’还有’’event ports’’.目的在于尽可能的为操作系统提供提示,这些提示是为了及时地获得网络进出流量,磁盘操作,套接字读取和写入,超时等事件的异步反馈。nginx极大的优化了基于Unix的操作系统多路传输和高级IO操作的方法。

图14.1展示了nginx的高层架构

‘’Worker’’模型

之前提到过,nginx不会为每个连接产生一个进程或线程。反之,每个’’worker’’为处理成千上万个连接,会监听一个共享的套接字来接受请求并在内部执行一个高效的循环。在nginx的’’worker’’中不存在仲裁器和分发器,这项工作由操作系统内核完成。在启动之初,nginx会创建并初始化一系列的监听套接字,之后’’worker’’进程不断地通过这些套接字接受、读取HTTP请求和输出响应。

nginx’’worker’’中最复杂的部分就是事件处理循环。它包括了全面的内部调用,并且极其依赖异步任务处理思想。异步操作通过模块化、事件通知、大量回调函数以及微调定时器等实现。总的来说,关键原则就是尽可能的非阻塞。nginx’’worker’’进程被阻塞的唯一情况就是磁盘存储性能不足。

因为nginx没有为每个连接产生进程或线程,所以内存的使用在大多数情况下是节约且高效的。同时nginx也节省CPU时间,因为对于进程或线程来说并不存在持续不断的创建-销毁状态。nginx要做的就是检查网络和存储的状态、初始化新的连接、把它们加入事件循环、在完成之前做异步处理,到那时,连接请求已经解除分配并且从时间循环中移除了。兼具精心设计的系统调用和诸如内存池等支持接口的精确实现,nginx在极端负载的情况下通常能做到中低CPU使用率。

因为nginx派生多个’’worker’’进程来处理连接,所以在多核CPU中有很好的扩展性。通常来说,一个单独的’’worker’’占有一个CPU可以充分利用多核架构,而且可以避免线程抖动和锁。那么在一个’’worker’’进程中就不存在资源匮乏的问题,资源控制机制也是隔离的。这个模型也允许了物理存储设备之间的扩展,提高磁盘利用率并避免磁盘IO上的阻塞。其结果就是,通过将工作负载分配到多个worker进程上,服务器资源可以得到高效的利用。

在某些磁盘使用和CPU负载模式下,nginx’’worker’’进程的数量需要做适当的调整。这里都是些基本规则,系统管理员应该根据工作负载多尝试几种配置。

下面是一些通常的推荐规则:

  • 如果负载模式是CPU密集型,例如处理大量TCP/IP协议,使用SSL,或者压缩数据等,nginx worker进程应该和CPU核心数相匹配

  • 如果是磁盘密集型,例如从存储中提供多种内容服务,或者是大量的代理服务,worker的进程数应该是1.5到2倍的CPU核心数

一些工程师根据独立存储单元的数量来选择’’worker’’进程的数量,虽然这种方式的效率依赖于磁盘存储的类型和配置。

nginx的开发者在未来版本中要解决的主要问题就是怎么样避免磁盘IO上的阻塞。在目前的版本中,如果没有足够的存储性能为一个’’worker’’进程的磁盘操作提供服务,那么’’worker’’进程依然会阻塞在磁盘读写上。现在有很多的机制和配置文件命令可以缓解这样的磁盘IO阻塞的情形。最显著的像sendfile和AIO的结合使用通常可以减少很多磁盘性能的消耗。所以nginx的安装应该基于数据集、可用内存数和底层存储架构来规划。

现有的’’worker’’模型存在的另一个问题是对嵌入式脚本支持有限。举个例子,在现在nginx的布局中,只支持perl语言作为嵌入脚本。原因解释起来很简单:一个嵌入脚本可能在任何的操作中阻塞或者异常退出。这两种行为都会立即导致’’worker’’进程被挂起,同时影响到成千上万的链接。更多的使nginx嵌入脚本更简单、更可靠、更适用于广泛应用的工作已经在计划中。

nginx进程角色

nginx在内存中运行着许多进程,其中包括一个单一的主进程和许多’’worker’’进程。同时还有一些特殊用途的进程,例如缓存加载和缓存管理进程。在nginx的1.x版本中,所有的进程都是单线程的,所有的进程主要都通过内存共享的机制进行进程间通信,主进程运行在root权限下,缓存加载器、缓存管理器以及其他进程运行在非特权用户下。

主进程负责下列任务:

* 读取和校验配置文件

* 创建、绑定、关闭套接字

* 启动、终止和维护配置数目的''worker''进程

* 在不中断服务的情况在重新配置

* 控制不停止的程序升级(启动新的程序并在必要时回滚)

* 重新打开日志文件

* 编译嵌入的Perl脚本

‘’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服务器来说至关重要。当我们要维护大量复杂的虚拟服务器、目录、位置和数据集的配置文件时,我们就会遇到一些主要的扩展性问题。在一个规模相当大的网站的建立过程中,如果没有在应用层进行恰当的配置,那么配置就是工程师们的噩梦。

结果是,nginx的配置系统就是为简化每日操作所设计,并且为web服务器将来的扩展提供了简单的方法。

nginx的配置信息一般保存在位于’’/usr/local/etc/nginx’’或者是’’/etc/nginx’’的文本文件中。主配置文件通常叫做’’nginx.conf’’。为了使配置保持整洁,部分配置可以放到一些分开的文件当中,这些文件可以自动的包括在主配置文件中。然而值得注意的是,目前nginx并不支持Apache风格的分布式配置(例如:’’.htaccess’’文件)。所有的有关nginx的web服务器配置都应该位于一个集中的配置文件集中。

配置文件最初是由主进程读取和校验。’’worker’’进程可以使用一份编译好的只读的配置文件,因为它们都是由主进程产生的。配置信息结构会通过常见的虚拟内存管理机制自动共享。

nginx的配置有很多的上下文,有:’’main’’、’’http’’、’’server’’、’’upstream’’、’’location’’(还有’’mail’’用于邮件代理)这些命令模块。这些上下文从不重叠,比如,从来没有把’’location’’块放到主模块的命令中的。除此以外,为了避免不必要的歧义nginx也没有像“全局服务器配置”这样的东西。nginx的配置是整洁并富有逻辑性的,允许用户去维护一个包含成千上万条指令的复杂的配置文件。在一次私人访谈中,Sysoev说:“我从来都不喜欢Apache全局服务器配置中位置、目录还有其他模块这样的特性,这也就是nginx从未实现过它们的原因。”

配置语法、格式和定义遵循一个所谓的C风格协定。这种特别的配置文件的方法已经用于各种开源和商业软件。根据设计,C风格的配置非常符合嵌套描述,又因为富有逻辑性并易于编写、阅读和维护受广大工程师喜爱。此外,C风格的nginx配置还易于自动化。

尽管nginx中的一些指令与Apache配置中的一些指令很相似,但是搭建一个nginx实例是一个相当不同的体验。例如,虽然nginx支持重写规则,但是它需要一个管理员手动的调整遗留的Apache重写配置以使其符合nginx重写风格。当然重写引擎的实现也不一样。

通常来说,nginx提供了对一些原始机制的支持,这对一个精简服务器的配置来说非常有用。在这里我们简短的谈一谈变量和’’try_files’’指令,这非常有意义,因为它们差不多是nginx独有的。Nginx开发了变量用于提供附加的更强大的机制来控制运行时的web服务器配置。变量为快速求值做了优化并且在内部就被预编译为索引。求值是要立即计算的,例如,变量的值通常在一个请求的生命周期中只计算一次。变量可以用在不同的配置指令中,为描述条件请求的处理行为提供了额外的灵活性。

‘’try_files’’起初是想以一个更恰当的方式逐渐替代表示条件的’’if’’配置语句,并且它被设计有快速且高效的try/match来应对URI与内容之间的映射。总的来说,’’try_files’’指令非常有效并且极其高效和有用。所以我们非常推荐读者完整地阅读 http://nginx.org/en/docs/http/ngx_http_core_module.html#try_files |try_files指令 ,并且任何能用的时候使用它。

14.4.nginx内部构件

正如之前所提到的,nginx的基本代码由一个核心和大量模块组成。nginx的核心负责提供web服务器、web和反向代理邮件功能的基础;实现底层网络协议,构建必要的运行时环境,并且保证不同模块之间的无缝衔接。然而,大量协议上的、应用上的特性都是模块实现的,而跟核心没关系。

从内部讲,nginx通过模块流水线或模块链来处理连接。换句话说,每一个操作都有对应的模块来做相关工作,例如:压缩,修改内容,执行SSI,通过FastCGI或uwsgi协议同后端应用服务器通信,以及同memcached通信等。

有一些模块处于核心与实际“功能”模块之间,它们是’’http’’和’’mail’’.这两个模块在核心和底层构件之间提供了一个额外的抽象层。在这些模块中实现了HTTP、SMTP、IMAP等各自应用层事件序列的处理。这些上层模块和nginx核心搭配起来一起负责以正确的次序调用各自的功能模块。尽管目前HTTP协议是作为’’http’’模块的一部分实现的,我们仍然有计划在将来根据支持像SPDY这样的其他协议的需求把它分离成一个功能模块(参见 http://www.chromium.org/spdy/spdy-whitepaper |SPDY: An experimental protocol for a faster web )。

功能模块可以分为事件模块、阶段处理器、输出过滤器、变量过滤器、协议、上游和负载均衡器。大部分的这些模块补足了nginxHTTP的功能,虽然事件模块和协议也用在’’mail’’模块中。事件模块提供了一种特别的与操作系统相关的事件通知机制像’’kqueue’’和’’epoll’’.nginx所使用的事件模块依赖于操作系统的能力和构建配置。协议模块允许nginx通过HTTPS,TLS/SSL,SMTP,POP3和IMAP协议进行通信。

一个典型的HTTP请求处理周期如下:

  • 客户端发送HTTP请求。
  • nginx核心找出与请求匹配的配置好的位置,并根据这个位置选择合适的阶段处理器。
  • 如果配置中要求了,负载均衡器就会挑选一个上游服务器做反向代理。
  • 阶段处理器开始工作并把每一个输出缓冲区送向第一个过滤器。
  • 第一个过滤器把输出传递给第二个过滤器。
  • 第二个过滤器把输出传递给第三个过滤器(以此类推)。
  • 最终响应会发送至客户端。

nginx模块的调用是极其可制定化的。它通过使用指针指向可执行的函数来完成一系列的回调行为。然而,这样的副作用是它给想开发自己模块的程序员带来了很大的负担,因为他们必须精确定义出它们的模块什么时候以及怎么运行。我们正在改进nginx的API和开发者文档以减轻这部分的负担。

关于在哪里可以添加模块,可以参考如下例子:

  • 配置文件读取和处理之前
  • 每个location和server的指令出现的时候
  • 主配置初始化的时候
  • 服务器(例如主机/端口)初始化的时候
  • 服务器配置与主配置合并的时候
  • 位置配置初始化或与它的上级服务器配置合并的时候
  • 主进程启动或退出的时候
  • 一个新的worker进程启动或退出的时候
  • 处理一个请求的时候
  • 过滤响应头和响应体的时候
  • 挑选、初始化和重新初始化上游服务器的时候
  • 处理一个来自上游服务器的响应的时候
  • 与一个上游服务器完成一次交互的时候

在一个’’worker’’内部,一个事件处理回环由一串行为序列产生,从而生成响应,过程如下:

  • 开始’’ngx_worker_process_cycle()’’
  • 通过具体操作系统的机制来处理事件(比如’’kqueue’’或’’epoll’’)
  • 接收事件并派发相应的行为
  • 处理/代理请求头和请求体(译者注:原文“proxy”含义不明并且包含语法错误)
  • 生成响应内容(响应头、响应体)并以流的形式发送给客户端
  • 完成请求处理
  • 重新初始化定时器和事件

事件处理回环本身(步骤5和6)保证了响应的增量生成并且以流的形式发送给客户端

下面是处理HTTP请求的更多细节:

  • 初始化请求处理
  • 处理头部
  • 处理内容
  • 调用相关的处理器
  • 执行所有的处理阶段

这些细节谈论到了处理阶段这个概念。当nginx处理一个HTTP请求时,它会让请求经过很多个处理阶段,在每个阶段都会有相应的处理器来调用。通常来说,阶段处理器处理一个请求并产生相应的输出。阶段处理器与配置文件中定义的位置绑定。

阶段处理器通常做四件事:获取位置配置,生成恰当的响应,发送响应头,发送响应体。一个处理器只有一个参数:描述请求的具体的结构。一个请求结构体包含着许多与客户端请求有关的信息,比如请求方式、URI、头部。

当HTTP请求的头部被读取的时候,nginx会查找相关的虚拟服务器配置。如果虚拟服务器被找到,那么请求将会经历六个阶段:

  • 服务器重写阶段
  • 定位阶段
  • 位置重写阶段(这个阶段可以把请求退回上一阶段)
  • 访问控制阶段
  • try-files阶段
  • 日志阶段

为了在与请求对应的响应中写入必要的内容,nginx把请求传递给一个合适的内容处理器。依据准确的位置配置信息,nginx可能会先尝试所谓的无条件处理器,如:’’perl’’,’’proxy_pass’’,’’flv’’,’’mp4’’等。如果请求无法与上述的任何一个处理器匹配,那么它就会被下面的某一个处理器挑选并处理,这个挑选过程有明确的顺序:’’random index’’,’’index’’,’’autoindex’’,’’gzip_static’’,’’static’’.

我们可以在nginx的文档中找到index模块的细节信息,但是这些模块只能处理结尾是斜杠的请求。如果是一个像’’mp4’’和’’autoindex’’这样专门的模块都无法匹配上的请求,那么这个请求的内容就会被认为是磁盘上的一个文件或目录(也就是说是静态的),并由’’static’’内容处理器处理。对于一个目录,它总是会自动的把URI的结尾重写成一个斜杠(然后发起一个HTTP重定向)。

内容过滤器的内容之后会传递给过滤器。过滤器与位置也有联系,一个位置信息可以配置多个过滤器。过滤器加工处理器产生的输出,其执行顺序在编译时刻决定。对于nginx本来就有的可以直接使用的过滤器,其执行顺序早就确定了,对于第三方的过滤器,其执行顺序可以在编译阶段配置。在当前nginx的实现中,过滤器只能做输出数据的修改,目前还没有机制支持过滤器对输入内容进行修改。这个功能将会在nginx未来版本中实现。

过滤器遵循一个特定的设计模式。一个过滤器被调用,开始工作后会调用下一个过滤器,直到过滤器链的最后一个过滤器被调用为止。在这之后,nginx结束响应处理。过滤器不用等到前一个过滤器结束,一旦上一个过滤器的输入已经可用(功能上很像Unix的管道)链上的下一个过滤器就开始工作。因而,在从上游服务器接收到所有的响应之前,所生成的输出响应已经被发送给客户端。

过滤器分为header过滤器和body过滤器(译者注:在这里把header和body可以翻译为响应头和响应体,但是会影响句子流畅性),nginx会把响应的header和body分别发送给相关的过滤器。

header的过滤过程包含以下三个步骤:

  • 决定是否对这个相应进行操作
  • 对这个相应进行操作
  • 调用下一个过滤器

body过滤器转换生成的内容,body过滤器的例子有:

  • SSI
  • XSLT过滤器
  • 图片过滤器(例如调整图片大小)
  • 字符集修正
  • ‘’gzip’’压缩
  • 块编码

在经过过滤器链之后,响应会被传递到writer,有一些额外的特殊目的的过滤器与writer有关,叫做’’copy’’过滤器和’’postpone’’过滤器。’’copy’’过滤器负责将相关的响应内容填充到内存缓冲区,这些响应内容有可能存储在反向代理的临时目录。’’postpone’’过滤器负责子请求处理。

子请求处理是请求/响应处理流程中的一个非常重要的机制,它也是nginx最强大的一个方面之一。通过子请求处理机制,nginx可以返回不同的URL的响应,这些URL不是原来客户端所请求的URL.一些网站框架把它叫做内部重定向。然而,nginx能做的更多,它不仅能让过滤器处理多个子请求并把多个子请求合并成一个响应输出,还能让子请求嵌套和分级。一个子请求可以产生自己的子-子请求,子-子请求还可以产生子-子-子请求。子请求可以映射到磁盘文件、其他的处理器或者是上游服务器。子请求是在原有的响应数据的基础上插入额外的数据的最有用的方法。例如,SSI模块使用一个过滤器来解析返回文档的内容,然后用指定URL的内容替换’’include’’指令。或者另一个例子,可以用它来做一个过滤器,这个过滤器可以把整个文档的内容作为一个过滤器来进行取回,然后在这个URL后面添加新的文档。

上游和负载均衡器也值得我们简短描述一下。上游是用来实现反向代理处理器(’’proxy_pass’’处理器)这样的内容处理器。上游模块大多数情况下是来准备把请求发送给上游服务器(或“后端”),然后从上游服务器接收响应。这个过程不用调用输出过滤器。准确的说上游模块做的是设置好上游服务器读写时所需要的回调函数。回调函数实现了如下功能:

  • 创建一个请求缓冲区(或缓冲链)并发送给上游服务器
  • 重新初始化或重新设置与上游服务器的连接(刚好发生在重新发起请求之前)
  • 处理上游响应的前几个bit并保存从上游服务器接收的负载的指针
  • 放弃请求(当客户端过早关闭连接时)
  • 在nginx结束从上游服务器读取信息时结束请求
  • 修整响应体(例如去除尾部标号)

负载均衡器模块可以附加在’’proxy_pass’’处理器上,在有多个合适的上游服务器时选择合适的上游服务器。负载均衡器注册了一个配置文件指令,提供了额外的上游服务器初始化功能(通过DNS解析上游服务器名字等),初始化连接结构,决定请求的转发地址,并且更新状态信息。目前nginx支持两种标准的上游服务器负载均衡规则:轮询和ip哈希。

上游和负载均衡处理机制包括一些算法,这些算法主要是监测上游服务器的异常并把请求重新路由到可用的上游服务器上,虽然现在还有很多计划中的工作来加强这个功能。总的来说,有关负载均衡器的一些工作已经在计划中,在nginx的下几个版本中,上游服务器健康检查和将负载分发到不同的上游服务器的机制将会得到很大的提升。

在nginx中还有一些有意思的模块,它们在配置文件中提供了一些额外的变量来使用。尽管nginx的变量是在不同模块中创造和更新的,还是有两个nginx的模块是完全用于变量的:’’map’’和’’geo’’.’’geo’’模块是用来使根据IP地址跟踪客户端更加容易。这个模块可以根据客户端IP地址创建任意数量的变量。另一个模块,’’map’’,允许通过其它变量来创造变量,本质上是提供了灵活的将主机名向运行时的其他变量映射的能力。这类模块可以叫做变量处理器。

单个nginx’’worker’’进程中实现的内存分配机制,从某种意义上来说,也是受Apache启发。高层次的nginx内存管理的描述可以看一下内容:对于每一次连接,nginx会自动的分配、链接、使用必要的内存来储存和处理请求和响应的header和body,然后在连接释放时释放。这里有很重要的一点需要提醒,就是nginx会尽可能的避免在内存中复制数据,并且传递数据的方式是传递指针,而不是调用’’memcpy’’方法。

进一步说,当模块生成一个响应时,这个响应的内容会放入一个缓冲区,这个缓冲区会加入一条缓冲区链。这个缓冲区链同样适用于子请求处理。nginx中的缓冲区链是相当复杂的,因为根据处理模块类型的不同有非常多的处理场景。例如,在实现body过滤器模块时,要想实现精确的缓冲区管理是非常困难的。这种模块在一个缓冲区(缓冲区链上的)上只能进行一次操作,并且它必须决定是重写输入缓冲区,还是用新分配的缓冲区替换原来的缓冲区,还是在当前的缓冲区之前或之后插入一个缓冲区。更复杂的情况,有时一个模块会接收到多个缓冲区导致它现在必须要操作的缓冲区链是不完整的。然而在现阶段,nginx只提供了一个底层的API来处理缓冲区链的问题,所以在实际开发一个第三方模块之前,开发者们必须要掌握这晦涩难懂的一部分。

以上提到的内容有一点需要注意,就是内存缓冲区是为连接的整个生命周期分配的,所以对于长时间的连接就需要保留额外的内存。与此同时,对于一个空闲的keepalive(就是保持活跃状态的)连接,nginx只消耗550自己的内存。一个在未来nginx版本中可以优化的地方就是重用和共享长时间连接的内存缓冲区。

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

不幸的是,上述的所有内容都没有写进一个一致的、简单的指南之中,使得nginx的第三方扩展开发变得异常困难。尽管有一些nginx内部的好文档,例如Evan Miller写的一些,但是这些文档还需要一些逆向工程的工作,所以对于很多人来说,nginx模块的实现还是跟变魔术一样。

尽管nginx第三方模块的开发有一定的困难,nginx社区最近还是涌现大量有用的第三方模块。比如,将Lua解释器嵌入nginx的模块,负载均衡的额外模块,完整的WebDAV的支持,高级缓存控制,还有在本章作者所鼓励的、将来所支持的工作。

14.5.经验总结

在Igor Sysoev开始写nginx的时候,大部分构建互联网的软件都已经存在,这些软件的架构通常都遵循着传统的服务器、网络硬件、操作系统和旧的互联网架构。但是这并没有阻止Igor考虑在web服务器领域做进一步的工作。所以我们第一条经验显而易见,那就是:所有的事物总会有它的提升空间。

带着使web软件更好的想法,Igor花了很长的时间开发原始的代码结构,并学习在各种操作系统上优化代码的不同方法。十年之后,他正在开发nginx2.0版本的原型,这是考虑到nginx的第一代已经经历了十年的活跃开发。显而易见,一个新架构的原型、原始代码结构对未来软件产品是多么的至关重要。

另一点值得提到的就是我们应该聚焦开发。nginx的Windows版本就是一个很好的例子,它告诉我们避免稀释一些开发工作的价值,这些开发工作既不属于开发者的核心竞争力又不是他们的目标应用。同样努力加强nginx重写引擎对现存遗留配置的后向兼容能力,也是值得的。

最后但是一样值得提及的,就是尽管nginx开发者的社区并不是非常大,nginx的第三方模块和扩展还是nginx广受欢迎的一个很重要的因素。nginx用户社区和它的开发者们都非常感激Evan Miller,Piotr Sikora,Valery Kholodkov,Zhang Yichun(agentzh)以及其他优秀软件工程师所做的工作。