[译] 开源项目之 Nginx

nginx(读做 "engine x")是一位名叫 Igor Sysoev 的俄罗斯软件工程师开发的。自 2004 年发布以来,nginx 就一直专一于实现高性能,高并发和低内存占用。nginx 的额外功能,好比:负载均衡、缓存和流量控制以及高效集成在 Web 服务上的能力,使得它成为了当今网站架构的必选。现在,nginx 已经成为互联网中第二受欢迎的开源 Web 服务器。html

14.1 高并发为什么如此重要?

现在,互联网早已无处不在,咱们已经很难想象十年前没有互联网的样子。如今的互联网发生了翻天覆地的变化,从基于 NSCA 的能够点击 HTML 页面和基于 Apache 的 Web 服务,到现在可以实现超过 20 亿人实时的沟通。随着 PC、手机和平板的的蔓延,互联网已经将全球经济数字化。面向信息和娱乐的在线服务变得更加优质。线上商业活动的安全方面也发生了明显变化。所以,网站也比之前更加的复杂而且须要大量的工程投入来确保鲁棒性和可扩展性。前端

并发性成为了网站架构设计的最大挑战之一。自从 web 服务开始的时候,并发性的等级就在持续上升。对于一个热门网站来讲,支持几百甚至是几百万用户同时访问来讲也不是什么稀罕事情。20 年前,产生并发的缘由主要仍是客户端的 ADSL 或者拨号(dial-up)链接。现在,并发的产生来源于手机端和以及新型的应用架构,这些架构主要能够支持长链接来提供新闻、信息流发布和朋友间的 feed 流等等。另外一方面,致使高并发还因为现代浏览器的工做发生变化,一般是为了提升网页加载速度同时打开 4 到 6 个链接。android

为了表述清楚缓慢这种问题,设想一下,一个基于 Apache 的,能够提供 100KB 大小带有文字或者图片的简单 web 服务器。生成或者从新产生这个网页只须要极少不到一秒的时间。可是在带宽只有 80kps 的状况下(下载速度 10kb/s),传输数据到客户端却会花掉 10s。本质上,服务器产生 100kb 数据的速度是相对较快的,随后在传输数据到客户端直至释放链接的过程倒是相对较慢的。如今设想,你同时有 1,000 个独立的客户端连到你的服务器而且请求一样的内容。若是对于每一个独立的链接,都会占用额外的 1MB 内存,那么对于 1,000 个链接来讲就对致使多占用 1000 MB(1G)的内存,而这些仅仅是为了给 1000 个客户端提供 100kb 的内容。实际上,一个典型的 Apache 服务器一般会为了一个链接占用超过 1MB 的内存,遗憾的是几十 k 的带宽足够让手机之间高效通信。尽管从某种程度而言,发送数据给客户端是慢的,提升操做系统内核的 socket 缓冲大小是能够的,这个不是一个一般的解决方法,而且会有不良影响。ios

在持久链接中,处理并发会作起来总比提及来有更多的问题,由于要在新建 HTTP 链接的时候避免延迟,让客户端保持链接而且确保对于每一个链接服务端都可以保证有足够内存可供使用。nginx

所以,为了可以处理由于用户量增加产生高并发由此带来的负载上升,网站的就必须基于经过必定数目的高效模块来架设。同时,在从得到客户端链接请求,处处理完请求期间,像硬件(CPU,memory,disk),网络容量以及数据存储也是一样重要的。所以,web 服务器须要能在同时请求数和每秒请求频率这两方面都拥有扩展性。git

Apache 不合适吗?

Apache,开始于 1990s,现在依旧统治着互联网。最初它的架构知足于当时的操做系统和硬件,同时也知足于当时的只有一个独立的物理机运行一个 Apache 服务器的互联网状态。在 2000 年始,一个独立的服务器难以知足增加起来的 Web 服务的状况愈来愈明显。尽管 Apache 提供了一个可靠的基金会用于将来发展,然而,它这种为了每一个新链接复制自身的架构,已经再也不适用于非线性的网站扩张。最终,Apache 成为了一个有着许多不一样特性,第三方扩展,和一些广泛用于 web 应用开发的功能的 web 服务器。然而,没有什么东西是十全十美的,Apache 有者丰富功能的同时,对于每一个链接产生的 CPU 和内存消耗使得它不能很好的扩展。程序员

所以,当服务器的硬件、操做系统和网络条件成为了网站增加的瓶颈时,全世界的 web 工程师开始寻找一种更加高效的方法。大约十年前,一位名叫 Daniel Kegel 的杰出工程师宣称:"是时候让 web 服务可以支持 10k 并发了。"同时他还预测了咱们如今会叫互联网云服务。c10k 问题一产生,就引来了许许多多的解决方案用以优化实时的高并发。nginx 成为了其中最出色的解决方案之一。github

为了解决 C10k 问题中的 10,000 个实时的链接,nginx 用了一种不同凡响的架构,这种架构会更适合在同时处理大量的链接和一秒钟内完成屡次请求环境中,问题规模的增加是非线性的。nginx 是事件驱动的(event-based,因此它不会用 Apache 的那种为每个 web 请求都申请一个进程或者线程。结果即是,即便负载升高,内存和 CPU 都仍是处于掌控之中。nginx 目前能够在一台普通的机器上,同时处理上万的并发。web

nginx 的第一个版本主要是和 Apache 服务器一块儿部署,用来单独处理本来是 Apache 处理的 HTML, CSS, JavaScript 和图片这样的静态资源。在随后的迭代中,nginx 支持像 FastCGI, ,uswgi 或者 SCGI 协议集成到应用当中部署,而且能够利用像 memcached 这样的分布式缓存系统。同时像反向代理,负载均衡这样的特性也随之加上。这些额外的特色让 nginx 成为了构建可扩展性 web 服务的高效的基础组件的工具之一。正则表达式

2012 年二月,Apache 2.4.x 分支发布。尽管,这个最新版本的 Apache 增长了多核处理器支持模块和用于提高可扩展性和并发的模块,然而它的性能,并发能力,以及资源利用能力与纯事件驱动的 web 服务器比,依旧难以望其项背。 很乐意看到新版的 Apache 服务器有着更好的可扩展性,尽管这样能够减小自身的瓶颈,然而像典型的 nginx-plus-Apache 配置依旧会被使用。

使用 nginx 会有更多的优点吗?

可以高性能地处理高并发一直是部署了 nginx 以后得到的最主要的好处。然而,还有一些更有趣的东西。

在过去几年中,网站架构就一直在拥抱解耦并从 web 服务器中拆分出一些基础组件。然而,那些本来存在于 LAMP-based 的网站中的基础组件,在 LEMP-based(E 表明着 Nginx 的读音) 的网站中,却能让 web 服务器成为基础组件以及用一种不一样的方式去集成相同的或者改进了的应用和数据库工具。

nginx 很是适合作这个,由于它能够方便提供一个并发支持,延迟超时处理,SSL 支持,静态文件支持,压缩和缓存,甚至是 http 流媒体的高效的层级,而这些功能本来处于应用层。nginx 也能够直接集成一些像 Redis/memcached 这样的 NoSQL 用以优化大用户量场景。

当近代的开发语言和工具流行起来的时候,愈来愈多的公司正在改变他们的开发和部署方式。nginx 成为了改变过程当中最重要的部分,同时,nginx 让不少公司在有限的预算中,快速地启动开发他们的服务。

nginx 是从 2002 年开始开发。到 2004 年,它以 two-clause BSD license 发布。随后,nginx 用户量开始增高,修改建议,bug 报告,观察报告等都在社区中不断完善 ngix。

nginx 最初的源码是用 C 完成的。nginx 已经能够部署在许多架构和操做系统中,好比 Linux, FreeBSD, Solaris, Mac OS X, AIX and Microsoft Windows。nginx 拥有本身的库而且并无大量使用 C 标准库,一些像 zlib, PCRE and OpenSSL 这一类的库由于有证书冲突而没有被采用。

在 Windows 上部署 nginx 更像是一个实现 nginx 的理论证实而不是一个功能完善的项目。因为内核限制,nginx 的一些功能特性并不能发挥出来。在 windows 上的 nginx 并发能力、性能会更低,也没有缓存和带宽策略。未来 windows 上的 nginx 版本会继续完善。

14.2. nginx 架构总览

传统的解决并发的方式是每一个单独的请求一个进程或者线程,而且网络和 io 操做都是阻塞式的。在传统的应用当中,这种作法会因为 CPU 和内存开销致使低效。开启一个独立的进程或者线程会须要预加载一个新的运行时环境和上下文。这些东西也会占用一些额外的 CPU 时间,线程频繁轮换致使的上下文切换带来的开销最终致使了低性能。这些问题在一些旧的 web 服务架构,好比 Apache 中获得了证明。这是在提供丰富广泛特性与优化服务器开销以前的一种权衡。

从最先开始,nginx 就被设定为在网站用户动态增加期间,用来提升网站性能和服务器资源利用率的工具,以致于它拥有一种不同凡响的模型。这是受一些操做系统的事件驱动概念启发。这也产生了 nginx 的核心架构:模块化,事件驱动,异步,单线程,非阻塞。

nginx 大量采用多路复用(multiplex)和事件通知,并对每一个 nginx 进程分配了特定的任务。链接被有限个数单线程的 worker 进程高效轮询(run-loop)处理。 每一个 worker 均可以同时处理数千个并发链接和每秒请求。

Code Structure 代码结构

worker 代码包含了核心和功能模块。nginx 核心负责维护一个紧凑的轮询,并在处理请求的每一个阶段都执行模块中对应的部分。模块构成了大部分表示层和应用层功能。模块从网络和存储介质中进行数据的读写,传输内容,过滤出站内容,执行服务端的动做和当代理功能被打开的时候传递请求到被代理的(upstream)服务器。

nginx 模块化的架构可让开发者在不修改核心代码的状况下加入一些自定也的扩展。nginx 模块稍微有点不一样,好比核心模块、事件模块、阶段处理器、协议、变量处理器、filter,upstream 和负载均衡。目前,nginx 再也不支持动态加载模块。模块在 nginx build 阶段就会被编译。然而,在未来 nginx 会在主版本上提供 loadable 模块和 ABI。更多关于不一样模块的信息详见 Section 14.4.

在处理一些关于网络接收,处理和管理以及内容检索的时候,nginx 使用了事件通知(event notification)机制以及一些操做系统( Linux, Solaris and BSD-based)的磁盘 IO 优化,好比:kqueue, epoll, and event ports。目的是为操做系统提供尽量多的提示,以便为入站和出站流量、磁盘操做、socket 读写、超时等获取及时的异步反馈。针对 nginx 运行的每一个 unix-like 的操做系统,对多路复用和高级 I/O 操做使用不一样的方法进行了大量优化。

更多 nginx 架构高级概述详见 Figure 14.1.

Figure 14.1: Diagram of nginx's architecture

Workers 的模型

正如以前提到的,nginx 并不为每一个链接开一个进程或者线程。相反,worker 进程为每一个新链接都采用一个共用的监听 socket 并在轮询中高效处理着数千个链接。对于 nginx 的 worker,没有采用一些特别的链接机制,都是由操做系统内核来完成的。一旦启动,一些监听 socket 就会被建立。worker 就会持续地接受链接,处理 http 请求和从对应的这些 socket 中读写数据。

轮询是 nginx 代码中最复杂的部分。它包括了综合(comprehensive)的内部调用和依赖大量的异步任务处理思想。异步操做经过模块化,事件通知,函数回调和计时器实现。整体上,关键在于尽量的非阻塞。惟一让 nginx worker 阻塞的只有磁盘不足的状况。

由于 nginx 不会为每一个链接新开进程或者线程,内存占用在不少场景下都不会高。nginx 节约了 cpu 占用也是由于没有进程线程的建立和销毁。nginx 要作的就是检查网络和存储,建立新链接,把新链接加入到轮询,而且在完成以前都异步处理。nginx 谨慎采用了一些系统调用好比资源池化和内存分配,以致于在极端的状况下也不会有很高的 CPU 占用。

因为 nginx 处理链接就开了几个 worker,在多核状况下能够很好的扩展。大体就是一个核心一个 worker,这样每一个 worker 充分利用 cpu 核心,避免了线程切换和锁等待。不会产生资源不足而且每一个单线程的 worker 进程中都存在资源管理策略。这种模型容许在不一样存储设备之间有更好的扩展性,促进磁盘利用而且避免了磁盘 IO 阻塞。总的来讲,服务器资源在多个 worker 工做的状况下被更高效使利用了。

对于某些磁盘使用和 CPU 负载模式,应该调整 nginx worker 的数量。这些规则在这里有点基础,系统管理员应该基于他们的工做负载尝试一些配置。通常建议以下:若是负载模式是 CPU 密集型的—例如,处理大量 TCP/IP、执行 SSL 或压缩,nginx worker 的数量应该与 CPU 核心的数量相匹配;若是负载主要是磁盘 I/O 限制。例如,从存储中提供不一样的内容,或者大量的反向代理,workers 的数量多是内核数量的 1.5 到 2 倍。有些工程师根据单个存储单元(磁盘分区)的数量来选择 workers 的数量,这种方法的效率取决于磁盘存储的类型和配置。

nginx 开发人员在即将发布的版本中要解决的一个主要问题是如何避免磁盘 I/O 上的大部分阻塞。目前,若是没有足够的存储性能来服务于由特定的 worker 生成的磁盘操做,那么 worker 仍然可能阻塞从磁盘读取 / 写入。存在许多机制和配置文件指令来减轻此类磁盘 I/O 阻塞场景。最值得注意的是,sendfile 和 AIO 等选项的组合一般会为磁盘性能带来很大的空间。应该根据数据存储、可用的内存大小和底层存储体系结构来计划 nginx 的安装。

现有 worker 模型的另外一个问题是关于内嵌脚本支持的限制。首先,使用标准的 nginx 发行版,只支持嵌入 Perl 脚本。对此有一个简单的解释:关键问题是内嵌脚本可能阻止任何操做或意外退出。这两种类型的行为都会当即致使 worker 被挂起,同时影响数千个链接。须要更多的工做来让 nginx 的嵌入式脚本更简单、更可靠、适合更多的应用程序。

nginx 进程角色

nginx 在内存中运行几个进程;有一个 master 进程和几个 worker 进程。还有一些特殊用途的进程,特别是缓存加载器和缓存管理器。版本 1.x 中的全部进程都是单线程的。全部进程主要使用共享内存机制进行进程间通讯。主进程做为 root 用户运行。缓存加载器、缓存管理器和 worker 做为非特权用户运行。

master 进程主要有如下任务

  • 读取并验证配置文件
  • 建立、绑定和关闭 socket
  • 启动,终止和维护配置好了个数的 worker 进程
  • 不中断状况下从新加载配置
  • 控制热更新(从二进制文件启动和必要状况下回滚)
  • 打开日志文件
  • 编译内嵌的 perl 脚本

worker 进程接受和处理来自客户机的链接,提供反向代理和过滤功能,并完成 nginx 可以作的几乎全部其余事情。关于监视 nginx 实例的情况,系统管理员应该关注 worker 进程,由于他们是反映 web 服务器实际平常操做的过程。

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

缓存管理器主要负责缓存过时和失效。在正常的 nginx 操做过程当中,它保持在内存中,在失败的状况下由主进程从新启动。

nginx 缓存简览

nginx 中的缓存是以文件系统上分层数据存储的形式实现的。缓存 key 是可配置的,可使用不一样的特定于请求的参数来控制进入缓存的内容。缓存 key 和缓存元数据存储在共享内存段中,缓存加载器、缓存管理器和 worker 进程能够访问共享内存段。目前,除了操做系统的虚拟文件系统机制产生的优化以外,没有任何文件的是缓存在内存当中。每一个缓存的读取都放在文件系统上的不一样文件中。层次结构(级别和命名细节)是经过 nginx 配置指令控制的。当将响应写入缓存目录结构时,路径和文件的名称来自代理 URL 的 MD5 值。

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

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

nginx 配置有几个不一样的内容:main, http, server, upstream, location (同时 mail 至关于邮件服务代理)。配置文件内容不重叠。例如,在 main 中不存在 location。此外,为了不没必要要的歧义,没有任何相似于“全局 web 服务器”配置的东西。nginx 的配置是干净和合乎逻辑的,容许用户维护包含数千个指令的复杂配置文件。在一次私人谈话中,Sysoev 说,“全局服务器配置中的 location、directory 和其余块是我在 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 directive

14.4. nginx 内部

如前所述,nginx 代码库由核心和许多模块组成。 nginx 的核心是负责提供 Web 服务器,Web 和邮件反向代理功能的基础;它支持使用底层网络协议,构建必要的运行时环境,并确保不一样模块之间的无缝交互。可是,大多数协议和特定的应用程都是由 nginx 功能模块完成的,而不是核心模块。

在内部,nginx 经过由模块组成的的管道或模块链来处理链接。换句话说,对于每一个操做,都有一个正在进行相关工做的模块;例如,压缩,修改内容,执行服务器端,经过 FastCGI 或 uwsgi 协议与 upstream 应用服务器通讯,或与 memcached 通讯。

有几个 nginx 模块位于核心和真正的“功能”模块之间。这些模块是httpmail。这两个模块在核心和较低级别组件之间提供了额外的抽象级别。在这些模块中,实现了与诸如 HTTP,SMTP 或 IMAP 的相应应用层协议相关联的事件序列的处理。结合 nginx 核心,这些上层模块负责维护对各个功能模块的正确调用顺序。虽然 HTTP 协议目前是做为http模块的一部分实现的,但因为须要支持 SPDY 等其余协议,所以计划未来将其分离为功能模块。更多 SPDY 协议详见 SPDY: An experimental protocol for a faster web

功能模块可分为事件模块,阶段处理程序,输出 filter,变量处理程序,协议,上游和负载平衡器。大多数这些模块补充了 nginx 的 HTTP 功能,但事件模块和协议也用于mail。事件模块提供特定的 OS 依赖事件通知机制,如kqueueepoll。 nginx 使用的事件模块取决于操做系统功能和构建配置。协议模块容许 nginx 经过 HTTPS,TLS / SSL,SMTP,POP3 和 IMAP 进行通讯。

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

  1. 客户端发送 http 请求。
  2. nginx core 依据配置文件中的 location 选择合适的阶段处理器。
  3. 若是配置生效,负载均衡器就会选择一个 upstream 服务器代理。
  4. 阶段处理器执行任务,并把缓冲区的内容传递给第一个 filter。
  5. 第一个 filter 将内容传递给第二个 filter
  6. 第二个 filter 传递给第三个(迭代执行)
  7. 将最后的 response 发送给客户端。

nginx 模块调用是很是可定制的。它使用指向可执行函数的指针来执行一系列回调。然而,这样作的缺点是它可能给想要编写本身的模块的程序员带来很大的负担,由于他们必须准肯定义模块应该如何以及什么时候运行。 nginx API 和开发人员的文档都在不断改进,而且能够更多地用来缓解这个问题。

下面这些列子是能够添加模块的位置:

  • 在读和处理配置文件以前
  • 在每一个服务器出现以及配置文件指向的地方
  • 当 主配置 被初始化的时候
  • 当服务器被初始化的时候
  • 当 server configuration 被合并到 主配置的时候
  • 当 location configuration 初始化或者合并到 parent server configuraton 的时候
  • 当 master 进程启动或者存在的时候
  • 当一个新的 worker 进程启动或者存在的时候
  • 当处理一个请求的时候
  • 当过滤请求 header 和请求 body 的时候
  • 当请求转发到 upstream 服务器的时候
  • 服务器中的响应的时候
  • 当完成与一个 upstream 服务器的交互的时候

在 worker 进程中,致使生成响应的运行循环的 action 序列以下所示:

  1. 启动 ngx_worker_process_cycle().
  2. 使用操做系统特定的机制来处理事件(such as epoll or kqueue
  3. 接收事件而且分发给相关的 action
  4. 处理 / 代理请求 header 和 body
  5. 产生响应内容 (header, body) 并传递给客户端
  6. 结束请求
  7. 重启 timers,events

轮询自己(步骤 5 和 6)确保增量生成响应并将其流式传输到客户端。

处理 HTTP 请求的更详细过程可能以下所示

  1. 初始化请求处理
  2. 处理 header
  3. 处理 body
  4. 调用相关的 nginx 处理器
  5. 执行每一个处理阶段

这将咱们带到了每一个阶段。当 nginx 处理 HTTP 请求时,它会将其传递给许多处理阶段。在每一个阶段都有处理程序能够调用。一般,阶段处理程序处理请求并生成相关输出。阶段处理程序被附加到配置文件中定义的位置。

阶段处理程序一般执行如下四项操做:获取位置配置,生成适当的响应,发送 header 以及发送 body。处理程序有一个参数:描述请求的特定结构。请求结构有不少关于客户端请求的有用信息,例如请求 method,URI 和 header。

读取 HTTP 请求 header 时,nginx 会查找关联的虚拟服务器配置。若是找到虚拟服务器,请求将经历六个阶段:

  1. 服务器重写阶段
  2. location 阶段
  3. location 重写阶段(将请求带回到上一个阶段)
  4. 链接控制阶段
  5. try_files 阶段
  6. 日志阶段

为了响应请求生成必要的内容,nginx 将请求传递给合适的内容处理程序。根据确切的位置配置,nginx 可能首先尝试所谓的无条件处理程序,如perlproxy_passflvmp4等。若是请求与上述任何内容处理程序都不匹配,则由如下处理程序之一按照如下顺序选择:random indexindexautoindexgzip_staticstatic

索引模块的详细信息能够在 nginx 文档中找到,但这些是使用尾部斜杠处理请求的模块。若是像mp4autoindex这样的专用模块则不合适,内容被认为只是磁盘上的文件或目录(即静态),并由static内容处理程序提供服务。对于目录,它会自动重写 URI,以便始终存在尾部斜杠(而后发出 HTTP 重定向)。

而后将内容处理程序的内容传递给 filter。filter 也附加到 location,而且能够为 location 配置多个 filter。filter 执行操做处理程序生成的输出的任务。对于预先定义的开箱即用 filter,执行的顺序在编译时就肯定。对于第三方 filter,能够在构建阶段对其进行配置。在现有的 nginx 实现中,filter 只能进行出站更改,而且目前没有机制来编写和附加 filter 来进行输入内容转换。输入过滤将出如今 nginx 的将来版本中。

filter 遵循特定的设计模式。调用 filter,开始工做,并调用下一个 filter,直到调用链中的最终 filter。以后,nginx 完成响应。filter 没必要等待前一个 filter 完成。调用链中的下一个 filter 能够在上一个 filter 的输入可用时当即开始工做(功能上与 Unix 管道很是类似)。反过来,生成的输出响应能够在接收到来自上游服务器的整个响应以前传递给客户端。

还有 header filter 和 body filter;nginx 会分别用相关的 filter 来给相应 header 和 body 添加数据

header filter 主要有下面三个步骤

  1. 决定是否对这个响应进行操做
  2. 操做这个响应
  3. 调用下一个 filter

body filter 修改生成的数据,下面是 body filter 的一些案例

  • 服务端 includes
  • XSLT 过滤
  • 图片过滤(好比修改图片尺寸)
  • 修改编码
  • gzip压缩
  • chunked encoding

在 filter chain 以后,响应将传递给 writer。除了 writer 以外,还有一些额外特殊用途的 filter,即copypostponefilter。 copyfilter 负责使用可能存储在代理临时目录中的相关响应内容填充内存缓冲区。 postponefilter 用于子请求。

子请求是请求 / 响应处理的很是重要的机制。子请求也是 nginx 最强大的方面之一。对于子请求,nginx 能够从与客户端最初请求的 URL 不一样的 URL 返回结果。一些 Web 框架将此称为内部重定向。可是,nginx 更进一步 - 过滤器不只能够执行多个子请求,并且能够将输出组合成单个响应,但子请求也能够嵌套和分层。子请求能够执行其本身的子子请求,而且子子请求能够发起子子子请求。子请求能够映射到硬盘,其余处理程序或上游服务器上的文件。子请求对于根据原始响应中的数据插入其余内容很是有用。例如,SSI(服务器端包含)模块使用过滤器来解析返回文档的内容,而后将“include”指令替换为指定 URL 的内容。或者,它能够是一个过滤器,将文档的整个内容视为要检索的 URL,而后将新文档附加到 URL 自己

upstream 和负载均衡器也值得简要描述。upstream 用于实现能够被识别为反向代理(proxy_pass处理程序的内容。upstream 模块主要准备将请求发送到 upstream 服务器(或“后端”)并接收响应。这里没有调用输出 filter。当 upstream 服务器准备好被写入和读取时,upstream 模块确切地作的是设置要调用的回调。存在实现如下功能的回调:

  • 建立的请求缓冲被发送到 upstream 服务器的
  • 从新链接到 upstream 服务器(在请求产生以前)
  • 处理 upstream 服务器响应的内容而且存储指向从 upstream 服务器内容的指针。
  • 放弃请求(主要是客户端过早断开链接)
  • 从 upstream 服务器读完内容以后结束请求
  • 整理响应 body(好比删除 http 响应 trailer)

负载均衡器模块链接到proxy_pass处理程序,以便在多个 upstream 服务器符合条件时提供选择上游服务器的功能。负载均衡器注册启用配置文件指令,提供额外的上游初始化函数(以解析 DNS 中的上游名称等),初始化链接结构,决定在何处路由请求以及更新统计信息。目前,nginx 支持两种标准规则,用于对 upstream 服务器进行负载均衡:循环和 ip-hash。

upstream 和负载均衡处理机制包括用于检测失败的上游服务器以及将新请求从新路由到其他服务器的算法 - 尽管计划进行大量额外工做以加强此功能。总的来讲,nginx 开发团队计划对负载均衡器进行更多的工做,而且在下一版本的 nginx 中,将大大改进跨不一样上游服务器分配负载以及运行情况检查的机制。

还有一些其余有趣的模块提供了一组额外的变量供配置文件使用。虽然 nginx 中的变量是在不一样的模块中建立和更新的,但有两个模块彻底专用于变量:geomapgeo模块用于根据客户端的 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 Mille r 生成的那些文档 - 须要大量的逆向工程工做,而且 nginx 模块的实现仍然是许多人的黑科技。

尽管与第三方模块开发相关的某些困难,nginx 用户社区最近看到了许多有用的第三方模块。例如,有一个用于 nginx 的嵌入式 Lua 解释器模块,用于负载均衡的附加模块,完整的 WebDAV 支持,高级缓存控制以及本章做者鼓励并将在将来支持的其余有趣的第三方工做。(参考 Open Resty -- 译者注)

14.5. 收获

当 Igor Sysoev 开始编写 nginx 时,大多数给互联网赋能的软件已经存在,而且这种软件的体系结构一般遵循传统服务器和网络硬件,操做系统和旧的互联网体系结构。然而,这并无阻止 Igor 认为他可以继续改进 Web 服务器领域的东西。因此,虽然第一课可能看起来很简单,但事实是:总有改进的余地。

考虑到更好的 Web 软件的想法,Igor 花了不少时间开发初始代码结构并研究为各类操做系统优化代码的不一样方法。十年后,参考在版本 1 上的多年积极开发,他现在正在开发 nginx 版本 2.0 的原型。很明显,一个软件产品的新架构的初始原型和初始代码结构对于将来的重要性是很是重要的。

值得一提的另外一点是发展应该集中。Windows 版本的 nginx 多是一个很好的例子,说明如何避免在既不是开发人员的核心竞争力或目标应用程序的状况下稀释开发工做。它一样适用于重写引擎,该引擎在屡次尝试加强 nginx 时出现,具备更多功能以便与现有的旧设置向后兼容。

但值得一提的是,尽管 nginx 开发者社区不是很大,但 nginx 的第三方模块和扩展一直是其受欢迎程度的重要组成部分。 Evan Miller,Piotr Sikora,Valery Kholodkov,Zhang Yichun(agentzh 中文名:章亦春)以及其余才华横溢的软件工程师所作的工做获得了 nginx 用户社区及其原始开发人员的赞扬。


This work is made available under the Creative Commons Attribution 3.0 Unported license. Please see the full description of the license for details.

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索