最近和 Nginx 杠上了!

3365 字丨阅读本文需 10 分钟

【51CTO.com原创稿件】Nginx(engine x)是一个高性能的 HTTP 和反向代理 Web 服务器,同时也提供了 IMAP/POP3/SMTP 服务。

Nginx 以高性能和高可用性备受广大程序员的青睐,今天我们会从 Nginx 的整体架构入手,介绍 Nginx 进程结构,进程之间的关系以及如何对进程进行控制和管理。

今天大家会学到如下内容:

对于传统的 HTTP 和反向代理服务器而言,在处理并发请求的时候会使用单进程或线程的模式处理,同时会止网络或输入/输出操作。

这种方式会消耗大量的内存和 CPU 资源。因为每产生一个单独的进程或线程需要准备一套新的运行时环境,包括分配堆和堆栈内存,以及创建新的执行上下文。

可以想象在处理多请求时会生成对应数目的线程或进程,导致由于线程在不断上下文切换上耗费大量资源。

由于上面的原因,Nginx 在设计之初就使用了模块化、事件驱动、异步处理,非阻塞的架构。

图 1:Nginx 总体架构

让我们通过一张图来了解 Nginx 的总结架构,如图 1 所示:

①Nginx 启动时,并不会马上处理网络请求,负责调度工作进程。包括 Load configuration(加载配置),Launch workers(启动工作进程)以及 Non-stop upgrade(平滑升级)。

因此在 Nginx 启动以后,在操作系统中会看到 Master 和 Worker 两类进程。在图上方的 Master 进程负责加载分析配置文件、启动/管理 Worker 进程以及平滑升级。

一个 Master 进程可以管理多个 Worker 进程,而 Worker 进程负责处理并响应用户请求,也就是来自图左边的 HTTP/HTTPS 请求。

②由于网络请求属于 IO 请求,为了应对高并发 Nginx 采取了 kevent/epoll/ select 模式的多路复用技术。由于采取了这种技术每个 Worker 进程都可以同时处理数以千计的网络请求。

③为了处理网络请求在 Worker 中会包含模块,分为核心模块和功能性模块。

核心模块负责维持一个运行循环(run-loop),执行网络请求处理的不同阶段的模块功能,如网络读写、存储读写、内容传输、外出过滤,以及将请求发往上游服务器等。

而围绕着核心模块会有一些功能模块,就是实现具体的请求处理功能的。例如有处理 http 请求的 ht_core 模块,有实现负载均衡的 ht_upstream 模块,以及实现 FastCGI 的 ht_fastcgi 模块。

这些模块会负责与后端的服务器进行交互,完成用户请求,同时可以根据需要的功能自由加载模块,甚至可以扩展第三方的模块。

④Worker 进程可以和本地磁盘进行数据通信,支持 Advanced I/O(高级I/O)、sendfile 机制、AIO 机制、mmap 等机制等。

通过上面的介绍发现 Nginx 不会为每个连接生成一个进程或线程,而是通过 Worker 进程使用多路复用的方式处理多个请求。

这里会使用到共享监听套接字的方式接受新请求,并在每个 Worker 内执行高效的运行循环,从而达到每个 Worker 处理成千上万的连接。

Work 启动后将创建一组侦听套接字,并且在处理 HTTP 请求和响应过程中不断接受,读取和写入套接字信息。

运行循环(run-loop)包括全面的内部调用,并且在很大程度上依赖异步任务处理。

通过模块化,事件通知,回调函数和定时器来支撑异步操作的实现。其目的是为了实现高并发请求下的不阻塞(尽可能不阻赛)。

基于上面的机制,Nginx 通过检查网络和存储的状态并初始化新连接,将其添加到运行循环中,并异步处理直到其完成,处理完毕的连接会被重新分配并从运行循环中删除。因此 Nginx 可以在极端工作负载下实现较低的CPU使用率。

另外,由于 Work 会在磁盘上进行写入操作,为了避免磁盘 I/O 上的阻塞请求,特别是磁盘满的情况。

可以设置机制和配置文件指令来减轻此类磁盘 I/O 阻塞情况的发生。例如使用 sendfile 和 AIO 组合选项提升磁盘性能。

从架构介绍我们知道 Nginx 是由不同的进程组成的,这些进程各司其职用来处理高并发下的网络请求,接下来就看看他们的定义和如何工作的。

上面介绍了 Nginx 的总体架构,其中重点提到了 Master 进程和 Worker 进程,其实还有另外两个进程在架构中也起到了重要的作用。

图 2:Nginx 的四类进程

这里我们一起通过图 2 来认识他们:

从上图大家一定注意到了只有 Worker 进程是多个,这是因为 Nginx 采用了事件驱动的模型。

为了提高处理请求的效率每个 Worker 进程会找那个一个 CPU内核,提高 CPU 的缓存命中率,将某个 Worker 进程与一个 CPU 核绑定在一起。

需要说明的是,我们需要根据具体的应用场景来定义 Worker 进程的数量:

上面提到了 Master 通过控制多个 Worker 进程来处理网络请求,对于 Worker 的独立进程来说使用资源的时候不需要考虑不需要加锁的问题,节省了因为加锁带来的系统开销。同时多进程的设计让进程之间不会互相影响。

当一个进程退出后,其它进程还在工作,Nginx 所提供的网络请求服务不会因为其中一个进程的退出而中断,Master 进程一旦发现有 Worker 进程退出会启动新的 Worker 进程。

这里我们会发现 Master 进程为了控制 Worker 需要对其进行通信,同时 Worker 进程也需要与 Master 进程交换信息。

谈到了 Master 进程如此的重要,那么一起来看看 Nginx 进程的启动过程。

图 3:Nginx 启动 Master 进程

如图 3 所示,在 Nginx 启动的时候会根据配置文件进行解析和初始化的工作,同时会从主进程中 fork 出一个 Master 进程作为自己的子进程,也就是启动 Master 进程,此时 Master 进程就诞生了。

Nginx 的主进程在 fork 出 Master 进程以后就退出了。然后 Master 进程会 fork 并启动 Worker 进程,以及 Cache Manager 、Cache Loader 进程,接着 Master 进程会进入主循环。

需要注意的是,这里使用的 fork 会复制一个和当前启动进程具有相同代码段、数据段、堆和栈、fd 等信息的子进程。

也就是说我们说的四类进程都是通过 Nginx 启动进程复制出来的子进程。

接着上面的流程 Master 进程被 fork 之后,会执行 ngx_master_process_cycle 函数。

图 4:Master 进程执行 ngx_master_process_cycle 函数

如图 4 所示,这个函数主要进行如下操作:

上面的流程中提到了几个概念,这里对其进行说明,以便我们更好的理解 Master 进程的执行过程。

①信号:用来完成进程中信息传递的媒介。Master 进程的主循环里面,一直通过等待各种信号事件,来处理不同的指令。

这个信号可以传递给 Master 进程,也可以从 Master 进程传递给其他的进程。

信号分为标准信号和实时信号,标准信号是从 1-31,实时信号是从 32-64。例如:INT、QUIT、KILL 就是标准信号。Master 进程监听的信号也是标准信号。

标准信号和实时信号的区别是:标准信号,是基于位的标记,假设在阻塞等待的时候,多个相同的信号到来,最终解除阻塞时,只会传递一次信号,无法统计等待期间信号的计数。

而实时信号是通过队列来实现,在阻塞等待的时候,多个相同的实时信号会存放到队列中。一旦解除阻塞的时候,会将队列中的信号都进行传递,结果会收到多次信号。

②信号处理器:信号处理器是指当捕获指定信号时(传递给进程)时将会调用的一个函数,它存在与进程中,它可以随时打断进程的主程序流程。

③发送信号:发送信号的操作可以使用 kill 这个 shell 命令完成。比如 kill -9 pid,就是发送 KILL 信号。

kill -INT pid 就是发送 INT 信号。与 shell 命令类似,可以使用 kill 系统调用来向进程发送信号。

④信号掩码:用来控制信号阻赛的编码方式。每个进程都有一个信号掩码(signal mask),也称为信号屏蔽字,它规定了当前要屏蔽或要阻塞递送到该进程的信号集。

对于每种可能的信号,该掩码中都有一位与之对应。对于某种信号,若其对应位(bit)已设置,则它当前是被阻塞的。

简单地说,信号掩码是一个“位图”,其中每一位都对应着一种信号。如果位图中的某一位为 1,就表示在执行当前信号集的处理程序期间相应的信号暂时被“屏蔽”或“阻塞”,使得在执行的过程中不会嵌套地响应那个信号。

说白了就是使用信号编码来阻赛信号,告诉其他发送信号的进程说:“我在忙着处理事情,你先等等,等会我再处理你的信号。”

有了上面的基础以后再回头看看 Nginx 中进程中是如何进行信息交互的,以及 Master 是如何通过信号与 Worker 进行沟通的。

图 5:Master 与 Worker 通信

从图 5 中可以看出,Master 可以接受 TERM,INT、QUIT、HUP、USR1、USR2、WINCH 这些信号,这些信号的含义会在后面提供一张表给大家解释。

同时 Master 进程也可以给Worker进程传递信号,于是 Worker 进程可以接收以下信号:TERM,INT、QUIT、HUP、USR1、WINCH。之后 Worker 再去响应 Client 的请求。

这里先将信号的对应的命令和含义通过表格的方式列出来,再来对其进行讲解。

从表格上面看,每个信号都有特定的含义。比如 QUIT 信号表示优雅地关闭服务,并且对应 quit 命令。

这里的 quit 命令指的是可以通过命令行的方式对 Master 进程下命令,从而达到发送信号的效果,当然 Master 接受到命令以后会转化为信号发送给对应的 Worker 达到关闭服务的效果。

需要说明的是,Worker 是不会接受命令的,而是通过 Worker 接受命令来统一管理所有 Worker 的行为。

知道 Master 与 Worker 之间如何通信之后再来看看它们是如何合作完成客户端请求的。

图 6:Client 请求流程

如图 6 所示,这里描绘了 Master 创建 Listen 以及 fork 出 Worker 的过程,以及客户端请求和 Worker 响应请求的过程。

①先从最上面开始看,顺着从上至下红色的箭头看,Master 进程创建以后会通过 socket 方法创建 socket 的 IO 通道。

接着执行 bind 方法将其与监听器 listen 进行绑定,然后通过 fork 方法 fork 出多个 Worker 进程(绿色虚线)。

②在每个 Worker 进程中的 accept 方法就监听 socket 请求了,一旦 listen 监听到 socket 请求 Worker 进程就可以通过 accept 接受到。

③再看最下面的 client 模块,当 client 通过 connect 方法与 Nginx 发生连接时,所有拥有 accept 方法的 Worker 进程都会接受到来自 listen 的通知,但是只有一个 Worker 进程能够成功 accept 到,其他的进程则会失败。

这里 Nginx 提供了一把共享锁 accept_mutex 来保证同一时刻只有一个 Worker 进程在 accept 连接,从而解决惊群问题。

④当 Worker 进程 accept 到 socket 请求以后,client 会通过 send 方法发送请求(绿色虚线)给 Worker。

而 Worker 使用 recv 方法接受请求你,同时通过 parse(解析)、process(处理)、generate(生成响应)几个步骤将返回的响应通过 send 方法传送给 client,而 client 会使用 recv 方法接受响应。

最后 Worker 调用 close 方法断开和 client 的连接。

本文从 Nginx 总体架构开始,介绍了 Nginx 的主要组成部分和处理流程。然后介绍 Nginx 的 4 个进程,以及 Nginx 在启动过程中这些进程都是如何产生的。

然后聚焦到最为主要的 Master 进程的启动过程做了哪些具体的事情,特别是 Master 进程和 Worker、Cache Manager、Cache Load 之间的关系。

在进程之间的信号发送方式的章节中,我们建立了信号、发送信号、信号处理、信号掩码的概念,这有助于理解进程之间的通信。

最后,趁热打铁把 Nginx 接受网络请求以及进程之间如何合作处理请求的过程进行了讲解。

作者:崔皓

简介:十六年开发和架构经验,曾担任过惠普武汉交付中心技术专家,需求分析师,项目经理,后在创业公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构与研发管理。

编辑:陶家龙

【51CTO原创稿件,合作站点转载请注明原文作者和出处】

【编辑推荐】

来源:51CTO崔皓

免责声明:凡注明来源本网的所有作品,均为本网合法拥有版权或有权使用的作品,欢迎转载,注明出处本网。非本网作品均来自其他媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如您发现有任何侵权内容,请依照下方联系方式进行沟通,我们将第一时间进行处理。

0赞 好资讯,需要你的鼓励
来自:51CTO
0

参与评论

登录后参与讨论 0/1000

为你推荐

加载中...