nginx+lua打造10K qps+的web应用

背景篇

  由于项目流量越来越大,之前的nginx+php-fpm的架构已经难以承受峰值流量的冲击,春节期间集群负载一度长时间维持0%的idle,于是这段时间逐渐对旧系统进行重构。

  受高人指点,发现lua这个好东西。因此在技术选型上,我们使用lua代替部分的php逻辑,比如请求的过滤。lua是一种可以嵌入nginx配置文件的动态语言,结合nginx的请求处理过程(参见另一篇博文),lua可以在这些阶段接管请求的处理。

  我们的环境使用openresty搭建,openresty包括了很多nginx常用扩展,对于没有定制过nginx代码的我们来说比较方便。

  这里有一句比较关键的话,nginx配置文件的定义,是“声明”性质的,而不是“过程”性质的。nginx处理请求的阶段,是按一定顺序执行的,无论配置文件写的顺序如何都不影响它们的执行顺序,比如set一定在content之前。我们在项目中常能用到的:set_by_lua,可以用来进行变量的计算,access_by_lua,可以用来设置访问权限,content_by_lua是用来生成返回的内容,log_by_lua用来设置日志。

lua的基本语法可以先参考这篇http://17173ops.com/tag/nginx_lua#toc12,个人觉得写的很清楚,很易懂。lua中需要用到的nginx的api参考http://wiki.nginx.org/HttpLuaModule)

使用lua编程要注意的问题:

1.lua不能对空数组(nil)进行索引!

2.lua的异常处理。比如的cjson库,在解析失败的时候,会直接抛异常从而中断脚本的执行,这里可以用cjson.safe来代替cjson,也可以采用这样的写法:

1 cache = switcher:get(key)
2 ret,errmsg = pcall(cjson.decode,cache);
3 if ret then
4     return errmsg;
5 else
6     return false;
7 end

就相当于在脚本中捕获异常,也可以封装try...catch

3.lua的字符串连接操作,也就是..,只支持字符串之间的连接,不支持字符串+数字或者是字符串+布尔,必须要显式转换类型

4.不要使用lua原生的io库,这会导致nginx进程阻塞!最好使用例如ngx.location.capture这样的函数,将io事件托管给nginx

 

实现篇

  我们的应用场景,是应对大量客户端(android,ios)的请求(4台linux服务器,应对10K+的qps),而业务逻辑相对简单,更多的是希望做流量的过滤。为了保护后端模块不会被突然上升的流量击垮,我们必须有一个强有力的前端,能较为轻松的抗住最大峰值流量,并进行相应的操作。这里我们用白名单的实现为例。贴上部分业务逻辑代码。因为某些原因,代码经过了删减,不能保证能运行,只是示例。

  1 local cjson = require "cjson";
  2 local agent = ngx.req.get_headers()["user-agent"];
  3 local switcher = ngx.shared.dict;
  4 
  5 local UPLOAD_OK = {"errno":0,"msg":""};
  6 local UPLOAD_FAIL = {"errno":-1,"msg":""};
  7 local SHUT_DOWN = {"errno":1,"msg":""};
  8 
  9 local CACHE_TIME_OUT = 10; --in second
 10 
 11 local say = UPLOAD_FAIL;
 12 
 13 function parseInput(agent)
 14     ret,errmsg = pcall(cjson.decode,agent);
 15     if ret then
 16         return errmsg;
 17     else
 18         return false;
 19     end
 20 end
 21 
 22 function checkCache(key)
 23     if switcher == nil then
 24         return false;
 25     else
 26         cache = switcher:get(key)
 27         ret,errmsg = pcall(cjson.decode,cache);
 28         if ret then
 29             return errmsg;
 30         else
 31             return false;
 32         end
 33     end
 34 end
 35 
 36 function check(input)
 37     appkey = input["arg0"];
 38     appvn = input["arg1"];
 39     if switcher == nil then
 40         ngx.log(ngx.INFO, "switcher nil");
 41         return false;
 42     else
 43         status = checkCache(appkey..appvn);
 44         if not status then
 45             ngx.log(ngx.INFO, "parse response failed");
 46             return false;
 47         else
 48             if status["lastmod"] == nil then
 49                 ngx.log(ngx.INFO, "lastmod nil");
 50                 return false;
 51             elseif status["lastmod"] < ( ngx.now() - CACHE_TIME_OUT ) then
 52                 ngx.log(ngx.INFO, "lastmod:"..status["lastmod"]..",outdated");
 53                 return false;
 54             else
 55                 return status["switch"];
 56             end
 57         end
 58     end
 59 end
 60 
 61 function reload(arg0, arg1)
 62     response = ngx.location.capture("/switch_url");
 63     status = cjson.decode(response.body);
 64     result = {};
 65     result["switch"] = status["switch"];
 66     result["lastmod"] = ngx.now();
 67     switcher:set(arg0..arg1, cjson.encode(result));
 68     return status["switch"];
 69 end
 70 
 71 function reply(result)
 72     if result == 0 then
 73         ngx.log(ngx.WARN, "it has been shut down");
 74         ngx.say(SHUT_DOWN);
 75     else
 76         request = {
 77             method = ngx.HTTP_POST,
 78             body = ngx.req.read_body(),
 79         }
 80         response = ngx.location.capture("real_url", request);
 81         ret,errmsg = pcall(cjson.decode,response.body);
 82         if ret then
 83             if "your_contidion" then
 84                 return UPLOAD_OK;
 85             else
 86                 return UPLOAD_FAIL;
 87             end
 88         else
 89             return UPLOAD_FAIL;
 90         end
 91     end
 92 end
 93 
 94 --switch 0=off 1=on
 95 if agent == nil then
 96     --input empty
 97     ngx.say(say);
 98 else
 99     ngx.log(ngx.INFO, "agent:"..agent);
100     input = parseInput(agent);
101     if input then
102         --input correct
103         ngx.log(ngx.INFO, "input correct");
104         result = check(input)
105         if result == false then
106             --no cache or cache outdated, needs reload
107             ngx.log(ngx.INFO, "invalid cache, needs reload");
108             result = reload(input["arg0"],input["arg1"]);
109             say = reply(result);
110         else
111             --cache ok
112             ngx.log(ngx.INFO, "cache ok");
113             say = reply(result);
114         end
115     else
116         --input error
117         say = UPLOAD_FAIL;
118     end
119 end
120 ngx.log(ngx.INFO, "ngx says:"..say);
121 ngx.say(say);

 

上述代码实现了一个简单的高性能开关,每10秒从后端php加载一次开关状态(switch_url),根据请求的arg0和arg1来判断是不是要转发到real_url,从而保护真实服务不被流量冲击。在这里使用了nginx的共享内存。

在nginx的location里这样配置

lua_code_cache off; //开发的时候off,
set_form_input $name;
content_by_lua_file ‘conf/switch.lua‘; 
error_log logs/pipedir/lua.log info;

在http配置里务必要记得配置共享内存

lua_shared_dict dict 10m;

 

性能测试:

nginx+lua:

技术分享

php:800qps,就不上图了。。

一些个人感想:

看了一些帖子,都是通过lua直接访问redis获取白名单,或者是memcache,mysql,访问其他数据,个人觉得这样其实违背了系统设计的依赖关系,在lua中拼redis key很容易引发由高耦合引发的问题,例如拼错了key,但是怎么也找不到bug,因此我这里设计成了lua中通过ngx.location.capture访问现成的服务,相当于lua之依赖这个接口,实现了解耦

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。