NginxDirectiveExecOrderTutorialCn04

= Nginx 配置指令的执行顺序（四） =

ngx_lua 模块提供了配置指令 access_by_lua，用于在  请求处理阶段插入用户 Lua 代码. 这条指令运行于  阶段的末尾，因此总是在 allow 和 deny 这样的指令之后运行，虽然它们同属   阶段. 一般我们通过 access_by_lua 在 ngx_access 这样的模块检查过客户端 IP 地址之后，再通过 Lua 代码执行一系列更为复杂的请求验证操作，比如实时查询数据库或者其他后端服务，以验证当前用户的身份或权限.

我们来看一个简单的例子，利用 access_by_lua 来实现 ngx_access 模块的 IP 地址过滤功能：

location /hello { access_by_lua ' if ngx.var.remote_addr == "127.0.0.1" then return end

ngx.exit(403) ';

echo "hello world"; }

这里在 Lua 代码中通过引用 Nginx 标准的内建变量 $remote_addr 来获取字符串形式的客户端 IP 地址，然后用 Lua 的  语句判断是否为本机地址，即是否等于. 如果是本机地址，则直接利用 Lua 的  语句返回，让 Nginx 继续执行后续的请求处理阶段（包括 echo 指令所处的   阶段）；而如果不是本机地址，则通过 ngx_lua 模块提供的 Lua 函数 ngx.exit 中断当前的整个请求处理流程，直接返回   错误页给客户端.

这个例子在功能上完全等价于先前在 （三） 中介绍过的那个使用 ngx_access 模块的例子：

location /hello { allow 127.0.0.1; deny all;

echo "hello world"; }

虽然这两个例子在功能上完全相同，但在性能上还是有区别的，毕竟 ngx_access 是用纯 C 实现的专门化的 Nginx 模块.

下面我们不妨来实际测量一下这两个例子的性能差别. 因为我们使用 Nginx 就是为了追求性能，而量化的性能比较，在工程上具有很大的现实意义，所以我们顺便介绍一下重要的测量技术. 由于无论是 ngx_access 还是 ngx_lua 在进行 IP 地址验证方面的性能都非常之高，所以为了减少测量误差，我们希望能对  阶段的用时进行直接测量. 为了做到这一点，传统的做法一般会涉及到修改 Nginx 源码，自己插入专门的计时代码和统计输出代码，抑或是重新编译 Nginx 以启用像  这样专门的性能监测工具.

幸运的是，在新一点的 Solaris, Mac OS X, 以及 FreeBSD 等系统上存在一个叫做  的工具，可以对任意的用户程序进行微观性能分析（以及行为分析），而无须对用户程序的源码进行修改或者对用户程序进行重新编译. 因为 Mac OS X 10.5 以后就自带了 ，所以为方便起见，下面在我的 MacBook Air 笔记本上演示一下这里的测量过程.

首先，在 Mac OS X 系统中打开一个命令行终端，在某一个文件目录下面创建一个名为  的文件，并编辑内容如下：

#!/usr/bin/env dtrace -s

pid$1::ngx_http_handler:entry {       elapsed = 0; }

pid$1::ngx_http_core_access_phase:entry {       begin = timestamp; }

pid$1::ngx_http_core_access_phase:return /begin > 0/ {       elapsed += timestamp - begin; begin = 0; }

pid$1::ngx_http_finalize_request:return /elapsed > 0/ {       @elapsed = avg(elapsed); elapsed = 0; }

保存好此文件后，再赋予它可执行权限：

$ chmod +x ./nginx-access-time.d

这个  文件中的代码是用   工具自己提供的   语言来编写的（注意，这里的   语言并不同于 Walter Bright 作为另一种“更好的 C++”而设计的   语言）. 由于本系列教程并不打算介绍如何编写  的   脚本，同时理解这个脚本需要不少有关 Nginx 内部源码实现的细节，所以这里我们不展开介绍. 大家只需要知道这个脚本的功能是：统计指定的 Nginx worker 进程在处理每个请求时，平均花费在  阶段上的时间.

现在来演示一下这个  脚本的运行方法. 这个脚本接受一个命令行参数用于指定监视的 Nginx worker 进程的进程号（pid）. 由于 Nginx 支持多 worker 进程，所以我们测试时发起的 HTTP 请求可能由其中任意一个 worker 进程服务. 为了确保所有测试请求都为固定的 worker 进程处理，不妨在  配置文件中指定只启用一个 worker 进程：

worker_processes 1;

重启 Nginx 服务器之后，可以利用  命令得到当前 worker 进程的进程号：

$ ps ax|grep nginx|grep worker|grep -v grep

在我机器上的一次典型输出是

10975  ??  S      0:34.28 nginx: worker process

其中第一列的数值便是我的 nginx worker 进程的进程号，. 如果你得到的输出不止一行，则通常意味着你的系统中同时运行着多个 Nginx 服务器实例，或者当前 Nginx 实例启用了多个 worker 进程.

接下来使用刚刚得到的 worker 进程号以及 root 身份来运行  脚本：

$ sudo ./nginx-access-time.d 10975

如果一切正常，则会看到这样一行输出：

dtrace: script './nginx-access-time.d' matched 4 probes

这行输出是说，我们的  脚本已成功向目标进程动态植入了 4 个   “探针”（probe）. 紧接着这个脚本就挂起了，表明  工具正在对进程   进行持续监视.

然后我们再打开一个新终端，在那里使用  这样的工具多次请求我们正在监视的接口

$ curl 'http://localhost:8080/hello' hello world

$ curl 'http://localhost:8080/hello' hello world

最后我们回到原先那个一直在运行  脚本的终端，按下   组合键中止   的运行. 而该脚本在退出时会向终端打印出最终统计结果. 例如我的终端此时是这个样子的：

$ sudo ./nginx-access-time.d 10975 dtrace: script './nginx-access-time.d' matched 4 probes ^C 19219

最后一行输出  便是那几次   请求在   阶段的平均用时（以纳秒，即 10 的负 9 次方秒为单位）.

通过上面介绍的步骤，可以通过  脚本分别统计出各种不同的 Nginx 配置下   阶段的平均用时. 针对我们感兴趣的三种情况可以进行三组平行试验，即使用 ngx_access 过滤 IP 地址的情况，使用 access_by_lua 过滤 IP 地址的情况，以及不在  阶段使用任何配置指令的情况. 最后一种情况属于“空白对照组”，用于校正测试过程中因  探针等其他因素而引入的“系统误差”. 另外，为了最小化各种不可控的“随机误差”，可以用  这样的批量测试工具来取代   发起连续十万次以上的请求，例如

$ ab -k -c1 -n100000 'http://127.0.0.1:8080/hello'

这样我们的  脚本统计出来的平均值将更加接近“真实值”.

在我的苹果系统上，一次典型的测试结果如下：

ngx_access 组              18146 access_by_lua 组           35011 空白对照组                  15887

把前两组的结果分别减去“空白对照组”的结果可以得到

ngx_access 组              2259 access_by_lua 组          19124

可以看到，ngx_access 组比 access_by_lua 组快了大约一个数量级，这正是我们所预期的. 不过其绝对时间差是极小的，对于我的  的 CPU 而言，也只有区区十几微秒，或者说是在十万分之一秒的量级.

当然，上面使用 access_by_lua 的例子还可以通过换用 $binary_remote_addr 内建变量进行优化，因为 $binary_remote_addr 读出的是二进制形式的 IP 地址，而 $remote_addr 则返回更长一些的字符串形式的地址. 更短的地址意味着用 Lua 进行字符串比较时通常可以更快.

值得注意的是，如果按 （一） 中介绍的方法为 Nginx 开启了“调试日志”的话，上面统计出来的时间会显著增加，因为“调试日志”自身的开销是很大的.