Nginx Inc. 联合创始人 Andrew Alexeev 在 AOSA 上发表的文章 [1] ,涵盖 了 Nginx 项目想要解决的问题、程序整体架构、配置文件结构、请求处理流程和开发 Nginx 过程中的心得体会等等话题。本节对该文中提到的关键点进行摘录和整理。
以解决 C10K 问题 [2] 为使命的 Nginx,在底层架构上选择了事件驱动机制。事件 驱动机在需要对并发连接和吞吐率提供非线性伸缩 (nonlinear scalability) 的场景中比 多进程/多线程更适合:负载持续上升时,CPU 和内存占用依然在可控的范围内;在普 通硬件配置上,Nginx 就可以服务数以万计的并发连接。
工作进程中的 runloop 是 Nginx 代码中最复杂的部分,它的实现基于异步任务处 理思想。Nginx 通过使用模块化、事件通知、大量的回调函数和仔细调整的计时器等 手段实现异步操作。
读写性能不足的硬盘的操作是 Nginx 中唯一可能发生阻塞的地方;
Nginx 谨慎地使用系统调用实现了诸如内存池( pool )、片内存分配器 ( slab memory allocator )等高效的数据结构,再加上选取的并发模型,可以有 效节省对 CPU、内存等系统资源的占用。即使在极高的负载下,Nginx 的 CPU 使用率通 常也是很低的。
Nginx 使用 slab memory allocator 管理共享内存。共享内存一般用来存储 acept mutex, cache metadata, SSL session cache 等需要在进程间共 享的数据。同时,Nginx 使用 mutex semaphore 等加锁机制保证共享数据的 安全。
Nginx 提供了针对阻塞磁盘 IO 的优化措施: sendfile, AIO, thread pool (1.7.11+);
除了 master 进程和 worker 进程,Nginx 还会启动 cache loader 进程 和 cache manager 进程,这些进程之间使用共享内存作为主要通信方式。
cache manager 进程长驻后台,并负责维护缓存数据的有效性,清理过期缓存数 据;
try_files 配置项和配置文件变量 (variables) 是 Nginx 独有的特性;
The try_files directive was initially meant to gradually replace
conditional ``if`` configuration statements in a more proper way, and it
was designed to quickly and efficiently try/match against different
URI-to-content mappings...adopt its use whenever applicable.
Variables in Nginx were developed to provide an additional
even-more-powerful mechanism to control run-time configuration of a web
server. Variables are optimized for quick evaluation and are internally
pre-compiled to indices. Evaluation is done on command; i.e., the value
of a variable is typically calculated only once and cached for the
lifetime of a particular request. Variables can be used with different
configuration directives, providing additional flexibility for describing
conditional request process behavior.
Nginx 是一个模块化/插件化的程序,1.9.11 版本之前,Nginx 只支持静态链接模块 代码,从 1.9.11 版本之后,才引入动态加载模块的功能。
Nginx 代码由一个核心模块(core module)和很多功能模块组成(funcional module), 核心模块实现了 Web 服务器的基础、Web 和 Mail 反向代理功能等,为 Nginx 提供了 底层网络协议支持,构造了必要的运行环境,并保证其它不同功能模块的无封结合。而 其它模块则负责提供应用协议相关的特性。
在核心模块和功能模块之间,还有像 http, mail 这样的中层模块(upper-level module),它们为功能模块提供了核心模块和其它底层功能的抽像。这些模块也根据应用 层协议决定「事件」的处理顺序,同时,它们和核心模块结合,维护其它相关功能模块的 调用顺序。
功能模块分为: event module, phase handler, output filter, variable handler, protocol, upstream, 和 balancer 。
Nginx 为模块提供了很多不同的调用时机,模块初始化时可以为不同的时机注册不同的 回调函数。时机到来时,Nginx 就会调用相应的回调函数。Nginx 提供的调用时机有:
* Before the configuration file is read and processed.
* For each configuration directive for the the location and the server
where it appears
* When the main configuration is initialized.
* When the server is initialized.
* When the server configuration is merged with the main configuration.
* When the location configuration is initialized or merged with its parent
server configuration.
* When the master process starts or exits.
* When a new worker process starts or exits.
* When handling a request.
* When filtering the response header and the body.
* When picking, initiating and reinitiating a request to an upstream
server.
* When process the response from and upstream server.
* When finishing an interation with an upstream server.
1. Client sends HTTP request.
2. Nginx core choose the appropriate phase handler based on the configured
location matching the request.
3. If configured to do so, a load balancer picks an upstream server for
proxying.
4. Phase handler does its job and passes each outout buffer to the first filter.
5. First filter passes the outout to the second filter (and so on).
6. Final response is send to the client.
1. server rewrite phase
2. location phase
3. location rewrite phase (which can bring the request back to the previous phase)
4. access control phase
5. try_files phase
6. content phase (content handler first, then content phase handler)
7. log phase
header filter 主要工作步骤如下:
1. Decide whether to operate on this response.
2. Operate on the response
3. Call the next filter.
body filter 用于操作响应包体。比如,使用 gzip 压缩响应数据, 使用 chunked 编码响应数据等;
两个特殊的 filter : copy filter 负责填充用于存放响应数据的内存 缓冲区(比如,使用临时文件的数据); postpone filter 负责子请求 (subrequest)相关的处理。
子请求 (subrequest)是 Nginx 的强大特性之一,它是请求处理逻辑中非常重 要的一环。通过子请求机制,Nginx 可以:
将不同 URL 的请求响应数据返回给当前请求(即主请求);
将多个子请求的响应数据合并,返回给当前请求;
将子请求再分裂为更多的子请求(sub-subrequest),子子请求( sub-sub-subrequest),形成子请求的层次关系;
使用子请求读取磁盘文件、访问 Upstream 服务器等;
在 filter 中,根据当前请求的响应数据触发子请求,例如:
For example, the SSI module uses a filter to parse the contents of the
returned document, and then replaces ``include`` directives with the
contents of specified URLs. Or, it can be an example of making a filter
that treats the entire contents of a document as a URL to be retrieved,
and then appends the new document to the URL itself.
从上面的描述可以看出,Nginx 各模块结合紧密,模块逻辑复杂,使用 C 开发模块难度 较高,开发效率较低。幸运的是, lua-nginx-module, stream-lua-nginx-module 等第三方模块和官方 ngx-http-js-module 模块出现,降低的模块开发的门槛。开发 者借助它们,甚至可以开发运行于 Nginx 内部的业务应用了。
Lession learned:
There is always room for improvement.
It is worth avoiding the dilution of development efforts on something that
is neither the developer's core competence or the target application.