Nginx 是如何设计的

本节内容译自 Nginx 官方博客的一篇文章 《Inside NGINX: How We Designed for Performance & Scale》。

Nginx 之所以在性能方面表现优异,完全得益于其优良的设计。虽然有许多网页服务器和应用服务器同样使用单一线程或进程的架构,Nginx 仍然能异军突起,它精细的事件驱动的架构,使其能够在当代硬件中,轻易地扩展为十万级高并发连接。

Nginx 进程模型

image-center

要想更好地理解它的设计,需要理解 Nginx 的工作原理。Nginx 有一个主进程,它会进行一些特权级的操作,如读取配置文件、绑定端口;还有一些工人进程和辅助进程。

># service nginx restart
* Restarting nginx
# ps -ef --forest | grep nginx
root     32475     1  0 13:36 ?        00:00:00 nginx: master process /usr/sbin/nginx
                                                -c /etc/nginx/nginx.conf
nginx    32476 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32477 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32479 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32480 32475  0 13:36 ?        00:00:00  _ nginx: worker process
nginx    32481 32475  0 13:36 ?        00:00:00  _ nginx: cache manager process
nginx    32482 32475  0 13:36 ?        00:00:00  _ nginx: cache loader process

在这个四核心的服务器上,Nginx 主进程创建了四个工人进程,以及几个缓存辅助进程,用来管理磁盘中的内容缓存。

架构为什么重要?

任何 Unix 程序的基础都是线程或进程。从 Linux 操作系统的角度来看,线程和进程基本是一回事,主要区别在于是否共享内存。

线程或进程是独立自足的一套指令,操作系统可以调度其在 CPU 核心上运行。大多数复杂的应用都会同时运行多个线程或进程,主要原因为:

  • 可以同时使用更多的计算核心
  • 更易于实现并行操作

进程和线程会消耗资源,它们会占用内存和其它操作系统的资源,而且需要交付、脱离核心,该操作称为上下文切换,即 context switch。现在的服务器大多能同时处理几百个小的、激活的线程或进程,但一旦内存耗尽,或高 I/O 负载带来大量的上下文切换时,性能会急剧下降。

上下文切换:指 CPU 从一个进程或线程切换到另一个进程或线程。需要先保存当前进程的状态并恢复另一个进程的状态。
切换时,CPU 会停止处理当前运行的程序,保存当前程序运行的具体位置,以便之后继续运行。当前运行的任务转为就绪、挂起或删除状态,任务的运行环境被保存;另一个任务成为当前任务,其运行环境被恢复。进程上下文用进程的 PCB 来表示,它包括进程状态、CPU 寄存器的值、堆栈中的内容等。
在三种情况下可能会发生上下文切换:中断处理、多任务处理、用户太切换。
上下文切换通常是计算密集型的,需要相当可观的 CPU 时间,对系统来说意味着消耗大量的 CPU 时间。因此它对性能会造成负面影响。

在设计网络应用时,通常的做法是为每个连接分配一个线程或进程,这种架构比较简单,而且容易部署,但当应用需要应付几千个并发连接时,没有足够的可扩展性。

Nginx 是如何工作的

Nginx 使用一个可预知的进程模型,该模型已经为现有的硬件资源进行了优化。

  • 主进程 :进行特权级操作,如读取配置、绑定端口,然后创建少量的子进程。
  • 缓存加载子进程 :在启动时运行,把磁盘中的缓存加载到内存中,然后退出。它是用传统的计划任务实现的,因此资源需求很少。
  • 缓存管理子进程 :周期性运行,从磁盘缓存中清除过期条目,让缓存保持在配置的大小。
  • 工人子进程 :执行 Nginx 的全部工作。它们处理网络连接,从磁盘读取内容,向磁盘写入内容,与后端服务器通信。

在大多数情况下推荐的配置为:每一个 CPU 核心运行一个工人子进程,这样可以最充分利用硬件资源。通过如下配置来实现:

worker_processes auto;

在 Nginx 服务器运行期间,只有工人进程是忙碌的。每个工人进程都会以非阻塞方式处理多个连接,从而减少上下文切换的次数。

每个工人进程都是单线程的,均独立运行,抓取新的连接并进行处理。进行间可以用共享内存来通信,如共享缓存数据、会话数据及其它共享资源。

Nginx 工人进程是如何运转的

image-center

每个 Nginx 工人进程都是用 Nginx 的配置初始化的,并且会由主进程提供一组侦听套接字。

工人进程启动以后,就开始在侦听套接字上等待事件的发生。事件是由新的传入连接引起的,这些连接被分配给一个状态机,state machine,通常是 HTTP 状态机,除此之外 Nginx 还会为 TCP 数据流以及邮件协议部署状态机。

image-center

状态机也是一组指令,用于告诉 Nginx 如何处理一个请求。多数的网页服务器为了提供同样的功能,都会使用类似的状态机,只是部署方式有所不同。

状态机的调度

状态机很像象棋的规则,每个 HTTP 事务就好比一场象棋比赛。一端是网页服务器,这里有一位大师可以快速地做出决策;另一端是远程客户端,即在一个相对慢速的网络另一端的,访问网站的浏览器或应用。

然而游戏的规则可以非常复杂。例如,网页服务器可能需要与后端应用进行通信,或与授权的服务器进行通信,网页服务器第三方的模块甚至可以扩展游戏规则本身。

处于阻塞状态的状态机

还记得我们说过,线程或进程是独立自足的一套指令,操作系统可以调度其在 CPU 核心上运行。大部分的网页服务器和网页应用会使用每连接一个进程或每连接一个线程的模式,在服务器运行进程期间,该进程大部分时间处于阻塞状态,即等待客户端的下一个动作。

image-center

  1. 网页服务器进程在套接字上侦听新的连接。
  2. 有新的传入连接时,进行开始工作,在每个操作之后进入阻塞状态,等待客户端的响应。
  3. 所有工作完成后,网页服务器进程可能会等待并观察,客户端是否想要开始新的连接(keepalive),如果连接被关闭(客户端消失或发生超时),网页服务器进程则返回,继续侦听新的连接。

非常重要的一点:每个激活的 HTTP 连接都需要一个专用的进程或线程来处理,这种架构比较简单,可以比较容易地通过第三方模块进行扩展。

但是,极其不平衡的一点是:相对轻量级的 HTTP 连接 (由文件描述符和少量内存表示) 映射到一个单独的线程或进程上面,而它们却是一个非常重量级的操作系统对象。虽然方便了编程,却带来极大的资源浪费。

Nginx 是真正的大师

image-center

Nginx 的工人进程就像图中的象棋大师一样,可以同时处理多个棋局。每个工人进程同时可以处理十万级的连接。

image-center

  1. 工人进程在侦听套接字和连接套接字上等待事件
  2. 套接字上发生事件,工人进程着手处理:
    • 侦听套接字上的事件:表示客户端发起了新的连接,工人进程会创建一个新的连接套接字
    • 连接套接字上的事件:表示客户端产生了新的操作,工人进程进行响应

工人进程绝对不会在网络流量上进行阻塞,然后等待客户端的响应。在工人进程进行完当前操作以后,工人会立即着手处理下一个等待处理的连接,或处理新传入的连接。

为什么会比阻塞的多进程架构要快?

Nginx 的可扩展性非常强,每个工人进程可以支持十万级并发连接。每个新连接都会在工人进程中创建一个文件描述符,并额外占用其一小部分内存,每个连接的开销非常小。Nginx 的进程可以持续绑定在 CPU 上,不会经常进行上下文切换,只有在无事可做时才会发生。

在每个进程只负责一个连接的方法中,如果发生了阻塞,每个连接都需要大量的额外资源及开销,而且上下文切换会非常频繁。

在经过适当的配置之后,Nginx 可以吸收流量峰值,不会错过一个连接。

更新配置与升级 Nginx

Nginx 的进程架构使用很少量的工人进程,无论是进行配置的更新还是自身的升级,都是非常高效的。

更新配置

image-center

更新 Nginx 的配置是一个非常简单、轻量、可靠的操作。通常只需要运行 nginx -s reload 命令即可实现,该命令会检查磁盘中的配置,然后给主进程发送一个 SIGHUP 信号。

当主进程收到 SIGHUP 信号时:

  1. 它会重新读取配置,并生成一组新的工人进程。这些工人会立即开始接收连接,处理流量。
  2. 向原工人进程发信号,要求优雅退出。这些工人进程会停止接收新连接。一旦当前的 HTTP 请求全部完成,工人进程会彻底关闭连接,不会有保活。一旦所有连接关闭,所有工人进程即退出。

该重启的过程会在 CPU 和内存的使用上引起一个小的峰值,但与激活一个连接相比基本上是很细微的。你可以在一秒钟内进行多欠重新读取配置的操作,事实上确实有许多 Nginx 用户是这样做的。当同时存在很多代 Nginx 的工人进程时,也很少出问题,即使出了也可以快速解决。

Nginx 升级

image-center

Nginx 的升级是可以在线实时完成的,不需要断开任何连接,也不需要状态机或中断服务。

二进制文件的升级过程与更新配置类似。一个新的 Nginx 主进程会与原主进程并行,它们会共享侦听套接字。这两个进程都是激活的,它们各自的工人进程处理各自的流量。然后可以向原主进程发送信号,令其工人优雅退出。

与 Apache 的区别

Apache 1995 年就开始被广泛使用,支撑了最多的网站,其次就是微软的 IIS。

开源的 Apache 这么多年来如此广泛地应用,拥有如此多的用户,因此产生了很多的模块来扩展其功能,这些模块大多也是开源的。

但 Apache 在高负载下会变得缓慢,因为它总是要产生新的进程,会消耗更多的内存。它创建的新的线程还会与其它线程争夺内存与 CPU 资源。当流量达到进程限制时,Apache 会拒绝新的连接。

Nginx 是一个开源的网页服务器,它的开发是为了解决 Apache 性能可扩展性 的问题。Nginx 是开源、免费的,但同样提供收费的版本 Nginx Plus。

Nginx 是基于事件的,因此其架构为事件驱动,而且是异步的。而 Apache 依赖于进程和线程。

Apache 的工作原理及其局限性

Apache 通过 创建进程和线程 来管理额外的连接,管理员通过配置服务器来控制最大进程数。

最大进程的数量取决于主机可用的内存数量,太多的进程会耗尽内存,最终会促进主机把内存中的数据交换到磁盘,对性能的损害非常大。不仅如此,一旦达到进程上限,Apache 会开始拒绝新的连接。

Apache 有两种运行模式,即 pre-forked 和工人模式(多进程,MPM)运行,无论哪种方式,针对传入的新连接,它都会创建新的进程。

这两种模式的区别:

  • pre-forked 模式为每个进程创建 一个线程,每个 进程 处理一个请求。
  • 工人模式也会产生新进程,但每个进程有 多个线程,每个 线程 处理一个请求。

因此,一个工人模式的进程可以处理多个连接,而一个 pre-forked 模式的进程只能处理一个连接。

工人模式使用的内存更少,因为进程比线程要消耗更多的内存。

在优化 Apache 过程中,受限的因素主要是内存,以及潜在的、争夺相同 CPU 和内存的死锁线程。如果一个线程被暂停了,用户就只能一直等待,直到其它进程把资源让出来,网页才会显示出来。如果线程被死锁,它就无从知晓如何重启了,于是会一直卡在那里。

Nginx

Nginx 使用是完全不同的架构,更适合非线性扩展,无论是针对并发连接数还是每秒请求数。

它不会为每个新连接创建新进程。即使在负载增加时,内存和 CPU 的使用也始终可控。在通常的硬件配置下,Nginx 在一台服务器上可以同时处理上万个并发连接。

Nginx 的主要优势在于 以高性能处理高并发,以及其工作的 高效。它通过模块提供多个功能,可以很便捷地处理并发、延迟、SSL、静态内容、压缩、缓存、连接、请求控制、流媒体等,还可以直接与 memcached 或 Redis 整合,在面对大量并发用户时,实现性能的大幅提升。

Nginx 与 Apache 的工作方式的区别,主要体现在处理线程的方式上。

Nginx 不会为每个网页请求创建新的进程,而是通过配置来指定其主进程允许创建多少个工人进程,根据经验,每个 CPU 可以有一个工人进程。这些工人进程都是 单线程 的,每个工人可以处理成千上万的并发连接,它用一个线程通过 异步 的方式来实现,从而无需使用多个线程。

Nginx 还会用缓存加载程序和缓存管理器进程从磁盘中读取数据,并将其加载到缓存中,并在用过之后将其从缓存中移除。

Nginx 由一些模块组成,它们是在编译时加入的。因此,用户可以下载源码,根据需要来选择编译哪些模块。有些模块用于连接后端的应用服务器、负载均衡、代理服务器等,没有给 PHP 使用的模块,因为 Nginx 自己就可以编译 PHP 代码。

Nginx 与 Apache 2.4 MPM 的比较

Apache 2.4 包含了 MPM 事件模块,用来异步处理一些连接类型,但处理的方式与 Nginx 不同。其目的是为了在负载增加时,减小对内破损需求。

Apache 2.4 除了工人模式与 pre-forked 模式之外,还加增加了一个 mpm_event_module,即 MPM 事件模块,用来解决 “线程必须持续等待用户连接以期生成的请求” 的问题。MPM 专门使用一个线程来处理侦听状态及保活状态的套接字。通过减少创建线程和进程的数量,解决了一些旧版本中的内存问题。

Apache MPM 事件模块与 Nginx 是不同的,因为它对于新的请求,仍然需要创建新进程。而 Nginx 不会针对一个用户连接创建多个进程的。Apache 2.4 所带来的改进是:Apache 所创建的进程会比普通的工人模式生成更少的线程,因为一个线程已经可以处理不止一个连接了。

Nginx 和 Apache 同时使用

Apache 的功能丰富,而 Nginx 速度更快。这就意味 Nginx 可以更快地提供静态内容,但 Apache 包含多个模块,用来与后端应用服务器协同工作,以及用来运行脚本语言。

Apache 和 Nginx 都可以用作代理服务器,但通常把 Nginx 用作代理服务器,把 Apache 用作后端服务器。Nginx 包含高级的负载均衡和缓存功能。可以部署多个 Apache 服务器,然后用负载均衡器来管理。

php-fpm:PHP FastCGI Process Manager

另一种配置是部署 Nginx 时,使用单独的 php-fpm 应用,之所以把 php-fpm 称为应用,是因为它不是 .dll 或 .so,它不会像 Apache 的模块一样在执行时被加载。php-fpm 可以与 Nginx 配合使用,使 Nginx 可以解析 php 脚本,这些非静态内容。

更适合 Apache 的场景

Apache 天生支持 PHP、Python、Perl 等语言。例如,mod_python 和 mod_php 模块用来处理 PHP 和 Perl 代码,mod_python 在使用 FastCGI 时更高效,因为 FastCGI 无需为每个请求加载 Python 解释器。mod_rails 和 mod_rack 也是如此,它们用来在 Rails 中运行 Ruby,它们在 Apache 进程中运行会更快。

因此,如果网站内容绝大多数是 Python 或 Ruby,则更适合使用 Apache 来做应用服务器,因为 Apache 无需使用 CGI。如果对于 PHP 来说就无所谓了,因为 Nginx 也天生支持 PHP。

Nginx 的架构

本节内容译自 The Architecture of Open Source Applications (Volume 2): nginx, by Andrew Alexeev

为了处理并发连接,传统的基于进程或线程的模型,会用一个单独的进程或线程来应对,而且会在网络或 I/O 操作上进行阻塞,对内存和 CPU 的消耗会导致效率低下。生成一个独立的进程或线程需要准备新的运行时环境,包括分配内存中的堆和栈,还包括创建一个新的执行上下文。这些都需要花费额外的 CPU 时间,结果会导致很差的性能,因为过于频繁的上下文切换会引起线程的抖动。所有这些都体现在 Apache 这样的旧的网页服务器体系结构中。这是一个权衡过程,要么通用,要么优化资源的使用。

从其诞生开始,Nginx 的使命就是获得高性能、高密度、省资源、高弹性,因此它使用的是完全不同的模型。实际上,它是从一些操作系统中开发中的基于事件的机制而激发的灵感。结果就是获得了一个模块化的、事件驱动的、异步的、单线程的、非阻塞的架构,这成为 Nginx 代码的基石。

Nginx 大量使用多路传输和事件通知,并把一些特定的任务分配给单独的进程来处理。所有的连接都在一个高效的 运行环 (run-loop)中进行处理,每个工人进程可以处理成千上万的并发连接和请求。

代码结构

Nginx 工人进程的代码包含 核心模块功能模块

核心模块负责维护一个严密的运行环,并在处理请求的每个阶段负责执行对应的模块代码。这些模块实现了大部分表示层和应用层的功能。它们从网络和存储中读取、向其写入、转换内容、进行出站过滤、应用服务端的 include 操作、把请求传递给后端服务器。

Nginx 模块化的架构使得开发人员无需修改其内核,就可以扩展其功能。Nginx 的模块有多种不同的形式,分别为核心模块、事件模块、阶段处理器、协议、变量处理器、过滤器、后端、负载均衡器。

在处理一系列与 “接收、处理、管理网络连接及内容检索” 相关的操作时,Nginx 会使用 事件通知机制,以及一些提升磁盘 I/O 性能的操作,如 kqueue、epoll、event ports。其目的在于为操作系统提供尽可能多的线索,以获得对入站和出站流量、磁盘操作、读写套接字、超时等的 及时的异步反馈。对于 Nginx 运行的每个基于 Unix 的操作系统,对多路复用和高级 I/O 操作的不同方法的使用均进行了大量的优化。

Nginx 的架构:

image-center

工人模型

image-center

每个进程都会消耗额外的内存,每次上下文切换都会消耗 CPU 时间,都需要清除 CPU 缓存。

Nginx 不会针对每个连接都生成一个进程,而是由工人进程从一个 共享的侦听套接字 上接受新的请求,并在每个工人中执行一个极其高效的 运行环,来处理成千上万的连接。在 Nginx 中,没有专门的组件用来把连接分配给工人进程,这项工作是由操作系统的内核机制完成的。在 Nginx 启动时,会创建一系列初始的侦听套接字,之后,工人进程在处理 HTTP 请求以及响应时,便可以持续地接收连接、读写套接字。

运行环是工人代码中最复杂的部分,其中包含多种内部调用,并极其依赖异步任务处理。异步操作的实现是通过模块化、事件通知、大量使用回调函数、精密调整的计时器等。总的来说,关键的原则是尽可能地保持非阻塞状态,Nginx 唯一可能发生阻塞的情况,就是可用磁盘存储性能低到无法满足工人进程的时候。

高效使用资源

因为 Nginx 不会针对每个连接生成进程或线程,其内存的使用非常节省,在绝大多数情况下都极其高效。Nginx 的 CPU 使用也非常节省,因为对于进程或线程来说,不存在 “先创建再销毁” 的模式。

Nginx 所做的是检查网络与存储的状态,生成新的连接,将其加入运行环,异步处理直到完成,断开连接,将其从运行环中移除。

即使在极端严重的负载下,Nginx 通常也能维持中度及以下的 CPU 使用率。

因为 Nginx 生成工人进程来处理连接,在面对多核心环境时很容易扩展。通常给一个核心分配一个单独的工人就可以充分发挥多核架构的性能,并可以防止线程抖动和锁死。不会发生资源短缺的情况,资源的控制机制是隔离在每个单线程工人进程中的。该模型同样也容易扩展到使用多个物理存储设备,提供更大存储带宽,避免在磁盘 I/O 上发生阻塞。因此,把负载平摊在几个工人身上,可以非常有效地利用服务器资源。

调整工人数量

在某些情况下,使用磁盘和 CPU 的模式有所不同,需要调整工人进程的数量:

  • CPU 密集型负载:如处理大量 TCP/IP、SSL、压缩等,工人数量应与 CPU 核心数量相同
  • 磁盘 I/O 密集型:如在磁盘上提供多种不同的内容,或严重使用代理,工人数量为 CPU 核心数量的 1.5 ~ 2 倍

如何避免磁盘 I/O 发生阻塞

阻塞的概念:

image-center

图中的销售人员只有一个,当遇到商店中没有货品时,他要亲自去很远的库房取货,期间整个队伍都要干等着。

image-center

很长时间以来,在存储性能无法满足工人的磁盘操作时,工人仍然会在读取时产生阻塞。曾经的解决办法是组合使用 sendfile 系统调用和 AIO。

针对该问题,从 Nginx 1.7.11 版本起引入了线程池的概念,即 thread pool。

image-center

销售人员为需要去库房取货的顾客单独开辟了一个通道,遇到这种情况时,直接请客人上专门前往库房的车,他转身继续为后面的人服务。

线程池充当了快递服务的角色,它包含一个任务队列以及一些专门的线程,专门来处理这个任务列队。当工人进程需要进行一个耗时的操作时,它会把任务扔到线程池中,在那里,该任务会由任何空闲的线程来处理。

image-center

是的,这里单独为阻塞的任务创建了一个队列,但这个队列可用的资源是受到严格控制的。虽然我们受磁盘的物理限制,无法飞速完成磁盘 I/O,但起码不会影响后面的其它事件的处理。

Nginx 进程的角色

Nginx 在内存中运行多个进程。有一个主进程和几个工人子进程,以及几个专用的辅助进程,如缓存加载器及缓存管理器。在 Nginx 1.x 中,所有这些进程都是单线程的。所有这些进程都使用共享内存机制进行进行间通信。主进程是以 root 身份运行的,而缓存加载器、缓存管理器和工人进程则是以非特权用户运行的。

主进程的主要职责:

  • 读取、验证配置文件
  • 创建、绑定、关闭套接字
  • 启动、终止、维护工人进程
  • 更新配置,无需中断服务
  • 升级自身二进制文件,无需中断服务
  • 重新打开日志文件
  • 编译嵌入的脚本

工人进程接收并处理从客户端发来的连接,提供反向代理和过滤功能,几乎所有 Nginx 的功能都是它实现的。因此,如果要监视 Nginx 实例的行为,应该留意工人进程,它们反映了真实的日常操作。

缓存加载器进程只在 Nginx 启动时运行一次,它负责检查磁盘中的缓存条目,并用缓存条目的 元数据 填充内存中的 Nginx 数据库,即缓存。

缓存管理器主要负责缓存超时及验证,在 Nginx 运行期间,它驻留在内存中,一旦出错,主进程会将其重新启动。

Nginx 的缓存

在 Nginx 中,缓存是以文件系统中的分层数据存储的形式实现的。可以配置缓存键,可以使用不同的请求特定参数来控制进入缓存的内容。缓存键和缓存元数据存储在共享内存段中,缓存加载器、缓存管理器和工人进程均可以访问。

缓存键:cache key。它是 Nginx 为每个可缓存的资源分配的唯一标识。该键的值默认是由请求的基本参数组成的,如协议、主机名、URI、其它字符参数。根据需要可以扩展缓存键的内容,加入与 cookie 或认证相关的信息。简单的缓存键:httplocalhost:8002/time.php

缓存中不会直接保存任何文件,这一点不同于操作系统的虚拟文件系统机制的优化方法。

每个缓存下来的响应都会被保存为文件系统中不同的文件,缓存的这种文件系统层次结构是由 Nginx 的配置指令控制的。当一个响应被写入缓存目录结构时,会从缓存键的 MD5 校验值中提取字符用作路径名和文件名。

把内容放到缓存的基本步骤:

当 Nginx 从后端服务器读取响应时,该内容先是被写入一个 临时文件,该临时文件位于缓存目录结构外部。当 Nginx 完成对该请求的处理时,它会 重命名 该临时文件,将其 移动 到缓存目录。

如果保存该临时文件的目录位于其它文件系统中,则需要把文件复制过来。因此,建议把临时文件目录与缓存目录保存在同一个文件系统中,以提升性能。

当缓存目录中的文件需要被显式清除时,可以很安全地删掉。

控制 nginx 运行时进程

主进程与工人进程

nginx 使用一个主进程与一或多个工人进程。如果启用了缓存,在启动服务时还会运行缓存启动器进程和缓存管理器进程。

工人进程的任务则是处理请求。nginx 靠的是依赖于操作系统的机制,把请求高效地分配给工人进程。工人进程的数量由配置文件定义,可以是固定的,也可以根据可用 CPU 核心的数量来自动调整。

nginx 的控制

用信号控制进程

nginx 可以由信号来控制,主进程的 PID 默认被写入 /run/nginx.pid 文件。

主进程支持以下信号:

GERM,INT :快速关闭

QUIT :优雅关闭

HUP :修改配置,修改了时区,用新配置开启新工人进程,优雅关闭旧工人进程

USR1 :重新打开日志文件

USR2 :升级可执行文件

WINCH :优雅关闭工人进程

工人进程支持的信号:

单独的工人进程也可以由信号来控制,虽然并不需要这么做。

TERM,INT :快速关闭

QUIT :优雅关闭

USR1 :重新打开日志文件

WINCH :异常终止,用于调试,需启用 debug_points

重载配置

要想重载配置,可以停止或重启 nginx ,或向主进程发送信号。可以用 nginx -s 命令来发送信号:

nginx -s <SIGNAL>

<SIGNAL> 可以是以下信号:

  • stop :快速关闭
  • quit :优雅关闭
  • reload :重新加载配置文件
  • reopen :重新打开日志文件
nginx -s reload
kill 发信号

还可以用 kill 命令直接给主进程发信号,这种情况下会直接把信号发给指定 PID 的进程。

ps -ax | grep nginx
  3129 ?        Ss     0:00 nginx: master process /usr/sbin/nginx

kill -s QUIT 3129

修改配置

为了让 nginx 重新读取配置文件,可以向主进程发送 HUP 信号。

主进程首先会检查语法是否正确,然后会尝试应用新的配置,即,打开日志文件及新的侦听套接字。

如果失败,回滚本次所做的修改,继续用旧配置工作。

如果成功,开启新的工人进程,并向旧工人进程发送消息,要求它们优雅关闭。旧工人进程关闭侦听套接字,继续处理之前的请求,处理完毕以后,旧工人进程关闭。

滚动日志文件

滚动日志文件之前,需要先重命名旧日志文件。

之后,应该向主进程发送 USR1 信号。主进程会把当前打开的日志文件重新打开,并为其分配一个普通权限的用户,以该用户身份来运行工人进程。

在成功地重新打开之后,主进程关闭所有打开的文件,向工人进程发送消息,要求它们重新打开文件。

工人进程同样地马上打开新文件,关闭旧文件。

此时,旧文件几乎可以立即用于后期处理,如压缩。

在线升级可执行文件

升级可执行文件时,要先用新的可执行文件把旧的替换掉。

之后,向主进程发送 USR2 信号。

主进程先是重命名其 PID 文件,在原文件名基础上添加后缀 .oldbin,如 nginx.pid.oldbin;然后再运行新的可执行文件,随之启动新的工人进程。

之后,所有的新旧工人进程继续接受请求。如果向第一个主进程发送了 WINCH 信号,它会向其工人进程发送消息,要求它们优雅关闭,它们会开始退出。

过了一段时间,只有新的工人来处理请求。

要注意的一点是,旧的主进程不会关闭其侦听套接字,需要时可以再次启动其工人进程。

如果新的可执行文件由于某种原因工作不正常,可以选择以下操作之一:

  • 向旧主进程发送 HUP 信号。旧主进程会启动新的工人进程,不会重新读取配置。然后,向新主进程发送 QUIT 信号,其工人进程可被优雅关闭。
  • 向新主进程发送 TERM 信号。它会发消息给其工人进程,要求它们立即退出,于是这些工人进程几乎会立即退出。如果没能退出,可以向它们发送 KILL 信号来强制退出。新主进程退出之后,旧主进程会自动启动新工人进程。

如果新的主进程退出,则旧主进程会丢弃 .oldbin 后缀,恢复 PID 文件名。

如果升级成功,应该向旧主进程发送 QUIT 信号,最后只留下新进程。

Nginx 的配置

Nginx 的配置系统是受 Apache 的启发而设计的,默认配置文件名为 /etc/nginx/nginx.conf。通常将该配置文件做为主配置文件,当需要扩展时,使用额外单独的文件。

为了让配置更易于维护,建议把配置文件分割成一组特定功能的文件,保存在/etc/nginx/conf.d 目录中,然后在主配置文件中用 include 指令来引用。

在 Nginx 启动时,主进程会读取这些配置文件,它还会编译出一个只读的版本,用来给工人进程读取。

nginx 由模块组成,这些模块由配置文件中的各个指令来控制。这些指令被分成简单指令和块指令。

简单指令由名称和参数组成,用空格分隔,以分号 ; 结尾。

块指令有相同的结构,但它是由一组由大括号包围的指令结束。块指令可以嵌套,此时它又称为 context。

context

主配置文件的结构是树状的,由多组 { } 组成,称为 context,为了方便,以下就称为段落。它把应用到不同流量类型的指令集中到一起。这些段落形成了一个有组织的结构,其中包含一些 条件 逻辑,用于判断是否需要应用其中的配置指令。

main
├── event
├── http
│   └── server
│       └── location
│       └── location
└── mail
    └── server

因为段落可以互相嵌套,范围最大的段落中的指令会 自动继承 到内部的子段落。同时,子段落中的指令会 覆盖 其上级段落中的同一个指令。

范例:

http {
    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include /etc/nginx/conf.d/*.conf;

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}

核心段落

为了创建整体结构,有几个是最核心的段落:

events :一般连接的处理

http :HTTP 流量

mail :邮件流量

stream :TCP 和 UDP 流量

main

最通用的段落叫 mainglobal,只有这部分的内容不需要用大括号 { } 引用。换句话说,只要不在所有其它段落中的指令都属于 main,即主配置,或全局配置。

main 所配置的是最大的环境,用来配置最基本的、影响整个应用全局的参数。

main 中配置的通常为:

  • 运行工人进程的用户与组
  • 工人进程的数量
  • 用来保存主进程 PID 的文件
  • 工人进程的 nice 值
  • 错误日志

events

events 段落位于 main 段落中,废话,所有其它段落都位于 main 中。它用来指定 Nginx 如何处理连接,配置文件中 只能有一个 events 段落。

# main context
events {
    # events context
    . . .
}

Nginx 使用的是事件驱动的连接进程模型,因此这些指令用来决定工人进程应当如何处理连接。比如,选择用来处理连接的技术,或指定具体如何实施。

具体选择哪种方法来处理连接,通常是基于当前平台拥有的最高效的选择而自动决定的。对于 Linux 来说,epoll 通常是最佳选择。

其它的可配置项目如:

  • 每个工人进程可处理的连接数
  • 在收到连接被挂起通知时,工人进程应该只处理一个连接,还是处理所有挂起的连接
  • 工人进程是否需要依次来对事件做出响应

http

Nginx 做为网页服务器或反向代理时,需要使用 http 段落来配置。该段落用来指定如何处理 HTTP 或 HTTPS 连接。

httpevents 是兄弟段落,因此只能并列,不能嵌套,它们都在 main 段落中。

# main context
events {
    # events context
    . . .
}

http {
    # http context
    . . .
}

http 中可配置的内容:

  • server :子段落,用来定义虚拟服务器
  • 日志:access_logerror_log
  • 文件操作的异步 I/O:aiosendfiledirectio
  • 发生错误时显示的错误页面:error_page
  • 压缩:gzipgzip_disable
  • TCP 保活:keepalive_dialbekeepalive_requestskeepalive_timeout
  • 优化数据包与系统调用:sendfiletcp_nodelaytcp_nopush
  • 网站根目录及索引文件:rootindex
  • 用来保存各种类型数据的散列表:*_hash_bucket_size*_hash_max_sizeserver_namestypesvariables

server

server 段落要嵌套在 http 中,可以并列存在多个 server 段落。

在每个用来处理流量的段落中,可以包含一个或多个 server,用来控制对请求的处理。在 server 中可以容纳的指令依流量类型而有所不同。

对于 HTTP 流量来说,每个 server 指令是用来控制那些 “对特定域名或 IP 地址中资源的” 请求的。server 中,用一个或多个 location 来定义如何处理特定的 URI。

对于 mail 和 TCP/UDP 流量来说,每个 server 指令用来控制 “抵达特定 TCP 端口或 UNIX 套接字的” 流量的处理。

# main context

http {
    # http context

    server {
        # first server context
    }

    server {
        # second server context
    }
}

并列的每个 server 用来定义不同的虚拟服务器,来处理不同的客户端请求。

因为可以同时有多个 server 段落,自然就需要使用选择算法来决定为新连接使用哪个 server。每个连接请求只能由一个 server 来处理,因此 Nginx 必须根据请求的细节来决定哪个 server 最适用。

用来进行选择的指令为:

  • listen:该虚拟服务器的 IP 地址与端口号。
  • server_name:该虚拟服务器的名称,将其与请求中的 Host 标头进行比对。可以指定多个名称,用空格分隔;也可以使用通配符 *;还可以使用正则表达式。

该段落中的指令会覆盖 http 段落中相同的指令,例如:

  • 日志
  • 网站根目录
  • 压缩

其它可配置的指令:

  • try_files :按顺序检查指定的文件是否存在,并使用找到的第一个做为响应来处理请求
  • return:停止处理,并向客户端返回指定的代码,如 400、413 等
  • rewrite:重写请求的 URI
  • set:设置变量

location

location 嵌套在 server 段落中,用于处理 特定类型 的请求,可以指定多个 location 段落。在基于请求的 IP 地址/端口、主机名称 选择特定的虚拟服务器以后,location 段落会针对其具体的 URI 来匹配,以决定如何处理该请求。

通过选择算法的匹配来判定哪些请求由哪些 location 来处理,匹配的条件需要在 location 指令同一行指定:

location match_modifier location_match {
    . . .
}

一个 location 可以嵌套其它 location。可以通过这种方式先指定一个通用的 location 段落,来粗略匹配特定的流量,进而再匹配更加精细的流量:

# main context

server {
    # server context

    location /match/criteria {
        # first location context
    }

    location /other/criteria {
        # second location context

        location nested_match {
            # first nested location
        }

        location other_nested {
            # second nested location
        }
    }
}

例如:如果客户端请求 http://www.example.com/bloghttpwww.example.com80 端口 全都会用来判断使用哪个虚拟服务器,在确定了虚拟服务器以后,/blog 即请求的 URI 将被用来匹配 location,以决定使用哪个段落来处理该请求。

location 的的许多指令通常也可用于上一级段落,本层的指令可以:

  • 指定网站根目录以外的目录,使用 alias
  • 把该段落标记为只能内部访问,使用 interanl
  • 代理到其它服务器或 location

其他段落

其他的段落单独在这里说明,因为它们有的依赖于可选的模块,有的只在特定的情况下才使用,有的很少使用。

upstream

该段落用来定义、配置后端服务器,该段落会定义一个命名的服务器池,Nginx 可以把请求代理给池中的各个服务器。

upstream 需要嵌套在 http 段落中,位于所有 server 段落之外。

# main context
http {
    # http context
    upstream upstream_name {
        # upstream context
        server proxy_server1;
        server proxy_server2;
        . . .
    }
    server {
        # server context
    }
}

有了命名的 upstream 段落,就可以在 serverlocation 中用其名字来引用它,把特定类型的请求传递给后端服务器池。然后 upstream 会使用一种算法(通常是 round-robin)来决定使用哪个服务器处理请求。该段落让 Nginx 实现了 负载均衡

mail

除了网页服务器和反向代理以外,Nginx 还可以做为一个高性能的邮件代理服务器。mail 段落位于主段落中。

Nginx 可以把认证请求重定向给外部的认证服务器,然后再访问邮件服务器。如果需要,也可以连接到 SMTP 中继主机。

# main context
events {
    # events context
}

mail {
    # mail context
}

if

该段落用于为某些指令设定条件,如果条件为真则执行这些指令。if 段落是由重写模块提供的,这也是该段落的主要用途。官方博客也曾经声明,最好不要用于其它用途。

出问题的原因主要是在于 Nginx 处理的顺序经常会导致不可预料的结果,会破坏 if 段落的语义。只有 returnrewrite 这两个语句可以安全地用在该段落而不会出错。

  • return ...;
  • rewrite ... last;

另一点要注意的是:如果与 if 段落同级使用, try_files 会失效。

因此,通常仅在判断是否需要重写 URI 以及返回错误代码时才会使用 if 段落,而且通常是在 location 段落中使用:

# main context
http {
    # http context
    server {
        # server context
        location location_match {
            # location context
            if (test_condition) {
                # if context
            }
        }
    }
}

limit_except

该段落用于在 location 段落中限制特定的 HTTP 方法的使用。例如,只有特定的客户端才允许 POST,而其它人只允许读取,就可以使用该段落来定义。

可以定义的方法有:

GET, HEAD, POST, PUT, DELETE, MKCOL, COPY, MOVE, OPTIONS, PROPFIND, PROPPATCH, LOCK, UNLOCK, PATCH

注意:这个指令的根本意义是,只能使用该指令所指定的方法,禁止使用其它的方法。而在段落中可以用 allow 标记出哪些客户端是不受该限制的,用 deny 标记哪些客户端是受此限制的。

. . .
# server or location context
location /restricted-write {
    # location context
    limit_except GET HEAD {
        # limit_except context
        allow 192.168.1.1/24;
        deny all;
    }
}

该范例的结果就是,所有客户端都可以使用 GET 和 HEAD,而来自 192.168.1.1/24 的客户端,不仅可以使用这两个方法,还可以使用其它的方法。

段落的规则

在可用的最高级段落使用指令

有许多指令在多个段落中都有效,例如,有很多指令在 httpserverlocation 段落中都可使用,这就给我们很大的灵活性。

然而,做为一般的规则,最好在最高层级的段落中使用指令,然后根据需要在更低层级的段落中进行覆盖。

在高层级使用指令可以避免兄弟段落间发生不必要的重复:

http {
    server {
        location / {
            root /var/www/html;
            . . .
        }
        location /another {
            root /var/www/html;
            . . .
        }
    }
}

上例中,完全可以把两个 location 中的 root 移动到 server 段落或 http 段落:

http {
    root /var/www/html;
    server {
        location / {
            . . .
        }
        location /another {
            . . .
        }
    }
}

多数时候放在 server 层级是最正确的,但放在更高的层级自然有其优势。节省指令的同时,还便于把默认值级联到所有子段落,可以防止因为忘记给某个子段落加指令而产生错误。尤其是在配置文件很长的时候,更容易发生。

多用兄弟段落,少用逻辑处理

如果想要根据请求中的某些信息,进行不同方式的处理,用户经常会想到 if,希望用它来设定条件,但正如上面所述,它非常不可靠。

首先,if 经常会返回预料之外的结果。

其次,Nginx 已经为我们准备了优化好的、为此特制的指令。Nginx 拥有文档详细的 选择算法,用于诸如选择 serverlocation 段落。因此,应该尽可能地把不同的配置放进各自的段落中,以便使用该算法来进行逻辑选择。

例如:要想把用户请求转换成我们需要的格式,我们没有必要过分依赖 rewrite,完全可以通过配置两个段落来实现。一个段落用来表示我们需要的方法,另一段落用来捕获杂乱的请求,将其重定向到正确的段落。这样得到的结果通常很容易阅读,而且更加有效。正确的请求不会受到额外的处理,不正确的请求会被重定向而不是重写,开销更小。