大多数 Nginx 新手都会频繁遇到这样一个困惑,那就是当同一个 location 配置块使用了多个 Nginx 模块的配置指令时,这些指令的执行顺序很可能会跟它们的书写顺序大相径庭。于是许多人选择了“试错法”,然后他们的配置文件就时常被改得一片狼藉。这个系列的教程就旨在帮助读者逐步地理解这些配置指令背后的执行时间和先后顺序的奥秘。

现在就来看这样一个令人困惑的例子:

    ? location /test {
    ?     set $a 32;
    ?     echo $a;
    ?
    ?     set $a 56;
    ?     echo $a;
    ? }

从这个例子的本意来看,我们期望的输出是一行 32 和一行 56,因为我们第一次用 echo 配置指令输出了 $a变量的值以后,又紧接着使用 set 配置指令修改了 $a. 然而不幸的是,事实并非如此:

    $ curl 'http://localhost:8080/test
    56
    56

我们看到,语句 set $a 56 似乎在第一条 echo $a 语句之前就执行过了。这究竟是为什么呢?难道我们遇到了 Nginx 中的一个 bug?

显然,这里并没有 Nginx 的 bug;要理解这里发生的事情,就首先需要知道 Nginx 处理每一个用户请求时,都是按照若干个不同阶段(phase)依次处理的。

Nginx 的请求处理阶段共有 11 个之多,我们先介绍其中 3 个比较常见的。按照它们执行时的先后顺序,依次是 rewrite 阶段、access 阶段以及 content 阶段(后面我们还有机会见到其他更多的处理阶段)。

所有 Nginx 模块提供的配置指令一般只会注册并运行在其中的某一个处理阶段。比如上例中的 set 指令就是在 rewrite 阶段运行的,而 echo 指令就只会在 content 阶段运行。前面我们已经知道,在单个请求的处理过程中,rewrite 阶段总是在 content 阶段之前执行,因此属于 rewrite 阶段的配置指令也总是会无条件地在content 阶段的配置指令之前执行。于是在同一个 location 配置块中,set 指令总是会在 echo 指令之前执行,即使我们在配置文件中有意把 set 语句写在 echo 语句的后面。

回到刚才那个例子,

    set $a 32;
    echo $a;
 
    set $a 56;
    echo $a;

实际的执行顺序应当是

    set $a 32;
    set $a 56;
    echo $a;
    echo $a;

即先在 rewrite 阶段执行完这里的两条 set 赋值语句,然后再在后面的 content 阶段依次执行那两条 echo语句。分属两个不同处理阶段的配置指令之间是不能穿插着运行的。

为了进一步验证这一点,我们不妨借助 Nginx 的“调试日志”来一窥 Nginx 的实际执行过程。

因为这是我们第一次提及 Nginx 的“调试日志”,所以有必要先简单介绍一下它的启用方法。调试日志默认是禁用的,因为它会引入比较大的运行时开销,让 Nginx 服务器显著变慢。一般我们需要重新编译和构造 Nginx 可执行文件,并且在调用 Nginx 源码包提供的 ./configure 脚本时传入 --with-debug 命令行选项。例如我们下载完 Nginx 源码包后在 Linux 或者 Mac OS X 等系统上构建时,典型的步骤是这样的:

    tar xvf nginx-1.0.10.tar.gz
    cd nginx-1.0.10/
    ./configure --with-debug
    make
    sudu make install

如果你使用的是我维护的 ngx_openresty 软件包,则同样可以向它的 ./configure 脚本传递 --with-debug 命令行选项。

当我们启用 --with-debug 选项重新构建好调试版的 Nginx 之后,还需要同时在配置文件中通过标准的error_log 配置指令为错误日志使用 debug 日志级别(这同时也是最低的日志级别):

    error_log logs/error.log debug;

这里重要的是 error_log 指令的第二个参数,debug,而前面第一个参数是错误日志文件的路径,logs/error.log. 当然,你也可以指定其他路径,但后面我们会检查这个文件的内容,所以请特别留意一下这里实际配置的文件路径。

现在我们重新启动 Nginx(注意,如果 Nginx 可执行文件也被更新过,仅仅让 Nginx 重新加载配置是不够的,需要关闭再启动 Nginx 主服务进程),然后再请求一下我们刚才那个示例接口:

    $ curl 'http://localhost:8080/test'
    56
    56

现在可以检查一下前面配置的 Nginx 错误日志文件中的输出。因为文件中的输出比较多(在我的机器上有 700 多行),所以不妨用 grep 命令在终端上过滤出我们感兴趣的部分:

    grep -E 'http (output filter|script (set|value))' logs/error.log

在我机器上的输出是这个样子的(为了方便呈现,这里对 grep 命令的实际输出作了一些简单的编辑,略去了每一行的行首时间戳):

    [debug] 5363#0: *1 http script value: "32"
    [debug] 5363#0: *1 http script set $a
    [debug] 5363#0: *1 http script value: "56"
    [debug] 5363#0: *1 http script set $a
    [debug] 5363#0: *1 http output filter "/test?"
    [debug] 5363#0: *1 http output filter "/test?"
    [debug] 5363#0: *1 http output filter "/test?"

这里需要稍微解释一下这些调试信息的具体含义。set 配置指令在实际运行时会打印出两行以 http script 起始的调试信息,其中第一行信息是 set 语句中被赋予的值,而第二行则是 set 语句中被赋值的 Nginx 变量名。于是上面首先过滤出来的

    [debug] 5363#0: *1 http script value: "32"
    [debug] 5363#0: *1 http script set $a

这两行就对应我们例子中的配置语句

    set $a 32;

而接下来这两行调试信息

    [debug] 5363#0: *1 http script value: "56"
    [debug] 5363#0: *1 http script set $a

则对应配置语句

    set $a 56;

此外,凡在 Nginx 中输出响应体数据时,都会调用 Nginx 的所谓“输出过滤器”(output filter),我们一直在使用的 echo 指令自然也不例外。而一旦调用 Nginx 的“输出过滤器”,便会产生类似下面这样的调试信息:

    [debug] 5363#0: *1 http output filter "/test?"

当然,这里的 "/test?" 部分对于其他接口可能会发生变化,因为它显示的是当前请求的 URI. 这样联系起来看,就不难发现,上例中的那两条 set 语句确实都是在那两条 echo 语句之前执行的。

细心的读者可能会问,为什么这个例子明明只使用了两条 echo 语句进行输出,但却有三行 http output filter 调试信息呢?其实,前两行 http output filter 信息确实分别对应那两条 echo 语句,而最后那一行信息则是对应 ngx_echo 模块输出指示响应体末尾的结束标记。正是为了输出这个特殊的结束标记,才会多出一次对 Nginx “输出过滤器”的调用。包括 ngx_proxy 在内的许多模块在输出响应体数据流时都具有此种行为。

现在我们就不会再为前面那个例子输出两行一模一样的 56 而感到惊讶了。我们根本没有机会在第二条 set语句之前用 echo 输出。幸运的是,仍然可以借助一些小技巧来达到最初的目的:

    location /test {
        set $a 32;
        set $saved_a $a;
        set $a 56;
 
        echo $saved_a;
        echo $a;
    }

此时的输出便符合那个问题示例的初衷了:

    $ curl 'http://localhost:8080/test'
    32
    56

这里通过引入新的用户变量 $saved_a,在改写 $a 之前及时保存了 $a 的初始值。而对于多条 set 指令而言,它们之间的执行顺序是由 ngx_rewrite 模块来保证与书写顺序相一致的。同理,ngx_echo 模块自身也会保证它的多条 echo 指令之间的执行顺序。

细心的读者应当发现,我们在 Nginx 变量漫谈系列 的示例中已经广泛使用了这种技巧,来绕过因处理阶段而引起的指令执行顺序上的限制。

看到这里,有的读者可能会问:“那么我在使用一条陌生的配置指令之前,如何知道它究竟运行在哪一个处理阶段呢?”答案是:查看该指令的文档(当然,高级开发人员也可以直接查看模块的 C 源码)。在许多模块的文档中,都会专门标记其配置指令所运行的具体阶段。例如 echo 指令的文档中有这么一行:

    phase: content

这一行便是说,当前配置指令运行在 content 阶段。如果你使用的 Nginx 模块碰巧没有指示运行阶段的文档,可以直接联系该模块的作者请求补充。不过,值得一提的是,并非所有的配置指令都与某个处理阶段相关联,例如我们先前在 Nginx 变量漫谈(一) 中提到过的 geo 指令以及在 Nginx 变量漫谈(四) 中介绍过的 map 指令。这些不与处理阶段相关联的配置指令基本上都是“声明性的”(declarative),即不直接产生某种动作或者过程。Nginx 的作者 Igor Sysoev 在公开场合曾不止一次地强调,Nginx 配置文件所使用的语言本质上是“声明性的”,而非“过程性的”(procedural)。

Nginx 配置指令的执行顺序(一)的更多相关文章

  1. Nginx 配置指令的执行顺序(八)

    前面我们详细讨论了 rewrite.access 和 content 这三个最为常见的 Nginx 请求处理阶段,在此过程中,也顺便介绍了运行在这三个阶段的众多 Nginx 模块及其配置指令.同时可以 ...

  2. Nginx 配置指令的执行顺序(五)

    Nginx 的 content 阶段是所有请求处理阶段中最为重要的一个,因为运行在这个阶段的配置指令一般都肩负着生成“内容”(content)并输出 HTTP 响应的使命.正因为其重要性,这个阶段的配 ...

  3. Nginx配置指令的执行顺序

    rewrite阶段 rewrite阶段是一个比较早的请求处理阶段,这个阶段的配置指令一般用来对当前请求进行各种修改(比如对URI和URL参数进行改写),或者创建并初始化一系列后续处理阶段可能需要的Ng ...

  4. Nginx 配置指令的执行顺序(十)

    运行在 post-rewrite 阶段之后的是所谓的 preaccess 阶段.该阶段在 access 阶段之前执行,故名preaccess. 标准模块 ngx_limit_req 和 ngx_lim ...

  5. Nginx 配置指令的执行顺序(六)

    前面我们在 (五) 中提到,在一个 location 中使用 content 阶段指令时,通常情况下就是对应的 Nginx 模块注册该 location 中的“内容处理程序”.那么当一个 locati ...

  6. Nginx 配置指令的执行顺序(三)

    如前文所述,除非像 ngx_set_misc 模块那样使用特殊技术,其他模块的配置指令即使是在 rewrite 阶段运行,也不能和 ngx_rewrite 模块的指令混合使用.不妨来看几个这样的例子. ...

  7. Nginx 配置指令的执行顺序(二)

    我们前面已经知道,当 set 指令用在 location 配置块中时,都是在当前请求的 rewrite 阶段运行的.事实上,在此上下文中,ngx_rewrite 模块中的几乎全部指令,都运行在 rew ...

  8. Nginx 配置指令的执行顺序

    在一个 location 中使用 content 阶段指令时,通常情况下就是对应的 Nginx 模块注册该 location 中的“内容处理程序”.那么当一个 location 中未使用任何 cont ...

  9. Nginx 配置指令的执行顺序(十一)

    紧跟在 post-access 阶段之后的是 try-files 阶段.这个阶段专门用于实现标准配置指令 try_files 的功能,并不支持 Nginx 模块注册处理程序.由于 try_files  ...

随机推荐

  1. sql 添加约束

    在表中添加约束,基本常用的有两种类型,一个是创建表时同时添加约束,另一个是创建好表通过修改表添加约束,在这里是创建表时同时添加约束,但是有两种不同的用写法. 在这里列举出一些创建约束的形式,共参考(均 ...

  2. 【转】Android 平台下使用 i2c-tools

    原文网址:http://my.oschina.net/luoly/blog/368881 Android 平台下使用 i2c-tools Andorid 开发板为 Freescale imx6 的 S ...

  3. [Oracle] Data Guard 系列(5) - 创建逻辑备库

    在创建逻辑备库之前,必须得先创建物理备库,关于如何创建物理备库,请参考<Data Guard 系列(4) - 在不停主库的情况下创建物理备库>. 1. 在物理备库上停止日志应用服务 SYS ...

  4. Unity 两张背景的切换平移

    两张背景图片向左移动,当屏幕看见的时候. 使用的是Unity自带的Sprite,当然也可以使用NGUI Sprite using UnityEngine; using System.Collectio ...

  5. IOS开发之---触摸和手势

    Touch:在与设备的多点触摸屏交互时生成. 响应者对象 响应者对象就是可以响应事件并对事件作出处理.在iOS中,存在UIResponder类,它定义了响应者对象的所有方法.UIApplication ...

  6. Android开发之Intent.Action

    1 Intent.ACTION_MAIN String: android.intent.action.MAIN 标识Activity为一个程序的開始.比較经常使用. Input:nothing Out ...

  7. 用了 CSDN 的 markdown 编辑器吐槽下~~

    吐槽一下.. . 第一次用 CSDN 的这个 markdown 编辑器,首先感官上看起来还是非常大气相比曾经那个 HTML 编辑器实在时上了N个档次,但实际使用的体验实在是比較糟糕的.希望能改进下哦: ...

  8. ffmpeg学习笔记

           对于每一个刚開始学习的人,刚開始接触ffmpeg时,想必会有三个问题最为关心,即ffmpeg是什么?能干什么?怎么開始学习?本人前段时间開始接触ffmpeg,在刚開始学习过程中.这三个问 ...

  9. sybase用户管理(创建、授权、删除)

    一.登录用户管理:1.创建用户:sp_addlogin loginame, passwd [, defdb] [, deflanguage] [, fullname] [, passwdexp] [, ...

  10. 黑马程序员——java基础之文件复制

    ---------------------- ASP.Net+Unity开发..Net培训.期待与您交流!---------------------- <a href="http:// ...