Nginx指令的执行顺序
在我们接触的大部分计算机语言中,代码的执行都是有顺序的,而且大部分都是“过程性的”,通俗点就是代码的执行顺序跟书写顺序是一致的。如果把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
绑定的过程大致如下:
-
首先是计算出需要ngx_http_phase_handler_t结构体的个数,这取决于cmcf->phases中注册的handler个数和不可介入阶段的个数(有些不可介入阶段如果没有使用是不算数的),基本上一个阶段handler对应一个该结构体(一个不可介入阶段也对应一个)
-
接着是为这些结构体分配内存空间,并赋值给cmcf中的phase_engine.handlers字段,而phase_engine就是nginx真中用来执行阶段的引擎结构体(ngx_http_phase_engine_t)
-
剩下的就是通过遍历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;
}
}
根据上面的图表关系执行过程如下:
-
第一次执行到ngx_http_core_rewrite_phases时,它对应的是server-rewrite阶段,该阶段对应的模块rewrite有一条指令set $a tomcatA,等对应的“阶段handler”执行完毕后,变量$a的值为“tomcatA”。
-
第二次执行到ngx_http_core_rewrite_phases时,它对应的是rewrite阶段,此时该阶段对应的rewirte模块也有一条指令set $a tomcatB,等对应的“阶段handler”执行完毕后,变量$a的值为“tomcatB”。
-
最后执行到ngx_http_core_content_phase时,该checker判断出当前匹配的location是proxy模块的“互斥注册”,所以会执行该模块对应的“阶段handler方法”。
-
最终输出结果“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/
根据上面的图表关系执行过程如下:
- 首先按照顺序执行图表中的“checker-handler”,第一次执行到ngx_http_core_rewrite_phases时,对应的是server-rewrite阶段,但上例在该阶段没有任何指令,所以没有产生任何执行脚本的动作。
- 第二次执行到ngx_http_core_rewrite_phases时,对应的是rewrite阶段,此时该阶段对应的rewirte模块有一条指令set $a tomcatA,等对应的“阶段handler”执行完毕后,变量$a的值为“tomcatA”。
- 第一次执行到ngx_http_core_content_phase时(对应content阶段),该checker判断出当前不是 “互斥注册”,所以会执行对应的“阶段handler方法”ngx_http_index_init()。而该方法则判断出当前请求以“/”结尾,随后去根目录下寻找是否存在index.html的文件,如果存在则直接吐出这个文件的内容,并告诉阶段引擎可以停止后续“checker-handler”的执行了,否则执行权交给对应的checker方法,checker方法则把执行权交给阶段引擎,引擎会继续执行下一个“checker-handler”。
- 第二次执行到ngx_http_core_content_phase时(对应content阶段),该checker再次判断出当前不是“互斥注册”,所以会执行对应的ngx_http_autoindex_handler()方法,然后该方法判断出当前配置虽然是以“/”结尾,但并没有打开“目录清单”功能,所以不会打印出根目录的文件,随后把执行权交给对应的checker方法,checker方法则把执行权交给阶段引擎,引擎会继续执行下一个“checker-handler”。
- 第三次执行到ngx_http_core_content_phase时(对应content阶段),该checker继续判断出当前不是“互斥注册”,然后执行对应的ngx_http_static_handler()方法,该方法判断出当前请求是以“/”结尾的,它处理不了这种请求,所以最后再次把执行权交给对应的checker方法(xxx_content_phase)。但这一次checker判断出,在阶段引擎容器中已经没有任何“checker-handler”了,最后,它会先输出403,然后把执行权限交给阶段引擎。
- 最后,阶段引擎也判断出没有任何“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的内部原理,请继续关注本系列文章。