Nginx基础架构
Web服务器设计中的关键约束
1.性能
性能是Nginx的根本,如果性能无法超越Apache,那么它也就没有存在的意义了。这里 所说的性能主体是Web服务器,因此,性能这个概念主要是从网络角度出发的,它包含以下 3个概念。
- 网络性能
这里的网络性能不是针对一个用户而言的,而是针对Nginx服务而言的。网络性能是指 在不同负载下,Web服务在网络通信上的吞吐量。而带宽这个概念,就是指在特定的网络连 接上可以达到的最大吞吐量。因此,网络性能肯定会受制于带宽,当然更多的是受制于Web 服务的软件架构。
在大多数场景下,随着服务器上并发连接数的增加,网络性能都会有所下降。目前,我 们在谈网络性能时,更多的是对应于高并发场景。例如,在几万或者几十万并发连接下,要 求我们的服务器仍然可以保持较高的网络吞吐量,而不是当并发连接数达到一定数量时,服 务器的CPU等资源大都浪费在进程间切换、休眠、等待等其他活动上,导致吞吐量大幅下降。
- 单次请求的延迟性
单次请求的延迟性与上面说的网络性能的差别很明显,这里只是针对一个用户而言的。 对于Web服务器,延迟性就是指服务器初次接收到一个用户请求直至返回响应之间持续的时间。
服务器在低并发和高并发连接数量下,单个请求的平均延迟时间肯定是不同的。Nginx 在设计时更应该考虑的是在高并发下如何保持平均时延性,使其不要上升得太快。 - 网络效率
网络效率很好理解,就是使用网络的效率。例如,使用长连接(keepalive)代替短连接 以减少建立、关闭连接带来的网络交互,使用压缩算法来增加相同吞吐量下的信息携带量, 使用缓存来减少网络交互次数等,它们都可以提高网络效率。
2.可伸缩性
可伸缩性指架构可以通过添加组件来提升服务,或者允许组件之间具有交互功能。一般
可以通过简化组件、降低组件间的耦合度、将服务分散到许多组件等方法来改善可伸缩性。
可伸缩性受到组件间的交互频率,以及组件对一个请求是使用同步还是异步的方式来处理等
条件制约。
3.简单性
简单性通常指组件的简单程度,每个组件越简单,就会越容易理解和实现,也就越容易
被验证(被测试)。一般,我们通过分离关注点原则来设计组件,对于整体架构来说,通常
使用通用性原则,统一组件的接口,这样就减少了架构中的变数。
4.可修改性
可修改性就是在当前架构下对于系统功能做出修改的难易程度,对于Web服务器来说,它还包括动态的可修改性,也就是部署好Web服务器后可以在不停止、不重启服 务的前提下,提供给用户不同的、符合需求的功能。可修改性可以进一步分解为可进化性、 可扩展性、可定制性、可配置性和可重用性。
- 可进化性
可进化性表示我们在修改一个组件时,对其他组件产生负面影响的程度。当然,每个组
件的可进化性都是不同的,越是核心的组件其可进化性可能会越低,也就是说,对这个组件
的功能做出修改时可能同时必须修改其他大量的相关组件。 - 可扩展性
可扩展性表示将一个新的功能添加到系统中的能力(不影响其他功能)。与可进化性一
样,除了静态可扩展性外,还有动态可扩展性(如果已经部署的服务在不停止、不重启情况
下添加新的功能,就称为动态可扩展性)。 - 可定制性
可定制性是指可以临时性地重新规定一个组件或其他架构元素的特性,从而提供一种非
常规服务的能力。如果某一个组件是可定制的,那么是指用户能够扩展该组件的服务,而不
会对其他客户产生影响。支持可定制性的风格一般会提高简单性和可扩展性,因为通常情况
下只会实现最常用的功能,不太常用的功能则交由用户重新定制使用,这样组件的复杂性就
降低了,整个服务也会更容易扩展。 - 可配置性
可配置性是指在Web服务部署后,通过对服务提供的配置文件进行修改,来提供不同的 功能。它与可扩展性、可重用性相关。 - 可重用性
可重用性指的是一个应用中的功能组件在不被修改的情况下,可以在其他应用中重用的
程度。
5.可见性
在Web服务器这个应用场景中,可见性通常是指一些关键组件的运行情况可以被监控的 程度。例如,服务中正在交互的网络连接数、缓存的使用情况等。通过这种监控,可以改善 服务的性能,尤其是可靠性。
6.可移植性
可移植性是指服务可以跨平台运行,这也是当下Nginx被大规模使用的必要条件。
7.可靠性
可靠性可以看做是在服务出现部分故障时,一个架构容易受到系统层面故障影响的程
度。可以通过以下方法提高可靠性:避免单点故障、增加冗余、允许监视,以及用可恢复的
动作来缩小故障的范围。
Nginx的架构设计
1 优秀的模块化设计
高度模块化的设计是Nginx的架构基础。在Nginx中,除了少量的核心代码,其他一切皆 为模块。这种模块化设计同时具有以下几个特点:
- 高度抽象的模块接口
所有的模块都遵循着同样的ngx_module_t接口设计规范,这减少了整个系统中的变数,这种方式带来了良好的简单性、静态可扩展性、可重用性。 - 模块接口非常简单,具有很高的灵活性
- 配置模块的设计
- 核心模块接口的简单化
- 多层次、多类别的模块设计
所有的模块间是分层次、分类别的,官方Nginx共有五大类型的模块:核心模块、配置 模块、事件模块、HTTP模块、mail模块。
2 事件驱动架构
所谓事件驱动架构,简单来说,就是由一些事件发生源来产生事件,由一个或者多个事件收集器来收集、分发事件,然后许多事件处理器会注册自己感兴趣的事件,同时会“消费”这些事件。
对于Nginx这个Web服务器而言,一般会由网卡、磁盘产生事件,事件模块将负责事件的收集、分发操作,而所有的模块都可能是事件消费者,它们首先需要向事 件模块注册感兴趣的事件类型,这样,在有事件产生时,事件模块会把事件分发到相应的模块中进行处理。
Nginx采用完全的事件驱动架构来处理业务,这与传统的Web服务器(如Apache)是不同 的。对于传统Web服务器而言,采用的所谓事件驱动往往局限在TCP连接建立、关闭事件上,一个连接建立以后,在其关闭之前的所有操作都不再是事件驱动,这时会退化成按序执行每个操作的批处理模式,这样每个请求在连接建立后都将始终占用着系统资源,直到连接 关闭才会释放资源。要知道,这段时间可能会非常长,从1毫秒到1分钟都有可能,而且这段 时间内占用着内存、CPU等资源也许并没有意义,整个事件消费进程只是在等待某个条件而 已,造成了服务器资源的极大浪费,影响了系统可以处理的并发连接数。如下图所示,这种传统Web服务器往往把一个进程或线程作为事件消费者,当一个请求产生的事件被该进程 处理时,直到这个请求处理结束时进程资源都将被这一个请求所占用。
Nginx则不然,它不会使用进程或线程来作为事件消费者,所谓的事件消费者只能是某 个模块(在这里没有进程的概念)。只有事件收集、分发器才有资格占用进程资源,它们会 在分发某个事件时调用事件消费模块使用当前占用的进程资源,如下图所示。
下图列出了5个不同的事件,在事件收集、分发者进程的一次处理过程中,这5个事 件按照顺序被收集后,将开始使用当前进程分发事件,从而调用相应的事件消费者模块来处 理事件。当然,这种分发、调用也是有序的。
从上面的内容可以看出传统Web服务器与Nginx间的重要差别:前者是每个事件消费者独 占一个进程资源,后者的事件消费者只是被事件分发者进程短期调用而已。这种设计使得网 络性能、用户感知的请求时延(延时性)都得到了提升,每个用户的请求所产生的事件会及 时响应,整个服务器的网络吞吐量都会由于事件的及时响应而增大。但这也会带来一个重要 的弊端,即每个事件消费者都不能有阻塞行为,否则将会由于长时间占用事件分发者进程而 导致其他事件得不到及时响应。尤其是每个事件消费者不可以让进程转变为休眠状态或等待 状态,如在等待一个信号量条件的满足时会使进程进入休眠状态。
3 请求的多阶段异步处理
多阶段异步处理请求与事件驱动架构是密切相关的,换句话说,请求的多阶段异步处理只能基于事件驱动架构实现。什么意思呢?就是把一个请求的处理过程按照事件的触发方式划分为多个阶段,每个阶段都可以由事件收集、分发器来触发。
异步处理和多阶段是相辅相成的,只有把请求分为多个阶段,才有所谓的异步处理。也 就是说,当一个事件被分发到事件消费者中进行处理时,事件消费者处理完这个事件只相当 于处理完1个请求的某个阶段。什么时候可以处理下一个阶段呢?这只能等待内核的通知, 即当下一次事件出现时,epoll等事件分发器将会获取到通知,再继续调用事件消费者处理请 求。这样,每个阶段中的事件消费者都不清楚本次完整的操作究竟什么时候会完成,只能异 步被动地等待下一次事件的通知。
求的多阶段异步处理优势在哪里?这种设计配合事件驱动架构,将会极大地提高网络 性能,同时使得每个进程都能全力运转,不会或者尽量少地出现进程休眠状况。因为一旦出 现进程休眠,必然减少并发处理事件的数目,一定会降低网络性能,同时会增加请求处理时 间的平均时延!这时,如果网络性能无法满足业务需求将只能增加进程数目,进程数目过多 就会增加操作系统内核的额外操作:进程间切换,可是频繁地进行进程间切换仍会消耗CPU等资源,从而降低网络性能。同时,休眠的进程会使进程占用的内存得不到有效释放,这最终必然导致系统可用内存的下降,从而影响系统能够处理的最大并发连接数。
根据什么原则来划分请求的阶段呢?一般是找到请求处理流程中的阻塞方法(或者造成 阻塞的代码段),在阻塞代码段上按照下面4种方式来划分阶段:
- 将阻塞进程的方法按照相关的触发事件分解为两个阶段
一个本身可能导致进程休眠的方法或系统调用,一般都能够分解为多个更小的方法或者
系统调用,这些调用间可以通过事件触发关联起来。大部分情况下,一个阻塞进程的方法调
用时可以划分为两个阶段:阻塞方法改为非阻塞方法调用,这个调用非阻塞方法并将进程归
还给事件分发器的阶段就是第一阶段;增加新的处理阶段(第二阶段)用于处理非阻塞方法
最终返回的结果,这里的结果返回事件就是第二阶段的触发事件。 - 将阻塞方法调用按照时间分解为多个阶段的方法调用
注意,系统中的事件收集、分发者并非可以处理任何事件。如果按照前一种方式试图划
分某个方法时,那么可能会发现找出的触发事件不能够被事件收集、分发器所处理,这时只
能按照执行时间来拆分这个方法了。 - 在“无所事事”且必须等待系统的响应,从而导致进程空转时,使用定时器划分阶段
有时阻塞的代码段可能是这样的:进行某个无阻塞的系统调用后,必须通过持续的检查
标志位来确定是否继续向下执行,当标志位没有获得满足时就循环地检查下去。这样的代码
段本身没有阻塞方法调用,可实际上是阻塞进程的。这时,应该使用定时器来代替循环检查
标志,这样定时器事件发生时就会先检查标志,如果标志位不满足,就立刻归还进程控制
权,同时继续加入期望的下一个定时器事件。 - 如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法
如果某个方法调用时可能导致进程休眠,或者占用进程时间过长,可是又无法将该方法分解为不阻塞的方法,那么这种情况是与事件驱动架构相违背的。通常是由于这个方法的实现者没有开放非阻塞接口所导致,这时必须通过产生新的进程或者指定某个非事件分发者进程来执行阻塞方法,并在阻塞方法执行完毕时向事件收集、分发者进程发送事件通知继续执行。因此,至少要拆分为两个阶段:阻塞方法执行前阶段、阻塞方法执行后阶段,而阻塞方法的执行要使用单独的进程去调度,并在方法返回后发送事件通知。一旦出现上面这种设计,我们必须审视这样的事件消费者是否足够合理,有没有必要用这种违反事件驱动架构的方式来解决阻塞问题。
请求的多阶段异步处理将会提高网络性能、降低请求的时延,在与事件驱动架构配合工作后,可以使得Web服务器同时处理十万甚至百万级别的并发连接,我们在开发Nginx模块时必须遵循这一原则。
4 管理进程、多工作进程设计
Nginx采用一个master管理进程、多个worker工作进程的设计方式,如下图所示
这种设计带来以下优点:
- 利用多核系统的并发处理能力
操作系统已经支持多核CPU架构,这使得多个进程可以占用不同的CPU核心来工 作。如果只有一个进程在处理请求,则必然会造成CPU资源的浪费!如果多个进程间的地位 不平等,则必然会有某一级同一地位的进程成为瓶颈,因此,Nginx中所有的worker工作进 程都是完全平等的。这提高了网络性能、降低了请求的时延。 - 负载均衡
多个worker工作进程间通过进程间通信来实现负载均衡,也就是说,一个请求到来时更 容易被分配到负载较轻的worker工作进程中处理。这将降低请求的时延,并在一定程度上提 高网络性能。 - 管理进程会负责监控工作进程的状态,并负责管理其行为
管理进程不会占用多少系统资源,它只是用来启动、停止、监控或使用其他行为来控制 工作进程。首先,这提高了系统的可靠性,当工作进程出现问题时,管理进程可以启动新的 工作进程来避免系统性能的下降。其次,管理进程支持Nginx服务运行中的程序升级、配置 项的修改等操作,这种设计使得动态可扩展性、动态定制性、动态可进化性较容易实现。
5 内存池的设计
为了避免出现内存碎片、减少向操作系统申请内存的次数、降低各个模块的开发复杂 度,Nginx设计了简单的内存池。这个内存池没有很复杂的功能:通常它不负责回收内存池 中已经分配出的内存。这种内存池最大的优点在于:把多次向系统申请内存的操作整合成一 次,这大大减少了CPU资源的消耗,同时减少了内存碎片。
因此,通常每一个请求都有一个这种简易的独立内存池,而在请求结束时则会销毁整个内存池,把曾经分配的内存一次性归还给 操作系统。这种设计大大提高了模块开发的简单性,而且因为分配内存次数的减少使得请求执行的时延得到 了降低,同时,通过减少内存碎片,提高了内存的有效利用率和系统可处理的并发连接数, 从而增强了网络性能。
worker进程是如何工作的
master进程如何通知worker进程停止服务或更换日志文件呢?对于这样控制进程运行的 进程间通信方式,Nginx采用的是信号。因此,worker进程中会有一个方法来 处理信号,它就是ngx_signal_handler方法。
对于worker进程的工作方法ngx_worker_process_cycle来说,它会关注以下4个全局标志位。
sig_atomic_t ngx_terminate;
sig_atomic_t ngx_quit;
ngx_uint_t ngx_exiting;
sig_atomic_t ngx_reopen;
其中的ngx_terminate、ngx_quit、ngx_reopen都将由ngx_signal_handler方法根据接收到的信 号来设置。例如,当接收到QUIT信号时,ngx_quit标志位会设为1,这是在告诉worker进程需 要优雅地关闭进程;当接收到TERM信号时,ngx_terminate标志位会设为1,这是在告诉 worker进程需要强制关闭进程;当接收到USR1信号时,ngx_reopen标志位会设为1,这是在 告诉Nginx需要重新打开文件(如切换日志文件时)。
在ngx_worker_process_cycle方法中,通过检查ngx_exiting、ngx_terminate、ngx_quit、 ngx_reopen这4个标志位来决定后续动作。
master进程是如何工作的
master进程不需要处理网络事件,它不负责业务的执行,只会通过管理worker等子进程 来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。它会通过检查以下7个标志位来决定ngx_master_process_cycle方法的运行。
sig_atomic_t ngx_reap;
sig_atomic_t ngx_terminate;
sig_atomic_t ngx_quit;
sig_atomic_t ngx_reconfigure;
sig_atomic_t ngx_reopen;
sig_atomic_t ngx_change_binary;
sig_atomic_t ngx_noaccept;
ngx_signal_handler方法会根据接收到的信号设置ngx_reap、ngx_quit、ngx_terminate、 ngx_reconfigure、ngx_reopen、ngx_change_binary、ngx_noaccept这些标志位。