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

第5章
Nginx的工作流程

详细了解Nginx的主要工作流程,可以让我们更好地认识Nginx,从而更好地使用Nginx,同时可以让我们更好地掌握Lua在Nginx中的工作流程。根据对Nginx核心技术和架构的了解,我们知道了管理进程/工作进程机制,以及HTTP核心模块和配置对Nginx的重要性,也知道了Nginx的框架代码不多,只负责环境管理,本章将介绍这些部分的工作流程。

5.1 Nginx的启动流程

Nginx启动时分为两部分:

1)框架程序启动过程:这个阶段会创建各核心模块和非核心模块。

2)模块启动过程:模块内部完成自己的启动和初始化部分。

Nginx启动过程如图5-1所示。每个过程具体的任务如下:

1)启动时,Nginx接受命令行参数,解析出各主要参数。因为Nginx的参数主要放在nginx.conf中,所以最重要的参数是nginx.conf的路径。

2)平滑升级是指不重启服务而进行升级,不重启管理进程而启动新版本的Nginx程序。旧的管理进程先调用fork函数创建(即分叉)一个新进程,然后新进程通过execve系统调用启动新版本管理进程,旧版本管理进程首先设置环境变量,新版本管理进程启动时检查对应环境变量知道是平滑升级,并对通过环境变量传递的旧版本Nginx服务监听的句柄做继承处理。

3)框架通过调用核心模块的create_conf方法让核心模块创建用于存储对应配置信息的结构体创建核心模块。一直到第8步,都是基于配置文件对环境和模块进行初始化的。这一步为后面的配置文件解析做好准备工作。

4)调用配置模块的解析方法,解析nginx.conf中的配置项。调用对应核心模块的方法将属于各核心模块的配置项保存到核心模块的配置数据结构中。

5)调用所有核心模块的init_conf方法,用于让核心模块根据写入内部配置数据结构的数据对模块做处理和初始化。

6)配置文件中可能配置了缓存文件、库文件、日志文件等,同时包括共享内存,在这一步对这些文件和共享内存进行创建、打开操作。

7)对于配置了监听端口的模块,按配置开始监听配置的端口。一般HTTP模块、stream模块都会有监听端口。

8)调用所有模块的init_module方法,使用配置信息初始化模块。

9)如果nginx.conf中配置了Nginx为master模式(一般都是这种模式),则创建管理进程。

10)管理进程根据配置的工作进程数,使用一个循环将所需要的工作进程分叉出来。

11)管理进程根据配置解析过程时解析出来的配置信息,检查相应path配置是否配置值,如果配置了,则分叉出独立的cache manager进程(这是一个和工作进程并级的进程)。将后端服务器的应答使用文件缓存下来,下次请求时不需要再向后端发送请求,一般用在upstream{}中。缓存管理器这个进程定期检查缓存状态、查看缓存总量是否超出限制,如果超出则删除最少使用的部分。cache manager会定期删除过期缓存文件。

12)同第11步,管理进程根据配置文件查看是否对应的path路径配置了路径,如果配置了,则分叉出cache loader进程,并且延迟1分钟运行。cache loader进程会遍历配置文件中proxy_cache_path指定的缓存路径中所有的缓存文件。根据缓存文件的MD5编码遍历由cache manager进程生成的内存中的缓存文件红黑树和节点结构(ngx_http_file_cache_node_t)。如果不存在,则创建新的节点,并将对象的rbnode和queue分别插到红黑树和过期队列中;如果存在,则更新相应属性。cache loader进程实现的是根据缓存文件进行索引重建工作,即在Nginx服务重新启动将之前的缓存文件重建索引起来。该进程工作一段时间后将自动退出。

13)管理进程调用所有模块的init_process方法。此时,工作进程的启动工作就完成了,工作进程进入自己的消息循环中开始等待处理用户请求。

14)如果Nginx是single模式,则直接调用所有模块的init_process方法,直接以single模式启动完毕。单进程模式下,网络端口监听、数据处理等均由管理进程处理,多进程模式下,网络连接和数据处理等由工作进程处理。不管哪种模式,网络端口都是由管理进程创建的。single模式一般用于调试。

框架代码负责创建核心模块和功能模块,然后由各进程和模块配合起来向用户提供服务。下面将分别解析主要进程和服务的工作流程。

图5-1 Nginx启动流程

5.2 管理进程的工作流程

管理进程的工作比较简单,它只是管理工作等子进程,实现重启服务、平滑升级、更换日志文件、动态重新装载配置文件等操作,不需要处理网络任务。

用户通过信号操作管理进程。管理进程内部设置了信号,并注册了相应的handler,信号发生时,会调用相应的handler处理对应的请求。我们通过命令行参数操作Nginx,内部的实现是通过管理进程接收用户输入的信号并处理实现的。

管理进程信号定义如表5-1所示。

表5-1 管理进程信号定义

管理进程收到信号后,会设置内部定义的7个变量,在主循环中,根据这7个变量决定ngx_master_process_cycle方法如何执行,实现内部调用,完成预定的逻辑。

管理进程通过fork命令创建工作进程,并将工作进程号保存到内部进程变量表(ngx_processes)中,管理进程依靠信号改变表中进程的状态。当子进程意外退出时,管理进程作为父进程,会收到Linux内核发来的CHILD信号,根据进程ID修改内存中的进程状态。

Nginx设计管理进程/工作进程机制的目的是让管理进程监控和管理工作进程,当工作进程意外终止时,管理进程需要启动新的工作进程接替终止进程,实现系统的连续运行能力,提供高可靠性。下面介绍管理进程的工作循环机制,从中也可以看到其如何创建新工作进程的机制如图5-2所示。

管理进程的工作流程如图5-2所示。

管理进程的工作循环根据8个状态位(7个信号对应状态位与1个no_noaccept状态位)执行不同的代码路径。每当一个循环执行完毕后进程便被挂起,直到有新的信号才会被激活并继续执行。

1)如果ngx_reap为0,则执行第2步;如果ngx_reap为1,表示要监控所有的子进程,检查每个子进程的状态,非正常退出的子进程会重新启动。本阶段会返回一个live状态,0表示所有子进程均已经正常退出,1表示所有子进程均正常。

2)当live为0,同时ngx_terminate为1或者ngx_quit为1时,退出管理进程,并首先删除保存管理进程号的pid文件。

3)调用所有模块的exit_master方法。

4)关闭所有监听端口。

5)销毁内存池,退出管理进程。

6)如果ngx_terminate为1,则向所有子进程发送TERM信号,通知子进程执行强行退出流程,然后跳转到第1步挂起进程。

7)如果ngx_quit为1,表示需要“优雅”地退出服务,向所有子进程发送QUIT信号;否则判断ngx_reconfigure标志。

图5-2 管理进程的工作流程

8)继续执行ngx_quit为1的分支操作,关闭所有监听端口,然后跳转到第1步挂起进程。

9)当ngx_reconfigure为1时,重新读取配置文件。在这个过程中,管理进程首先重新初始化配置结构体,用来读取新的配置文件,再创建新的工作进程,然后销毁旧的工作进程。

10)使用新配置创建新的进程。

11)根据缓存模块中的配置,决定是否创建新的cache manager或cache loader进程,同时将内部live标志置1。

12)向旧的子进程发送QUIT信号,旧子进程“优雅”地退出。

13)启动子进程。

14)根据是否有缓存文件决定启动cache manage或cache loader,同时将live置1。

15)如果ngx_reopen标志为1,重新打开所有文件。

16)向所有子进程发送USR1信号,要求子进程重新打开所有文件。

17)检查ngx_change_binary标志位,为1表示需要平滑升级,则创建新的工作进程。

18)如果ngx_noaccept标志位为1,则向所有子进程发送QUIT信号,让子进程“优雅”地退出。如果ngx_noaccept为0则跳转到第1步。

5.3 工作进程的工作流程

Nginx的业务处理是在工作进程中完成的,但实际上是通过工作进程协调各模块组件完成任务的。工作进程由管理进程管理,它们之间的工作机制是通过信号实现的,工作进程中有一个专用的方法处理信号,工作进程关注4个信号,对应到4个全局变量,分别为ngx_terminate、ngx_quit、ngx_exiting、ngx_reopen。

工作进程的工作流程如图5-3所示。

图5-3 工作进程的工作流程

工作进程处理4个信号量,作为主流程,否则工作循环处理网络event事件,处理HTTP业务流程。

5.4 配置加载流程

Nginx服务是通过nginx.conf配置文件实现的,Nginx是多模块架构,在框架启动流程中,每个模块都会为自己创建一个配置信息数据结构,而框架又会调用模块init_conf接口,将配置项加载到模块一级。所有配置项中的配置以配置块为单位,而配置块又是与内部模块对应的。配置项配置信息保存在模块中。

下面是一个典型的nginx.conf配置文件。

#user  nobody;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }

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

在这个最典型的简单例子中,最外层未在块中的配置信息是全局配置信息,保存在框架变量中。events{}配置块则保存到event模块中。http{}配置块则由HTTP模块解析并保存,而当HTTP模块遇到http{}、server{}、location{}配置块时,会创建新的配置块数据结构保存信息,如果http{}配置块中有多个server{}或location{},则内存将会有多个配置数据结构。

框架程序在解析配置文件时,通过3个HTTP模块的回调函数将配置信息传给HTTP模块:create_main_conf、create_svr_conf、create_loc_conf,3个函数分别对应http{}、server{}、location{}配置块。create_main_conf只会被调用一次,而create_svr_conf和create_loc_conf则可能会被调用多次,这取决于配置情况。HTTP模块会多次生成配置数据结构保存配置信息。

取决于Nginx的这种配置实现,Nginx其实就有了一些特性,可以在不同的位置配置多个location,我们来看下面这个配置例子:

http{
    my_test hello;

    server{
        listen 80;
        my_test is80;

        location /t1{
            my_test test1;
        }

        location /t2{
            my_test test2;
        }
    }

    server{
        listen 8080;
        my_test is8080;
        loation /t3{
            my_test test3;
        };
    }
}

在这个例子中,http{}中定义了2个server,同时在3个location中重新赋值了my_test,那么,my_test的哪个值生效呢?my_test一共有hello、is80、test1、test2、is8080、test3这几个取值。本配置共监听了2个端口:80和8080,共提供了/t1、/t2、/t33个URL请求。my_test的值其实是由访问的URL决定的,即

http://localhost/t1               test1
http://localhost/t2             test2
http://localhost/t3             404
http://localhost:8080/t1        404
http://localhost:8080/t3        test3

上面解析中提供了2个错误URL请求的示例,按照配置正确访问3个URL则可以得到对应的my_test值。

1. 配置文件加载和解析的过程

配置文件加载和解析的过程参见图5-1的Nginx启动流程。主要过程如下:

1)从Nginx命令行得到配置文件路径。

2)调用所有核心模块的create_conf方法,生成配置项结构体。

3)框架代码解析nginx.conf核心模块部分。

4)调用所有核心模块的init_conf方法,解析对应的配置块。

2. HTTP配置块解析过程

核心模块将配置主业务分解后,下一阶段就是核心模块的配置加载和解析过程了,下面以HTTP模块为例解析具体解析过程。

图5-4描述了在主架构内HTTP配置块解析过程。

图5-4 HTTP配置项解析过程

整个过程比较简洁易懂,不再重复描述。程序主架构指的是Nginx的框架代码,在系统启动时调用配置文件解析器解析nginx.conf文件。

Nginx系统配置文件解析还有一个渠道,就是管理进程提供的重新载入命令,一般是“nginx-s reoad”,在管理进程收到这个命令或消息时,会初始化存放配置信息的数据结构,然后使用新配置启动新的工作进程,然后杀掉旧的工作进程。这个过程也会触发上面的流程再次执行。

5.5 HTTP框架初始化流程

HTTP模块是Nginx中的核心模块,作为一个Web服务器而言,Nginx是工作在HTTP协议之上的,所以HTTP业务的处理是最重要的工作。处理HTTP协议,是基于网络层的,HTTP是TCP协议。HTTP部分是由核心模块ngx_http_module、ngx_http_core_module、ngx_http_upstream_module模块组成的。

HTTP模块的主要职责如下:

1)调用HTTP框架提供的接口发送HTTP应答。

2)分解出若干子请求进行复杂业务的处理,子请求也是全异步非阻塞式处理。

3)将一个HTTP请求分为顺序化的多个处理阶段,依次进行流水线式处理。如果前一个流水线阶段决定中断处理,则后续流水线处理部分将不会被调用到。

4)异步接收HTTP请求中的包体,可以将数据缓存到文件中。

5)异步访问第三方服务。

6)处理已经解析完的HTTP请求。

7)解析nginx.conf中属于自己的配置项。在不同的http{}、server{}、location{}下的配置项,都需要正确解析。

HTTP框架代码需要具备下述功能:

1)向HTTP模块提供磁盘I/O功能和网络I/O功能。

2)提供upstream机制使HTTP模块可以访问第三方服务。

3)使用event模块,以处理所有网络事件。

4)提供子请求机制实现子请求功能。

5)辨别接收到的TCP流是否是完整的HTTP报文。

6)根据请求中的URI和头域,根据配置文件将请求分发到对应的模块,调用模块注册的函数处理请求。

7)解析和处理nginx.conf文件,解析http{}配置块,解析http{}下的server{}、location{}等子配置块,可以处理同名配置项出现在不同配置块中的情况。

图5-5描述了HTTP框架初始化流程。

图5-5 HTTP框架初始化流程

主要流程简述如下:

1)初始化ngx_module数据中所有HTTP模块的ctx_index字段,从0开始递增。这个索引就是请求响应时调用的顺序,而这个顺序最终是由编译Nginx时的模块顺序决定的,同时初始化存放配置信息的ngx_http_conf_ctx_t数据结构。

2)依次调用所有HTTP模块的create_main_conf方法,产生的配置结构体指针按照各模块的ctx_index字段顺序放入ngx_http_conf_ctx_t的main_conf数组。

3)依次调用所有HTTP模块的create_svr_conf方法,产生的配置结构体指针按照各模块的ctx_index字段顺序放入ngx_http_conf_ctx_t的svr_conf数组。

4)依次调用所有HTTP模块的create_loc_conf方法,产生的配置结构体指针按照各模块的ctx_index字段顺序放入ngx_http_conf_ctx_t的loc_conf数组。

5)依次调用所有模块的preconfiguration方法,preconfiguration回调函数完成了对应模块的预处理操作,其主要工作是创建模块用到的变量。

6)调用所有HTTP模块的init_conf方法,告诉模块配置解析完成。

7)合并配置项。

8)Nginx将HTTP处理过程划分成了11个阶段,使多个模块可以介入到不同的阶段进行流水线式操作,充分发挥模块式架构的优势,并实现请求过程异步化。其中有7个阶段是允许用户介入的:NGX_HTTP_POST_READ_PHASE、NGX_HTTP_SERVER_REWRITE_PHASE、NGX_HTTP_REWRITE_PHASE、NGX_HTTP_PREACCESS_PHASE、NGX_HTTP_ACCESS_PHASE、NGX_HTTP_CONTENT_PHASE、NGX_HTTP_LOG_PHASE。调用ngx_http_init_phases方法初始化这7个动态数组,数据保存在phases数组中。

9)依次调用所有HTTP模块的postconfiguration方法,使HTTP模块可以处理HTTP阶段,将HTTP模块的ngx_http_handler_pt处理方法添加到HTTP阶段中。

10)构建虚拟主机的查找散列表。虚拟主机配置在server{}中,为了提高请求时查找的速度,使用散列表对主机server name进行了索引。

11)建立server与监听端口间的关联,同时设置新连接的回调方法。

5.6 HTTP模块调用流程

HTTP框架执行流程涉及底层事件模型,全异步的工作方式比较复杂,对于Nginx下的Lua开发指导意义不大,所以本书不会展开描述这部分知识,有需要的读者请自行研究。本节对HTTP模块间的调用流程进行简单介绍,这个流程将向我们展示配置的模块间的关系和分工,便于对HTTP模块有一个整体了解。

图5-6描述了一个简略的HTTP模块调用流程,精简了很多过程,去除了异步的处理机制。

图5-6 HTTP模块调用流程

工作进程在主循环调用事件模型,检测网络事件,当有新连接请求时,则建立TCP连接,然后根据nginx.conf配置,将请求交由HTTP框架处理。框架首先尝试接收HTTP头部,接收到完整HTTP头部后,将请求分发到具体的HTTP模块处理。通常根据URI和nginx.conf里的location匹配程度决定分发策略。请求处理结束时,通常都向客户端发送响应,这时一般自动依次调用所有的HTTP过滤模块,每个模块根据配置文件中定义的策略决定自己的行为,如可调用gzip模块根据nginx.conf中“gzip on|off;”决定是否将响应压缩。如果设置了子请求调用,在返回前还会执行异步的子请求调用。

5.7 HTTP请求处理流程

Nginx中的request指的是HTTP请求,在Nginx中对应的数据结构是ngx_http_request_t。ngx_http_request_t是对HTTP请求的封装。根据HTTP规范,一个HTTP请求包含请求行、请求头、请求体、响应行、响应头、响应体。

HTTP请求是典型的请求–响应类型的网络协议,而HTTP是文件协议,所以在分析请求行与请求头,以及输出响应行与响应头时,往往是一行一行地处理。如果自己编写一个HTTP服务器,通常在一个连接建立好后,客户端会发送请求过来。读取一行数据,分析请求行中包含的method、uri、http_version信息。然后一行一行处理请求头,并根据请求method与请求头的信息来决定是否有请求体以及请求体的长度,再去读取请求体。得到请求后,我们处理请求产生需要输出的数据,生成响应行、响应头以及响应体。在将响应发送给客户端之后,一个完整的请求就处理完了。这是最简单的webserver的处理方式,其实Nginx也是这样做的,只是有一些小的区别,例如,当请求头读取完成后,就开始进行请求的处理了。Nginx通过ngx_http_request_t来保存解析请求并输出响应相关的数据。

Nginx处理一个完整的请求过程是这样的。对于Nginx来说,从ngx_http_init_request开始处理一个请求,在这个函数中,会设置读事件为ngx_http_process_request_line,表示接下来的网络事件,会由ngx_http_process_request_line执行。ngx_http_process_request_line是来处理请求行的。通过ngx_http_read_request_header读取请求数据,然后调用ngx_http_parse_request_line函数解析请求行。Nginx为提高效率,采用状态机解析请求行,而且在进行method比较时,没有直接使用字符串比较,而是将4个字符转换成1个整型数据,然后一次比较以减少CPU的指令数。一个请求行包含请求的方法、URI、版本,请求行也可以包含host。例如,“GET http://www.google.com/uri HTTP/1.0 ”这样一个请求行是合法的,host是www.google.com。这个时候,Nginx会忽略请求头中的host域,而以请求行中的为准查找虚拟主机。另外,HTTP 0.9是不支持请求头的,所以这里也要特别地处理。整个请求行解析的参数会保存到ngx_http_request_t结构当中。

在解析完请求行后,Nginx会使用ngx_http_process_request_headers设置读事件的handler,然后后续的请求就在ngx_http_process_request_headers中进行读取与解析。ngx_http_process_request_headers函数用来读取请求头,跟请求行一样,还是调用ngx_http_read_request_header读取请求头,调用ngx_http_parse_header_line解析一行请求头,解析到的请求头会保存到ngx_http_request_t的域headers_in中,headers_in是一个链表结构,保存所有的请求头。而HTTP中有些请求是需要特别处理的,这些请求头与请求处理函数存放在一个映射表里面,即ngx_http_headers_in。在初始化时,会生成一个hash表,每当解析一个请求头后,就会先在hash表中查找,如果找到,则调用相应的处理函数处理这个请求头。例如,host头的处理函数是ngx_http_process_host。

当Nginx解析到两个回车换行符时,就表示请求头已经结束,此时会调用ngx_http_process_request处理请求。ngx_http_process_request会设置当前连接的读写事件处理函数为ngx_http_request_handler,然后调用ngx_http_handler真正开始处理一个完整的HTTP请求。读写事件处理函数ngx_http_request_handler在代码中会根据当前事件是读事件还是写事件,分别调用ngx_http_request_t中的read_event_handler或者write_event_handler。由于此时,请求头已经读取完成,因为Nginx的做法是先不读取请求body,所以这里面设置read_event_handler为ngx_http_block_reading,即不读取数据。真正开始处理数据,是在ngx_http_handler这个函数里面,这个函数会设置write_event_handler为ngx_http_core_run_phases,并执行ngx_http_core_run_phases函数。ngx_http_core_run_phases函数将执行多阶段请求处理,Nginx将一个HTTP请求的处理分为多个阶段,那么这个函数执行这些阶段来产生数据。ngx_http_core_run_phases最终调用处理请求,产生的响应头会放在ngx_http_request_t的headers_out中。Nginx的各种阶段会对请求进行处理,最后调用filter过滤数据,对数据进行加工,如truncked传输、gzip压缩等。filter是一个链表结构,分别有header filter(对响应头进行处理)与body filter(对响应体进行处理),先执行header filter中的所有filter,然后执行body filter中的所有filter。header filter中的最后一个filter,即ngx_http_header_filter,这个filter会遍历所有的响应头,最后需要输出的响应头在一个连续的内存中,然后调用ngx_http_write_filter进行输出。ngx_http_write_filter是body filter中的最后一个,所以Nginx收到的body信息经过一系列的body filter之后,最后也会调用ngx_http_write_filter输出。

这里要注意的是,Nginx会将整个请求头都放在一个buffer里面,这个buffer的大小通过配置项client_header_buffer_size设置。如果用户请求头太大,该buffer装不下,Nginx会重新分配一个更大的buffer装载请求头。这个更大的buffer可以通过large_client_header_buffers设置,这个大buffer是一组buffer,如配置48KB,表示有4个8KB大小的buffer可以用。注意,为了保存请求行或请求头的完整性,一个完整的请求行或请求头需要放在一个连续的内存里面,所以,一个完整的请求行或请求头只会保存在一个buffer里面。这样,如果请求行大于一个buffer的大小,就会返回414错误,如果一个请求头大于一个buffer的大小,就会返回400错误。了解了这些参数的值,以及Nginx的实际做法之后,在具体的应用场景下,就可以根据实际的需求调整这些参数。

5.8 小结

本章介绍了Nginx的启动流程、管理进程与工作进程的工作流程、配置项加载流程、HTTP模块初始化流程、HTTP模块调用流程及HTTP请求处理流程。通过对流程的介绍,可以让我们对各个阶段工作的上下文比较了解,更容易理解API的工作条件,有助于我们开发更高性能的Lua应用。 aGMbL0WpcOotx+b90cuwqiHJD8Hl1P4Waj93ornAbG+VjSxBVhtkRbY3iF1+3HXO

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