购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第4章
Nginx核心技术

在开发Nginx时,Apache已经是一个成熟的Web服务器了,性能不错、功能丰富、应用广泛。Nginx从设计之初就定位为高性能、高可靠性、高扩展性、高并发性的Web服务器,比Apache性能更好。Nginx是基于HTTP协议的Web服务器,受HTTP协议的制约,有些问题解决起来比较棘手,所以采用了一系列技术解决这些问题。

本章介绍这些有特色的技术,以便我们更了解Nginx,知道Lua开发的工作机制,这些技术和思想也可以应用到我们的项目中去。

4.1 Nginx设计目标

Nginx的设计目标体现7个方面,为了实现这7个方面的目标,Nginx采用了一系列架构和技术,具体如下。

1. 性能

Nginx最大的特点是性能优异,客户接入的并发性出众,这是Apache无法比拟的。这个性能主要体现在网络传输方面。

1)网络性能:Nginx服务器整体的吞吐能力。Nginx在Linux上使用了Epoll网络模型,在全异步模式及多进程而非多线程模式的支持下,可以处理几万至几十万的并发请求。Nginx在用户干预请求之前,采用了预处理机制,保证更多的连接可以接入并被以最快的速度处理。而连接池等技术的使用也进一步提高了接入能力。全异步的处理机制使得响应速度更快,从而可以处理更多的请求。因为使用进程模式,减少了大并发情况下线程间切换、休眠等消耗,使得CPU消费很小。而低的内存占用,也减少了系统内存使用率。

2)网络效率:为了提高网络效率,Nginx使用了长连接(keepalive)代替短连接以减少建立、关闭带来的网络交互。同时使用压缩算法提高网络利用率,减少交互次数。

3)时延:Nginx使用了带宽控制技术,使各会话之间带宽尽量相等,保证每个连接都尽量通信,同时保证相同请求链路间低带宽和高带宽变化不会太大。全异步的模式也保证了每个请求都会以最快的速度被处理而不会被阻塞,而且每个请求的处理时间是可以预期的,保证了每个连接时延都是均等且可预期的,总体时延是相对较低的。

2. 可靠性

可靠性是Nginx第二大特点。Nginx服务本身采用了主从机制以看门狗形式管理工作进程,一旦有工作进程崩溃,会马上启动新的进程代替。Nginx支持负载均衡机制,可以平行增加冗余点。

3. 伸缩性

Nginx使用了组件技术,可以减少或增加使用的组件,或使用自行开发的新组件,介入到HTTP请求处理的中间环节,改变处理行为。Nginx提供了6个核心模块组成整个服务,核心模块中还可以串行增加新的模块以改变服务。

4. 简单性

多组件以及多阶段的方式使一个HTTP处理过程被分为了11个小的阶段,每个阶段都可以非常简单,这使得每个阶段都容易理解和实现,也容易验证。Nginx的这种组件+分阶段的模式比通常系统中的模块化更进了一步,更加细化。直接的好处就是使HTTP处理过程变成了流水线模式,每个模块只是流水线上的一道环节的加工者,工作相对非常简单。一个间接的好处是,因为分了阶段,就要求模块间要按接口开发,接口相对固化、简化、统一,模块更高效、稳定。

5. 可修改性

可修改性指的是当前架构下对系统功能修改的难易程度。对于Nginx这种定位专用的Web服务器,还需要具备动态修改配置、动态升级、动态部署的能力。动态修改配置指的是根据业务情况实时修改配置而不需要重启服务。动态升级指的是不重启服务而将Web服务器升级到新版本的能力。可修改性还可以理解为可扩展性、可定制性、可重用性等。

6. 可见性

可见性指的是在Web服务器的应用场景中,系统的关键组件的运行情况可以被监控,如网络吞吐量、网络连接数、缓存使用情况等。通过这些数据的监控可以及时修改系统配置和服务配置,以改善服务性能。

7. 可移植性

可移植性是指跨平台能力,通常在大型的Web应用中,操作系统是Linux和UNIX,Nginx使用Epoll,可以发挥最大性能。同时Nginx也支持Windows,但是在Windows上使用的是select,性能比较低,适合应用于小型应用,如专业型行业平台的Web容器,而不是大型Web应用。

4.2 Nginx架构

Nginx使用了很多提升稳定性和性能的架构,这些技术都非常有效,虽然有的技术本身看起来比较简单。

总体来看,Nginx使用事件驱动的服务模型。为此,Nginx在它的模块机制中专门定义了event模块实现事件驱动。在事件的基础上,Nginx使用了多阶段的异步模型。Nginx的异步模式将处理过程划分为阶段,进一步将异步的作用范围缩小。把一个处理过得的过程(如HTTP请求)划分为7个、9个或11个阶段,每一个阶段都异步处理,将请求和处理结果异步化处理。将请求多阶段处理,可以进一步控制每个请求的总体处理时间,因为每个阶段都细化,不会出现某个阶段过多占用CPU处理时间的问题。

这种多阶段的机制是保证Nginx能大并发处理多请求的基础,同时为使用模块介入请求处理提供了基础,Lua开发就是在这几个阶段中实现的。

管理进程和工作进程的机制使Nginx可以充分利用多处理器机制,充分利用了SMP机制的硬件资源,又可以减少过多线程、进程的调度开销。管理进程作为工作进程的管理者,监控工作进程的工作状态,可以做到动态升级,当工程进程终止时,管理进程可以快速启动新的进程,保证系统的可用性。

对于高并发下的多工程进程容易引起的“惊群”以及负载均衡问题,Nginx都做了比较好的处理。同时对于其他容易引起更多系统资源消费的访问,例如,每个请求阶段中频繁出现的时间比较操作,Nginx做了时间缓存机制。在其他通用的内存管理、连接池、跨平台管理等方面,Nginx都做得比较好。下面分别介绍这些架构和技术。

4.2.1 事件驱动

Nginx是事件驱动型服务,注册各种事件处理器以处理事件。对于Nginx,事件主要来源于网络和磁盘。事件(event)模块负责事件收集、管理和分发事件,其他的模块都是事件的处理者和消费者,会根据注册的事件得到事件的分发。

在不同的操作系统上,在nginx.conf的event{}块中配置相应的事件模块,就可以启用对应事件模型,而且可以根据应用场景随时切换事件模块,这也实现了Nginx的跨平台机制。这个具体的event模块被核心的ngx_events_module管理,ngx_events_module是核心模块。

Nginx为不同的操作系统和不同内核版本提供了共9个事件模块,分别为ngx_select_module、ngx_eventport_module、ngx_epoll_module、ngx_poll_module、ngx_devpoll_module、ngx_kqueue_module、ngx_aio_module、ngx_rtsig_module,以及一个Windows版本ngx_select_module。

图4-1描述Nginx事件处理的模型,即事件、事件管理器和事件消费者之间的一个概貌。

图4-1 Nginx事件处理模型

图4-1中所示的模型分为事件、事件管理器、事件消费者。事件是由生产者产生的,事件管理器负责收集、管理、分发,事件消费者使用和消费这些事件。而事件,不单单是网络事件,还有Nginx自定义的事件。

Nginx定义了丰富的事件,这些事件的消费者全部是模块,模块可以注册不同的事件,而事件可以细化到一个操作的不同阶段。图4-1中列出了5个事件,被5个事件消费者处理。这种细化的事件加上非阻塞的处理,可以使响应速度大大加快。区别于传统的Web处理机制,每个事件消费者都不去阻塞事件,处理完自己的事件后如果条件不满足,可以进入休眠状态,从而简化了开发过程的数据同步、进程同步、重入等问题。如果要管理一个事件,就需要首先注册一个handler,这个handler负责这个事件处理,ngx_lua正是利用注册handler实现的自定义开发,对HTTP请求处理过程进行干预。

Nginx的事件机制是完全的事件驱动,与传统Web服务不同。传统服务每个事件消费者都独占一个进程资源,而Nginx的完全事件驱动只是被事件分发者进程短期调用。每个用户请求产生的事件都会得到及时的响应,每个消费者不允许阻塞程序,不允许消费者使进程变为休眠状态或等待状态。

4.2.2 异步多阶段处理

Nginx的异步多阶段处理是基于事件驱动架构的。Nginx把一个请求划分成多个阶段,每个阶段都可以由事件、分发器来分发,注册的阶段管理器(消费者、handler)进行对应阶段的处理。所以,Nginx的阶段划分相当于人为创造了很多事件。例如,获取一个静态文件的HTTP请求可以划分为下面的几个阶段。

1)建立TCP连接阶段:收到TCP的SYN包。

2)开始接收请求:接收到TCP中的ACK包表示连接建立成功。

3)接收到用户请求并分析请求是否完整:接收到用户的数据包。

4)接收到完整用户请求后开始处理:接收到用户的数据包。

5)由静态文件读取部分内容:接收到用户数据包或接收到TCP中的ACK包,TCP窗口向前划动。这个阶段可多次触发,直到把文件完全读完。不一次性读完是为了避免长时间阻塞事件分发器。

6)发送完成后:收到最后一个包的ACK。对于非keepalive请求,发送完成后主动关闭连接。

7)用户主动关闭连接:收到TCP中的FIN报文。

只有多阶段划分,才能更好地实现异步处理。当一个事件被分发到事件消费者中处理时,消费者只是处理完了一个阶段的事件,只有一个请求的所有阶段都被处理完成,一个完整的流程才算走完,一个完整的请求被划分成很多个过程,有的过程(如读取)还会进一步被多次调用。每一个事件、每一个阶段均由event模块负责调用和激活。event模块监听系统内核消息,以激活Nginx事件。

这种异步多阶段处理模式的好处是高效。这将极大地提高网络性能,使每个进程都全力运转,不会或者尽量少地出现进程休眠情况。一旦有进程休眠,必然减少并发处理事件的数量,会降低网络性能。如果网络性能无法满足业务需求,将会增加进程,进程数目过多会造成进程间更多地切换,这也会消耗系统CPU资源,反过来影响网络性能。

Nginx中的阶段划分方法使用了若干种有效的方法,从上往下依次如下。

(1)将系统本身的事件和网络异步事件划分为阶段

如上面的7个阶段划分,这是网络模型通常的几个阶段,而现有的网络模型都在这个基础上划分为若干个事件。

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

分析可能导致进程休眠的方法或系统调用,将阻塞的方法改成非阻塞的方法,调用这个方法的过程定义为第一个阶段。增加第二个阶段,用于处理非阻塞方法回调。非阻塞方法的返回结果是第二个阶段的触发事件。例如,将阻塞send接口改成非阻塞send接口。以send为节点将send和send之后划分为两个阶段。

(3)将阻塞方法按调用时间分解为多个阶段调用

在阻塞方法不能按上面方法划分多阶段情况下(如触发事件不可以被捕获),使用按照执行时间拆分方法。

Nginx主要处理网络收发任务,epoll的网络处理能力非常强大,支持完整的异步模式。一般无法划分的阻塞方法是发生在文件I/O上。在不支持异步文件I/O的系统上,如果读取10MB数据将会遇到文件不连续、中间不定时会等待的情况。这时将文件读取划分为小块读取,如每次读取10KB,这样每一块读取的时间都比较均匀,这个事件接收器不会占用进程多少时间,系统有机会处理其他的事件。但是没有读取完成的事件,通过将读取和网络发送直接关联,就可以在发送完成事件里知道上次读取已经完成并且已经发送出去,需要读取下一个10KB了。在不使用网络的情况下,可以将读取和一个专用定时器关联起来,因为每10KB的读取时间相对可控,比较均匀,所以时钟的命中率就比较高,一旦时钟检测到数据读取完成,就可以触发对应第二阶段的事件。

(4)在必须等待的情况下,使用定时器切分阶段

例如,我们经常在代码中做状态和标志检测的工作,直到某个条件满足时才往下执行,这种情况使用定时器检测标志,如果标志位不满足就立刻归还控制权,同时继续加入下一个定时器事件。

(5)阻塞方法无法继续划分,则必须使用独立的进程执行这个阻塞方法

如果某个阻塞方法没有提供非阻塞接口,则将其划分为阻塞方法调用前和阻塞方法调用后,而调用前需要使用独立进程调度。

Nginx提供的API以及ngx_lua提供的API和库都是非阻塞的,也都使用多阶段的异步机制。我们只是在此基础上开发应用,一般不会遇到阻塞的情况。但如果我们使用Lua原生库,则可能会碰到这个情况,典型的就是文件类API,这时我们也需要使用上面的方法进行多阶段划分。再如,我们的线程函数里就可能出现第4种情况,需要用对应的方法处理,可使用时钟解决这个问题。对于我们使用非OpenResty的库,使用其他的库,而库函数没有非阻塞接口,则会遇到第5种情况的问题,我们需要使用对应方法解决。否则,我们的代码将阻塞Nginx的工作进程,降低系统整体性能。这与Nginx和我们的初衷是违背的,需要注意。

Nginx中的定时器是由Nginx实现的,非内核态的,而且使用内存缓冲时间实现高性能读取,所以使用定时器时的性能是可以保证的。

4.2.3 模块化设计

模块化设计是Nginx中重要的架构,除了少量的核心代码,其他功能都是在模块中实现的,新功能的扩展是通过按照标准接口和数据结构开发新模块实现的,核心代码和核心模块不需要改动。模块化设计的好处是给Nginx带来了更好的扩展性、可靠性。

1. Nginx模块化设计的特点和技术

(1)使用模块接口

所有的模块遵循统一的接口设计规范,接口设计规范定义在ngx_module_t中,这样使接口简化、统一、可扩展。

(2)配置功能接口化

Nginx将配置信息也定义和开发成模块,配置模块专注于配置的解析和数据保存,是唯一一个只有一个模块的模块类型。ngx_module_t接口中定义了一个type成员,用于描述和定义模块类型。配置模块是ngx_conf_module,是其他模块的基础模块,因为Nginx模块全部使用配置模块来定义和配置,依赖于配置模块。

(3)模块分层设计

Nginx设计了6个基础类型模块(称为核心模块),实现了Nginx的6个主要部分,以及HTTP协议主流程。这样设计的目的是使框架程序只关注于如何调用核心模块,核心模块实现Nginx的核心功能,实现了第一层的流水线。核心模块之外是非核心模块,由对应的核心模块进行初始化和调用。这些模块可以动态添加,通过重新编译包含进Nginx,通过配置文件将模块使能,给Nginx带来了扩展性、灵活性。

event模块、HTTP模块、mail模块的非核心模块中有一个对应的core模块,代理核心模块的行为,例如,event模块均由ngx_events_module模块定义,加载由ngx_event_core_module模块负责。这3种模块不直接执行框架代码的操作,而是由对应的core代理模块实现,它们只重新定义了下面可以管理的模块。

(4)核心模块接口简单化

核心模块的接口非常简单,将ngx_module_t中的ctx上下文实例化为ngx_core_module_t结构,该结构是以配置项的解析为基础的。nginx.conf中解析出来的配置项会放到这个数据结构,通过提供的init_conf回调使用解析出的配置项初始化核心模块。Nginx框架允许核心模块接口、功能可自定义,这样就可以使核心模块自己定义新的模块类型,所以就有了event、mail和HTTP模块的模块簇。这使得Nginx可以灵活扩展、不停止服务升级、动态配置。

2. 核心模块

核心模块是ngx_core_module。目前核心模块中有6个模块:ngx_core_module、ngx_events_module、ngx_openssl_module、ngx_http_module、ngx_mail_module、ngx_errlog_module。核心的功能都封装在这些核心模块中,框架代码只是这些模块的使用者,可以专注于主处理流程开发。模块使系统模块化,可以分别升级和进化,使系统功能和稳定性提升,但又不影响整体功能和稳定性。这是一种典型的模块化面向接口编程技术。

模块化使Nginx可以实现动态可配置性、动态扩展性、动态可定制性,可以实现不停止服务、不重启服务实现更新和添加。

这6个核心模块只是定义了6类业务的业务流程,具体的工具并不由这些模块执行,例如,event模块由ngx_events_module定义,但是由ngx_event_core_module模块加载;HTTP模块由ngx_http_module定义以及加载,但业务核心逻辑以及具体请求应该调用哪个HTTP模块处理由ngx_http_core_module实现。

图4-2描述了常用模块之间的关系。

图4-2 Nginx常用模块之间的关系

配置模块和核心模块都是框架所面对的一层重要模块。其他模块则像部门经理,各自统领一个部门,负责不同的业务实现。

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

为支持现在流行的多CPU和多核架构,Nginx使用了管理进程+工作进程的设计。管理进程作为工作进程的管理进程和父进程,还可以带来高可靠性:工作进程终止,管理进程可以及时启动新的实例接替。这种模式的优点体现在以下3个方面。

1)充分利用多核系统的并发处理能力。Nginx中所有的工作进程都是平等的,并且可以在nginx.conf中将工作进程和处理器一一绑定,这样配合负载均衡机制,不会让某核繁忙,整体处理能力得到提高。

现代的服务器都支持多处理器架构,且处理器内还是多核心架构,只有多进程和多线程机制才能发挥硬件体系的最佳性能。而设计合理的多进程模式比多线程模式性能要好些,因为滥用线程带来的线程切换开销也是不容忽视的。

2)负载均衡。工作进程间通过进程间通信实现负载均衡,请求容易被分配到负载较轻的工作进程中,这将提高系统的整体性能。

3)方便状态监管。管理进程任务很轻,只负责启动、停止、监控工作进程,当某个工作进程不正常工作时,可以立即启动新的进程接管。同时管理进程支持服务运行中的程序升级、配置项修改等操作。使系统整体具备了动态扩展性、动态可定制性、动态可进化性。

Nginx管理/工作多进程模式如图4-3所示。

图4-3 Nginx管理/工作进程模式

管理进程向用户提供了命令行服务,包括启动、停止、重载配置文件、平滑升级程序等。管理进程运行时需要较大的权限。一般会使用root用户启动管理进程,而工作进程权限较小,使管理进程可以完全管理工作进程。当任意一个工作进程出现错误coredump时,管理进程会立刻启动新的工作进程继续服务。

多工作进程处理请求可以提高服务的健壮性,即若有工作进程意外退出,则会有新的工作进程启动代替,更可以充分利用SMP多核架构,实现真正的多核并发处理。Apache的每个进程在一个时刻只处理一个请求,因此在多任务处理阶段,Apache的进程数或线程数要设置得很多,一般一台服务器可以达到几百个进程,这种情况下大量的进程间切换将带来无谓的系统资源消耗。Nginx的工作进程之间处理并发请求时几乎没有同步锁的问题,而工作进程采用全异步的操作模式,处理速度快,所占内存非常小。所以,当Nginx的进程数与CPU核心数相等时,进程间切换的开销是最小的。

1. “惊群”问题

管理进程+工作进程模式有很多优点,同时也有一些问题需要解决。

Nginx里的工作进程一般是按系统CPU核数配置的,有多少个CPU核心,就会配置多少个工作进程,工作进程启动时就会利用fork函数创建多少个工作进程,并且所有的工作进程都监听在nginx.conf内配置的监听端口上,这样可以充分利用多核机器的性能。网络事件通过底层的events模块管理,当客户端连接请求到来时,一个新连接事件会上报,各个工作进程就会发生对事件的抢夺,这就是“惊群”问题。工作进程越多,问题越明显,这会造成系统性能下降,所以,必须避免“惊群”问题。详细来说,“惊群”问题的典型场景是这样的:在没有用户请求的时候,所有的工作进程都在休眠,此时,一个用户向服务器发起了连接请求,例如,在epoll模式下,内核在收到了TCP的SYN包时,会激活所有休眠的工作进程,最先接收连接请求的工作进程可以成功建立新连接,其他工作进程的接收会失败。这些失败的唤醒是不必要的,引发了不必要的进程上下文切换,增加了系统开销,这就是“惊群”问题。

Nginx应用层制定了一个机制解决这个问题:规定同一时刻只能有唯一一个工作进程监听Web端口,这样,新的连接事件只能唤醒唯一一个工作进程。内部的实现实际上是使用了一个进程间的同步锁,工作进程每次唤醒都先尝试这把锁,保证同一时间只有一个工作进程可以进入锁,获得锁的进程设置监听连接的读事件,以处理未来的新连接请求,并处理已连接上的事件;未能进入监听锁的工作进程则不监听新连接事件,只处理已连接上的事件,将唤醒的工作进程分为了两类,一类(只有1个)是可以监听新连接的,另一类是正常处理已有连接请求的。

设置了连接事件监听的进程在连接事件到来时会被唤醒并检查系统变量,发现新连接队列中有连接则释放锁,并调用对应事件的handler方法。这种技术既解决了“惊群”问题,也避免了一个进程过长占用锁使新连接得不到及时处理的问题,接收了一个连接后,把连接放入队列后马上释放锁,如果恰巧有新连接马上进来,则会由一个新的工作进程接收连接,起到一定的负载均衡作用。放入队列的请求事件会在后续阶段处理。

2. 负载均衡

Nginx的负载均衡可以从两个层面来讲。

(1)系统级的负载均衡

如电子商城一类的大型网站,需要多台Web服务器组成集群以应对海量的访问,需要在不同的Web服务器之间实现负载均衡,一般有如下的做法。

1)系统级的负载均衡,实现方法是使用一个Nginx服务通过upstream机制将请求分配到上游后端服务器,而这里可以使用模块内置的一些负载均衡机制将请求均衡地分配到服务器组中。

2)使用一个单独的Nginx服务以自定义负载均衡算法实现代理模式,实现负载均衡集群。

(2)单Nginx服务内部工作进程间的负载均衡

不让某个进程“累死”,其他的进程“闲死”,才能发挥系统的最佳性能。

这里讲的是管理/工作进程模式下工作进程内的负载均衡机制。

Nginx内部有一个ngx_accept_disabled变量,设置的是负载均衡的阈值,是一个整数数值。events{}中的worker_connections参数,用于设置每个工作进程的连接数,这个连接数会影响连接池的大小,连接池在内部是一个数据结构,内部分为free_connections和connections,用完的连接都插入free_connections头部,新的连接从free_connections尾部取出一个连接,然后放入connections,这些数据都是链表操作。

图4-4描述了ngx_connection的结构。

图4-4 ngx_connection结构

ngx_accept_disabled是一个进程内的全局变量,在Nginx启动时是负数,每次接收一个新连接时都会赋值,值为连接总数的7/8。当本变量值为负数时,不会进行负载均衡操作,会参与到新连接的接收尝试中,尝试获取同步锁;当值为正数时,表示连接已经过多,则会放弃一次争夺,并将值减1。值为正数表示本进程处理的进程已经过多了,已经达到了上限的7/8,所以,只有值为正数时才启动均衡算法,可以使各工作进程相应地均衡。可以看出这个算法是比较简单的,也是比较有效的。

4.2.5 内存池

Nginx在内部设计并使用了一个简单的内存池,特点是,每一个TCP连接建立时分配一个内存池,而在请求结束时销毁整个内存池,把曾经分配的内存一次性归还给操作系统。而它不负责回收内存池中已经分配出去的内存,这些内存由请求方负责回收。内存池减少了分配内存带来的资源消耗,同时减少了内存碎片。一般Nginx内部的内存池有以下两种模式:①申请了永远保存;②申请了,请求结束全部释放。对于内存的使用,有缓冲写满了从头覆盖使用的方法等。

这种内存池是内部使用的,对于基于C的模块可以使用这个内存池机制,内部提供了ngx_palloc、ngx_pnalloc、ngx_pcall这3个系统调用模块用于内存申请。内存池机制是为没有垃圾回收机制的C语言提供的一个补充机制,因为在C语言中容易出现内存泄漏问题。当内存申请和释放逻辑比较远时,容易出错,例如释放两次这种异常。内存池在开发上可以降低使用错误的机率,模块开发者只需要关注内存的使用情况,释放则由内存池来负责。

对于Lua开发,这种机制并不能直接使用。Nginx的这种机制提高了系统整体的可用性,方便了Nginx模块的开发,我们在Lua开发中用到的很多模块就使用了内存池机制。

4.2.6 连接池

Nginx为了减少反复创建TCP连接以及创建套接字的次数,从而提高网络响应速度,在内部提供了连接池机制,在众多配置命令里也可以经常看到连接池的配置选项和单独命令。

连接池在Nginx启动阶段,由管理进程在配置文件中解析出来对应的配置项,配置项放到配置结构体中。在event核心模块ngx_events_module初始化事件模型时,ngx_event_core_module模块第一个被初始化,这个模块将根据配置结构体中的连接池大小配置创建连接池,如果没有配置项,则使用系统默认值创建连接池。注意,配置指令中worker_connections配置的连接池大小是工作进程级别的,所以实际的连接池大小是worker_connections*worker_processes。

所有使用连接池的接口都有keepalive()方法,会将一个连接放入连接池中,对于应用来讲,连接将得到close状态,而连接实际没有释放。

Nginx内部封装了两个连接池方法:ngx_get_connection和ngx_free_connection,用于模块开发者使用内置的连接池,所以很多Nginx配套的模块,都使用了这个连接池。在后面的实战章节里,我们将会看到各种连接缓存、数据库的库都支持使用连接池,特别是OpenResty系列数据访问组件,支持内置的连接池。

4.2.7 时间缓存

Nginx内部提供了很多时间函数,而且内部的操作大多数要控制超时值,需要使用当前时间进行判断。由于对OS的时间函数gettimeofday的调用是内核态的系统调用,如果频繁调用会降低系统可用性,Nginx在自己内部对系统时间进行了缓冲,避免频繁进行系统调用,内部访问时间实际上访问了内存中的几个变量。

nginx.conf中的timer_resolution配置可以指定多长时间更新一次时间缓存。这也是一个降低系统资源占用的细节。因为,考虑到大并发大任务下的处理过程中,要处理很多的过程,那么这个占用带来的开销是不容忽视的,而其他的系统往往又会忽视这个因素。

时间缓存在系统初始化时被赋值,另一个修改的机会是在ngx_epoll_process_events调用epoll_wait返回时有可能会更新。

因为Nginx的多工作进程机制,可能导致时间缓存读写不一致问题,即前一个进程在读时间缓存时正好被中断了,而时间缓存又被另一个进程因为ngx_epoll_process_events导致了时间更新了,导致前后读取不一致。所以采用64个缓存时间,引入时间缓存数组(共64个成员),每次都更新数组中的下一个元素;读取时间缓存时,也是读取最新的时间,从而实现读写一致性。这是内部实现的机制,对于上层的应用开发是透明的。

4.2.8 延迟关闭

延迟关闭,即当Nginx要关闭连接时,并不马上关闭连接,而是先关闭TCP连接的写操作,等待一段时间后再关掉连接的读操作。假设有这样一个场景:Nginx在接收客户端请求时,可能由于客户端或服务端出错,要立即响应错误信息给客户端,Nginx在响应错误信息后,大部分情况下需要关闭当前连接。Nginx执行完write()系统调用把错误信息发送给客户端,write()系统调用返回成功并不表示数据已经发送到客户端,有可能还在TCP连接的写缓冲区里。如果紧接着执行close()系统调用关闭TCP连接,内核会首先检查TCP的读缓冲区里有没有客户端发送过来的数据留在内核态没有被用户态进程读取。如果有则发送给客户端RST报文关闭TCP连接,丢弃写缓冲区里的数据;如果没有则等待写缓冲区里的数据发送完毕,然后经过正常的4次分手报文断开连接。所以,若在某些场景下TCP写缓冲区里的数据在write()系统调用之后到close()系统调用执行之前没有发送完毕,且TCP读缓冲区里面还有数据没有读,则close()系统调用会导致客户端收到RST报文且不会拿到服务端发送过来的错误信息数据。客户端就会经常在没有错误信息的情况下被重置连接。

在这个场景中,关键点是服务端给客户端发送了RST包,导致自己发送的数据在客户端被忽略掉了。所以,解决问题的重点是,不让服务端发RST包。服务端发送RST包是因为关掉了连接,关掉连接是因为不想再处理此连接了,也不会有任何数据产生。对于全双工的TCP连接来说,只需要关掉写连接就行了,读可以继续进行,只需要丢掉读到的任何数据就可以了,当关掉连接后,当客户端再发过来数据时,就不会收到RST。设置一个超时时间,在这个时间过后,就关掉读连接,客户端再发送的数据就会被忽略掉,服务端会在超时时间内关掉读端。通过lingering_timeout选项设置这个超时值,如果在lingering_timeout时间内还没有收到数据,则直接关掉连接。Nginx还支持设置一个总的读取时间,通过lingering_time设置,这个时间也就是Nginx关闭写之后,保留套接字的时间,客户端需要在这个时间内发送完所有的数据,否则Nginx在这个时间过后,会直接关掉连接。Nginx支持配置是否打开延迟关闭选项,通过lingering_close选项配置。延迟关闭的主要作用是保持更好的客户端兼容性,但是需要消耗更多的额外资源,因为连接会一直占用。

4.2.9 跨平台

Nginx使用C语言开发,开发过程中可减少平台相关调用。在关键的网络部分,因为使用了模块技术,Nginx按不同平台提供了不同的模块,可以在nginx.conf中配置使用,适应了不同平台的环境;同时在内部重新封装了各种数据结构和容器代码,重新封装了日志。所以,Nginx可以在各种平台上运行,实现跨平台工作。

4.2.10 HTTP模块管道过滤模式

Nginx中定义了一种HTTP过滤模块。过滤模块有输入端和输出端,输入端和输出端有统一的接口。过滤模块按照配置时的次序依次连接,组成一个过滤链,每个模块处理接收到的数据,处理完成后输出到下一个模块,每一个模块都增量式地处理数据,可以正确处理完整数据流的一部分。

这种模式允许把整个HTTP过滤系统输入/输出简化为可以组合的机制。用户可以将任意的过滤模块按照业务要求组合起来,实现特定的要求。开发一个新的模块后,可以简单地将其添加到现有过滤系统中,提高了可验证性和可测试性;可以灵活地变动这个过滤模块流水线以验证功能,验证完毕可以使系统方便地扩展HTTP过滤模块,提高了扩展性,更便于开发和验证。

4.2.11 keepalive

keepalive是HTTP长连接,为了提高传输效率,HTTP协议中定义了这个特性。keepalive可以有效提高网络效率,所以Nginx对这个协议特性进行了运行,在配置指令以及API中,可以大量看到相关的配置和API。

可以看到,除了HTTP 1.0不带Content-Length以及HTTP 1.1非chunked传输不带Content-Length外,body的长度是可知的。这种情况下,当服务端输出完body之后,可以考虑使用长连接。能否使用长连接,也是有条件限制的。如果客户端的请求头中的connection为close,则表示客户端需要关掉长连接。如果客户端的请求头中的connection为keepalive,则客户端需要打开长连接,如果客户端的请求中没有connection这个头,那么根据协议,如果是HTTP 1.0,则默认为close,如果是HTTP 1.1,则默认为keepalive。如果结果为keepalive,Nginx在输出完响应体后,会设置当前连接的keepalive属性,然后等待客户端下一次请求。当然,Nginx不可能一直等待下去,如果客户端一直不发数据过来,连接将会被一直占用。所以当Nginx设置了keepalive等待下一次请求时,同时会设置一个最大等待时间,这个时间是通过选项keepalive_timeout配置的,如果配置为0,则表示关掉keepalive,此时,HTTP版本无论是1.1还是1.0,客户端的connection不管是close还是keepalive,都会强制为close。

如果服务端最后的决定是打开keepalive,那么在响应的HTTP头里面,也会包含connection头域,其值是keepalive,否则就是close。如果connection值为close,在Nginx响应完数据后,会主动关掉连接。对于请求量比较大的Nginx来说,关掉keepalive会产生比较多的time-wait状态socket。一般来说,当客户端的一次访问,需要多次访问同一个服务器时,打开keepalive的优势非常大,如对于图片服务器,通常一个网页会包含很多图片,打开keepalive会大量减少time-wait状态。

4.2.12 pipeline

在HTTP 1.1中,引入了一种新的特性,即pipeline。pipeline是流水线作业,可以看作keepalive的一种提高,因为pipeline也是基于长连接的,目的是利用一个连接做多次请求。如果客户端要提交多个请求,对于keepalive来说,那么第二个请求必须要等到第一个请求的响应接收完全后,才能发起,这和TCP的停止等待协议是一样的,得到两个响应的时间至少为2RTT;而对于pipeline来说,客户端不必等到第一个请求处理完后,就可以马上发起第二个请求,得到两个响应的时间可能达到RTT。Nginx支持pipeline,但是,Nginx对pipeline中的多个请求的处理不是并行的,依然是一个请求接一个请求地处理,只是在处理第一个请求的时候,客户端就可以发起第二个请求。这样,Nginx利用pipeline减少了处理完一个请求后,等待第二个请求请求头数据的时间。Nginx的做法很简单:Nginx在读取数据时,会将读取的数据放到一个buffer里面,如果Nginx在处理完前一个请求后,发现buffer里面还有数据,就认为剩下的数据是下一个请求的开始,接下来处理下一个请求,否则就将连接设置为keepalive。

4.3 小结

本章对Nginx的核心架构和关键技术进行了介绍。Nginx核心是事件驱动的纯异步架构。Nginx在代码上使用了核心模块和其他模块分开但是共同工作的模块机制。在程序模型上使用了管理进程/工作进程的工作机制,并对管理进程/工程进程机制引起的“惊群”问题和负载均衡的调度做了处理。Nginx中使用了一些提高系统性能的关键技术,这些技术对于开发高性能服务器是非常有效的,对我们进行Nginx配置也会有帮助,我们会知道这些配置项会对哪些模块产生影响,会知道这些取值的背景意义。 Bnorv/1cCvZDv/ZWoQ/TQ2fMMOr7w9VGooJ7Y89rMs4fVGjYo1pGdsK2Nfpv8ybC

点击中间区域
呼出菜单
上一章
目录
下一章
×