Linux入门教程:Nginx技术原理介绍,运维每天看什么日志本节对该文中提到的关


Nginx Inc. 联合创始人 Andrew Alexeev 在 AOSA 上发表的文章 [1] ,涵盖 了 Nginx 项目想要解决的问题、程序整体架构、配置文件结构、请求处理流程和开发 Nginx 过程中的心得体会等等话题。本节对该文中提到的关键点进行摘录和整理。

一些信息

发音:( engine x ); 俄罗斯软件工程师 Igor Sysoev 于 2004 年发布的开源 Web 服务器软件; 目前,在开源 Web 服务器按市场占有率排列的名单中,Nginx 位列第二名(Apache HTTPD 第一名)(据 2015年8月10日 w3techs 的统计数字,Nginx 已经跃居榜首); Nginx 专注于高性能、高并发和低内存占用; 除基本的 Web 服务功能外,Nginx 还提供了诸如「负载均衡」、「缓存」、「访问控 制」、「带宽控制」、「HTTP/TCP/UDP 反向代理」等功能,它还能高效得和其它各类 应用结合使用; Nginx 使用 C 语言开发,除了 libc, zlib, PCRE 和 OpenSSL 外, 不依赖其它开发库。 Nginx 是一个「模块化的」、「基于事件驱动的」、「异步的」、「单线程的」、「非 阻赛的」的 Web 服务器; Nginx 使用 C 语言风格的配置文件;

技术原理

事件驱动 vs. 多进程/线程

1995 年首次发布的 Apache HTTPD,在 2012 年 2.4 稳定版之前,在 Linux 平台上只 能选择 mpm-prefork [6] 或者 mpm-worker [7] 作为并发模式。在这两种工作模式里,Apache 会为每个 HTTP 请求分配独立的进程或 者线程,请求处理完成后,进程或线程才会被回放释放。2.4 版增加了 mpm-event 模式 [8],虽然使用了 epoll, kqueue 等事件驱动机制, 但是请求处理依然由线程完成。 在上世纪90年代,由于操作系统、计算机硬件和当时互联网状态的限制,Apache 选择 上述的并发模型是合理的,并且它也提供了相当丰富的功能和特性。但是,这些特点的 代价是,在每个连接会使用更多的内存资源,不能很好地支持网站非线性伸缩 (nonlinear scalability)。 创建进程和线程(新的堆、栈和执行上下文)内存和 CPU 等资源消耗大; 过多的上下文切换(context switching)会造成 thread thrashing ; 随着硬件、操作系统和网络带宽的发展,它们不再是制约网站规模增长的瓶径: C10K Problem [2] Why threads are a bad idea (for most purposes) – 1995 [4] vs. Why events are a bad idea (for high-concurrency servers) – 2003 [3] 各操作系统对事件驱动的支持 Unix-like/Windows – select/poll – 未考证,很早 FreeBSD/NetBSD/OpenBSD/OS X – kqueue – 2000 Windows/AIX/solaris 10 – IOCP – Windows NT 3.5 Linux – epoll – 2.5.44, 2003 年前,2001 年初稿 [5] It’s time for web servers to handle ten thousand clients simultaneously. Lighttpd [10] Nginx [9] libevent [11] libuv [12]

并发模型

以解决 C10K 问题 [2] 为使命的 Nginx,在底层架构上选择了事件驱动机制。事件 驱动机在需要对并发连接和吞吐率提供非线性伸缩 (nonlinear scalability) 的场景中比 多进程/多线程更适合:负载持续上升时,CPU 和内存占用依然在可控的范围内;在普 通硬件配置上,Nginx 就可以服务数以万计的并发连接。

多路复用(multiplexting)、事件通知(event notification); 每个使用单线程的工作进程(worker)通过高效的 runloop 处理数以千记的并发连 接; 工作进程从共享 listening socket ,接收新连接并将其加入到自己的 runloop 中。新连接的分发由操作系统决定(thundering herd 问题 [13]); 对工作进程个数选择的一般建议: CPU 密集型业务,比如,启用了 SSL、数据解压缩等场景下,工作进程个数由 CPU 核 数决定; 磁盘 IO 密集型业务,工作进程个数可以设为 CPU 核数的 1.5 倍到 2 倍;

实现细节

Ngnix 会根据不同操作系统的特点进行不同的优化,以提高 IO 性能;

工作进程中的 runloop 是 Nginx 代码中最复杂的部分,它的实现基于异步任务处 理思想。Nginx 通过使用模块化、事件通知、大量的回调函数和仔细调整的计时器等 手段实现异步操作。

基本原则就是尽量使用非阻塞操作;

读写性能不足的硬盘的操作是 Nginx 中唯一可能发生阻塞的地方;

Nginx 谨慎地使用系统调用实现了诸如内存池( pool )、片内存分配器 ( slab memory allocator )等高效的数据结构,再加上选取的并发模型,可以有 效节省对 CPU、内存等系统资源的占用。即使在极高的负载下,Nginx 的 CPU 使用率通 常也是很低的。

在某种程度上,Nginx 借鉴了 Apache 的内存分配机制: 它尽可能在内部通过传递指针的方式地减少内存拷贝; Nginx 为每个连接创建一个内存池,为该连接生命期内的各种操作分配内存。比 如,连接内存池可以用来存储请求包头和包体数据,可以用来创建请求结构体 ( ngx_http_request_t )。Nginx 在关闭连接时,释放连接内存池; Nginx 还会为请求分配请求内存池,为该请求生命期内的各种操作分配内存。比 如,与该请求相关的变量值,解析后的请求包头信息等。Nginx 在请求处理完毕后 ,释放请求内存池;

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 进程,这些进程之间使用共享内存作为主要通信方式。

master 进程的主要工作如下: 读取和验证配置文件; 创建、绑定和并闭 socket; 启动和停止 worker 进程,并按照配置的工作进程个数维护工作进程; 在不中断服务的前提下,重新加载配置文件; 实现 Nginx 二进制热切换; 根据信号重新打开日志文件; 编译内嵌 Perl 脚本; worker 进程负责接收和处理客户端连接、反向代理客户端请求和响应过滤 等等 Nginx 的对外提供其它功能; cache loader 进程负责遍历和检查磁盘缓存数据,并使用获取到的缓存元数据为 Nginx 建立内存数据库,加快运行期间的缓存查找速度; 每个被缓存的响应都作为独立文件存储于文件系统中; 用户可以通过配置文件控制缓存目录的分级结构; 从 upstream 得到的响应数据会先存储到临时文件中,随后将响应数据发送给 客户端后,Nginx 将此临时文件挪到缓存目录下;

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.

请求处理

一个 HTTP 请求的处理流程如下:
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.
Nginx 将 HTTP 请求的处理过程分成了几个阶段,每个阶段都有相应的处理函数( phase handler )。阶段处理函数由配置文件 location {} 中的配置项决定。 HTTP 请求包头接受完毕后,Nginx 为该请求匹配合适的虚拟主机,随后请求会再交由 如下几个阶段的处理函数处理:
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
Nginx 为 HTTP 请求生成响应数据后,将响应数据交给 filter 模块处理。 filter 模块可以分别对响应头和响应体进行处理。据此, filter 模块 可以分为 header filter 和 body filter 两大类;

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.

相关内容