背景篇
由於項目流量越來越大,之前的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之依賴這個接口,實現了解耦