Nginx 指令的执行顺序
首先很感谢@agentzh的Nginx教程,通过这个文章,也是能够基本了解了Nginx的执行过程。在这里,简单的做一个记录,方便未来的翻看。
nginx的处理过程总共分为11个阶段,分别是post-read
、server-rewrite
、find-config
、rewrite
、post-rewrite
、preaccess
、access
、post-access
、try-files
、content
以及 log
.
post-read阶段:
该阶段是在Nginx读取并解析完请求头(request headers)之后就立即开始执行的,标准模块的ngx_realip就是在post-read阶段注册了处理程序。它的功能是迫使 Nginx 认为当前请求的来源地址是指定的某一个请求头的值。
server {
listen 8080;
set_real_ip_from 127.0.0.1;
real_ip_header X-My-IP;
location /test {
set $addr $remote_addr;
echo "from: $addr";
}
}
这里的配置是让 Nginx 把那些来自 127.0.0.1 的所有请求的来源地址,都改写为请求头 X-My-IP 所指定的值。同时该例使用了标准内建变量 $remote_addr 来输出当前请求的来源地址,以确认是否被成功改写。
server-rewrite阶段
当 ngx_rewrite 模块的配置指令直接书写在 server 配置块中时,基本上都是运行在 server-rewrite 阶段
server {
listen 8080;
location /test {
set $b "$a, world";
echo $b;
}
set $a hello;
}
配置语句 set $a hello 直接写在了 server 配置块中,因此它就运行在 server-rewrite 阶段
find-config 阶段
紧接在 server-rewrite 阶段后边的是 find-config 阶段。这个阶段并不支持 Nginx 模块注册处理程序,而是由 Nginx 核心来完成当前请求与 location 配置块之间的配对工作。换句话说,在此阶段之前,请求并没有与任何 location 配置块相关联。因此,对于运行在 find-config 阶段之前的 post-read 和 server-rewrite 阶段来说,只有 server 配置块以及更外层作用域中的配置指令才会起作用。
location /hello {
echo "hello world";
}
如果启用了 Nginx 的“调试日志”,那么当请求 /hello 接口时,便可以在 error.log 文件中过滤出下面这一行信息:
$ grep 'using config' logs/error.log
[debug] 84579#0: *1 using configuration "/hello"
rewrite 阶段
运行在 find-config 阶段之后的便是我们的老朋友 rewrite 阶段。由于 Nginx 已经在 find-config 阶段完成了当前请求与 location 的配对,所以从 rewrite 阶段开始,location 配置块中的指令便可以产生作用。前面已经介绍过,当 ngx_rewrite 模块的指令用于 location 块中时,便是运行在这个 rewrite 阶段。
post-rewrite 阶段
rewrite 阶段再往后便是所谓的 post-rewrite 阶段。这个阶段也像 find-config 阶段那样不接受 Nginx 模块注册处理程序,而是由 Nginx 核心完成 rewrite 阶段所要求的“内部跳转”操作(如果 rewrite 阶段有此要求的话)。
server {
listen 8080;
location /foo {
set $a hello;
rewrite ^ /bar;
}
location /bar {
echo "a = [$a]";
}
}
这里在 location /foo 中通过 rewrite
指令把当前请求的 URI 无条件地改写为 /bar,同时发起一个“内部跳转”,最终跳进了 location /bar 中。这里比较有趣的地方是内部跳转
的工作原理。内部跳转
本质上其实就是把当前的请求处理阶段强行倒退到 find-config
阶段,以便重新进行请求 URI 与 location 配置块的配对。比如上例中,运行在 rewrite
阶段的 rewrite 指令就让当前请求的处理阶段倒退回了 find-config
阶段。由于此时当前请求的 URI 已经被 rewrite 指令修改为了 /bar,所以这一次换成了 location /bar 与当前请求相关联,然后再接着从 rewrite
阶段往下执行。
不过这里更有趣的地方是,倒退回 find-config
阶段的动作并不是发生在 rewrite
阶段,而是发生在后面的 post-rewrite
阶段。上例中的 rewrite 指令只是简单地指示 Nginx 有必要在 post-rewrite 阶段发起“内部跳转”。这个设计对于 Nginx 初学者来说,或许显得有些古怪:“为什么不直接在 rewrite 指令执行时立即进行跳转呢?”答案其实很简单,那就是为了在最初匹配的 location 块中支持多次反复地改写 URI,例如:
location /foo {
rewrite ^ /bar;
rewrite ^ /baz;
echo foo;
}
location /bar {
echo bar;
}
location /baz {
echo baz;
}
这里在 location /foo 中连续把当前请求的 URI 改写了两遍:第一遍先无条件地改写为 /bar,第二遍再无条件地改写为 /baz. 而这两条 rewrite 语句只会最终导致 post-rewrite 阶段发生一次“内部跳转”操作,从而不至于在第一次改写 URI 时就直接跳离了当前的 location 而导致后面的 rewrite 语句没有机会执行。请求 /foo 接口的结果证实了这一点:
$ curl localhost:8080/foo
baz
从输出结果可以看到,上例确实成功地从 /foo 一步跳到了 /baz 中。如果启用 Nginx “调试日志”的话,还可以从 find-config 阶段生成的 location 块的匹配信息中进一步证实这一点:
$ grep 'using config' logs/error.log
[debug] 89449#0: *1 using configuration "/foo"
[debug] 89449#0: *1 using configuration "/baz"
我们看到,对于该次请求,Nginx 一共只匹配过 /foo 和 /baz 这两个 location,从而只发生过一次“内部跳转”。
当然,如果在 server 配置块中直接使用 rewrite 配置指令对请求 URI 进行改写,则不会涉及“内部跳转”,因为此时 URI 改写发生在 server-rewrite 阶段,早于执行 location 配对的 find-config 阶段。比如下面这个例子:
server {
listen 8080;
rewrite ^/foo /bar;
location /foo {
echo foo;
}
location /bar {
echo bar;
}
}
这里,我们在 server-rewrite 阶段就把那些以 /foo 起始的 URI 改写为 /bar,而此时请求并没有和任何 location 相关联,所以 Nginx 正常往下运行 find-config 阶段,完成最终的 location 匹配。如果我们请求上例中的 /foo 接口,那么 location /foo 根本就没有机会匹配,因为在第一次(也是唯一的一次)运行 find-config 阶段时,当前请求的 URI 已经被改写为 /bar,从而只会匹配 location /bar.
preaccess 阶段
标准模块 ngx_limit_req 和 ngx_limit_zone 就运行在此阶段,前者可以控制请求的访问频度,而后者可以限制访问的并发度。
access 阶段
标准模块 ngx_access、第三方模块 ngx_auth_request 以及第三方模块 ngx_lua 的 access_by_lua 指令就运行在这个阶段。
post-access 阶段
post-access 阶段主要用于配合 access 阶段实现标准 ngx_http_core 模块提供的配置指令 satisfy 的功能。
对于多个 Nginx 模块注册在 access 阶段的处理程序, satisfy 配置指令可以用于控制它们彼此之间的协作方式。比如模块 A 和 B 都在 access 阶段注册了与访问控制相关的处理程序,那就有两种协作方式,一是模块 A 和模块 B 都得通过验证才算通过,二是模块 A 和模块 B 只要其中任一个通过验证就算通过。第一种协作方式称为 all 方式(或者说“与关系”),第二种方式则被称为 any 方式(或者说“或关系”)。默认情况下,Nginx 使用的是 all 方式。下面是一个例子:
location /test {
satisfy all;
deny all;
access_by_lua 'ngx.exit(ngx.OK)';
echo something important;
}
这里,我们在 /test 接口中同时配置了 ngx_access 模块和 ngx_lua 模块,这样 access 阶段就由这两个模块一起来做检验工作。其中,语句 deny all 会让 ngx_access 模块的处理程序总是拒绝当前请求,而语句 access_by_lua 'ngx.exit(ngx.OK)' 则总是允许访问。当我们通过 satisfy 指令配置了 all 方式时,就需要 access 阶段的所有模块都通过验证,但不幸的是,这里 ngx_access 模块总是会拒绝访问,所以整个请求就会被拒.
try-files 阶段
这个阶段专门用于实现标准配置指令 try_files 的功能,并不支持 Nginx 模块注册处理程序。
try_files 指令接受两个以上任意数量的参数,每个参数都指定了一个 URI. 这里假设配置了 N 个参数,则 Nginx 会在 try-files 阶段,依次把前 N-1 个参数映射为文件系统上的对象(文件或者目录),然后检查这些对象是否存在。一旦 Nginx 发现某个文件系统对象存在,就会在 try-files 阶段把当前请求的 URI 改写为该对象所对应的参数 URI(但不会包含末尾的斜杠字符,也不会发生 “内部跳转”)。如果前 N-1 个参数所对应的文件系统对象都不存在,try-files 阶段就会立即发起“内部跳转”到最后一个参数(即第 N 个参数)所指定的 URI.
root /var/www/;
location /test {
try_files /foo /bar/ /baz;
echo "uri: $uri";
}
location /foo {
echo foo;
}
location /bar/ {
echo bar;
}
location /baz {
echo baz;
}
这里通过 root 指令把“文档根目录”配置为 /var/www/,如果你系统中的 /var/www/ 路径下存放有重要数据,则可以把它替换为其他任意路径,但此路径对运行 Nginx worker 进程的系统帐号至少有可读权限。我们在 location /test 中使用了 try_files 配置指令,并提供了三个参数,/foo、/bar/ 和 /baz. 根据前面对 try_files 指令的介绍,我们可以知道,它会在 try-files 阶段依次检查前两个参数 /foo 和 /bar/ 所对应的文件系统对象是否存在。
不妨先来做一组实验。假设现在 /var/www/ 路径下是空的,则第一个参数 /foo 映射成的文件 /var/www/foo 是不存在的;同样,对于第二个参数 /bar/ 所映射成的目录 /var/www/bar/ 也是不存在的。于是此时 Nginx 会在 try-files 阶段发起到最后一个参数所指定的 URI(即 /baz)的“内部跳转”。实际的请求结果证实了这一点:
$ curl localhost:8080/test
baz
显然,该请求最终和 location /baz 绑定在一起,执行了输出 baz 字符串的工作。上例中定义的 location /foo 和 location /bar/ 完全不会参与这里的运行过程,因为对于 try_files 的前 N-1 个参数,Nginx 只会检查文件系统,而不会去执行 URI 与 location 之间的匹配。
try-files 阶段发生的事情:Nginx 依次检查了文件 /var/www/foo 和目录 /var/www/bar,末了又处理了最后一个参数 /baz. 这里最后一条“调试信息”容易产生误解,会让人误以为 Nginx 也把最后一个参数 /baz 给映射成了文件系统对象进行检查,事实并非如此。当 try_files 指令处理到它的最后一个参数时,总是直接执行“内部跳转”,而不论其对应的文件系统对象是否存在。
content 阶段
content阶段主要是处理内容输出的阶段。这里要注意的是绝大多数 Nginx 模块在向 content 阶段注册配置指令时,本质上是在当前的 location 配置块中注册所谓的“内容处理程序”(content handler)。每一个 location 只能有一个“内容处理程序”,因此,当在 location 中同时使用多个模块的 content 阶段指令时,只有其中一个模块能成功注册“内容处理程序”。例如 echo 和 content_by_lua 如果同时注册,最终只会有一个生效,但具体是哪一个生效是不稳定的。
log 阶段
日志的记录阶段