欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

关于阶段

程序员文章站 2022-05-22 21:18:04
...

阶段一词来自于英文中的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

 

根据这个输出结果,我们来描述一下请求数据的过程:

  1. 首先是与web服务器(ip是127.0.0.1,端口时80)建立一个链接
  2. 根据http协议,在建好的链路上发送一个请求行(要单独存在一行)
  3. 接着继续发送相应的请求头(为了节省篇幅,我们这里只发送了一个Host请求头,实际上可以发送多个,但每个请求头必须单独存在一行)
  4. 所有请求头发送完毕后紧接着再有一个空行
  5. 如果是一个GET请求,那么至此http的请求就算发送完毕
  6. 如果是一个POST请求,则需在空行后输入post请求体,请求体发送完毕后整个请求也就算发送完毕了。另外,请求体大小由请求头指定(可以直接指定大小,也可以使用chunked方式)
  7. 剩下的输出是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中的阶段是什么,以及为什么要有阶段和各个阶段的作用是什么。

 

下一篇主要描述指令的“执行”顺序,到时会详细介绍阶段的介入方式、执行方式、以及各个模块方法在阶段容器中的先后顺序,因为这跟指令的“执行”顺序息息相关,另外还会介绍一些其它跟阶段关联比较紧密的功能,感兴趣的同学可以持续关注本系列。