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之依赖这个接口,实现了解耦