`
deyimsf
  • 浏览: 66702 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

关于阶段

 
阅读更多

阶段一词来自于英文中的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中的阶段是什么,以及为什么要有阶段和各个阶段的作用是什么。

 

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

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics