Nginx 介绍(译文IV–深入nginx内核)
- 原文 nginx
- 作者 Andrew Alexeev
本章目录
深入nginx内核
如前文所提及,nginx的源码主要包括一个核心(core)和多个模块(modules)。其中核心(core)部分主要用于提供以下功能:
- 作为web服务器的基础
- 提供web及邮箱的反向代理(mail reverse proxy)的功能
- 允许使用底层网络协议
- 创建必要的运行时环境
- 保证各个模块间的无缝交互
需要注意的是:大部分的协议和为应用程序定制的特性,由nginx的模块(modules)而非核心(core)完成。
在nginx的内部,通过各个模块间的管道(pipeline)或模块链(chain)来处理所有连接。或者说,对于每个操作,都有相应的模块在处理该工作,例如
- 压缩
- 修改内容
- 执行SSI(server-side includes)
- 通过FastCGI、uwsgi协议与后端应用服务器交互
- 与memcache交互。
在核心(core)与实际功能模块(real “functional” modules)之间,还有一对模块,即http和mail。这两个模块在核心(core)和更底层的组建中间提供了一个附加的抽象层。在这些模块中,处理与各自应用层协议相关的事件序列,如已实现的HTTP,SMTP,IMAP。
结合Nginx核心(core),这些上层的模块负责维护调用不同功能模块的正确顺序。虽然目前HTTP协议是作为http模块的一部分实现的,但为支持其他协议如SPDY(参考“SPDY: An experimental protocol for a faster web),将http独立为一个功能模块已列入计划中。
功能模块
功能模块可以分为以下几类:
- 事件模块
- 阶段处理器
- 输出过滤器
- 变量处理器
- 协议模块
- 上游及负载均衡器
虽然mail模块中用到了事件模块和协议,但以上大部分模块用于补充nginx的HTTP功能。
- 事件模块提供了类似于kqueue和epool的基于操作系统的事件通知机制,主要取决于操作系统的能力与编译配置。
- 协议模块允许nginx通过HTTPS, TLS/SSL, SMTP, POP3 和 IMAP等协议通信。
典型的HTTP请求处理周期如下:
- 客户端发送HTTP请求。
- nginx核心根据配置匹配该请求的location,选择对应的阶段处理器。
- 根据配置需要,负载均衡器挑选一个上游服务器用于转发请求。
- 阶段处理器完成工作,并将每个输出缓冲区传递给第一个过滤器。
- 第一个过滤器将输出传给第二个过滤器。
- 第二个过滤器传递输出给第三个(。。。)。
- 最终将响应发送给客户端。
Nginx模块调用是高度可定制的。它主要通过一系列的回调展开工作,而这些回调则通过使用指向可执行函数的指针来实现。因而,对于那些想要自己编写模块的开发者,就必须准确地定义这些自定义模块如何运行、何时运行,从而大大加重了负担。为了缓解该负担,使之能够更好地执行,Nginx的API和开发者文档都在不断地优化中。
一些在nginx中插入模块的案例:
- 读取和处理配置文件之前
- Location和server的每个配置指令生效时
- Main配置被初始化后
- Server配置(host/port)初始化后
- Server配置合并到main配置后
- Location配置初始化或者被合并到父server配置时
- Master进程启动或退出时
- 新的worker进程启动或退出时
- 处理请求时
- 过滤响应头和响应体时
- 为request挑选,初始化和重新初始化上游服务器时
- 处理上游服务器响应时
- 完成与上游服务器的交互时
worker
在一个woker内部,通过以下操作来引导处理循坏(run-loop)在哪里生产响应:
- 始于ngx_worker_process_cycle
- 根据操作系统的特性处理事件(如epoll或kqueue)
- 接受事件、分发相关操作
- 处理/代理 请求头和请求体
- 生成响应内容(响应头、响应体)、流式返回给客户端
- 完成请求
- 重新初始化计时器和事件
处理循环(run-loop)本身通过步骤5、6来保证增量地产生响应并流式的返回给客户端
处理一个HTTP请求更详细的过程可能如下:
- 初始化请求处理
- 处理请求头
- 处理请求体
- 调用对应处理器
- 执行所有处理阶段
如此,便将我们带到了处理阶段。
处理阶段
Nginx处理一个请求时,往往会经历一系列的处理阶段。在每个处理阶段中,都会调用对应的处理器。阶段处理器与配置文件中定义的location关联,一般地,阶段处理器处理一个请求并生成对应输出。
阶段处理器处理以下四件典型事件:
- 获取location的配置
- 生成相应的响应
- 发送头(header)信息
- 发送主体(body)信息
一个处理器对应一个参数:一个描述请求的特定结构。请求体结构包含了客户端请求的很多有用信息,如请求方法、URI、请求头信息。
Nginx在读取完HTTP请求头以后,根据配置查找对应的虚拟服务器。如果存在对应的虚拟服务器,请求将按照以下阶段进行处理:
- 服务器重写阶段(server rewrite phase)
- 定位所在阶段(location phase)
- 位置重写阶段(location rewrite phase) – 这样便能够将请求带回给之前的阶段
- 到达控制阶段(access control phase)
- try_files阶段(try_files phase)
- 日志阶段(log phase)
为了给请求生成必要的响应内容,nginx将请求交传递给相应的内容处理器。根据准确的location配置,nginx会先尝试如perl, proxy_pass, flv,mp4等所谓的无条件处理器。如果请求与以上内容处理器均不匹配,那么将会严格按下面顺序选取一个处理器:random index, index, autoindex, gzip_static, static。
Index模块详细内容见Nginx官方文档,该模块只用于处理后缀是“/”的请求。如果没有匹配上如mp4或autoindex这样的专业模块,那么响应内容将被认为是磁盘上的一个文件或目录(即静态的),将由static内容处理器完成服务。目录的URI将被自动重写,以保证后缀是一个斜杠(从而发起一个HTTP重定向)。
过滤器
内容处理器处理完以后,会把内容传递到过滤器。过滤器同样与location相关联,且一个location可配置关联多个过滤器。过滤器用于管理处理器产生的输出,且各个过滤处理器的执行顺序在编译时决定。
自带的过滤器顺序是预定义好的,而第三方过滤器的顺序则可以在编译阶段设置。当前的nginx版本中,过滤器只能修改输出的数据,但尚未存在相关机制编写与关联过滤器用以修改输入内容。输入过滤器将在未来版本提供。
过滤器遵循一个特定的设计模式。工作过程如下:
- 一个过滤器被调用
- 开始工作
- 检查过滤器链,若存在下一个,继续进入步骤1。否则,进入步骤4
- nginx结束响应。
过滤器不用等待前面的过滤器结束。一旦上一个过滤器提供的输入已经可用,当前过滤器便可以马上启动自己的工作(功能上非常类似于Unix中的管道)。因而,在从上游服务器接收到所有的响应之前,所生成的输出响应便已被流式地发送给客户端。
过滤器可分为Header过滤器和body过滤器,由Nginx将返回body和header分发给与之关联的对应处理器。
其中一个header过滤主要由以下三步组成:
- 判定处理该响应
- 处理响应
- 调用下一个过滤器
body过滤器用于改变响应内容,举例如下:
- SSI(server-side includes)
- XSTL过滤器
- 图片过滤器(例如调整图片大小)
- 转换字符集
- gzip压缩
- chunked 编码
在过滤链处理完毕后,响应体被传给writer。与writer一起还有一对特定功能的附加过滤器:
-
拷贝过滤(copy filter):负责将相关响应内容填充到内存缓冲区,这些内容很可能被存于代理临时目录下。
-
延迟过滤 (postpone filter):用于处理子请求。
子请求
子请求是请求/响应处理中一个很重要的机制,同时也是Nginx最强大的方面之一。通过使用子请求,Nginx可返回一个与客户端请求URL不同的响应,即某些web框架中的内部重定向。而且,Nginx走得更远--过滤器不仅可以使用多个子请求、合并这些输出到一个单独的响应,还能互相嵌套、分层处理。一个子请求A可以产生自己的子请求B,同时,B也可以启动自己的子请求C等。子请求可以被映射到物理硬盘上的文件、其它处理器、或上流服务器。
将原始响应体中插入新的附加内容的功能,使子进程变得很有用。例如:
- SSI(server-side include)使用一个过滤器解析返回文件的内容,然后使用其指定的URL来替换include指令。
- 实现一个过滤器,使用整个文件内容作为URL被索引,并将新的文档内容附加到该URL本身。
上游和负载均衡器
上游
上游一般被认为是用于实现反向代理(proxy_pass处理器)的内容处理器。上游模块执行顺序一般如下:
- 准备请求
- 将请求发送给上游服务器(后端)
- 从上游服务器接收请求
在该过程中,不会调用到输出过滤器。
上游模块设置回调函数,供上游服务器准备好读写时使用。回调函数主要实现以下功能:
- 准备请求缓冲区(或缓冲区链),用于发送给上游服务器
- 重新初始化、重置到上游服务器的连接(准时发生于在再次发起请求之前)
- 处理上游服务器响应的首字节,并且保存该响应的指针
- 放弃请求(当客户端过早关闭连接时)
- 在Nginx完成读取上游服务器响应时,结束请求
- 整理响应体(如除去空白)
负载均衡器
当存在多个符合条件的上游服务器时,负载均衡器协助proxy_pass处理器为其提供一个选择上游服务器的能力。一个负载均衡器特性如下:
- 注册一个被启用的配置文件指令
- 提供附加的上游服务器初始化功能(使用DNS解析上游服务器名称等)
- 初始化连接结构体
- 决定如何路由请求
- 更新状态信息
目前,nginx支持两种标准的上游服务器负载均衡规则:轮询和ip哈希。
上游和负载均衡处理机制包含检测上游服务器异常、将请求重新路由到剩余上游服务器的算法--当然,为加强该能力,很多工作已列入计划。总之,更多的负载均衡相关工作已列入计划,此外,下一版本的nginx将大幅度提升以上能力,即:
- 基于多上游服务器的均衡负载能力
- 异常检测机制
变量处理器
当然还有很多其他有意思的模块,这些模块大大丰富了配置文件中使用的变量集。Nginx中的变量根据不同的模块生成和更新,其中有两个模块为变量专用:geo和map。
- geo模块用于促进基于客户端ip的跟踪。该模块可根据客户端ip地址创建任意变量。
- map模块允许从一个变量生成另一个变量,提供了灵活映射主机名和其他运行时变量的基本能力。
以上这类模块称为变量处理器。
内存分配机制
nginx的内存分配机制在一个单独的nginxworker中实现(从某些方面来讲,该思路受Apach启发)。
一个nginx内存管理高层描述如下:
对于每一个连接,必要的内存缓冲区特点如下:
- 动态生成(dynamically allocated)
- 关联(linked)
- 用于存储和管理请求头、请求体、响应
- 最后根据连接释放。
值得注意的是,很重要的一点是nginx尽可能的去避免在内存中拷贝数据,大部分的数据通过指针进行传递,而不是调用memcpy。
再深入一点,当一个模块生成响应时,这些响应内容放入内存缓冲区,而该缓存区最终将被添加到一个缓冲区链表。该缓冲区链同样适用于子请求处理工作。
根据不同的模块类型,存在着多个处理场景,因而nginx中的缓冲区链表相当复杂。
例如,在实现body filter模块中,精确地管理缓冲区是可能是相当棘手的。
这个模块在某一时刻只能处理缓冲区链中的一个缓冲区,且必须决定
- 是否覆盖输入缓冲区
- 是否用新分配的缓冲区替换当前缓冲区
- 是否在这个缓冲区之前或之后插入一个新缓冲区
更复杂的情况,有时一个模块收到多个缓冲区数据,因而必须处理一个不完整的缓冲区链。然而目前nginx在维护缓存链中仅提供了底层API,所以开发者需要在真正掌握nginx这一晦涩难懂的部分之后,再去开发第三方模块。
以上内容中需要注意的一点:Ninx中对于一个连接,存在着为连接的整个生命周期分配的内存缓冲区,所以对于长连接需要保留一些额外的内存。同时,对于一个空闲的keepalive连接,nginx仅消耗550字节内存。其中复用和共享内存缓冲区方面,将来的版本中可能会做相关的优化。
内存分配管理的任务由nginx内存池分配器完成。共享内存区用于:
- 存放接受互斥锁(accept mutex)
- 缓存元数据
- SSL会话缓存
- 带宽监控和管理(限速)相关的信息
Nginx实现了slab分配器用于管理共享内存。为保证共享内存使用过程中的并发安全,提供了一系列锁机制(互斥锁和信号量)。为了组织复杂的数据结构,nginx也提供了红黑树的实现。红黑树用于在共享内存中保存缓存元数据,跟踪非正则location定义,以及其他一系列的任务。
不幸的是,上述内容被从未一致、简单地描述过,以致Nginx的第三方扩展开发工作相当复杂。虽然有一些nginx内核介绍的优秀文档(如Evan Miller写的),但是这些文档需要做很多回归工程的努力,nginx模块的开发对很多人来说,依旧是黑盒。
虽然开发第三方模块的核心难点尚未解决,nginx社区最近还是涌现了大量有用的第三方模块。案例如下:
- 将Lua解释器嵌入nginx
- 负载均衡附加模块
- 完整的WebDAV支持
- 高级缓存控制
- 其他本文作者所鼓励和将来支持的有趣的第三方工作。