本章将会讲解基于LuaJIT的Lua-Nginx-Module,它作为Nginx的第三方开源软件,拥有十分丰富的功能,可以轻松完成高并发的业务需求。

注意:本书使用的Lua-Nginx-Module版本是0.10.13。Nginx API for Lua将被简称为Lua API,而Lua-Nginx-Module则被简称为Ngx_lua。后面章节中涉及到的Lua API大部分是包含参数的,如果参数以?结尾,代表这个参数是可选的,如在指令ngx.req.get_headers (max_headers?, raw?)中,max_headers和raw是可选的。

一、Nginx和OpenResty

首先,来认识一下OpenResty,它是一个基于Nginx和Lua开发的高性能的Web平台,包含大量成熟的第三方库,可快速搭建出高性能的Web服务器,支持常用的反向代理、网关系统、Web应用等。

如果在Nginx上使用Ngx_lua,需要先进行编译;而OpenResty已经包含此模块,不需要再进行编译了。读者可以自由选择使用Nginx或OpenResty来搭建服务,如果无法抉择,可参考如下场景。

  1. 使用Nginx编译Ngx_Lua的场景

HTTP代理服务器:复杂度较小,只需部分组件即可,且代理服务器一般由运维人员进行维护。使用Nginx的稳定版进行编译,在性能方面会更有保障,而OpenResty是Nginx的主线版,可能会不定期更新。

  1. OpenResty的使用场景
    API服务:业务需求多,需要大量组件。
    网关系统:需要大量组件和指令来实现动态组件功能。
    Web应用服务器:业务服务、页面服务等,如详情页业务的开发。
    使用Nginx编写的Lua代码都可以直接迁移到OpenResty上;反之却不一定可行,毕竟OpenResty的组件更多。

二、安装Ngx_lua

请先安装LuaJIT 2.1.0-beta3(详见第6.2节)并需要编译ngx_devel_kit模块。
下面是在Nginx上的安装方式(OpenResty自带此模块,不必安装编译):

# wget 'http://nginx.org/download/nginx-1.12.2.tar.gz'
# git clone https://github.com/simplresty/ngx_devel_kit.git
# git clone https://github.com/openresty/lua-nginx-module.git
# tar -xzvf nginx-1.12.2.tar.gz
# cd nginx-1.12.2/
# ./configure --prefix=/usr/local/nginx_1.12.2 \
     --add-module=../ngx_devel_kit \
     --add-module=../lua-nginx-module
     --with-ld-opt="-Wl,-rpath,$LUAJIT_LIB"

# make && make install

并不是每个Nginx版本都支持最新的Ngx_lua,目前已知支持最新Ngx_lua的Nginx版本如下:

1.13.x (last tested: 1.13.6)
1.12.x
1.11.x (last tested: 1.11.2)
1.10.x
1.9.x (last tested: 1.9.15)
1.8.x
1.7.x (last tested: 1.7.10)
1.6.x

如需获取最新版本的支持动态,请参考https://github.com/openresty/lua-nginx-module# nginx-compatibility。

三、牢记context标识

Ngx_lua API指令和Nginx的指令一样,都存在配置环境的约束问题,因此在使用过程中要确保指令的环境符合预期,例如:

ngx.var.VARIABLE
语法:ngx.var.VAR_NAME
context(配置环境):set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua,header_ filter_by_lua,body_filter_by_lua,log_by_lua*
context即配置环境,第一次接触Ngx_lua的读者看到这样的配置环境可能会觉得难以理解,因为这还涉及到Ngx_Lua的执行阶段(后面会有介绍)。

四、Hello world

首先,还是来一条经典语句“Hello, world”,在Nginx配置中加入一个server:

server {
    listen       80;
    server_name  testnginx.com;
    charset koi8-r;
    location = /test {
     #设置文件使用的默认MIME-type,将会增加一个Content-Type:text/plain的响应头
     default_type 'text/plain';    
     -- content_by_lua_block执行阶段
     content_by_lua_block {    
         ngx.say('Hello,world!')
     }
    }
}

访问这个server,输出如下:

#  curl -I http://testnginx.com/test
Hello,world!    

ngx.say将数据作为响应体输出,返回给客户端,并在末尾加上一个回车符。
代码中用到了content_by_lua_block这个指令块,它的主要作用是在HTTP的内容处理阶段生成数据,详见第8.6节。

五、避免I/O阻塞

当Nginx和Lua进行读取磁盘操作时会对Nginx的事件循环造成阻塞,所以在请求中应尽量避免操作磁盘,特别是当文件较大时。
如果Lua使用网络I/O,为了避免出现阻塞的情况,请使用基于Lua API开发的指令,并使用子请求(将在7.13节介绍)来发送网络I/O和磁盘I/O。如果需要频繁读取磁盘,请分离磁盘I/O的任务和网络I/O的任务,避免它们相互影响。

六、定义模块搜索路径

在开发过程中,常常需要编写自定义的模块,或者引入第三方的Lua或C模块,通过下面的配置可以定义相关模块的路径以方便快速查找。

6.1 定义Lua模块的搜索路径

lua_package_path用来设置默认的Lua模块的搜索路径,并配置在http阶段。它支持配置相对路径和绝对路径,其中相对路径是在Nginx启动时由-p PATH 决定的,如果在启动Nginx时没有配置-p PATH,就会使用编译时--prefix的值,此值一般存放在Nginx的$prefix(也可以用${prefix}来表示)变量中。使用lua_package_path设置Lua模块搜索路径的示例如下:

http {
    -- lua_package_path在配置中只能出现一次,使用下面的任何一个方法都可以
    lua_package_path "/usr/local/nginx_1.12.2/conf/lua_modules/?.lua;;";
    lua_package_path "conf/lua_modules/?.lua;;";
    lua_package_path "${prefix}conf/lua_modules/?.lua;;";

上述配置中的3种配置方式都指向同一个位置:
第1个是绝对路径;
第2个是相对路径,Nginx编译时用 --prefix=/usr/local/nginx_1.12.2;
第3个也是相对路径,Nginx编译时用 --prefix=/usr/local/nginx_1.12.2 或-p PATH 指定的位置。

第1个配置方式的缺点在于写出了具体文件搜索路径,迁移代码时会比较麻烦。第2个配置方式的缺点在于无法和-p PATH一起使用,如果-p换了位置就会导致这个配置无效。对于第3个配置方式,如果-p的位置换了,${prefix}的值会跟着变换,使用起来比较灵活。所以建议使用第3种配置方式来配置。

lua_package_path可以支持设置多个搜索路径,多个搜索路径之间使用分号分隔就可以了,如下:

lua_package_path "${prefix}conf/lua_modules/?.lua;/opt/lua/?.lua;;";

注意:上述配置中搜索路径的最后出现了;;两个半角分号,代表的是LuaJIT安装时的原始搜索路径,如果在前面的搜索路径里面无法搜索到需要的模块,就会依次搜索后面的路径。

6.2 定义C模块的搜索路径
lua_package_cpath:用来设置C模块的搜索路径,并配置在http阶段。使用方式和lua_package_path一样,如下:

lua_package_cpath "${prefix}conf/c_md/?.so;/opt/c/?.so;;";

七、读写Nginx的内置变量

如果需要读取Nginx的内置变量可以使用ngx.var.VARIABLE。

语法:ngx.var.VAR_NAME

配置环境:set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua,headerfilter by_lua,body_filter_by_lua,og_by_lua*

含义:读写Nginx的变量值。例如HTTP请求头、Nginx set的变量、URL参数,甚至Nginx通过正则表达式捕获的$1、$2等值(获取方式是ngx.var[1]、ngx.var[2],依此类推)。
示例如下:

server {
    listen       80;
    server_name  testnginx.com;
    location ~ ^/([a-z]+)/var.html {
        set $a '';
        set $b '';
        set $c '';
        set $d '';
        rewrite_by_lua_block {
           local ngx = require "ngx"
           --将1赋值给变量a
           ngx.var.a = '1'
           --获取HTTP请求头中user_agent的值并赋值给变量b
           ngx.var.b = ngx.var.http_user_agent
           --获取参数test的值赋值给变量c
           ngx.var.c = ngx.var.arg_test
           --获取location中正则表达式捕获的$1的值并赋值给变量d
           ngx.var.d = ngx.var[1]
        }
        echo $a;
        echo $b;
        echo $c;
        echo $d;
    }

执行结果如下:

# curl -i 'http://testnginx.com/nginx/var.html?test=12132&a=2&b=c&dd'
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Thu, 07 Jun 2018 07:22:32 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
1
curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.19.1 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
12132
nginx

如果是未定义的Nginx变量,是无法直接在Lua中进行读取的。而且有些变量只能读取,无法进行修改,如$query_string、$arg_PARAMETER和$http_NAME。

八、控制请求头
在4.1节中讲了Nginx中控制请求头的指令,在Lua API中也有类似的指令。

8.1 添加请求头

指令:ngx.req.set_header

语法:ngx.req.set_header(header_name, header_value)

配置环境:set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua,headerfilter by_lua,body_filter_by_lua

含义:添加或修改当前HTTP的请求头,如果请求头已经存在,则会被替换成新的值。通过此方式设置的请求头会被继承到子请求中。

示例:设置一个名为Test_Ngx_Ver,值为1.12.2的请求头:

ngx.req.set_header("Test_Ngx_Ver", "1.12.2")
ngx.req.set_header支持给同一个请求头设置多个值,用数组的方式添加:
ngx.req.set_header("Test", {"1", "2"})

多个值的输出结果:

Test: 1
Test: 2

8.2 清除请求头

指令:ngx.req.clear_header

语法:ngx.req.clear_header(header_name)

配置环境:set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua,headerfilter by_lua,body_filter_by_lua

含义:清除当前请求中指定的请求头。清除后,如果存在未执行的子请求,则子请求会继承清除后的请求头。

示例:
ngx.req.clear_header("Test_Ngx_Ver")
还有一种清除请求头的方式:
ngx.req.set_header("Test_Ngx_Ver", nil)

8.3 获取请求头

指令:ngx.req.get_headers

语法:headers = ngx.req.get_headers(max_headers?, raw?)

配置环境:set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua,headerfilter by_lua,body_filter_by_lua,log_by_lua*

含义:获取当前请求的全部请求头,并返回一个Lua的table类型的数据:

示例:

server {
    listen       80;
    server_name  testnginx.com;

     location  / {

        content_by_lua_block {
           local ngx = require "ngx";
           local h = ngx.req.get_headers()
           for k, v in pairs(h) do
               ngx.say('Header name: ',k, ' value:',v)
           end
           --因为是table,所以可以使用下面的方式读取单个响应头的值
           ngx.say(h["host"])
        }
    }
}

输出结果如下:

# curl -i 'http://testnginx.com/test?=12132&a=2&b=c&dd'
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Fri, 08 Jun 2018 07:46:38 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

Header name:host value: testnginx.com
Header name:accept value: */*
Header name:user-agent value: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.19.1 Basic ECC zlib/1.2.3 libidn/1.18 libssh2/1.4.2
testnginx.com