Nginx基础架构

Web服务器设计中的关键约束

  Nginx是一个功能堪比Apache的Web服务器。然而,在设计时,为了使其能够适应互联网用户的高速增长及其带来的多样化需求,在基本的功能需求之外,还有许多设计约束。
  Nginx作为Web服务器受制于Web传输协议本身的约束,另外,下面将说明的7个关注点也是Nginx架构设计中的关键约束。

1 性能

  性能是Nginx的根本,如果性能无法超越Apache,那么它也就没有存在的意义了。这里所说的性能主题是Web服务器,因此,性能这个概念主要是从网络角度出发的,它包含以下三个概念

网络性能

  这里的网络性能,不是针对一个用户而言的,而是针对Nginx服务而言的。网络性能是指,在不同负载下,Web服务在网络通信上的吞吐量。而带宽这个概念,就是指在特定的网络链接上,可以达到的最大吞吐量。因此,网络性能肯定会受制于带宽,当然更多的是受制于Web服务的软件架构。
  在大多数场景下,随着服务器上并发连接数的增加,网络性能都会有所下降。目前,我们在谈网络性能时,更多的是对应于高并发场景。例如,在几万或者几十万并发连接下,要求我们的服务器仍然可以保持较高的网络吞吐量,而不是当并发连接数达到一定数量时,服务器的CPU等资源大都浪费在进程间切换、休眠、等待等其他活动上,导致吞吐量大幅下降。

单次请求的延迟性

  单次请求的延迟性与上面说的网络性能的差别很明显,这里只是针对一个用户而言的。对于Web服务器,延迟性就是指服务器初次接收到一个用户请求直到返回响应结果之间持续的时间。
  服务器在低并发和高并发连接数量下,单个请求的平均延迟时间肯定是不同的。Nginx在设计时,更应该考虑的是再高并发下如何保持平均实验性,使其不要上升得太快。

网络效率

  网络效率很好理解,就是使用网络的效率。例如,使用长连接(keepalive)代替短连接以减少建立、关闭连接带来的网络交互,使用压缩算法来增加相同吞吐量下信息携带量,使用缓存来减少网络交互次数等,它们都可以提高网络效率。

2 可伸缩性

  可伸缩性指架构可以通过添加组件来提升服务,或者允许组件之间具有交互功能。一般,可以通过简化组件降低组件之间的耦合度将服务分散到许多组件等方法来改善可伸缩性。可伸缩性受到组件之间的交互频率,以及组件对一个请求是否使用异步处理方式等约束。

3 简单性

  简单性通常指组件的简单程度,每个组件越简单,就越容易理解和实现,也就越容易被验证(被测试)。一般,我们通过分离关注点原则来设计组件,对于整体架构来说,通常使用通用性原则,统一组件的接口,这样就减少了架构中的变数。

4 可修改性

  简单来讲,可修改性就是当前架构下,对系统功能作出修改的难易程度,对于Web服务器来说,它还包括动态的可修改,也就是部署好的Web服务器可以在不停止、不重启服务的前提下,提供给用户不同的、符合需求的功能。可修改性可以进一步分解为可进化性、可扩展性、可定制性、可配置性和可重用性,下面简单说明一下这些概念。

可进化性

  可进化性表示我们在修改一个组件时,对其它组件产生负面影响的程度。当然,每个组件的可进化性都是不同的,越是核心的组件,其可进化性可能会越低,也就是说,对这个组件的功能作出修改时,必须修改其他大量的相关组件。
&### 可扩展性
  可定制型是指可以临时性地重新规定一个组件或其他框架元素的特性,从而提供一种定制化服务的能力。如果某一个组件是可定制的,那么就是指用户能够扩展该组件的服务,而不会对其他客户产生影响。支持可定制化的风格一般会提高简单性和可扩展性,因为通常情况下,只会实现最常用的功能,不太常用的功能则交由用户重新定制使用,这样组件的复杂性就降低了,整个服务会更容易扩展。

可配置型

  可配置性是指在Web服务部署后,通过对服务提供的配置文件进行修改,来提供不同的功能。它与可扩展性、可重用性相关。

可重用性

  可重用性指的是一个应用的功能组件在不被修改的情况下,可以在其他应用中重用的程度。

5 可见性

  在Web服务器这个应用场景下,可见性通常是指一些关键组件的运行情况可以被监控的程度。例如,服务中正在交互的网络连接数、缓存的使用情况等。通过这种监控,可以改善服务的性能,尤其是可靠性。

6 可移植性

  可移植性是指服务可以跨平台运行,这也是当下Nginx被大规模使用的必要条件。

7 可靠性

  可靠性可以看做是在服务出现部分故障时,一个架构容易受到系统层面故障影响的程度。可以通过以下方法提高可靠性;避免单点故障、增加冗余、运行监视,以及用可恢复的动作来缩小故障的范围。

Nginx的架构设计

优秀的模块化设计

  高度模块化的设计是Nginx的架构基础。在Nginx中,处理少量核心代码,其他一切皆为模块。这种模块化设计同时具有以下几个特点:

高度抽象的模块接口

  所有的模块都遵循统一的接口设计规范,这样可以带了良好的简单性、静态可扩展性、可重用性。

模块接口非常简单,具有很高的灵活性

  模块的基本接口只涉及模块的初始化、退出以及对配置项的处理,使得Nginx比较简单的实现了动态可修改性。

核心模块接口的简单化

  Nginx定义了一种基础类型的模块:核心模块。为什么要定义核心模块呢?因为这样可以简化Nginx的设计,使得非模块化的框架代码只关注于如何调用6个核心模块。(大部分Nginx模块都是非核心模块)

多层次、多类型的模块设计

  所有模块间是分层次,分类别的。官方Nginx共有五大类型的模块:核心模块、配置模块、事件模块、HTTP模块、mail模块。
  配置模块和核心模块都是与Nginx框架密切相关的,是其他模块的基础。而事件模块则是HTTP模块和mail模块的基础。

事件驱动架构

  所谓事件驱动架构,简单说来,就是由一些事件发生源来产生事件,由一个或多个事件收集器来收集、分发事件,然后许多事件处理器会注册自己感兴趣的事件,同时会“消费”这些事件。
  对于Nginx这个Web服务器而言,一般会由网卡、磁盘产生事件,而事件模块负责事件的收集、分发操作,而所有的模块都可能是事件的消费者,它们首先需要向事件模块注册感兴趣的事件类型,这样,在有事件发生时,时间模块会把事件分发到相应的模块中进行修理。
  Nginx采用完全的事件驱动架构来处理业务,这与传统的Web服务器(如Apache)是不同的。
  对于传统的Web服务器而言,采用的所谓事件驱动往往局限在TCP连接建立、关闭事件上,一个连接建立以后,在其关闭之前的所有操作都不再是事件驱动,这时会退化成按序执行每个操作的批处理程序,这样每个请求在连接之后都将始终占用着系统资源,直到连接关闭才能释放资源。要知道,这段时间可能会非常长,从1毫秒到1分钟都有可能,而且这段时间内占用着内存、CPU等资源也许并没有意义,整个事件消费进程只是在等待某个条件而已,造成了服务器资源的极大浪费,影响了系统可以处理的并发连接数。这种传统Web服务器往往把一个进程或线程作为事件消费者,当一个请求产生的事件被该进程处理时,直到这个请求处理结束时,进程资源都被这一个请求占用。
在这里插入图片描述
  Nginx则不然,它不会使用进程和线程作为事件消费者,所谓的事件消费者只能是某个模块(在这里没有进程的概念)。在事件收集、分发者进程的一次处理过程中,这5个事件按照顺序被收集后,将开始使用当前进程分发事件,从而调用相应的时间消费者模块来处理时间。当然,这种分发、调用也是有序的。
在这里插入图片描述
  从上面的内容可以看出,传统Web服务器与Nginx之间的重要差别:前者是每个事件消费者独占一个进程资源,后者的事件消费者只是被事件分发者进程短期调用而已。这种设计使得网络性能、用户感知的请求时延(延时性)都得到了提升,每个用户的请求所产生的事件会及时响应、整个服务器的网络吞吐量都会由于事件的及时响应而增大。但这也带来了一个重要的弊端,即每个事件消费者都不能有阻塞行为,否则将会由于长时间占用事件分发者进程而导致其他事件得不到及时响应。尤其是每个事件消费者不可以让进程转变为休眠状态或等待状态,如果在等待一个信号量条件的满足时会使进程进入休眠状态。这加大了事件消费进程的开发者的编程难度,因此,这也导致了Nginx的模块开发相对于Apache来说复杂不少。

请求的多阶段异步处理

  这里所讲的多阶段异步处理请求与事件驱动架构是密切相关的,换句话说,请求的多阶段异步处理只能基于事件驱动架构实现。什么意思呢?就是把一个请求的处理过程按照事件的触发顺序划分为多个阶段,每个阶段都可以由事件收集、分发器来触发。
  例如,处理一个获取静态文件的HTTP请求可以分为以下几个阶段:
在这里插入图片描述

  这个例子中大致分为7个阶段,这些阶段是可以重复发生的,因此,一个下载静态资源请求可能会由于请求数据过大、网速不稳定等因素而被分解为成百上千个阶段。
  异步处理和多阶段是相辅相成的,只有把请求分为多个阶段,才有所谓的异步处理。也就是说,当一个事件被分发到事件消费者中进行处理时,事件消费者处理完这个事件只相当于处理完一个请求的一个阶段。什么时候可以处理下一个阶段呢?这只能等待内核的通知,即当下一次事件出现时,epoll等事件分发器将会获取到通知,再继续调用事件消费者处理请求。这样,每个阶段中的事件消费者都不清楚本次请求完整的操作究竟什么时候会完成,只能异步被动地等待下一次事件通知。
  请求的多阶段异步处理优势在哪里?这种设计配合事件驱动架构,将会极大地提高网络性能,同时使得每个进程都能全力运转,不会或者尽量少地出现进程休眠状况。因为,一旦出现进程休眠,必然减少并发处理事件的数目,一定会降低网络性能,同时会增加平均请求处理时延。这时,如果网络性能无法满足业务需求将只能增加进程数目,进程数据过多将会增加操作系统内核的额外操作:进程间切换,可是频繁的进程间切换会消耗cpu等资源,从而降低网络性能。同时,休眠的进程会使进程占用的内存得不到有效释放,这最终必然导致系统可用内存从下降,从而影响系统能够处理的最大并发连接数。

阶段划分原则

(1) 将阻塞进程的方法按照相关的触发事件划分为两个阶段

  一个本身可能导致进程休眠的方法或系统调用,一般都能够分解为多个更小的方法或者系统调用,这些调用间可以通过事件触发关联起来。大部分情况下,一个阻塞进程的方法调用可以划分为两个阶段:第一阶段,将阻塞方法改成非阻塞方法,调用非阻塞方法并将进程归还给事件分发器。第二阶段,处理非阻塞方法最终的返回结果,这里的结果返回事件就是第二阶段的触发事件。
  例如,在使用send调用发送数据给用户时,如果使用阻塞socket句柄,那么send调用在向操作系统内核发送数据包后就必须把当前进程休眠,直到成功发出数据才能“醒来”。这时的send调用发送数据并等待结果,因此,我们可以使用非阻塞的socket句柄,这样调用send发送数据后,进程是不会进入休眠的,这就是发送且不等待结果阶段。再把socket句柄加入到事件收集器中,就可以等待相应的事件触发下一个阶段,send发送的数据被对方收到后这个事件就会触发send结果返回阶段。这个send调用就是请求的划分阶段点。
(2)将阻塞方法调用按照时间分解为多个阶段的方法调用
  例如,读取文件的调用(非异步I/O),如果我们读取10MB的文件,这些文件在磁盘中的块未必是连续的,这意味着当这10MB文件内容不在操作系统的缓存中时,可能需要多次驱动硬盘寻址。在寻址过程中,进程多半会休眠或者等待。我们可能会希望像上文所说的那样将读取文件调用分解为两个阶段,发送读取命令且不等待结果阶段、读取结果返回阶段。这样当然很好,可惜的是,如果我们的事件收集、分发者不支持这么做怎么办?例如,在Linux上Nginx的事件模块在没有打开异步I/O时就不支持这种方案,像ngx_epoll_module模块主要是针对网络事件的,而主机的磁盘事件目前还不支持(必须通过内核异步I/O)。
  这时,我们可以这样来分解读取文件调用,把10MB的文件分成100份,每次只读取10KB。这样,读取10KB的时间就是可控的(如最多一次磁盘I/O),意味着这个事件接收器占用进程的时间不会太久,整个系统可以及时地处理其他请求。
  那么,在读取0KB ~ 10KB的阶段完成之后,怎么进入10KB ~ 20KB阶段呢?这有很多种方式,如读取完10KB文件后,可能需要使用网络来发送它们,这时可以由网络事件来触发。或者,如果没有网络事件,也可以设置一个简单的定时器,在某个时间点后再次调用下一个阶段。
(3)在“无所事事”且必须等待系统的相应,从而导致进程空转时,使用定时器划分阶段

  有时候,阻塞的代码可能是这样的:进行某个无阻塞的系统调用,必须通过持续的检查标志位来确定是否继续向下执行,当标志位没有获得满足时就循环地检测下去。这样的代码段本身没有阻塞方法调用,可实际上是阻塞进程的。这时,应该使用定时器来代替循环检测标志,这样定时器时间发生时就会先检查标志,如果标志不满足,就立即归还进程控制权,同时继续加入期望的下一个定时器事件。
(4)如果阻塞方法完全无法继续划分,则必须使用独立的进程执行这个阻塞方法

管理进程、多工作进程设计

在这里插入图片描述
  Nginx采用了一个master管理进程、多个worker工作进程的设计方式,如上图所示。图中,包括完全相同的worker进程、1个可选的cache manager进程以及1个可选的cahce loader进程。
  这种设计带来以下优点:
(1)利用多核系统的并发能力
  现代操作系统已经支持多核CPU架构,这使得多个进程可以只能用不同的CPU核心来工作。如果只有一个进程在处理请求,则必然会造成CPU资源的浪费!Nginx中所有的worker工作进程都是完全平等的,避免由于某个高优先级worker进程成为瓶颈。这提高的网络性能、降低了请求的时延。
(2)负载均衡
  多个worker工作进程间通过进程间通信来实现负载均衡,也就是说,一个请求到来时更容易被分配到负载较轻的worker工作进程中处理。这将降低请求的时延,并在一定程度上提高网络性能。
(3)管理进程会负责监控工作进程的状态,并负责管理行为
  管理进程不会占用多少系统资源,它只是用来启动、停止、监控或者使用其他行为来控制工作进程。首先,这提高了系统的可靠性,当工作进程出现问题时,管理进程可以启动新的进程来避免系统性能的下降。其次,管理进程支持Nginx运行服务中的程序更新、配置项修改等操作,这种设计使得动态可扩展性、动态定制型、动态可进化性较容易实现。

平台无关的代码实现

  在使用C语言实现的Nginx时,尽量减少使用与操作系统平台相关的代码,如某个操作系统上的第三方库。
Nginx重新封装了日志、各种基本数据结构、常用的算法等工具软件、在核心代码都使用了操作系统无关的代码实现,在于操作系统相关的系统调用上则分别针对各个操作系统都有独立的实现,这最终造就了Nginx的可移植性,实现了对主流操作系统的支持。

内存池的设计

  为了避免出现内存碎片,减少向操作系统申请内存的次数、降低各个模块的开发难度,Nginx设计了简单的内存池。这个内存池没有很复杂的功能:通常它不负责回收内存池中已经分配的内存。这种内存池最大的优点在于:把多次向系统申请内存的操作整合成一次 ,这大大减少了CPU资源的消耗,同时减少了内存碎片。

使用统一管道过滤器模式的HTTP过滤模块

  有一类HTTP模块被命名为HTTP过滤模块,其中每一个过滤模块都有输入端和输出端,这些输入端和输出端都具有统一的接口。这些过滤模块将按照configure执行时决定的顺序组成一个流程线式的加工HTTP响应的中心,每个过滤模块都是完全独立的,它处理着输入端接受到的数据,并由输出端传递给下一个过滤模块。每个过滤模块都必须可以增量地处理数据,也就是说能够正确处理完整数据流的一部分。
  这种统一管理过滤器的设计方式好处非常明显:首先它允许把整个HTTP过滤系统的输入/输出简化为一个个过滤模块的简单组合,这大大提高了简单性;其次,它提供了很好的可重用性,任意两个HTTP过滤模块都可以连接在一起;再次,整个过滤系统非常容易维护、增强。例如,开发了一个新的过滤模块后,可以非常方便的添加到过滤系统中,这是一种高可扩展性。又如,旧的过滤模块可以很容易地被升级为升级版本的过滤模块所替代,这是一种高可进化性;接着,它在可验证性和可测试性上非常友好,我们可以灵活地变动这个过滤模块流水线来验证功能;最后,这样的系统完全支持并发执行。
内容源自:《深入理解Nginx模块开发与架构解析》

发布了88 篇原创文章 · 获赞 317 · 访问量 35万+

猜你喜欢

转载自blog.csdn.net/sunxianghuang/article/details/86148762