背景篇
由于项目流量越来越大,之前的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之依赖这个接口,实现了解耦