用Lua替代subrequest

http://www.honwel.net/2014/04/19/nginx-subrequests.html


用Lua脚本编写Nginx的subrequest

在某些应用场景中,当nginx收到一个请求后需要向若干指定后端发起多个子请求,当所有子请求返回结果后还需要对这些结果进行过滤并发送给客户端。对于这样的需求以前我是采用编写handler模块和filter模块来实现的,但是用C编写模块的时间相对较长而且调试的时候比较痛苦(异步的原因)。所以最近就想着是否用Lua来写,并查找了相关资料,发现还真是方便。Lua的协程将nginx中的并发异步处理透明化了,让你觉得是在写串行程序,两个字形容:”真爽“。

用Lua重新编写完原来的程序后,发现代码行数少了很多,Lua写总共才160多行,而以前的C代码大约有1500行左右。虽然Lua没有学过,但是进行的比较顺利(以前从来没学过,只是知道),因为有丰富的官方文档。可是在调试时却发现怎么也无法有效发出HTTP POST子请求(数据比较大,小于100k左右),经过仔细的调试代码,并查阅相关文档,才发现是没有有效的发送出request body的缘故。

这里先贴出Lua脚本源码,首先是nginx.conf中的配置:

location / {
	    content_by_lua_file  /usr/local/nginx/conf/subrequest.lua; # 加载lua脚本文件
	}
	location /sub2 { # 发出的子请求location
  	    # 重写URL,主要是去掉sub2,并保留其他的参数信息,注意一定要使用break,保存不会多次重定向
  	    rewrite ^/sub2(.*)$ $1 break;  
  	    proxy_pass http://192.168.1.1:12345/;
	}
	location /sub1 { # 发出的子请求location
  	    rewrite ^/sub1(.*)$ $1 break;
  	    proxy_pass http://192.168.1.2:12345/;
	}

	location /sub3 { \# 发出的子请求location
  	    rewrite ^/sub3(.*)$ $1 break;
  	    proxy_pass http://192.168.1.3:12345/;
	}

然后是subrequest.lua脚本:

local action = ngx.var.request_method;
-- important very much!  #这里非常重要,一开始我没有添加这行,导致没有成功发出子请求
ngx.req.read_body();

-- get request body
local data = ngx.req.get_body_data();
-- get request uri's args
local args = ngx.req.get_uri_args();

if action == "POST" then
	arry = {method=ngx.HTTP_POST, body=data};
end

-- issue subrequest.
local res1,res2,res3 = ngx.location.capture_multi({
	{"/sub1"..ngx.var.request_uri, {method = ngx.HTTP_POST, body=data}},
	{"/sub2"..ngx.var.request_uri, {method = ngx.HTTP_POST, body=data}},
	{"/sub3"..ngx.var.request_uri, {method = ngx.HTTP_POST, body=data}}
	-- TODO: N's subrequests
})

-- 对返回结果进行业务处理
if res1.status == ngx.HTTP_OK then
	local body = res1.body;
	...................................
	...................................

为了能让Nginx执行上面的代码,你可能需要参考这里的说明进行安装,或者你直接安装最新的openresty也是可以的。上面贴出的源码中有很多nginx lua API的调用,你可以通过查阅官方的文档进行了解,比如ngx.location.capture_multi是发起多个子请求、获取URL参数ngx.req.get_uri_args,返回是Lua的table类型。是不是很简单?你也试试吧。

BTW:Jekyll语法中贴代码的处理我总是写的不好,每次更新博客时花在代码格式上的时间很多,很痛苦,上面代码还是排列不整齐啊!


安装-Nginx与Lua

火云邪神语录:天下武功,无坚不破,唯快不破!Nginx的看家本领就是速度,Lua的拿手好戏亦是速度,这两者的结合在速度上无疑有基因上的优势。

最先将Nginx,Lua组合到一起的是OpenResty,它有一个ngx_lua模块,将Lua嵌入到了Nginx里面;随后Tengine也包含了ngx_lua模块。至于二者的区别:OpenResty是Nginx的Bundle;而Tengine则是Nginx的Fork。值得一提的是,OpenResty和Tengine均是国人自己创建的项目,前者主要由春哥和晓哲开发,后者主要由淘宝打理。

至于OpenResty和Tengine孰优孰劣,留给大家自己判断,如下资料可供参考:

  • ngx_openresty: an Nginx ecosystem glued by Lua
  • 淘宝网Nginx应用、定制与开发实战

推荐看看春哥在Tech-Club上关于『由Lua粘合的Nginx生态环境』的演讲实录,有料!

安装

需要最新版的Nginx,LuaJIT,ngx_devel_kit,ngx_lua等安装文件。

安装Lua或者LuaJIT都是可以的,但是出于效率的考虑,推荐安装LuaJIT。

shell> wget http://luajit.org/download/LuaJIT-.tar.gz
shell> tar zxvf LuaJIT-.tar.gz
shell> cd LuaJIT-
shell> make
shell> make install

因为安装在缺省路径,所以LuaJIT对应的lib,include均在/usr/local目录里。

shell> export LUAJIT_LIB=/usr/local/lib
shell> export LUAJIT_INC=/usr/local/include/luajit-

下面就可以编译Nginx了:

shell> wget http://nginx.org/download/nginx-.tar.gz
shell> tar zxvf nginx-.tar.gz
shell> cd nginx-
shell> ./configure
    --add-module=/path/to/ngx_lua \
    --add-module=/path/to/ngx_devel_kit
shell> make
shell> make install

试着启动一下Nginx看看,如果你运气不好的话,可能会遇到如下错误:

cannot open shared object file: No such file or directory

这是神马情况?可以用ldd命令来看看:

shell> ldd /path/to/nginx
libluajit-.so => not found

此类问题通常使用ldconfig命令就能解决:

shell> echo "/usr/local/lib" > /etc/ld.so.conf.d/usr_local_lib.conf
shell> ldconfig

再试着启动Nginx看看,应该就OK了。

应用

我们先用一个简单的程序来暖暖场:把下面的代码加入到Nginx的配置文件nginx.conf,并重启Nginx,然后浏览,就能看到效果了。

location /lua {
    set $test "hello, world.";
    content_by_lua '
        ngx.header.content_type = "text/plain";
        ngx.say(ngx.var.test);
    ';
}

在深入学习ngx_lua之前,建议大家仔细阅读一遍春哥写的Nginx教程。

这里我就说关键的:Nginx配置文件所使用的语言本质上是『声明性的』,而非『过程性的』。Nginx处理请求的时候,指令的执行并不是由定义指令时的物理顺序来决定的,而是取决于指令所属的阶段,Nginx常用的阶段按先后顺序有:rewrite阶段,access阶段,content阶段等等。演示代码中的set指令属于rewrite阶段,content_by_lua指令属于content阶段,如果试着把两条指令的顺序交换一下,会发现程序依然能够正常运行。

下面我们尝试结合Redis写个更实战一点的例子。

首先,我们需要创建一个Redis配置文件config.json,内容如下:

{
    "host": "",
    "port": ""
}

然后,我们创建一个解析配置文件的脚本init.lua,其中用到了Lua CJSON模块:

local cjson = require "cjson";

local config = ngx.shared.config;

local file = io.open("config.json", "r");
local content = cjson.decode(file:read("*all"));
file:close();

for name, value in pairs(content) do
    config:set(name, value);
end

说明:代码里用到了共享内存,这样就不必每次请求都解析一遍配置文件了。

接着,我们创建一个渲染内容的脚本content.lua,用到了Resty Redis模块:

ngx.header.content_type = "text/plain";

local redis = require "resty.redis";

local config = ngx.shared.config;

local instance = redis:new();

local host = config:get("host");
local port = config:get("port");

local ok, err = instance:connect(host, port);
if not ok then
    ngx.log(ngx.ERR, err);
    ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE);
end

instance:set("name", "laowang");

local name = instance:get("name")

instance:close();

ngx.say("name: ", name);

说明:建议把Resty Redis模块放到vendor目录下,稍后在Nginx中统一设置。

最后,我们需要在Nginx配置文件里设置一下:

lua_shared_dict config 1m;
lua_package_path "/path/to/vendor/?.lua;;";

init_by_lua_file /path/to/init.lua;

server {
    lua_code_cache off;

    location /lua {
        content_by_lua_file /path/to/content.lua;
    }

    ...
}

说明:为了方便调试,我关闭了lua_code_cache,如果是生产环境,应该开启它。

另外,安装CJSON的时候,需要注意Makefile文件里头文件的路径,缺省是:

PREFIX = /usr/local
LUA_INCLUDE_DIR = $(PREFIX)/include

如果安装的是LuaJIT的话,最好把头文件拷贝到相应目录:

cp /usr/local/include/luajit-/* /usr/local/include/

我最近参与的一个项目,提供了一些用于Web轮询的接口,都是用Nginx+Lua实现的,虽然总共只有十几台服务器,但是每天可以提供几十亿次的请求量,贼拉拉的强。

最后,让我引用某位屌丝的语录做结束语吧:Lua,未婚男性程序员的最爱。




你可能感兴趣的:(nginx)