关于阶段
阶段一词来自于英文中的phase,对于刚接触nginx的同学来说,即便翻遍nginx的官方文档,你也不太可能找到官方对它的解释,因为它只是nginx的一个内部实现机制,是nginx处理http请求的一个固定流程或步骤。
在计算机程序的世界里,通常在做一件复杂功能的时候,为了让其更有调理,效率更高或者把功能变的不那么复杂。一般都会把功能进行相应的拆解,将其拆成更小的功能,然后再设定一套固有的流程或步骤来调度这些小功能,并最终完成整个复杂的大功能。并且在这期间,每个小功能都各司其职,不敢(不会)越雷霆半步。
对于上面说到的固有流程或步骤,一个比较接近的例子是在网络上传输数据用到的分层模型。比如我们从google查询数据时,从录入数据到google服务器接收到数据,大致会经历如下一个流程:
应用层 -> 传输层 -> 网络层 -> 数据链路层 -> 物理层
其中,在应用层,由浏览器负责收集用户数据,并将其传递给传输层; 接着由传输层给予TCP协议建立一个可靠的链接;然后是由网络层再给予某些协议(IP等)设置一些路由信息(比如把数据送到哪个ip);再然后数链路层用arp等协议确定数据应该具体走那条路;最终由物理层将这些信息转换成特定的模式,以有线或无线的方式进行传输。
nginx中的各种阶段,跟网络模型中的各种层其实是一个道理。当有请求进入nginx的时候,nginx也会按照一个固定的流程,一个阶段一个阶段的处理请求数据,并且就像网络中的各种层有对应的协议模块一样,nginx的每个阶段也有对应的处理方法。
nginx中的阶段除了拆解和简化复杂操作外,还有一个功能是提供给外界(第三方,或是内部模块)介入nginx内部流程的一种方式。掌握nginx都提供了哪些阶段(不同的阶段做不同的事),以及这些阶段都做了什么事,对后续使用和扩展nginx功能非常重要。在此之前,先来看看第一个阶段是什么时候开始的,以便我们可以知道最早介入nginx的时机。
1什么时候开始第一个阶段
我们知道,http协议是给予TCP/IP来传输数据的,而作为客户端(比如浏览器),在和web服务器(比如nginx)进行通信的时候,第一件事肯定是先跟web服务器建立tcp连接。相应的,web服务器要做的第一件事当然就是接收客户端的tcp连接请求了,但这并不代表nginx的第一个阶段就是接收客户端连接(为啥呢?往下看)。
本篇内容要介绍的“阶段(phase)”,其实仅限于nginx对http协议处理过程中的阶段,而当nginx刚接收客户端的tcp连接请求时,还没有http协议什么事,所以此时也不会有处理http请求的阶段介入。
在明确这个结论之前,我们先用一个例子来简单看一下http从发出请求到收到数据的一个完整过程:
location / {
return 200 hello;
}
用带-v参数的curl来向上面的配置发起请求:
curl http://127.0.0.1/ -v (输出仅留下关键信息)
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 5
<
Hello
根据这个输出结果,我们来描述一下请求数据的过程:
- 首先是与web服务器(ip是127.0.0.1,端口时80)建立一个链接
- 根据http协议,在建好的链路上发送一个请求行(要单独存在一行)
- 接着继续发送相应的请求头(为了节省篇幅,我们这里只发送了一个Host请求头,实际上可以发送多个,但每个请求头必须单独存在一行)
- 所有请求头发送完毕后紧接着再有一个空行
- 如果是一个GET请求,那么至此http的请求就算发送完毕
- 如果是一个POST请求,则需在空行后输入post请求体,请求体发送完毕后整个请求也就算发送完毕了。另外,请求体大小由请求头指定(可以直接指定大小,也可以使用chunked方式)
- 剩下的输出是nginx处理完请求后输出的内容,依次是响应行、响应头、相应内容
了解http整个请求过程之后,我们可以看一下第一个可以介入的几个位置。
首先,第一步是建立tcp连接,这个在之前已经被否定了,所以肯定不是。而第七步是nginx处理完请求后,所以肯定也不是。最后剩下的位置有:第二步,此时nginx内部可以拿到具体的请求方法(GET或POST等)、请求资源(/)、协议版本(HTTP/1.1或1.0);第三步,此时可以拿到具体请求头,但是并不知道什么时候请求头结束;第四步,此时可以拿到所有请求头;第六步,此时可以拿到整个请求体。
针对上面几个可介入位置,nginx选择的是第四步,因为此时可以拿到足够多的请求信息来处理或预处理(比如根据某个请求头来决定是否是非法请求等)问题。第二步和第三步因为拿不到足够的多信息,所以不合适。而第六步因为只有post请求才有,所以也是不合适的。
从nginx的内部实现上看,它在解析完所有请求头后依次调用方法是:
ngx_http_process_request()
ngx_http_handler()
ngx_http_core_run_phases()
而整个阶段的启动则发生在ngx_http_core_run_phases() 方法中,大致代码如下:
while (ph[r->phase_handler].checker) {
rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);
if (rc == NGX_OK) {
return;
}
}
大概意思是用一个循环去逐个执行每个阶段中的方法(代码中的checker就是各个阶段对应的方法),然后根据其返回值来确定是否继续执行下去,具体实现方式后续有详细介绍,这里不做过多描述。
另外,上面三个方法的位置是:/src/http/ngx_http_request.c(第一个方法)和/src/http/ngx_http_core_module.c(第二和第三个方法)中,意犹未尽的同学可以去仔细研究一下。
2都有哪些阶段,及其作用
nginx在处理http时共分成了11个阶段,在内部用一个枚举表示,具体表示方式如下:
/src/http/ngx_http_core_module.h
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE,
NGX_HTTP_FIND_CONFIG_PHASE,
NGX_HTTP_REWRITE_PHASE,
NGX_HTTP_POST_REWRITE_PHASE,
NGX_HTTP_PREACCESS_PHASE,
NGX_HTTP_ACCESS_PHASE,
NGX_HTTP_POST_ACCESS_PHASE,
NGX_HTTP_TRY_FILES_PHASE,
NGX_HTTP_CONTENT_PHASE,
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
第一个阶段NGX_HTTP_POST_READ_PHASE
这是外界可以介入nginx处理流程最早的一个阶段,可以翻译为“读后阶段”。这里的“读后”指的是,nginx读完所有http请求头并解析完之后。
这是官方提供的,可以介入nginx的最早阶段(仅限http流程)。因为该阶段在整个处理流程中比较靠前,所以nginx的某些特性此时可能无法使用。比如用set指令定义的自定义变量,因为其执行阶段更靠后,所以此时自定义变量完全无法使用。
因此,当你决定要把某个模块注册到该阶段前,你需要确保该模块要完成的功能不依赖后续的阶段(这条法则适用于任何阶段)。
目前介入到该阶段的官方模块只有ngx_http_realip_module模块,该模块主要用来获取客户端的真实ip。
第二个阶段NGX_HTTP_SERVER_REWRITE_PHASE
该阶段仍然是一个比较早的阶段,目前介入到该阶段的官方模块只有ngx_http_rewrite_module模块。
rewrite模块下有一些我们经常用到的基础指令,比如定义自定义变量的set、用来判断的if、以及用来更改请求uri的rewrite等指令,这些指令在nginx中应用比较广泛。通常情况下,你见到的这些指令出现最多的地方应该是在location{}区块内,但遗憾的是出现在这个区块内的rewrite_module模块的指令并不会在该阶段执行。
虽然都是rewrite_module模块指令,但只有出现在server{}块内的指令才会运行在该阶段,比如:
server {
if ($uri ~ /a) {
return 200 “hello”;
}
}
当你的请求uri有“/a”时,它会直接输出字符串“hello”。并且,此时的uri匹配优先级高于location的各种模式,因为负责匹配location的阶段运行在该阶段之后。
另外一个需要注意的是,虽然rewrite_module模块下的指令只有出现在server{}区块内时才会在该阶段执行,但并不代表出现在server{}块内的指令都运行在该阶段,也不带代表只有出现在server{}块内的指令才能运行在该阶段。
nginx本身并没有对指令的出现位置和阶段有绑定关系,理论上,你可自己实现任何注册在该阶段的模块,并且模块中的指令可以出现在任何有效的(http{}区块内)区块内,然后做任何你想做的事。但有一个原则(适用任何阶段):不要破坏nginx对该阶段的一个框架限制(或者功能限制),否则可能会出现一些意想不到的事。
第三个阶段NGX_HTTP_FIND_CONFIG_PHASE
该阶段是一个内置功能,nginx没有提供任何介入该阶段的方法,任何模块都无法注册到该阶段(包括官方模块),这是一个硬性限制。
该阶段只有一个作用,根据当前uri匹配配置文件中的location,当成功匹配到一个有效的location后,会把当前location关联的信息(比如当前location有哪些指令等)一并关联到当前请求,然后就直接进入下一阶段。
关于nginx如何匹配location,可以参看<深入理解location匹配规则>
第四个阶段NGX_HTTP_REWRITE_PHASE
同第二个阶段server_rewrite_phase一样,目前介入到该阶段的官方模块只有ngx_http_rewrite_module一个。
不仅同一个rewrite_module注册到了两个阶段,并且内部注册的方法也是同一个,也就是说该模块下的指令功能在两个阶段也是相同的,不一样的是只有出现在location{}区块下的指令才会“运行”在该阶段,出现在server{}块则“运行”在第二个阶段中。
虽然任何模块都可以注册到该阶段,但一个好的实践是将自己的指令融入到rewrite模块的脚本容器中(详细内容可以看<nginx中的脚本-理论篇>和<nginx中的脚本-实战篇>),就像lua-nginx-module模块中的set_by_lua指令那样,它的作用是通过一段lua代码在nginx定义自变量。该模块实际上并没有把自己注册到rewrite阶段,而是在解析指令时做的融入,这样可以保证自己的指令同rewrite模块下指令的执行顺序相同,相当于为rewirte模块做了一个扩展指令。
第五个阶段NGX_HTTP_POST_REWRITE_PHASE
又是一个不允许任何模块介入的阶段,该阶段是专门为server_rewrite和rewrite两个阶段服务的,所以叫“rewrite后阶段”。
该阶段的主要作用是,判断uri是否被改变过,如果被改变过,则重新从find_config阶段开始执行,否则继续下一个阶段。rewrite模块中的rewrite指令可以分别在server{}区块和locaiton{}区块中更改当前请求的uri,比如:
server {
rewrite /a /b;
location /e {
rewrite /e /c;
}
}
上面这个配置的基本执行流程像这样:
- >> 当请求是“/a”的时候,server{}块中的rewrite指令起作用,会先把uri变成“/b”,然后再打一个标记(记录uri被改变),之后是进入find_config阶段,该阶段把之前的标记清除掉,然后再进行uri匹配;
- >> 当请求是“/e”的时候,location{}块中的rewrite指令起作用,所以会先进入find_config阶段进行一次匹配,当匹配成功后进入rewrite阶段,然后rewrite指令执行,此时uri被改写成“/c”并被打一个uri被改变的标记,然后进入本阶段(post_rewrite阶段),本阶段会检查当前请求是否被打过uri被改变标记,如果有,则nginx重新从find_config阶段开始执行。
第六个阶段NGX_HTTP_PREACCESS_PHASE
该阶段一般用来在access阶段执行前做个预备工作,比如像官方模块中的控流模块ngx_http_limit_req_module和ngx_http_limit_conn_module,如果流量过大,那么也就没必要继续下一个阶段做更多无畏的操作了。
目前进入到该阶段的官方模块有如下模块:
ngx_http_realip_module
ngx_http_limit_req_module
ngx_http_limit_conn_module
ngx_http_degradation_module
第七个阶段NGX_HTTP_ACCESS_PHASE
该阶段一般用来做一些权限验证,用到该阶段的官方模块如下:
ngx_http_auth_basic_module
ngx_http_access_module
ngx_http_auth_request_module
看到该阶段和这些模块后应该会更理解上一个阶段的作用:在进入到该阶段之前,可以先在上一阶段做一些流控,提前kill掉“恶意”的请求,这样该阶段也会少做一些没必要的工作,从而减少资源浪费。
第八个阶段NGX_HTTP_POST_ACCESS_PHASE
此阶段也是一个不允许任何模块介入的阶段,如果在上一个阶段中检查出该请求没有权限,一般对应的阶段方法会返回NGX_HTTP_FORBIDDEN(403),当流转到该阶段后会直接结束请求并返回403响应码。
第九个阶段NGX_HTTP_TRY_FILES_PHASE
专门为try_files指令准备的阶段,它也是一个不允许外界介入的阶段。
该指令用来确定一个文件(或目录)是否存在,它会依次检查该指令后面的文件(以“/”结尾就是目录)是否存在,只要有一个存在就不会向下检查,之后会把当前请求的uri设置成检查成功的那个文件。比如这样一个配置:
location / {
try_files /a.html /b/ /c;
}
当请求过来后,如果nginx根目录下(/nginx_path/html/)下存在一个叫a.html的文件,则当前请求的uri设置为/a.html;如果不存在,则检查根目录下是否存在一个叫/b/的目录,如果存在则当前请求的uri设置为/b/,如果还不存在则直接内部发起一个“/c”的子请求并吐出结果。
从上面这段表述可以知道,除非该指令后配置的文件(或目录)都不存在(除了最后一个配置),否则是不会直接吐出文件(或目录)中的数据的,具体如何使用改变后的uri则由下一个阶段中的默认模块来处理。这其实有点类似rewrite阶段中的rewrite指令,都是前一个阶段“打标”,后一个阶段使用,不同的是该指令不会引起uri重新匹配(最后一个子请求除外)。
第十个阶段NGX_HTTP_CONTENT_PHASE
这是nginx中真正用来处理内容的阶段,不管是用来处理静态内容的模块还是处理动态内容的模块,基本都是注册在这个阶段,比如我们常用的反向代理模块ngx_http_proxy_module。
如果有一天你需要编写自己的模块对响应内容做一些特殊处理,比如替换响应内容中的某些数据,或者你需要编写一个新的压缩算法对数据进行压缩,那么,通过该阶段介入是一个不错的选择。
该阶段是唯一一个提供了两种注册方式的阶段:一种是常规注册(nginx内部用一个数组来接收注册的模块,每个阶段都有一个对应的数据容器来接收模块注册),可以注册任意多个模块方法,并且对所有请求都起作用;另一种是跟具体的location相关的,并且只能注册一个,多余的注册会被覆盖掉,所以这种方式也是排它的(或者叫互斥)。
目前介入到该阶段的官方常规方式注册的模块有:
ngx_http_dav_module
ngx_http_autoindex_module
ngx_http_index_module
ngx_http_gzip_static_module
ngx_http_random_index_module
ngx_http_static_module
介入到该阶段的官方互斥方式注册的模块有:
ngx_http_scgi_module
ngx_http_memcached_module
ngx_http_flv_module
ngx_http_perl_module
ngx_http_empty_gif_module
ngx_http_mp4_module
ngx_http_fastcgi_module
ngx_http_uwsgi_module
ngx_http_stub_status_module
ngx_http_proxy_module
最后一个阶段NGX_HTTP_LOG_PHASE
通过名字很容易知道,这是专门为日志输出准备的阶段,该阶段目前只有一个官方模块ngx_http_log_module,它可以指定日志输出的文件和内容格式。
不过需要注意的时,虽然它是nginx中的最后一个阶段,但是它并不在ngx_http_core_run_phases()方法中被执行,只有当请求真正要结束的时候nginx才会调用注册在该阶段中的所有模块方法。
3结束语
nginx在处理http协议时把整个处理流程分成了11个阶段,本编主要简单描述了nginx中的阶段是什么,以及为什么要有阶段和各个阶段的作用是什么。
下一篇主要描述指令的“执行”顺序,到时会详细介绍阶段的介入方式、执行方式、以及各个模块方法在阶段容器中的先后顺序,因为这跟指令的“执行”顺序息息相关,另外还会介绍一些其它跟阶段关联比较紧密的功能,感兴趣的同学可以持续关注本系列。
上一篇: DFS——深度优先算法(Depth First Search)
下一篇: URL编码