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

Nginx指令的执行顺序  

程序员文章站 2022-05-22 21:14:10
...

 

在我们接触的大部分计算机语言中,代码的执行都是有顺序的,而且大部分都是“过程性的”,通俗点就是代码的执行顺序跟书写顺序是一致的。如果把nginx的配置文件看成是一个“微型语言”,那么nginx中的指令,我们自然也会认为它是按照书写顺序执行的。

 

对于这样一个配置(并不完整,只贴出了关键指令)

server {

  set $a a;

 

  location / {

     set $a b;

     return 200 I am $a;

  }

}

按照它的书写顺序,先定义一个变量$a并赋值为“a”,然后在一个location块内重新为它赋值为“b”,如果指令的执行是过程性的,那么任何请求的输出结果都应该是“I am b”。

 

当我们用curl去验证的时候,它的输出结果也是符合预期的:

curl http://127.0.0.1/ 

I am b

对于一个看到这样结果的nginx新手,此时,你的内心应该是非常笃定,nginx的指令执行就是“过程性”的。

 

既然你的内心已经笃定,那么我们不妨再看一个例子来验证一下:

server {

   set $a a;

   location / {

      return 200 I am $a;

   }

   set $a b;

}

对于这个例子,按照它的书写顺序,当有请求时应该是这样执行:先定义一个变量$a并赋值为“a”,然后执行location中的return指令并返回结果“I am a”,最后一个赋值指令因为在location块之后,所以应该使用不到。

 

遗憾的是当我们用curl去验证的时候,这次的输出结果并没有符合预期

curl  http://127.0.0.1/

I am b

可以看到它的输出结果跟第一个例子是一样的,这个结果会让你觉得,对于指令“set $a b”位置的调整,似乎并没有影响它的执行。

 

而事实上也确实如此,因为在某些情况下,调整指令的位置并不会影响它的执行顺序。就像上面两个例子那样,虽然set指令书写顺序不一样,但其内部执行顺序完全一致。

 

我们在上一篇文章中提到过,nginx在处理http请求时是分阶段并且顺序执行的,而每个阶段执行的时候又会顺序的调用注册在该阶段上的模块handler(一个方法),该handler方法又决定了所在模块中指令的“执行”方式(或顺序),所以基本上你可以认为,在同一个阶段下某个模块提供的指令是“过程性的”,但如果阶段不同或模块不同,那它的执行顺序也是“不确定的”(指的是同阶段不同模块)。

 

为了让读者更清晰的了解指令的执行顺序,本篇会从两个方面来解释这块内容。一个是阶段的实现原理,知道了它就知道了指令(或模块)执行的内部奥秘。另一个在原理的基础上找一些例子,从实际的例子出发来验证原理的正确性。

 

1阶段的实现原理

    

因为“指令的执行顺序”基本上就是依附于阶段的,所以,要想了解“指令的执行顺序”,就必须先了解阶段的实现原理。为了让读者更好的理解这块内容,我把这块内容拆成了五部分,分别是“注册阶段”、“阶段对应的checker”、“checker和handler融合”、“阶段handler的收集顺序”、“阶段引擎执行阶段”。

下面我们先来看第一部分,如何把模块注册到阶段。

 

1.1如何把模块注册到阶段

如果一个模块想要介入到某个阶段,那么他要做的第一件事是将自己注册到该阶段。注册的方式还是比较简单的,nginx是为每个阶段准备一个容器(有序的),然后在某个时刻,模块将自己的方法(handler)放到这个容器中就可以了。

 

当然,注册的方法并不是随意定义的,需要符合nginx的规定,它必须满足如下形式:

ngx_int_t  ngx_http_xxx_handler(ngx_http_request_t *r);

在nginx中用ngx_http_handler_pt类型表示。

 

为了有一个更清晰的了解,我们拿ngx_http_rewrite模块来做个参考,该模块的对应的位置是

/src/http/modules/ngx_http_rewrite_module.c

用来负责注册的方法是

ngx_http_rewrite_init

因为该模块将自己注册到了两个阶段,所以在本方法中会有两次注册动作,它们的基本注册动作如下

ngx_http_handler_pt  *h;

ngx_http_core_main_conf_t  *cmcf;

 

cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

  

h = ngx_array_push(&cmcf->phases[NGX_HTTP_SERVER_REWRITE_PHASE].handlers)

   *h = ngx_http_rewrite_handler;

 

h = ngx_array_push(&cmcf->phases[NGX_HTTP_REWRITE_PHASE].handlers)

*h = ngx_http_rewrite_handler;

    

其中,cmcf是一个全局的结构体,可以简单理解为它和http{}区块是个一对一的关系,为了获取方便,nginx专门为其写了一个宏,就是上面的xxx_main_conf()。

 

而在cmcf中有一个用来统管所有阶段的phases字段,它的定义如下

ngx_http_phase_t  phases[NGX_HTTP_LOG_PHASE + 1];

可以看到它是一个类型是ngx_http_phase_t,长度是11的数组(NGX_HTTP_LOG_PHASE在枚举中表示最后一个阶段,且值为10),这样正好可存放11个阶段。其中真正用来存放模块注册方法的是ngx_http_phase_t中的handlers字段,它是一个动态数组,在nginx中动态数组用ngx_array_t表示。这个数组的用法并不是直接把整个元素放入数组,而是直接在数组中分配一块内存,然后返回这块内存的指针地址,有用户决定这块固定大小内存中的内容。

 

还有一个重要的方法是ngx_http_rewrite_handler,该方法每个模块就是用来注册阶段的方法,从上面的事例可以看到,rewirte模块在两个阶段都注册了同样的方法,所以该模块在这两个阶段的行为几乎也是完全一直的。当然,大部分模块只会注册一个阶段方法,如果你想注册更多的方法,nginx本身是没有限制的。

 

而用来触发阶段注册的方法是ngx_http_rewrite_init()方法,该方法会在所有配置文件解析完后由nginx负责调用。

 

在nginx中,所有可以介入的阶段都支持这种常规注册方式,不过我们在上一遍也提到过,nginx中还存在一种注册方式叫“互斥注册”,就是该阶段只能注册一个阶段方法,目前支持这种注册方式的只有NGX_HTTP_CONTENT_PHASE阶段。

 

关于在content阶段的“互斥注册”方式可以参考官方自带的proxy模块,该模块对应的代码位置在

/src/http/modules/ngx_http_proxy_module.c

对应的注册方法是

ngx_http_proxy_pass

注册的关键内容如下:

ngx_http_core_loc_conf_t  *clcf;

 

clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

clcf->handler = ngx_http_proxy_handler;

结构体clcf在之前介绍过,它在nginx可以代表一个locaiton{}区块,nginx同样为了获取方便,也为它专门写了一个宏(例子中的xxx_loc_conf)。该结构体中有一个handler字段,它的作用就是用来接收阶段要注册的方法的,该字段的定义如下

ngx_http_handler_pt  handler;

可以看到它并不是一个数组,所以它无法像常规注册方式那样注册多个方法。

 

另外一个不同的是用来触发该注册动作的ngx_http_proxy_pass()方法,它不是在所有配置文件加载完后执行的,而是在解析到的“proxy_pass”指令时执行的,这一点一定要注意,因为它是跟某个location{}块绑定的(或if in location),在解析的时候,只有进入到某个location{}块后才能拿到对应的clcf,拿到clcf后才能将方法注册到对应的clcf->handler中。

 

1.2 阶段对应的checker

尽管大部分模块都有自己对应的阶段方法,比如上面提到的rewrite模块的ngx_http_rewrite_handler()方法和proxy模块的ngx_http_proxy_handler()方法,但nginx在启动阶段执行的时候并不是直接调用这些方法,而是用了一系列叫做 checker的方法。

 

我们知道nginx中的每个阶段都会有一些特定的行为,而这些checker方法的作用其实就是用来控制这些行为的,它会把某个阶段的行为控制在一个允许的范围内。比如根据某个阶段方法(例如ngx_http_proxy_handler方法)的返回值来决定是继续执行本阶段的剩余注册方法,还是开始执行下一个阶段中的方法。再比如在find_config阶段主要的动作就是匹配location,匹配功能则执行下一阶段,匹配失败则跳过所有阶段,直接调用某个方法来结束请求。

 

在nginx中,每个checker方法会对应一个或多个阶段,但每个阶段只能对应一个checker方法,它们的对应关系在下面这个方法中形成:

ngx_http_init_phase_handlers

该方法的位置是:

 /src/http/ngx_http.c

对应的checker方法的位置是:

/src/http/ngx_http_core_module.c

对应的checker名字格式如下:

  ngx_http_core_xxx_phase

最终阶段和checker方法的对应关系如下:

阶段

checker

NGX_HTTP_POST_READ_PHASE

ngx_http_core_generic_phase

NGX_HTTP_SERVER_REWRITE_PHASE

ngx_http_core_rewrite_phase

NGX_HTTP_FIND_CONFIG_PHASE

ngx_http_core_find_config_phase

NGX_HTTP_REWRITE_PHASE

ngx_http_core_rewrite_phase

NGX_HTTP_POST_REWRITE_PHASE

ngx_http_core_post_rewrite_phase

NGX_HTTP_PREACCESS_PHASE

ngx_http_core_generic_phase

NGX_HTTP_ACCESS_PHASE

ngx_http_core_access_phase

NGX_HTTP_POST_ACCESS_PHASE

ngx_http_post_access_phase

NGX_HTTP_TRY_FILES_PHASE

ngx_http_core_try_files_phase

NGX_HTTP_CONTENT_PHASE

ngx_http_core_content_phase

NGX_HTTP_LOG_PHASE

无,它不在阶段引擎中执行


为了帮助读者对checker有一个更清晰的认识,我们就拿最常用的content阶段对应的checker方法来做个简单介绍。

 

首先从上表可以知道,content阶段对应的checker方法是ngx_http_core_content_phase(),打开该方法可以看到它的开头就是一个if判断:

if (r->content_handler) {

      // something

     ngx_http_finalize_request(r, r->content_handler(r));

     return NGX_OK;

}

其中r->content_handler的值就是我们之前提到的clcf->handler的值,如果在当前匹配的location中,有模块的绑定方式是“互斥”绑定,则nginx会直接执行该“互斥”方法并正常结束请求。

 

如果上面的if条件没有成立,则表明在当前匹配的location中不存在“互斥”绑定,那么接下来nginx会从当前阶段的常规绑定方法开始执行,如果执行到最后也没发现有效的handler(常规绑定方法或互斥绑定方法),则返回404或403响应码。

 

看了上面对content阶段对应的checker方法的介绍,我们还得出另外一个结论:在content阶段,“互斥”绑定的优先级大于常规绑定,如果两个同时存在,则会忽略常规绑定。

 

限于篇幅有限,剩下各阶段对应的checker方法这里就不多做介绍了,感兴趣的读者可以去下面这个文件里看:

/src/http/ngx_http_core_module.c

 

1.3 cheker和handler的融合

现在我们知道,nginx各个模块对应的阶段handler方法(模块用来将自己注册到某个(或某几个)阶段的方法,一般一个模块只会有一个这样的方法)都是注册在cmcf->phases容器中的,但整个阶段在执行的时候,并不是直接从cmcf->phases容器中取handler执行的,而是通过各阶段对应的checker方法间接执行的。因此,checker方法和阶段handler其实是有一个绑定关系的,这个绑定关系通过下面这个结构体来实现:

typedef struct ngx_http_phase_handler_s  ngx_http_phase_handler_t;

struct ngx_http_phase_handler_s {

    ngx_http_phase_handler_pt  checker;

    ngx_http_handler_pt        handler;

    ngx_uint_t                 next;

};

这个绑定的过程发生在如下方法中:

/src/http/ngx_http.c/ngx_http_init_phase_handlers

绑定的过程大致如下:

  1. 首先是计算出需要ngx_http_phase_handler_t结构体的个数,这取决于cmcf->phases中注册的handler个数和不可介入阶段的个数(有些不可介入阶段如果没有使用是不算数的),基本上一个阶段handler对应一个该结构体(一个不可介入阶段也对应一个)

  2. 接着是为这些结构体分配内存空间,并赋值给cmcf中的phase_engine.handlers字段,而phase_engine就是nginx真中用来执行阶段的引擎结构体(ngx_http_phase_engine_t)

  3. 剩下的就是通过遍历cmcf->phases容器,把容器中的阶段handler和checker对应上,并一个个放入到cmcf->phase_engine.handlers阶段引擎容器中。不过这里有一个小知识点需要注意一下:从整个阶段的角度来看,阶段引擎容器中的阶段handler方法顺序,和cmcf->phase容器中阶段的顺序是保持一致的,但对于同一个阶段的多个handler方法来说,他在cmcf->phase[某个阶段].handlers中的顺序,同phase_engine.handlers中的顺序是相反的

 

截止到目前我们可以知道,cmcf->phases在nginx解析配置过程中主要负责收集各个模块的handler方法,当收集完以后,“阶段handler”在cmcf->phases容器中的顺序如下:

阶段handler

阶段

模块

xxx_realip_handler

XXX_POST_READ_PHASE

realip

阶段handlerA

XXX_ACCESS_PHASE

模块1

阶段handlerB

XXX_ACCESS_PHASE

模块2

阶段handlerC

XXX_ACCESS_PHASE

模块2

阶段handlerD

XXX_CONTENT_PHASE

模块3

阶段handlerE

XXX_CONTENT_PHASE

模块4

..


而阶段引擎cmcf->phase_engine中的handlers容器主要负责维护阶段checker方法和阶段handler方法的一个对应关系,当完成映射关系后,“阶段handler”在引擎容器中的顺序如下:   

阶段handler

checker

阶段

xxx_realip_handler

xxx_core_generic_phase

XXX_POST_READ_PHASE

阶段handlerC

xxx_core_access_phase

XXX_ACCESS_PHASE

阶段handlerB

xxx_core_access_phase

XXX_ACCESS_PHASE

阶段handlerA

xxx_core_access_phase

XXX_ACCESS_PHASE

..

阶段handlerE

xxx_core_content_phase

XXX_CONTENT_PHASE

阶段handlerD

xxx_core_content_phase

XXX_CONTENT_PHASE

 

1.4 阶段handler的收集顺序

容器cmcf->phases在收集“阶段handler”时候的顺序,会直接影响其在cmcf->phase_engine.handlers容器中的顺序,而cmcf->phase_engine.handlers容器又是直接影响阶段执行顺序的关键。所以,了解“阶段handler”的收集顺序对深入理指令的执行顺序至关重要,因为指令的执行其实就发生在“阶段handler中”。

 

在1.1中介绍模块如何注册到阶段中时提到过,每个模块在进行注册时都会有方法来触发这个动作,比如rewrite模块中的ngx_http_rewrite_init()方法,基本上大部分使用常规方式注册阶段的模块,都会有一个这种形式的方法,并且会把这种方法放置在模块的“上下文”中,每个模块都有这样一个“上下文”用来表示这个它的结构体是:

typedef struct {

 ngx_int_t (*preconfiguration)(ngx_conf_t *cf);

 ngx_int_t (*postconfiguration)(ngx_conf_t *cf);

 

 void  *(*create_main_conf)(ngx_conf_t *cf);

 char  *(*init_main_conf)(ngx_conf_t *cf, void *conf);

 

 void *(*create_srv_conf)(ngx_conf_t *cf);

char  *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

 

 void  *(*create_loc_conf)(ngx_conf_t *cf);

 char *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);

} ngx_http_module_t;

总共有三组方法,用来触发注册动作的方法会放置在postconfiguration位置,从它的名字上很容看出来,该位置的方法会在配置文件解析完毕后执行。

 

在nginx内部,用来表示模块的结构体是ngx_module_t,每个模块都会把定义好的“上下文”ngx_http_module_t,放入该结构体中,最终nginx会从模块中取出这个“上下文”,然后再执行“上下文”中的postconfiguration位置的方法,并最终触发阶段注册动作。

 

调用postconfiguration位置方法的实现代码如下:

for (m = 0; ngx_modules[m]; m++) {

  if (ngx_modules[m]->type != NGX_HTTP_MODULE) {

     continue;

  }

   

  module = ngx_modules[m]->ctx;

 

  if (module->postconfiguration) {

    if (module->postconfiguration(cf) != NGX_OK) {

      return NGX_CONF_ERROR;

    }

  }

}

其中,ngx_modules数组中存放了注册在nginx中的所有模块,而ngx_modules[m]->ctx则是模块的“上下文”,因为是http模块,所以在执行postconfiguration之前,会过滤掉所有的非http模块。

 

所以我们看到,最终,容器cmcf->phases收集“阶段handler”的顺序又变成了,各个模块在ngx_modules数组中的顺序,那各个模块在该数组中的顺序又是怎么生成的呢?

 

当我们通过源码的方式安装nginx时,首先要做的是执行源码根目录下的

configure --prefix=[xxx] --with-[标准模块] --add-module=[第三方模块]

当这个动作完成后,会产生一个如下文件:

<nginx主目录>/objs/ngx_modules.c

打开这个文件后会看到对模块数组的定义

ngx_module_t  *ngx_modules[] = {

       // 各个模块的引用

}

此时数组里面存放了所有可用的模块引用,也就是说在这个时候,模块的顺序已经固定下来了,你当然可以手动修改数组中模块的顺序,但这是一个非常危险的动作,因为nginx会依赖某些标准模块顺序来完成正常功能控制。但对于非标准模块,比如第三方模块(使用--add-module安装的模块),nginx会把他们放置在一个固定的范围内,在这个范围内其实是可以*编排的。对于需要安装多个第三模块的需求,如果有必要,你可以通过两种方式来编排模块在ngx_modules[]中的顺序:一种就是执行完configure脚本后,直接在生成的objs/ngx_modules.c文件中修改;另一种是在执行configure脚本时,调整不同模块使用“--add-module=[xxx]”配置的先后顺序。

 

1.5.通过阶段引擎执行阶段

有了阶段cheker和对应的阶段handler对应关系后(ngx_http_phase_handler_t),阶段的执行就很简单了,直接遍历阶段引擎中handler容器并依次执行其中的checker就可以了。

 

阶段执行在ngx_http_core_run_phases() 方法中,且大致代码如下:

cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

ph = cmcf->phase_engine.handlers;

    

while (ph[r->phase_handler].checker) {

  rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);

       

  if (rc == NGX_OK) {

      return;

  }

}

其中r->phase_handler表示当前请求应该执行到哪个阶段handler方法,是阶段引擎容器ph的一个数组下标,这个值一般在对应的checker方法中做变动。通过该值可以拿到对应的checker,如果对应的checker方法存在,则调用该checker方法的同时,会把相应的阶段handler方法作为入参一并传入。随后引擎的执行会根据checker方法的返回值来决定是否结束,如果checker方法返回NGX_OK则本次阶段执行结束,否则继续执行下一个checker方法。

 

2一些例子

 

理论学的差不多了,下面我们通过一些例子把理论付诸实践吧。

 

2.1有指令模块的例子

现在假设tomcatA和tomcatB分别是当前配置文件中的两个upstream,无论任何请求打到这两个upstream都会直接输出他们对应的upsteam名字,在这个假设成立的前提下,我们来求解下面这个配置应该输出的内容:

server {

  set $a tomcatA;

 

  location / {

     proxy_pass http://${a} 

     set $a tomcatB;

  }

}

 

这次我们不使用curl的方式得出结果,而是使用上面学到的知识,从nginx内部运行机制来进行求解。

 

首先,刨除例子中的server{}和locaiton{}这两个指令块,最后还剩下两组指令,他们分别是率属于ngx_http_rewrite_module模块的set指令,和率属于ngx_http_proxy_module模块的proxy_pass指令。

 

从/objs/ngx_modules.c文件中可以看到,这两个模块的位置如下:

(这是我本地的顺序,你的版本不一定是这个顺序)

ngx_module_t  *ngx_modules[] {

   // other modules

   &ngx_http_rewrite_module,

   &ngx_http_proxy_module,

   // other modules

}

从上可知,两个模块在执行postconfiguration 的时候(如果有),rewrite模块是先于proxy模块的。

 

接下来试着从两个模块对应的代码文件中找出各自的postconfiguration方法,从/src/http/modules/ngx_http_rewrite_module.c文件中可以找出,rewrite模块对应的postconfiguration方法是ngx_http_rewrite_init(),通过翻阅该方法可以知道,rewrite模块按照常规注册方式,把自身的“阶段handler ”方法(ngx_http_rewrite_handler)注册到了两个阶段,分别是如下两个阶段:

NGX_HTTP_SERVER_REWRITE_PHASE

NGX_HTTP_REWRITE_PHASE

但是从/src/http/modules/ngx_http_proxy_module.c文件中并没有找到proxy模块对应的postconfiguration方法,这是因为该模块没有使用常规方式进行阶段注册,而是用的我们之前提到的“互斥注册”,这种方式发生在文件解析时,当nginx解析到“proxy_pass”指令时会触发ngx_http_proxy_pass()方法进行注册,它的“阶段handler”方法是ngx_http_proxy_handler()。虽然没有显示的注册到某个阶段,但该模块实际上是在NGX_HTTP_CONTENT_PHASE阶段执行的,可以简单理解为它就是注册到了该阶段。

 

通过以上分析,再结合之前提到的checker和阶段的对应关系,可以得到如下表: 

阶段

checker

模块

XXX_SERVER_REWRITE

xxx_core_rewrite

rewrite

XXX_REWRITE

xxx_core_rewrite

rewrite

XXX_CONTENT

xxx_core_content

proxy

 

继续往下看checker和“阶段handler”的融合,融合的过程是以cmcf->phases容器为依据,填充cmcf->phase_engine.handlers容器的过程。

 

融合完成以后,在cmcf->phase_engine.handlers容器形成如下局面:

handler

checker

阶段

xxx_rewrite_init()

xxx_core_rewrite

server_rewrite

xxx_rewrite_init()

xxx_core_rewrite

rewrite

常规方式注册的阶段handler

xxx_core_content

content

 

在这里有一点需要注意,因为proxy模块使用“互斥注册”,所以在cmcf->phase_engine.handlers容器中并不会存在一个proxy模块对应的cheker-handler关系。但这并不表示proxy模块的“阶段handler”方法不会被执行,我们之前说过,“互斥注册”方式的优先级是高于常规注册的,而这个优先级是有checker方法ngx_http_core_content_phase()来决定的。所以,只要nginx存在一个在content阶段注册的模块,那cmcf->phase_engine.handlers容器中肯定会出现一个在content阶段的checker-handler关系,这样proxy模块就可以利用这个checker-handler关系来执行自己的“阶段handler”方法了。

 

最后就是通过阶段引擎来执行这个checker-handler对应关系了,为了方便说明,我们把上面的例子拿到这里:

 server {

   set $a tomcatA;

 

   location / {

      proxy_pass http://${a} 

      set $a tomcatB;

   }

}

根据上面的图表关系执行过程如下:

  1. 第一次执行到ngx_http_core_rewrite_phases时,它对应的是server-rewrite阶段,该阶段对应的模块rewrite有一条指令set $a tomcatA,等对应的“阶段handler”执行完毕后,变量$a的值为“tomcatA”。

  2. 第二次执行到ngx_http_core_rewrite_phases时,它对应的是rewrite阶段,此时该阶段对应的rewirte模块也有一条指令set $a tomcatB,等对应的“阶段handler”执行完毕后,变量$a的值为“tomcatB”。

  3. 最后执行到ngx_http_core_content_phase时,该checker判断出当前匹配的location是proxy模块的“互斥注册”,所以会执行该模块对应的“阶段handler方法”。

  4. 最终输出结果“tomcatB”。

 

2.2无指令模块的例子

在nginx中,并非所有模块都有指令存在(或者说必须显示配置指令才能工作),当配置块中没有任何指令存在时,并不代表没有模块参与其中,比如下面的配置块:

location / {

   // nothing

}

当你试图向这个配置发起请求时,可能会有好几种结果,比如403、404或其它所谓的“首页”或“欢迎页面”。

 

但不管哪种结果,总归还是输出内容了,那这个内容究竟是怎么输出的呢?

 

在回答这个问题前,我们先了解nginx自带的三个在content阶段注册的模块:

ngx_http_static_module

ngx_http_autoindex_module

ngx_http_index_module

其中,index模块,只处理请求结尾是“/”的请求,对于如下配置:

location / {

  // nothing

}

当请求以“/”结尾时,如果对应的目录存在(比如请求是xxx/,对应根目录下有一个xxx目录),并且录下都有一个叫index.html的文件,那么nginx会吐出这个文件的内容(不是这个模块直接吐的,是一个内部重定向,最终会重定向到static模块),如果文件不存在则交给下一个模块去处理。如果目录不存在,则返回404。

 

如果请求不是以“/”结尾,那么由static模块(只处理结尾不是“/”的请求)处理这个请求,假设此时请求是“/a.html”,如果$root/目录下存在一个a.html文件,那么nginx输出这个文件内容,否则返回404。

 

而autoindex模块(只处理以“/”结尾的请求)用来打印目录清单,并且该模块默认是关闭的,当开启这个模块的时候,并且请求是“/”结尾的,并且对应目录下面没有index.html文件,则nginx直接展示目录下的文件信息,如果目录不存在不会走到这个模块(在index模块处就被拦截了)。

 

通过对这三个模块的解释,你应该已经知道了问题的答案,但这并不是我的目的,我的目的是通过这三个模块在nginx中的运行方式,再一次的阐述nginx中“指令的执行顺序”。

 

为了使例子不太单调,我在最开始的例子中加上一个set指令,如下:

location / {

  set $a tomcatA;

}

虽然这个set指令在实际生产环境并没有什么作用,但在本例中对解释“指令的执行顺序”很有帮助,所以拿过来借用一下。

 

首先,还是打开/objs/ngx_modules.c文件,从中可以看到四个模块的顺序如下:

ngx_module_t  *ngx_modules[] {

   // other modules

   &ngx_http_static_module,

   &ngx_http_autoindex_module,

   &ngx_http_index_module,

   // other modules

   &ngx_http_rewrite_module,

   // other modules

}

接下来再从以上模块对应的代码文件中找出各自的postconfiguration方法,static模块代码位置是:

/src/http/modules/ngx_http_static_module.c

对应的postconfiguration方法是

ngx_http_static_init()

从该方法可以看到,该模块的注册阶段和“阶段handler”如下

NGX_HTTP_CONTENT_PHASE

ngx_http_static_handler()

autoindex模块代码位置是

/src/http/modules/ngx_http_autoindex_module.c

对应的postconfiguration方法是

ngx_http_autoindex_init()

从这里可以看到该模块对应的注册阶段和“阶段handler”

NGX_HTTP_CONTENT_PHASE

ngx_http_autoindex_handler()

index模块位置是

/src/http/modules/ngx_http_index_module.c

对应的postconfiguration方法是

ngx_http_index_init()

它对应的阶段和“阶段handler”是

NGX_HTTP_CONTENT_PHASE

ngx_http_index_handler()

根据之前的介绍,rewrite模块的阶段和“阶段handler”是

NGX_HTTP_SERVER_REWRITE_PHASE

NGX_HTTP_REWRITE_PHASE

   

ngx_http_rewrite_handler()

因为模块在ngx_modules[]中的顺序,与其在cmcf->phases中的顺序是一致的,所以可以得到如下一个对应关系:

阶段

checker

模块

XXX_CONTENT

xxx_core_content

static

XXX_CONTENT

xxx_core_content

autoindex

XXX_CONTENT

xxx_core_content

index

XXX_SERVER_REWRITE

xxx_core_rewrite

rewrite

XXX_REWRITE

xxx_core_rewrite

rewrite

 

然后就是以cmcf->phases容器为依据,将checker和“阶段handler”融合到cmcf->phase_engine.handlers容器的过程。

 

不过这次因为有三个模块都注册在同一个阶段,所以会用到之前提到的一条规则“对于同一个阶段的多个handler方法来,它在cmcf->phase[某个阶段].handlers中的顺序,同phase_engine.handlers中的顺序是相反的”。

 

依据这条规则融合完成以后,在cmcf->phase_engine.handlers容器形成如下局面:

handler

checker

对应阶段

ngx_http_rewrite_init()

ngx_http_core_rewrite_phase

server_rewrite

ngx_http_rewrite_init()

ngx_http_core_rewrite_phase

rewrite

ngx_http_index_init()

ngx_http_core_content_phase

content

ngx_http_autoindex_handler()

ngx_http_core_content_phase

content

ngx_http_static_init()

ngx_http_core_content_phase

content

 

最后又是通过阶段引擎来执行这个checker-handler对应关系,为了方便说明,我们再次把上面的例子拿到这里:

location / {

   set $a tomcatA;

}

这次假设有如下请求:

curl http://127.0.0.1/

根据上面的图表关系执行过程如下:

  1. 首先按照顺序执行图表中的“checker-handler”,第一次执行到ngx_http_core_rewrite_phases时,对应的是server-rewrite阶段,但上例在该阶段没有任何指令,所以没有产生任何执行脚本的动作。
  2. 第二次执行到ngx_http_core_rewrite_phases时,对应的是rewrite阶段,此时该阶段对应的rewirte模块有一条指令set $a tomcatA,等对应的“阶段handler”执行完毕后,变量$a的值为“tomcatA”。
  3. 第一次执行到ngx_http_core_content_phase时(对应content阶段),该checker判断出当前不是 “互斥注册”,所以会执行对应的“阶段handler方法”ngx_http_index_init()。而该方法则判断出当前请求以“/”结尾,随后去根目录下寻找是否存在index.html的文件,如果存在则直接吐出这个文件的内容,并告诉阶段引擎可以停止后续“checker-handler”的执行了,否则执行权交给对应的checker方法,checker方法则把执行权交给阶段引擎,引擎会继续执行下一个“checker-handler”。
  4. 第二次执行到ngx_http_core_content_phase时(对应content阶段),该checker再次判断出当前不是“互斥注册”,所以会执行对应的ngx_http_autoindex_handler()方法,然后该方法判断出当前配置虽然是以“/”结尾,但并没有打开“目录清单”功能,所以不会打印出根目录的文件,随后把执行权交给对应的checker方法,checker方法则把执行权交给阶段引擎,引擎会继续执行下一个“checker-handler”。
  5. 第三次执行到ngx_http_core_content_phase时(对应content阶段),该checker继续判断出当前不是“互斥注册”,然后执行对应的ngx_http_static_handler()方法,该方法判断出当前请求是以“/”结尾的,它处理不了这种请求,所以最后再次把执行权交给对应的checker方法(xxx_content_phase)。但这一次checker判断出,在阶段引擎容器中已经没有任何“checker-handler”了,最后,它会先输出403,然后把执行权限交给阶段引擎。
  6. 最后,阶段引擎也判断出没有任何“checker-handler”了,随后将执行权限交给nginx主流程,此次请求结束。

 

2.3其它

在nginx中并非所有的指令都有执行顺序,我们这里所说的“顺序”仅限于http模块(比如官网中所有已ngx_http开头的模块),并且是依靠阶段执行的模块。

 

像upstream模块,虽然以ngx_http开头,但它的相关指令仅仅是声明式的。再比如像ngx_http_map_module模块中的map指令,它虽然类似于rewrite模块中的set指令,但它并没直接依靠阶段执行,它类似于在文件解析阶段时向变量容器中提前插入一个变量,基本上在此之后任何地方都可以使用,跟其它指令相比也没什么先后顺序。

 

还有一种可能跟执行顺序有点关系的是过滤器模块,比如ngx_http_addition_module模块,他可以在请求的前后插入一段数据,一个简单的例子如下:

location / {

  addition_types *;

 

  add_before_body  /before;

  return 200 “hello”;

  add_after_body  /after;

}

 

location /before {

  return 200 “before ”;

}

 

location /after {

  return 200 “ after”;

}

我们用如下请求访问这个配置

curl http://127.0.0.1/

可以看到它输出的结果是

before hello after

因为我们的例子写的中规中矩,所以有可能会让你误以为只有add_before_body指令在return指令之前,add_after_body指令在return指令之后才能看到这样的结果,实际上例子中的指令顺序不管如何随意调整,都会输出同样的结果。

 

另外还有一些第三方模块,它们的指令虽然依赖阶段执行,但它并没有把自己注测到某个阶段,而是把自己的指令融入到其它模块中,这样就可以有其它模块来控制其指令的执行。比如openresty中的set_by_lua指令和标准的set指令,虽然属于不同的模块,但是set_by_lua在实现的时候,就是将其融入到了set所在的模块(rewrite模块),效果相当于set_by_lua和set是同一个模块的。

3总结

    

本篇的核心是为了阐述nginx中指“令的执行顺序”,但读到最后你会发现,所谓的执行顺序,实际上是“阶段handler”的执行顺序,而“阶段handler”的执行顺序又跟它所在的阶段是分不开的。阶段靠前执行就靠前,阶段靠后执行就靠后,跟指令所在的位置其实没有直接关系。

 

所以,找到某个配置中“指令执行顺序”的关键是,找到该指令所在模块的注册阶段,指令对应的阶段找到后,执行顺序也就一目了然了。不过目前nginx官方文档并没有标识出模块所在的阶段,所以除了翻阅代码,好像也没什么其它好办法。不过好在nginx的模块名和代码布局还算规范,基本上模块的名字跟它所在的代码文件名是保持一致的,所以当你在官网上看到某个模块时,按照模块文档中的名字去源码目录中寻找对应的文件就可以了。

 

截止到这里,对于仅仅使用nginx的读者来说,如果能把之前的文章看完(排除关于代码的),并配合nginx文档,玩转nginx配置基本已经够用了,如果还想继续深入了解nginx的内部原理,请继续关注本系列文章。