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之依賴這個接口,實現了解耦


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM