Nginx+lua实践:简单的接口聚合实例,nginxlua


Nginx是高性能的HTTP和反向代理服务器。目前已经普遍应用到各大互联网公司,国内基于Nginx又分别改造推出了Tengix和Openresty,笔者所在的公司也在逐步推广Nginx作为业务系统的接入层,同时使用lua开发脚本,将业务逻辑前置来减轻后端服务器的压力(Nginx的并发能力非常强,很适合处理轻量级业务)。
接口聚合是很多业务系统需要实现的功能,可以前置到Nginx上做,聚合的目的一方面是减少用户请求次数,提高响应性能;另一方面,Nginx并发处理能力更强。下面给出一个简单的实现案例。

接口聚合的原理

假设页面展示要调用三个接口a.json,b.json,c.json,(a,b,c之间没有逻辑依赖关系,且对应A、B、C三个服务端系统),则客户端需要发送三次请求才能拿到完整的接口数据。

接口内容分别为:

a.json
{"name":"A","testA":[a1,a2,a3]}
b.json
{"name":"B","testB":[b1,b2,b3]}
c.json
{"name":"C","testC":[c1,c2,c3]}

接口聚合的原理如下图,将a.json,b.json,c.json 聚合为/api/abc.json,当请求到达Nginx,再分离成三个请求到不同服务端,拿到数据后再由Nginx组装好后返回给客户端。这样,减少了请求数,也减少了请求并发数(统一接入层的并发数减少,单个后端系统的并发数仍不变),同时提高了客户端和服务端的性能。

接口内容是:

{
ckey: "{"name":"C","testC":[c1,c2,c3]}",
akey: "{"name":"A","testA":[a1,a2,a3]}",
bkey: "{"name":"B","testB":[b1,b2,b3]}"
}

接口聚合的实现

下面我们实现一个简单的demo,我们使用openresty 1.9.15.1,这样就省去了重新编译lua-nginx-module 模块的时间,部署在服务器10.27.180.75上,作为服务统一接入层。假设a,b,c三个接口之间没有依赖关系,A系统IP为10.27.200.164,B系统IP为10.27.200.167,C系统IP为10.27.200.170。则75的nginx.conf配置如下:

 server {
        listen       80;
        server_name  localhost;

        location / {
            root   html;
            index  index.html index.htm;
        }

        location /abc {
            charset UTF-8;
            content_by_lua_file  /usr/local/openresty/nginx/conf/lua/merge_abc.lua;

        }
        location ^~ /api/a.json {
            proxy_connect_timeout        1s;
            proxy_read_timeout           2s;
            proxy_pass http://10.27.200.164;
        }
        location ^~ /api/b.json {
            proxy_connect_timeout        1s;
            proxy_read_timeout           2s;      
            proxy_pass http://10.27.200.167;
        }
        location ^~ /api/c.json {
            proxy_connect_timeout        1s;
            proxy_read_timeout           2s;   
            proxy_pass http://10.27.200.170;
        }
    }

其中,content_by_lua_file表示调用lua脚本来处理请求,proxy_pass表示转发路径。

merge_abc.lua的具体实现如下,使用openresty的好处之一也是因为cjson模块是集成好的,不需要再去重新编译,十分方便。
每一个json请求可能是带多个参数的,将每个请求的param-value存储在key中,比如,a.json?a1=1&a2=2&a3=3,则将a1=1&a2=2&a3=3放入akey中。再通过ngx.req.get_uri_args()将akey取出还原。
location.capture_multi发送请求到后端服务器。如果请求存在301/2跳转,则直接返回0,否则,拼装json报文并返回客户端。

--启用cjson处理json
gcjson = require("cjson");
--获取请求参数值
local requestmap= ngx.req.get_uri_args();

--为了解决不同请求见参数重名的问题,通过唯一key记录每个请求param-value
local req_a="/api/a.json";
if requestmap["akey"] and requestmap["akey"] ~="" then
  req_a=req_a.."?"..requestmap["akey"];
  ngx.say("akey:"..requestmap["akey"])
end
local req_b="/api/b.json";
if requestmap["bkey"] and requestmap["bkey"] ~="" then
  req_a=req_a.."?"..requestmap["bkey"];
  ngx.say("bkey:"..requestmap["bkey"])
end
local req_c="/api/c.json";
if requestmap["ckey"] and requestmap["ckey"] ~="" then
  req_a=req_a.."?"..requestmap["ckey"];
  ngx.say("ckey:"..requestmap["ckey"])
end

--location.capture_multi发送请求到后端服务器
--如果存在跳转,则返回0
--否则,拼装json报文并返回客户端
local requestArray={{req_a},{req_b},{req_c}};
local res_a,res_b,res_c= ngx.location.capture_multi(requestArray);
if res_a and res_a.status ==  ngx.HTTP_MOVED_TEMPORARILY then
  ngx.say("0");
  do return end;
end
if res_b and res_b.status ==  ngx.HTTP_MOVED_TEMPORARILY then
  ngx.say("0");
  do return end;
end
if res_c and res_c.status ==  ngx.HTTP_MOVED_TEMPORARILY then
  ngx.say("0");
  do return end;
end

local data ={};

if res_a and res_a.status ==  ngx.HTTP_OK then
 data["akey"]=res_a.body;
end;
if res_b and res_b.status ==  ngx.HTTP_OK then
 data["bkey"]=res_b.body;
end;
if res_c and res_c.status ==  ngx.HTTP_OK then
 data["ckey"]=res_c.body;
end;

ngx.say(gcjson.encode(data));

这样,一个简单的Nginx实现接口聚合的实例已经完成了。
考虑一个问题,假如a,b,c中有一个接口超时,Nginx是返回超时,还是返回部分结果?我们设置了每个接口的超时时间是1s,同时,在后端服务器C上增加了sleep(5000),结果发现,返回的是a,b接口的结果:

所以如果在线上真实场景中使用Nginx实现接口聚合,一定要考虑好异常捕获与处理,比如单个接口的超时与重试,保证服务高可用。

相关内容

    暂无相关文章