第14章 NginX
原文:http://www.aosabook.org/en/nginx.html
作者: Andrew Alexeev
nginx(发音"engine x")是俄罗斯软件工程师Igor Sysoev开发的免费开源web服务器软件。nginx于2004年发布,聚焦于高性能,高并发和低内存消耗问题。并且具有多种web服务器功能特性:负载均衡,缓存,访问控制,带宽控制,以及高效整合各种应用的能力,这些特性使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服务器就需要为每个连接上的客户端分配一定数量的内存。
因此,为了处理持续增长的用户带来的负载和更高量级的并发,网站需要大量高效的组件。而另一方面,web服务器软件运行在诸如硬件(CPU,内存,磁盘),网络带宽,应用和数据存储架构等之上,这些基础设施显然也很重要。因而,随着同时在线数和每秒请求数的增长,web服务器性能也应该能够非线性扩展。
Apache不再适用?
Apache web服务器软件发源于1990年代,目前在互联网网站上占有率第一。Apache的架构适合当时的操作系统和硬件,并且也符合当时的互联网状况:一个网站通常使用一台物理服务器运行一个Apache实例。2000年之后,显然这种单服务器模型已经无法简单扩展来满足日益增长的web服务需求。虽然Apache为新功能开发提供了坚实的基础,但他为每个新连接派生一个进程的做法(译注:Apache从2.4版本起已经支持事件模型),不适合网站的非线性扩展。最终,Apache成为一个通用的web服务器软件,聚焦于功能多样化,第三方扩展开发,以及web应用开发的通用性。然而,当硬件成本越来越低,每个连接消耗的CPU和内存越来越多,使用这样功能繁多的单一软件不再具有可伸缩性。
因而,当服务器硬件、操作系统和网络设施不再成为网站增长的主要限制因素时,网站开发者开始寻求更高效的手段来架设web服务器。大约十年前,著名软件工程师Daniel Kegel提出:“是时候让web服务器支持同时处理10000客户端了”,并且预言了现在称为云服务的技术。Kegel的C10K设想明显推动了许多人尝试解决这个问题--通过优化web服务器软件来支持大规模客户端连接的并发处理,nginx是其中做的最成功者之一。
为了解决10000个并发连接的C10K问题,nginx基于一个完全不同的架构—更适合每秒同时连接数和请求数非线性增长。Nginx基于事件模型,而没有模仿Apache为每个请求派生新进程或线程的做法。最终结果就是即使负载增加了,内存和CPU使用事件始终保持可预期。Nginx使用普通的硬件就能在一个服务器上处理数万的并发连接。
Nginx的第一个版本发布之后,一般被用来同Apache一同部署,HTML、CSS、JavaScript脚本和图片等静态内容由nginx处理,来降低Apache应用服务器的并发和延时。随着开发演进的过程,nginx增加了FastCGI、uswge和SCGI等协议的支持,以及对分布式内存对象缓存系统如memcached的支持。也增加了其他有用的功能,例如支持负载均衡和缓存的反向代理。这些附加功能使nginx成为一个高效的工具集,用于构建可伸缩的web基础设施。
2012年2月,Apache 2.4.x版本发布。虽然增加了新的并发处理核心模块和代理模块,用于加强可伸缩性和性能,但要说性能、并发能力和资源利用率是否能赶上或超过纯事件驱动模型的web服务器还为时尚早。Apache新版本具有了更好的性能值得高兴,对于nginx+Apache的web网站架构,虽然这能够缓解后端潜在的瓶颈,但并不能解决全部问题。
nginx有更多的优点吗?
部署nginx最关键的好处就是能够高性能高效的处理高并发。同时,还有更多有意思的好处。
最近几年,web架构拥抱解耦的理念并且将应用层设施从web服务器中分离。虽然现在仅仅是将原先基于LAMP(Linux, Apache, MySQL, PHP, Python or Perl)所构建的网站,变为基于LEMP(E表示Engine x)的。但是,越来越多的实践是将web服务器推入基础设施的边缘,并且用不同的方法整合这些相同或更新的应用和数据库工具集。
Nginx很适合做这些工作。他提供了必要的关键功能用于方便将下列功能从应用层剥离到更高效的边缘web服务器层:并发、长连接处理、SSL,静态内容、压缩和缓存、连接和请求限速,以及HTTP媒体流等。Nginx同时也允许直接整合memcached、Redis或者其他的NoSQL解决方案,增强为处理大规模并发用户的性能。
随着现代编程语言和开发包广泛使用,越来越多的公司改变了应用开发和部署的方式。Nginx已经成为这些改变范例之中的最重要的部件之一,并且已经帮助许多公司在预算内快速启动和开发他们的web服务。
Nginx开发始于2002年,2004年基于2-clause BSD授权正式对外发布。自发布起,Nginx用户就在不断增长,并且贡献提议,提交bug报告、建议和评测报告,这极大的帮助和促进了整个社区的发展。
Nginx代码完全用C语言从头写成,已经移植到许多体系结构和操作系统,包括:Linux、FreeBSD、Solaris、Mac OS X、AIX以及Microsoft Windows。Nginx有自己的函数库,并且除了zlib、PCRE和OpenSSL之外,标准模块只使用系统C库函数。而且,如果不需要或者考虑到潜在的授权冲突,可以不使用这些第三方库。
谈谈关于Windows版本nginx。当nignx在Windows环境下工作时,Windows版本的nginx更像是概念验证版本,而不是全功能移植。这是由于目前nginx和Windows内核架构之间交互的某些限制导致。Windows版本ngnix已知的问题包括:低并发连接数、性能降低、不支持缓存和带宽策略。未来Windows版本的nginx的功能会更接近主流版本。
14.2 Nginx架构综览
传统基于进程或线程的模型使用单独的进程或线程处理并发连接,因而会阻塞于网络或I/O操作。根据不同的应用,就内存和CPU而言,这是非常低效的。派生进程或线程需要准备新的运行环境,包括在内存上分配堆和栈、生成一个新的运行上下文。创建这些东西还需要额外的CPU时间,而且过度的上下文切换引起的线程抖动最终会导致性能低下。所有这些复杂性在如Apache web服务器的老架构上一览无遗。在提供丰富的通用应用功能和优化服务器资源使用之间需要做一个权衡。
最早的时候,nginx希望为动态增长的网站获得更好的性能,并且密集高效的使用服务器资源,所以其使用了另外一个模型。受不断发展的在不同操作系统上开发基于事件模型的技术驱动,最终一个模块化,事件驱动,异步,单线程,非阻塞架构成为nginx代码的基础。
Nginx大量使用多路复用和事件通知,并且给不同的进程分配不同的任务。数量有限的工作进程(Worker)使用高效的单线程循环处理连接。每个worker进程每秒可以处理数千个并发连接、请求。
代码结构
Nginx worker的代码包含核心和功能模块。核心负责维护一个紧凑的事件处理循环,并且在请求处理的每个阶段执行对应的模块代码段。模块完成了大部分展现和应用层功能。包括从网络和存储设备读取、写入,转换内容,进行输出过滤,SSI(server-side include)处理,或者如果启用代理则转发请求给后端服务器。
nginx模块化的架构允许开发者扩展web服务器的功能,而不需要修改nginx核心。Nginx模块可分为:核心、事件模块,阶段处理器,协议、变量处理器,过滤器,上游和负载均衡器等。目前,nginx不支持动态加载模块,即模块代码是和nginx核心代码一起编译的。模块动态加载和ABI已经计划在将来的某个版本开发。更多关于不同模块角色的详细信息可在14.4章找到。
Nginx在Linux、Solaris和BSD系统上使用kqueue、epoll和event ports等技术,通过事件通知机制来处理网络连接和内容获取,包括接受、处理和管理连接,并且大大增强了磁盘IO性能。目的在于尽可能的提供操作系统建议的手段,用于从网络进出流量,磁盘操作,套接字读取和写入,超时等事件中及时异步地获取反馈。Nginx为每个基于Unix的操作系统大量优化了这些多路复用和高级I/O操作的方法。
图14.1展示了nginx架构的高层设计。
图 14.1 nginx架构图
工作进程模型
前面提到过,nginx不为每个连接派生进程或线程,而是由worker进程通过监听共享套接字接受新请求,并且使用高效的循环来处理数千个连接。Nginx不使用仲裁器或分发器来分发连接,这个工作由操作系统内核机制完成。监听套接字在启动时就完成初始化,worker进程通过这些套接字接受、读取请求和输出响应。
事件处理循环是nginx worker代码中最复杂的部分,它包含复杂的内部调用,并且严重依赖异步任务处理的思想。异步操作通过模块化、事件通知、大量回调函数以及微调定时器等实现。总的来说,基本原则就是尽可能做到非阻塞。Nginx worker进程唯一会被阻塞的情形是磁盘性能不足。
由于nginx不为每个连接派生进程或线程,所以内存使用在大多数情况下是很节约并且高效的。同时由于不用频繁的生成和销毁进程或线程,所以nginx也很节省CPU时间。Nginx所做的就是检查网络和存储的状态,初始化新连接并添加到主循环,异步处理直到请求结束才从主循环中释放并删除。兼具精心设计的系统调用和诸如内存池等支持接口的精确实现,nginx在极端负载的情况下通常能做到中低CPU使用率。
nginx派生多个worker进程处理连接,所以能够很好的利用多核CPU。通常一个单独的worker进程使用一个处理器核,这样能完全利用多核体系结构,并且避免线程抖动和锁。在一个单线程的worker进程内部不存在资源匮乏,并且资源控制机制是隔离的。这个模型也允许在物理存储设备之间进行扩展,提高磁盘利用率以避免磁盘I/O导致的阻塞。将工作负载分布到多个worker进程上最终能使服务器资源被更高效的利用。
针对某些磁盘使用和CPU负载的模式,nginx worker进程数应该进行调整。这里的规则比较基本,系统管理员应根据负载多尝试几种配置。通常推荐:如果负载模式是CPU密集型,例如处理大量TCP/IP协议,使用SSL,或者压缩数据等,nginx worker进程应该和CPU核心数相匹配;如果是磁盘密集型,例如从存储中提供多种内容服务,或者是大量的代理服务,worker的进程数应该是1.5到2倍的CPU核心数。一些工程师基于独立存储单元的数目来决定worker进程数,虽然这个方法的有效性取决于磁盘存储配置的类型,。
Nginx开发者在下个版本中要解决的一个主要问题是怎么避免磁盘I/O引起的阻塞。目前,如果没有足够的存储性能为一个worker进程的磁盘操作提供服务,这个进程就会阻塞在磁盘读写操作上。一些机制和配置指令用于缓解这个磁盘I/O阻塞的场景,最显著的是sendfile和AIO指令,这通常可以降低许多磁盘利用率。应该根据数据集(data set),可用内存数,以及底层存储架构等来规划安装nginx。
当前的worker模型的另一个问题是对嵌入脚本的支持有限。举例来说,标准的nginx发布版只支持Perl作为嵌入脚本语言。这个原因很简单:嵌入脚本很可能会在任何操作上阻塞或者异常退出,这两个行为都会导致worker进程挂住而同时影响数千个连接。将脚本更简单,更可靠地嵌入nginx,并且更适合广泛应用的工作已经列入计划。
nginx 进程角色
Nginx在内存中运行多个进程,一个master进程和多个worker进程。同时还有一些特殊用途的进程,例如缓存加载和缓存管理进程。在nginx 1.x版本,所有进程都是单线程的,使用共享内存作为进程间通信机制。Master进程使用root用户权限运行,其他进程使用非特权用户权限运行。
master进程负责下列工作:
- 读取和校验配置文件
- 创建、绑定、关闭套接字
- 启动、终止、维护所配置数目的worker进程
- 不中断服务刷新配置文件
- 不中断服务升级程序(启动新程序或在需要时回滚)
- 重新打开日志文件
- 编译嵌入Perl脚本
Worker进程接受、处理来自客户端的连接,提供反向代理和过滤功能以及其他nginx所具有的所有功能。由于worker进程是web服务器每日操作的实际执行者,所以对于监控nginx实例行为,系统管理员应该保持关注worker进程。
缓存加载进程负责检查磁盘上的缓存数据并且在内存中维护缓存元数据的数据库。基本上,缓存加载进程使用特定分配好的目录结构来管理已经存储在磁盘上的文件,为nginx提供准备,它会遍历目录,检查缓存内容元数据,当所有数据可用时就更新相关的共享内存项。
缓存管理进程主要负责缓存过期和失效。它在nginx正常工作时常驻内存中,当有异常则由master进程重启。
Nginx缓存简介
Nginx在文件系统上使用分层数据存储实现缓存。缓存主键可配置,并且可使用不同特定请求参数来控制缓存内容。缓存主键和元数据存储在共享内存段中,缓存加载进程、缓存管理进程和worker进程都能访问。目前不支持在内存中缓存文件,但可以用操作系统的虚拟文件系统机制进行优化。每个缓存的响应存储到文件系统上的不同文件,Nginx配置指令控制存储的层级(分几级和命名方式)。如果响应需要缓存到缓存目录,就从URL的MD5哈希值中获取缓存的路径和文件名。
将响应内容缓存到磁盘的过程如下:当nginx从后端服务器读取响应时,响应内容先写到缓存目录之外的一个临时文件。nginx完成请求处理后,就将这个临时文件重命名并移到缓存目录。如果用于代理功能的临时目录位于另外一个文件系统,则临时文件会被拷贝一次,所以建议将临时目录和缓存目录放到同一个文件系统上。如果需要清除缓存目录,也可以很安全的删除文件。一些第三方扩展可以远程控制缓存内容,而且整合这些功能到主发布版的工作已经列入计划。
14.3 Nginx配置文件
Nginx配置系统来自于Igor Sysoev使用Apache的经验。他认为可扩展的配置系统是web服务器的基础。当维护庞大复杂的包括大量的虚拟服务器、目录、位置和数据集等配置时,会遇到可伸缩性问题。对于一个相对大点的网站,系统管理员如果没有在应用层进行恰当的配置,那么这将会是一个噩梦。
所以,nginx配置为简化日常维护而设计,并且提供了简单的手段用于web服务器将来的扩展。
配置文件是一些文本文件,通常位于/usr/local/etc/nginx
或/etc/nginx
。主配置文件通常命名为nginx.conf
。为了保持整洁,部分配置可以放到单独的文件中,再自动地被包含到主配置文件。但应该注意的是,nginx目前不支持Apache风格的分布式配置文件(如.htaccess文件),所有和nginx行为相关的配置都应该位于一个集中的配置文件目录中。
Master进程启动时读取和校验这些配置文件。由于worker进程是从master进程派生的,所以可以使用一份编译好、只读的配置信息。配置信息结构通过常见的虚拟内存管理机制自动共享。
Nginx配置具有多个不同的上下文,如:main, http, server, upstream, location (以及用于邮件代理的 mail ) 等指令块。这些上下文不重叠,例如,一个location 指令块是不能放入main指令块中。并且,为了避免不必要的歧义,不存在一个类似于“全局web服务器”的配置。Nginx配置特意做的整洁和富有逻辑性,允许用户可以建立包含上千个指令的复杂的配置文件。在一次私人谈话中,Sysoev说:“全局服务器配置中的位置、目录和其他一些指令是Apache中我所不喜欢的特性,所以这就是不在nginx实现这些的原因。”
配置语法、格式和定义遵循一个所谓的C风格协定。这种构建配置文件的方法以及在开源软件和商业软件中广泛的应用。通过设计,C风格配置很适合嵌套描述,富有逻辑性,易于创建、读取和维护,深受广大工程师喜欢。同时nginx的C风格配置也易于自动化。
虽然一些nginx配置指令看起来像Apahce配置的一部分,但是设置一个nginx实例是完全不同的体验。例如,虽然nginx支持重写规则,但是系统管理员要手工的转换Apache重写配置使之适合nginx风格。同样,重写引擎的实现也是不一样的。
通常来说,nginx设置也提供了几种原始机制的支持,对于高效的web服务器配置很有帮助。有必要简单了解下变量和try_files
指令,这些差不多是nginx所独有的。Nginx开发了变量用于提供附加的更强大的机制来控制运行时的web服务器配置。变量为快速赋值做了优化,并且在内部预编译为索引。赋值是按需计算的,例如,变量的值通常只在这个请求的生命周期中计算一次,而后缓存起来。变量可在不同的配置指令中使用,为描述条件请求处理行为提供了更多弹性。
try_files
指令对于用更适当的方式逐渐替换if 条件配置语句是很重要的,并且它设计来快速高效的尝试不同的URI与内容之间的映射。总的来说,try_files
指令很好用,并且及其高效和有用。推荐读者完整的看看这个指令,并在任何能用的地方用上它。
14.4 深入nginx
前面提到过,nginx代码包含核心和其他模块。核心负责提供web服务器的基础,web和邮件反向代理功能;实现底层网络协议,构建必要的运行环境,并且保证不同模块之间的无缝交互。但是,大部分协议相关以及应用相关的特性是由其他模块完成,而不是核心模块。
在内部,nginx通过模块流水线或模块链处理连接。换言之,每个操作都有一个模块做对应的工作。例如:压缩,修改内容,执行SSI,通过FastCGI或uwsgi协议同后端应用服务器通信,以及同memcached通信等。
在核心和实际功能模块之间,有两个模块http 和 mail。这两个模块在核心和底层组件之间提供了附加抽象层。这些模块处理同各自应用层协议相关的事件序列,如实现HTTP、SMTP或IMAP。与核心一起,这些上层模块负责以正确的次序调用各自的功能模块。虽然目前HTTP协议是作为http模块的一部分实现的,但将来计划将其独立为一个功能模块,以支持其他协议,如SPDY(参考“SPDY: An experimental protocol for a faster web”)。
功能模块可以分为事件模块,阶段处理器,输出过滤器,变量处理器,协议模块,上游和负载均衡器等类型。虽然事件模块和协议也用于mail模块,但是这些模块大部分用于补充nginx的HTTP功能。事件模块提供了基于操作系统的事件通知机制,如kqueue 或 epoll,这些取决于操作系统的能力和构建配置。协议模块允许nginx通过HTTPS, TLS/SSL, SMTP, POP3 和 IMAP等协议通信。
一个典型的HTTP请求处理周期如下: 1. 客户端发送HTTP请求。 2. nginx核心从配置文件查找匹配该请求的位置,根据这个位置信息选择适当的阶段处理器。 3. 如果配置为反向代理,负载均衡器挑选一个上游服务器用于转发请求。 4. 阶段处理器完成工作,并且传递每个输出缓冲区给第一个过滤器。 5. 第一个过滤器传递输出给第二个过滤器。 6. 第二个过滤器传递输出给第三个等等。 7. 最终响应发送给客户端。
Nginx模块是高度可定制化的。它通过一系列指向可执行函数的回调指针来工作。因而,带来的副作用就是为第三方开发者加重了负担,因为他们必须精确的定义模块应怎么运行和何时运行。Nginx的API和开发者文档都经过优化使之更具有可用性来减轻开发难度。
一些在nginx中插入模块的例子:
- 配置文件读取和处理之前
- Location和server的每个配置指令生效时
- Main配置初始化时
- Server配置初始化时
- Server配置合并到main配置时
- Location配置初始化或者合并到上级server配置时
- Master进程启动或退出时
- 新的worker进程启动或退出时
- 处理请求时
- 过滤响应头和响应体时
- 挑选,初始化和重新初始化上游服务器时
- 处理上游服务器响应时
- 完成与上游服务器的交互时
在Worker内部,生成响应的过程如下:
- 开始
ngx_worker_process_cycle()
- 通过操作系统的机制处理事件(如 epoll 或 kqueue)。
- 接受事件并调用对应的动作。
- 处理或转发请求头和请求体。
- 生成响应内容,并流式发送给客户端。
- 完成请求处理。
- 重新初始化定时器和事件。
事件循环自身(步骤5和6)确保增量产生响应并且流式发送给客户端。
更详细的处理HTTP请求过程如下:
- 初始化请求处理
- 处理请求头
- 处理请求体
- 调用对应的处理器
- 执行所有的处理阶段
当nginx处理一个HTTP请求时,会经过多个处理阶段。每个阶段都调用对应的处理器。通常,阶段处理器处理一个请求后产生对应的输出,阶段处理器在配置文件的location中定义。
阶段处理器一般做四件事情:获取location配置,产生适当的响应,发送响应头,发送响应体。处理器函数有一个参数:描述请求的结构体。请求结构体有许多关于客户端请求的有用信息,例如:请求方法类型,URI和请求头等。
当读取完HTTP请求头之后,nginx查找相关的虚拟服务器配置,如果找到虚拟服务器,请求会经过下面六个阶段:
- 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传递请求给匹配的内容处理器。根据location配置,nginx会先尝试无条件处理器,如perl
, proxy_pass
, flv
,mp4
等。如果这个请求不匹配这几个内容处理器,将会按下面顺序挑选一个处理器:random index
, index
, autoindex
, gzip_static
, static
。
Nginx文档中有Index模块的详细内容,这个模块只处理结尾为斜杠的请求。如果不匹配mp4
或autoindex
模块,则认为响应内容是磁盘上的一个文件或目录(即静态的),这由static
内容处理器完成服务。如果是目录,将自动重写URI保证结尾是一个斜杠(从而发起一个HTTP重定向)。
内容处理器产生的内容则被传递到过滤器。过滤器也同location相关,一个location可配置多个过滤器。过滤器加工处理器产生的输出。处理器的执行顺序在编译时决定,对于原生过滤器,顺序是已经定义好的,对于第三方过滤器,可以在编译阶段设置先后顺序。当前的nginx实现中,过滤器只能修改输出的数据,还不能编写修改输入的数据的过滤器。输入过滤器将在将来的版本提供。
过滤器遵循一个特定的设计模式。过滤器被调用后开始工作,调用下一个过滤器直到过滤器链中的最后一个。完成之后,nginx结束响应。过滤器不用等待前面的过滤器结束。一旦前一个过滤器提供的输入已经可用,下一个过滤器便可以启动自己的工作(很像Unix中的管道)。因而,在从上游服务器接收到所有的响应之前,所生成的输出响应已经被发送给客户端。
过滤器有header filter和body filter,nginx将响应的header和body分别发送给相关的过滤器。
Header filter包含3个基本步骤:
- 决定是否处理这个响应
- 处理响应
- 调用下一个过滤器
body filter转换所生成的内容。body filter的一些例子:
- SSI
- XSLT过滤
- 图片过滤(例如调整图片大小)
- 字符集转换
- Gzip压缩
- Chunked编码
经过过滤器链之后,响应被发送到writer。有两个额外的具有特定功能的过滤器与writer相关,copy filter和postpone filter。Copy filter负责将相关的响应内容填充到内存缓冲区,这些响应内容有可能存储在反向代理的临时目录。Postpone filter用于子请求处理。
子请求是一个处理请求、响应很重要的机制,同时也是nginx最强大的功能之一。通过子请求,Nginx可以返回另一个URL的响应,这个URL与客户端最初请求的URL不同。一些web框架称之为内部跳转,但nginx功能更强,不仅能运行多个子请求并将这些子请求的响应合并成一个,而且还能嵌套和分级。子请求可以产生子-子请求,子-子请求能产生子-子-子请求。子请求可以映射到磁盘文件,其他处理,或者上游服务器。子请求在根据原始响应数据插入附加内容时很有用。例如,SSI模块使用一个过滤器解析返回文档的内容,然后用指定URL的内容来替换include指令。或者做一个过滤器,能够在一个URL产生的响应内容之后附加一些新的文档内容。
上游(upstream)和负载均衡器同样也值得简单介绍一下。上游用于实现反向代理处理器(proxy_pass
处理器)。上游模块组装好请求发送给上游服务器(或称为“后端”),然后接收上游服务器返回的响应。这个过程不调用输出过滤器。上游模块仅仅设置回调函数,用于当上游服务器可读或可写时调用。回调函数实现下列功能:
- 准备请求缓冲区(或缓冲区链),用于发送给上游服务器
- 重新初始化、重置与上游服务器之间的连接(应在重新发起请求之前)
- 处理上游服务器响应的首字节,并且保存响应内容的指针
- 放弃请求(当客户端过早关闭连接时)
- 结束请求(当nginx完成读取上游服务器响应时)
- 修整响应体内容(例如除去空白)
如果上游服务器大于一个,负载均衡器模块可附加在proxy_pass
处理器上,用于提供选择上游服务器的能力。负载均衡器注册了一个配置文件指令,提供附加的上游服务器初始化功能(通过DNS解析上游服务器名字等),初始化连接结构体,决定如何路由请求,并且更新状态信息。目前,nginx支持两种标准的上游服务器负载均衡规则:轮询和ip哈希。
上游模块和负载均衡处理机制的算法能检测上游服务器异常,并将新请求重新路由到可用的上游服务器,还有更多的工作计划加强这个功能。总之,负载均衡器的改进计划更多些,下个版本的nginx将大幅度提升在不同上游服务器之间分发负载和健康检测的机制。
还有一些有意思的模块在配置文件中提供了额外的变量供使用。这些变量通过不同的模块生成和更新,有两个模块完全用于变量:geo
和map
。geo
模块用于更方便的基于IP地址追踪客户端地址,这个模块可以根据客户端IP地址生成任意变量。另一个map
模块允许从一个变量生成另一个变量,提供将主机名和其他变量方便的进行映射的基本能力。这类模块称为变量处理器。
nginx worker进程实现的内存分配机制从某方面来说来自于Apache。Nginx内存管理的高层描述:对于每个连接,必要的内存缓冲区是动态分配的,用于存储或操纵请求、响应的头和体,当连接关闭时释放。很重要的一点是nginx尽可能的去避免在内存中拷贝数据,大部分的数据通过指针进行传递,而不是调用memcpy。
更深入一点,当一个模块产生响应时,这些响应内容放入内存缓冲区,并被添加到一个缓冲区链。这个缓冲区链同样适用于子请求处理。由于根据模块类型不同存在多个处理场景,所以nginx中的缓冲区链相当复杂。例如,在实现body filter模块时,精确的管理缓冲区是很棘手的。这个模块同一时间只能处理缓冲区链中的一个缓冲区,它必须决定是否覆盖输入缓冲区,是否用新分配的缓冲区替换这个缓冲区,或者在这个缓冲区之前或之后插入一个新缓冲区。更复杂的情况,有时一个模块收到的数据需要多个缓冲区存储,因此它必须处理一个不完整的缓冲区链。但是由于目前nginx仅提供了底层API用于操纵缓冲区链,所以开发者应该真正掌握nginx这一晦涩难懂的部分之后,再去开发第三方模块。
上面提到的内容中需要注意的一点,内存缓冲区是为连接的整个生命周期分配的,所以对于长连接需要消耗额外的内存。同时,对于空闲的keepalive连接,nginx仅消耗550字节内存。将来的nginx版本可能进行优化以使长连接重用和共用内存缓冲区。
内存分配管理的任务由nginx内存池分配器完成。共享内存区用于存放接受互斥锁(accept mutex),缓存元数据,SSL会话缓存,以及和带宽策略管理(限速)相关的信息。Nginx实现了slab分配器用于管理共享内存,提供了一系列锁机制(互斥锁和信号量),以允许安全的并发使用共享内存。为了组织复杂的数据结构,nginx也提供了红黑树的实现。红黑树用于在内存中保存缓存元数据,查找非正则location定义,以及其他一些任务。
不幸的是,上述内容从未以一致并且简单的方式介绍过,以致开发第三方模块的工作相当复杂。虽然有一些nginx内部实现的好文档,例如,Evan Miller写的,但是这些文档需要做很多还原工作,nginx模块的开发还是像变魔术一样。
虽然开发第三方模块是如此困难,nginx社区最近还是涌现大量有用的第三方模块。例如,将Lua解释器嵌入nginx,负载均衡附加模块,完整的WebDAV支持,高级缓存控制,以及其他本章作者所鼓励和将来支持的有趣的第三方工作。
14.5 优秀实践
Igor Sysoev开始编写nginx时,大部分构建互联网的软件都已经存在,这些软件的架构一般遵循传统服务器和网络硬件、操作系统、以及过去互联网架构的定义。但是这并未阻止Igor考虑在web服务器领域做进一步的工作。所以,显然第一个优秀实践是:总有提升空间。
带着开发更好web软件的想法,Igor花了很多时间开发原始代码结构,并研究在多个操作系统下优化代码的不同手段。十年后,考虑到1.0版本已经经过十年活跃开发,Igor开发了2.0版本原型。很明显,这个新架构的初始原型和代码结构,对于软件的后续开发及其重要。
另外值得提到的一点是聚焦开发。Nginx 的windows版本是个好例子,说明无论在开发者的核心技能或应用目标上避免稀释开发工作是值得的。同样努力加强nginx重写引擎对现存遗留配置的后向兼容能力,也是值得的。
最后特别值得提到的是,尽管nginx开发者社区并不大,nginx的第三方模块和扩展还是成为nginx受欢迎的一个很重要的因素。Nginx用户社区和作者们很感谢Evan Miller, Piotr Sikora, Valery Kholodkov, Zhang Yichun (agentzh)以及其他优秀软件工程师所做的工作。