nginx架构解析:朴实中见真知
目录
- 前言
- 为什么高并发很重要
- Apache可以做到吗
- 使用nginx会更有优势吗?
- nginx架构概览
- 代码结构
- Workers模型
- nginx进程规则
- nginx缓存概览
- nginx配置
- nginx内部
- 典型的HTTP请求处理循环
- 课程总结
前言
nginx(发音“engine x”)是俄国的软件工程师Igor Sysoev写的一个免费的开源软件。从2004年开始公开发布,nginx聚焦于高性能、高并发以及减少内存消耗。web服务器的最显著特性,如负载均衡、缓存、访问和带宽控制以及高效率的和其他不同软件结合的能力,让现代网站架构选用nginx作为服务器成为了一个不错的选择。当前,nginx是因特网上第二流行的开源web服务器。
为什么高并发很重要
现在因特网如此的流行和无所不在,很难想象十年前我们所知的没有网的世界。互联网也发生了很大的进化,已经从基于NCSA和Apache的服务器上简单可交互文本HTML文档,到全球20亿人使用的一直在线的通讯媒介。随着不断增加的永久在线的PC设备、移动设备以及最近的平板电脑,互联网的状况正在发生迅速的变化,整体社会经济也变得数字化了。在线服务变得越来越注重即时消息获取以及在线娱乐。线上商业活动的安全性也发生了较大的改变。相应的,网站变得越来越复杂了,需要大量的工作去增加它的健壮性和可扩展性。
网站架构的最大的挑战之一就是并发性。从网络服务开始运行,并发量就一直在增加。同时服务于成千上万甚至百万级的用户已经不是什么稀罕事了。十年之前,并发问题的主要结症在于缓慢的客户端——用户使用ADSL或者拨号连接。现在,移动端设备和新的应用架构结合需要并发的协助,这种结合一般基于长连接,使用长连接则可以在移动设备上更新新闻、好友反馈等。另一个需要增加并发支持的重要因素就是浏览器行为的改变,这些浏览器会同时打开四到六个连接来增加页面加载速度。
描述下慢客户端的问题,想象一个简单的基于Apache的web服务器,产生一个100KB的响应——包含文本或者图形的web页面。仅需要不到一秒的时间来产生或者获取这个页面,但是在一个80kbps带宽的网络中需要10秒的时间来将页面传递给客户端。本质上,web服务器可以迅速的拉取100KB的内容,然后在释放连接之前会花10秒的将内容发送给客户端。现在试想,1000个客户端同时建立了连接,并且它们请求了类似的内容。如果每个客户端需要1MB的内存,最终结果也就是需要1000MB的额外内存来给1000个客户端提供100KB的内容服务。实际上,基于Apache的web服务器对每个连接通常分配的内存要大于1MB,遗憾的是移动端通信中最高速度很多情况下也就是数十kbps。尽管在某种程度上给慢客户端发送数据可以通过提高操作系统内核的套接字缓存来提升性能,这并不是一个普遍解决方法并且也有非预期的副作用。
在长连接的场景中,并发所带来的问题也就更加显著了,因为如果要避免建立新的连接带来的潜在问题,需要一直和客户端保持连接,对于客户端的每个连接web服务器就必须分配一定量的内存。
最终,如果要处理由越来越多受众也就是并发级数的提升所带来的工作负载持续增长的问题,而且随着用户不断的增多还要持续不断来处理,web服务器必须要有一系列非常高效的构建区块来支撑。另一等价的方面,诸如硬件(CPU,内存,磁盘),网络容量,应用程序和数据存储架构显然也很重要,而web服务器软件是来接受和处理客户端连接的载体。因此,在每秒同步连接和请求逐步增长时web服务器需要能够非线性的扩展。
Apache可以做到吗
Apache开始于19世纪90年代,并且仍然大规模主导当今的互联网。从源头上来讲,它的架构和当时操作系统和硬件匹配,也和当时的互联网状态匹配,因为那个时候网站一般就是一个独立的物理服务器来跑单个实例的Apache。在2000年之后,很显然单个的web服务器已经很难通过扩展来满足web业务增长的现实。尽管Apache为未来的开发提供了坚实的基础,它的架构仍然是针对每个连接复制一个进程,而这并不适用于web网站的非线性扩展要求。最终,Apache成了一个具有诸多不同特性的web服务器,具有大量的第三方扩展,并且对任何形式的web应用开发都适用。然而,万事总有代价,在单个软件上集成如此多的工具的缺点就是可扩展性变得更差了,因为对每个连接消耗了更多的CPU和内存。
这样,当服务器硬件、操作系统以及网络资源不再是web网站增长的主要限制因素时,全球的开发者们都开始去找一个更加高效的方法来运行web服务器。大约十年前,Daniel Kegal,杰出的软件工程师,呼吁“是时候让web服务器同时来服务成千上万客户端了”,并且预言了我们现在称为云计算的东西。Kegal的C10K声明激发了工程师去解决web服务器优化问题以同时服务大量的客户端的问题,于是nginx成为了它们中最成功作品之一。
致力于解决C10K中提出的10000个同步连接问题,nginx使用了一种不同的架构来完成——这个架构更加适用于无轮是大量的同步连接还是每秒请求处理数的非线性可扩展性要求。nginx是事件驱动的,所以它和Apache对每个web页面请求产生新的进程或者线程的方式不同。最终的结果就是,负载增长、内存和CPU使用都可控了。nginx现在可以基于一个典型的机器硬件之上处理成千上万的并发连接。
当nginx的第一版发布出来,其目标是随着Apache一起部署,这样像HTML、CSS、JavaScript以及图片这种静态内容可以由nginx来处理,以此降低并发和减少基于Apache的应用服务遗留问题。随着nginx的逐步完善,通过使用FastCGI、uswgi或者SCGI协议,以及和诸如memcached的这样的内存分发对象缓存系统的协作增加了同应用程序集成的能力。这些额外的特性将nginx打造成了一个高效的工具合集来满足可扩展的网络基础设施的需求。
在2012年2月份,Apache2.4.x分支对外发布。尽管最新的Apache版本已经增加了多进程处理核心模块,以及致力于平和可扩展新和性能的代理模块,然而尚且还不能辨别其性能、并发以及资源利用是否和纯事件驱动的服务器相当或者更好。Apache服务器的新版本具备更好的扩展性是喜闻乐见的,因为其可以潜在地减轻那些仍然使用nginx配合Apache的后端服务应用的瓶颈。
使用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服务。
2002年,才开始编写nginx的最初代码。2004年在遵守BSD协议的两个条款下被公开发布出来。从那时起nginx的使用者不断增多,共享想法以及提交bug报告,及意见和观察对整个社区产生了极大的帮助和也带来了充分的好处。
nginx的代码是原生的,是从头开始用C语言来完成的。nginx已经被移植到多个架构以及操作系统,包括Linux,FreeBSD,Solaris,MacOSX,AIX以及Microsoft Windows。nginx有其自己的库和以及其并不太超出C语言基础库的标准模块,除了zlib、PCRE和OpenSSL,由于潜在的授权冲突这三者可以在需要时在构建阶段排除,
对Windows版的nginx说两句。尽管nginx在windows的换机中可以工作,Windows版的nginx更像一个概念的验证而不是一个全量的移植。nginx和Windows内核架构之间并不总是配合的很好所以存在一定的限制。已知的Windows版nginx存在的问题包括并发连接支持要少很多、性能也不太好、没有缓存和带宽控制策略。后续的Windows版nginx会更好的匹配主流功能。
nginx架构概览
传统的进程或者线程模型来处理并发连接涉及到使用不同的进程或线程来处理每个连接,并且会在网络和输入/输入操作上进行阻塞。根据不同应用的情况,关于内存和CPU使用上可能会非常的低效。生产一个新的进程或者线程需要额外准备一个新的运行环境,包括分配堆和栈,以及创建新的的执行上下文。另外,也要分配CPU时间来创建这些项目,这最终会导致在线程切换或者大量的上下文切换时性能低下。所有这些混乱都在老的类似于Apache的web服务器架构上凸显出来。这就需要在功能丰富性和资源使用优化之间做出权衡。
从一开始,nginx就试图被打造成一个特殊的工具来满足更好的性能,在网站访问量动态增长时密集而经济的使用服务器资源,所以其遵从了一个不同的模型。nginx实际上是受在大量操作系统上广泛实践的高级事件驱动机制的启发。结果就是模块化、事件驱动、异步、单进程、非阻塞的架构成为了nginx代码的基石。
nginx大量使用多路通信和事件通知,并且给不同的进程设计了特性的任务。请求连接在一些单线程体中的高效的运行循环中被处理,这些单线程体称为workers,而且其个数是非常有限的。在每一个worker中,nginx可以每秒处理上千个并发连接和请求。
代码结构
nginx的worker代码包含核心功能以及功能模块。nginx的核心功能用于维护一个紧凑的运行循环以及在请求处理的每个阶段使用适当的模块来处理。模块大多数由应用层的功能组成。模块从网络和存储中读取和写入功能、转换内容、执行外向的过滤、执行服务器包含动作、以及当代理被开启时向上游服务器转发请求。
nginx的模块架构通常允许开发者扩展一系列的web服务器特性而无需修改nginx内核。nginx模块都是相互之间存在少许差异的化身,也就是核心模块、事件模块、周期处理器、协议、变量处理器、过滤、上游处理模块和负载均衡器。现在,nginx还不支持动态加载模块;也就是,模块随着构建阶段编译到内核模块中。然而,可加载模块和ABI的支持会在将来的主要发布中支持。更多的内容请参阅【nginx内部】一节。
尽管处理大量的请求关系到接受、处理以及控制网络连接和内容获取,nginx使用事件通知机制和一系列的磁盘I/O性能平衡机制,这些机制存在于Linux、Solaris以及基于BSD的操作系统上,例如kqueue,epoll以及event ports。主要的目标就是给操作系统提供尽可能多的提示来达到对输入输出、磁盘操作、套接字操作、超时等等及时的异步反馈通知。多路复用及高级的I/O操作的不同使用方法针对每个基于Unix的操作系统进行了大量的优化,这些操作系统都是nginx的运行平台。
Workers模型
如前所述,nginx并不是对每个连接产生一个进程或者线程。取而代之,worker进程从共享的“监听(listen)”套接字中接受新的请求,在每个worker中高效的执行循环以在每个worker中处理上千个连接。nginx的内部代码中并不存在将连接发往worker的特殊部分;这是通过操作系统的内核机制来完成的。在启动的时候,创建一些初始的监听套接字。workers不断地接受请求,在处理HTTP请求和响应时不断的从套接字中读取和向套接字写入。
运行循环是nginx的worker代码中最复杂的部分。它包含各种内部调用以及对异步任务处理的强依赖。异步处理操作通过模块化方式来实现,事件通知、函数回调的拓展使用以及协调一致的计时器。总体而言,最重要的原则就是尽量的非阻塞。nginx唯一要处理的情况就是当对worker没有足够的硬盘存储性能时可能的阻塞。
因为nginx对于每个连接并不创建进程和线程,内存使用是非常节约的并且能在大多数情况下也极度的高效。nginx也非常节约的使用CPU的运行周期,因为针对线程和进程并没有动态的创建和销毁。nginx所做的就是检查网络和存储的状态,初始化连接,将它们加入运行循环中,并且异步处理直到结束,之后连接就被从运行循环中移除。通过优雅的使用syscall以及连接池与内存分配的精确实现,nginx即使在极端的工作负载下也能保持适当较低的CPU使用。
因为nginx创建多个workers来处理连接,它也就非常容易在多核处理器上扩展了。通常,一个独立的worker分配一个处理器内核,这样可以充分利用多核架构,并且阻止线程抖动和死锁。单进程的worker内的资源控制机制是独立的。这个模型也给物理存储设备提供了更大的可扩展性,更充分的使用磁盘以及避免磁盘I/O阻塞。作为结果,将工作负载分散在多个workers上,使得服务器资源的利用更加高效。
在磁盘使用和CPU负载模式上,nginx的workers内核的个数可以适配起来。规则非常基础,系统管理员应该给每种工作负载提供不同的配置。通常推荐的做法是:如果负载模式是CPU密集的——例如,处理很多TCP/IP,处理SSL,或者压缩——workers的个数应该和CPU的内核数匹配;如果,负载模式和磁盘I/O绑定——例如,从存储中提供不同集合的内容服务,或者重代理——workers的个数可以设置成一个或者内核总数的一般到两倍。一些工程师会依据有多少个独立的磁盘存储单元来设置workers的个数,但是这种方式的效率依赖于磁盘存储配置和存储类型。
nginx的开发者面临的一个主要问题是如何避免磁盘I/O的阻塞,这个问题会在将要发布的版本中解决。现在,通过一个特定的worker来实现磁盘操作的性能还不够,这个worker仍然会在从磁盘中读取/写入时阻塞。有一系列机制和配置文件指令来减轻这种磁盘I/O阻塞的情况。最显著的方法是,通过诸如sendfile和AIO的结合使用来给磁盘性能提供更多的空间。nginx的安装应该基于数据集合、可用的内存大小以及潜在的存储架构来设计。
另一个worker模型存在的问题是和对嵌入的脚本支持有限相关。在nginx的标准版中,只有嵌入Perl脚本是支持的。这有一个简单的解释:主要的问题就是嵌入脚本会导致操作阻塞和异常退出。这两种情况都会导致worker挂起,从而立即影响上千的连接。未来更多的工作将计划集中在使嵌入的脚本更加简单、更加稳定更加适合大规模的使用。
nginx进程规则
nginx在内存中运行多个进程;有一个主进程和几个worker进程。也有一些特殊的进程,尤其是缓存负载和缓存管理。所有的进程在1.x版本中都是单线程的。所有的进程都主要使用共享的内存机制来进行内部进程通信。主进程作为root用户运行。缓存负载和缓存管理以及worker进程作为一般用户运行。
主进程完成以下任务:
读取和校验配置
创建,绑定以及关闭套接字
启动,终止以及维护worker进程的个数
在服务不中断下重新应用配置
控制热升级二进制程序(启动新的二进制程序,在必要时回滚)
重新打开日志文件
编译嵌入的Perl脚本
worker进程接受、处理从客户端来的连接,提供反向代理以及过滤功能并且完成几乎所有的事情。关于监控nginx实例的行为监控,系统管理员应该注意worker的状态因为他们是进程运行的反馈以及日常操作的主体。
缓存负载进程检查磁盘缓存项目并且将nginx的内存数据发送给缓存的元数据。本质上,缓存负载为nginx实例在和已存储在磁盘中的文件交互提供支持,其使用一种特殊的目录分配结构。它将目录反转,检查缓存内容的元数据,升级共享内存中的相关入口以及当所有事情都完成可被使用时退出。
缓存管理进程大多数是对缓存过期及失效做校验。在nginx的操作过程中将会保留在内存中,如果遇到失败的情况则主进程会重启它。
nginx缓存概览
nginx中的缓存使用文件系统中的层次化数据存储来实现。缓存的键值是可配置的,不同的请求参数可以控制什么样的内容进入缓存。缓存的键值和缓存的元数据存储在共享的内存片段中,缓存负载、缓存管理器和workers可以访问这块内存。目前还没有任何在内存中的文件缓存,除非通过操作系统的虚拟文件机制来进行优化。每个缓存响应都是放置在文件系统的不同位置。目录层级(级别和命名信息)通过nginx的配置指令来控制。当响应要被写入到缓存的目录结构中,文件的路径和名字从代理URL的MD5哈希值中获取。
将内容放置到缓存中的进程如下:当nginx从上游服务器读取响应,内容首先会被写入到临时文件,这个文件在缓存目录结构之外。当nginx完成处理和请求,会将临时文件重命名并且移动到缓存的目录中。如果用于代理的临时文件是另一个文件系统,这个文件会被实际拷贝,所以推荐将临时文件和缓存目录放置在同一个磁盘系统。当需要净化缓存时将文件从缓存目录中删除也是非常安全的。nginx也有第三方扩展实现在远程控制缓存,并且还会有更多的工作将要集中到在正式的发布版中集成这个功能。
nginx配置
nginx的配置系统受到Igor Sysoev在Apache工作上的启发。他的主要观点就是可扩展的配置系统对于web服务器是非常必要的。当维护大量复杂的配置来设置多个虚拟服务器、目录、地址以及数据集合时,可扩展问题就凸显了。在相对较大网站设置中,如果没有适当的配置应用级别或者不是由系统管理员所管理将是十分痛苦的。
结果,nginx的配置被设计可以进行简单的日常维护并为未后续的web服务器配置扩展提供方便的途径。
nginx的配置保存在一些纯文本的文件中,通常在/usr/local/etc/nginx或者/etc/nginx中。主配置文件一般都是nginx.conf。要保持配置的整洁,一些配置文件可以存放到其他的地方,而这些文件会被自动的加载到主文件中。然而,要注意nginx并不支持Apache的那种分散式配置(也就是,.htaccess)。所有和nginx相关的配置都保存在一个中心化的配置文件中。
配置文件开始由主进程读取和校验。其只读的编译后内容可以传递给worker进程,因为这些进程是由主进程创建的。配置结构通过一般的虚拟内存管理机制来自动共享。
nginx的配置有几种不同的上下文,即main、http、server、upstream、location(对email代理mail)指令区块。上下文不重叠。例如,不会有将location放置到main中的情况。同样,要避免不必要的模糊,没有诸如“全局服务器”的配置。nginx的配置应该要简洁有逻辑性,这样用便于开发者来维护复杂的配置文件,这些配置文件组成了大量的指令。在私下里,Sysoev说“Location、directories以及其他在全局服务器配置中的区块绝对不像Apache那样,这样就是它们不会在nginx出现的原因”。
配置文件的语法,依据所谓的C语言风格来设定格式和定义。这种特殊的方式来创建配置文件已经被很多其他的开源软件和商业软件所采用。从定义上,C类型的配置非常适合嵌套的描述,在逻辑上也很容易创建,阅读和维护,被广大的工程师们所喜爱。C语言风格的nginx配置文件也可以自动化配置。
尽管一些nginx的指令和Apache配置有点相似,设置nginx实例是非常不同的体验。例如,路径重写规则是被nginx支持的,尽管这需要管理员去手动的适配遗留的Apache路径重写规则来匹配nginx的语法。路径重写的引擎实现也不同。
通常,nginx设置也提供支持多个请求源的支持,作为一个web服务器这非常有用。这里也简单提一下变量和try_files指令,这是nginx所独有的。nginx的变量用于提供额外的甚至更加强大的机制来控制运行时的配置。变量是经过优化的来满足快速执行和内部也是预编译的。执行是按需进行的,也就是,变量的值是计算一次并在请求的生命周期内缓存。变量可以用于不同的配置指令,提供额外的可扩展性来提供条件式的请求处理。
try_files指令开始是用于优雅的以一种合适的方式替换if配置表达式,并且其被设计成更快和高效的try/match语句,来实现不同的URI到内容的映射。整体上,try_files指令非常高效和有用。推荐读者彻底的阅读try_files指令并在任何可能的地方使用。
nginx内部
如前所述,nginx的代码由一个内核和多个其他模块组成。nginx的内核用于提供web服务器的基础,web和邮件的反向代理功能;它开启了潜在的网络协议的使用,构建了必要的运行时环境,并且确保在不同模块中无缝的交互。然而,大多数的协议和应用相关的特性是通过nginx的模块来完成的,而不是内核。
在内部,nginx通过模块的管线或者模块链来处理连接,换句话说,对于每个操作,存在一个模块来处理相关的工作,例如,压缩,改变文件的内容,执行服务器端的工作,通过FastCGI或者uwsgi来和上游的应用服务器通信,或者和memcached会话。
在内核和实际的功能模块之间存在一些nginx模块。这些模块是http以及mail。这两个模块在内核和低级的模块间提供了额外一层的抽象。在这些模块中,事件的序列化处理和相应的应用层协议诸如HTTP、SMTP或者IMAP实现相关。在和nginx内核的结合中,这些高层的模块可以维护相应的功能模块的正确顺序。尽管,HTTP协议目前是作为http模块来实现的,有计划在未来将其放置到一个功能模块中,因为需要支持SPDY之类的协议。
功能模块可以分布到事件模块、周期处理器、输出过滤器、变量处理器、协议、上游和负载均衡器中。大多数的模块补充了nginx的HTTP功能,尽管事件模块和协议也用于mail。事件模块提供了特殊的OS相关的事件通知机制,例如kqueue或者epoll。nginx所使用的事件模块依据操作系统的能力来构建配置。协议模块允许nginx通过HTTPS,TLS/SSL,SMTP,POP3以及IMAP通信。
典型的HTTP请求处理循环
客户端发送HTTP请求
nginx内核选择合适的周期处理器,这依据配置location指令和请求的匹配
在配置的情况下,负载均衡器选择一个上游服务器来代理
周期处理器完成工作,并将每个输出缓存传递到第一个过滤器中
第一个过滤器将输出传递给第二个过滤器
第二个过滤器传递到第三个(如此往复)
最后的结果返回给客户端
nginx的模块执行是非常个性化的。通过一系列的回调指针来执行回调。然而,不利的一面就是给想要开发自己模块的程序员增加了较大的负担,因为他们必须额外指定如何以及何时去运行模块。nginx的API以及开发者文档都进行优化和补充来减轻开发者的这种负担。
一些模块可以运行的时间点如下:
在配置文件被读取和处理之前
在每个location和server指令中
当主配置文件被初始化
当服务器被初始化
当server配置被合并到主配置中
当location配置被初始化并被合并到父server配置中
当主进程开始或者退出
当新的worker进程开始或者退出
当处理一个请求
当过滤相应头部和响应体
当选取,初始化和重新初始化一个请求到上游服务器
当处理从上游服务器发回的响应
当结束和上游服务器的交互
在worker内,运行循环的执行序列通常如下:
开始ngx_worker_process_cycle()
使用OS特定机制来处理事件(例如,epoll和kqueue)
接受事件和分发相关动作
处理代理请求头部和请求体
产生响应内容(头部,或响应体)以及将其流式发送到客户端
完成请求
重新初始化计时器和事件
运行循环本身(第5和第6步)确保响应的生成并发送到客户端。
处理HTTP请求的详细步骤如下:
初始化请求处理
处理头部
处理消息体
调用关联的处理器
执行处理周期
接下来就到了处理周期。当nginx处理HTTP请求时,其将之传递给一系列的处理周期。每个处理周期中有处理器来调用。通常,周期处理器处理每个请求并产生相关的输出。周期处理器被挂载到配置文件定义location的地方。
周期处理器一般完成四件事情:获取location配置,产生适当的响应,将头部发送,并发送消息体。处理器有一个参数:形容请求结果的描述。请求的结构中有很多关于客户端请求的有用信息,例如请求方法、URI和头部。
当HTTP请求头部被读取,nginx寻找相关的虚拟服务配置。如果虚拟服务配置存在,请求将经过六个周期:
服务器路径重写周期
location周期
location重写周期(这可以将请求传递到之前的周期)
访问控制周期
try_files周期
日志周期
为了尝试生在请求的响应中生成必要的内容,nginx将请求传递给相应的内容处理器,依据不同的location配置,nginx可以首先尝试所谓的无条件处理器,例如Perl、proxy_pass、flv、mp4等。如果请求不和任何以上的内容处理器匹配,就会被如下的处理器选取,顺序保持一致:random index、index、autoindex、gzip_static、static。
索引模块的细节可以通过nginx文档查阅,但是这些是处理请求尾部有斜杠的模块。如果想mp4后者autoindex模块并能处理,其内容就被认为是一个磁盘上的文件或者目录并由static模块来处理。对于目录,会自动重写URI所以尾部的斜杠总是存在(也就是HTTP重定向问题)。
内容处理器随后将内容传递给过滤器。过滤器同样被挂在到location上,会有几个针对location的配置。过滤器会完成操作处理器传出内容的任务。过滤器执行的顺序在编译时决定。对于内置的过滤器是预定义的,第三方的过滤器可以在构建阶段配置。在已有的nginx实现中,过滤器仅仅能够完成向外输出的更改,并且当前没有机制来编写和挂载过滤器来处理输入内容转换。在未来版本的nginx中将会有输入过滤器。
过滤器遵从特殊的设计模式。一个过滤器可以被调用,开始工作以及调用下一个过滤器直到链中的最后一个过滤器被调用。之后,nginx返回响应。过滤器不一定要等待上一个过滤器执行完成。链中的下一个过滤器可以在上一个输入就绪是就可以立即开始运行了(就像Unix的管线)。相应的,生成的输出响应在从上游服务返完整的返回之前就可以传递给客户端了。
有头部过滤器和消息体过滤器;nginx将头部和消息体关联到不同的过滤器中。
头部过滤器有三个基本步骤构成:
决定是否操作响应
操作响应
调用下一个过滤器
消息体过滤器转换生成的内容。消息体的过滤器例子如下:
服务端包含(SSI)
XSLT过滤
图片过滤(例如,原地缩放图片)
字符编码转换
gzip压缩
通信块编码
在过滤器链后,响应传递给了流的写出者。写出者中存在一些额外的特殊目标过滤器,也就是copy过滤器,以及postpone过滤器。copy过滤器将相关的响应内容放入内存缓冲,而响应内容可能存储在一个代理的临时目录里。postpone过滤器用于子请求。
子请求是请求/响应处理的重要机制。子请求也是nginx最强大的特性之一。通过子请求nginx可以从不同的URL上返回而不是客户端原始访问的请求。一些web框架称这为内部重定向。然而,nginx做的更多——不仅仅过滤器可以执行多个子请求然后将输出合并到单个返回中,同时子请求也可以嵌套并形成层级。一个子请求可以执行其子子请求,一个子子请求可以初始化一个子子子请求。子请求可以将文件映射到硬盘上、其他的处理器或者上游的服务器。子请求在将额外的数据插入到原始的响应数据中。例如,SSI(服务器端包含)模块使用过滤器来解析返回的文档内容,并且用特定的URL的内容替换include指令。或者,可以让一个过滤器将整个文档内容作为从URL上获取的内容,然后往URL本身上追加新的文档。
上游和负载均很器也是值得简要描述的。上游用于实现作为反向代理的内容处理器(proxy_pass处理器)。上游模块大多数准备发送给上游服务器(或者后端)的请求,然后从上游服务器上接受响应。这里没有针对输出的过滤器。上游模块所做的就是设置当上游服务器就绪时的回调。回调实现了下面的功能:
创建一个请求缓存(或者缓存链)用于发送给上游服务器
重新初始化/重设连接到上游服务器的连接(在再次创建请求之前)
处理上游响应的第一批比特数据然后将指针存储
放弃请求(当客户端终止请求时发生)
当nginx处理完从上游服务器返回的数据时结束请求
修剪(trim)响应消息体(例如,移除尾部)
负载均衡模块挂载到了proxy_pass处理器上来提供在存在多个上游服务器时选择哪个上游服务器的能力。负载均衡注册了一个开启配置文件的指令,提供了额外的上游初始化函数(来从DNS中解析上游服务器的域名等),初始化连接结构,决定将请求路由到什么地方,以及更新状态信息。目前,nginx支持对负载均衡到上游服务器提供两种标准方式:round-robin以及ip-hash。
上游和负载均衡处理机制包含检测失效的上游服务器的算法然后将新的请求重新路由到其他的健康服务器上——通过很多额外的工作来增加功能性。在负载均衡器上有很多计划正在实施,在接下来的nginx版本中负载分发的机制以及健康检查将会得到很大的提升。
也有一些其他有趣的模块给配置文件提供了额外的变量合集。尽管nginx中的变量在不同的模块中创建和更新,有有两个模块完全的用于变量处理:geo和map。geo模块用于基于IP地址完成客户端的跟踪。这个模块可以基于客户端的IP地址创建任意的变量。另一个模块就是map,运行从其他变量中创建变量,本质上就是提供了扩展主机名和其他运行变量的能力。这种模块可以称作变量处理器。
内存分配机制在单个nginx的worker中实现,某种程度上,受Apached的启发。针对nginx内存管理的一个大致描述如下:对于每个连接,必要的内存缓存是动态分配、连接的,用于存储和操作头部以及请求和响应的消息体,当连接释放是也就释放内存。需要特别注意nginx试着去避免从内存中拷贝数据并且绝大多数数据是通过指针来传递的,而不是通过memcpy。
再深入点,当响应是通过模块生成的,获取到的内容会被放到内存缓存中,随后加到缓存链中。子请求也通过这个缓存链工作。缓存链在nginx中是非常复杂的,因为有多个处理场景都是随着模块类型不同而有所变化。这种模块仅可以在特定时间操作缓存(链)并且决定是否重写输入缓存,使用最新的分配缓存来替代当前缓存,或者在缓存之前或者之后插入新的缓存。复杂的情况下,有时一个模块会接受多个缓存以至于其不得不在不完整的缓存链上操作。然而,这是nginx提供了底层的API来操作缓存链,所以开发任何的第三方模块时,开发者应该对nginx的这一部分非常的熟悉。
上面提及的一个事项就是在连接的生命周期内进行内存缓存的分配,这样对于长时间存在的连接就有额外的内存开销。同时,对于一个闲置的存活连接,nginx仅会分配500字节的内存。将来的nginx版本中可能会优化这块来重用长时间存在的连接和共享内存缓存。
管理内存分配是通过nginx的池分配者来完成的。共享的内存区域用于接受互斥量、缓存的元数据、ssl会话缓存以及和带宽管理相关的信息。在nginx中有一个分配者来管理你共享的内存分配。为了满足共享内存在处理并发请求时的安全性,可以使用一些锁机制(互斥量和信号)。为了操作复杂的数据结构,nginx同样提供了红黑树实现。红黑树用于保持共享内存中的缓存元数据,追踪非正则地址定义以及一些其他的任务。
不幸的是,所有这些都不在nginx的文档中有所阐述,使得针对nginx开发第三方的扩展变得很艰难。尽管一些好文档隐藏在nginx的内部,例如,Evan Miller写的那些——这些文档需要大量的反向编译尝试,nginx的模块实现对于很多人仍然是一门黑色艺术。
即使在开发第三方的nginx扩展中存在一些困难,nginx的用户社区最近出现了很多有用的第三方扩展。例如一个Lua的解释器模块,一个额外的负载均衡模块,WebDAV支持模块,高级缓冲控制以及其他有意思的本章的作者所支持的第三方工作。
课程总结
当Igor Sysoev开始写nginx的时候,大多数的软件都已经开始和互联网连接了,这种类型的软件架构遵从于遗留服务器、网络硬件、操作系统、老的网络架构的定义。然而,这并没有阻止Igor在提供web服务方面提供优化的思考。所以,第一课就很显然是:总是有提升的空间的。
牢记更好的Web软件想法于心,Igor花了大量的时间来开发最开始的代码架构,学习使用不同的方法来给不同的操作系统优化代码。加上第一版的开发时间,历经十年,他开发了nginx2.0版的原型。很显然新架构的初始原型以及初始代码结构对后来的继续开发产生了关键的作用。
另一点值得提的事情就是应该关注开发。Windows版的nginx是一个很好的例子,避免在一些既不是开发者能够完成相应功能的平台上以及其本身就不是最终目标程序上浪费开发精力。这对重写引擎也是同样适用的,指在给nginx增加新的特性时仍然保持向后的兼容性以支持遗留的设置。
最后,尽管nginx开发者社区不是很大,第三方的模块和扩展总是成为了其流行的一部分。Evan Miller、Piotr Sikora, Kholodkov,以及Zhang Yichun(agentzh)所做的工作以及其他天资过人的软件工程师受到了nginx社区以及最初开发者的一致感谢。