Nginx+Lua開發及指令詳解
Nginx入門
本文目的是學習Nginx+Lua開發,對於Nginx基本知識可以參考如下文章:
nginx啟動、關閉、重啟
agentzh 的 Nginx 教程
Nginx+Lua入門
nginx 配置指令的執行順序
nginx與lua的執行順序和步驟說明
Nginx配置檔案nginx.conf中文詳解
Tengine的Nginx開發從入門到精通
官方文件
Lua入門
本文目的是學習Nginx+Lua開發,對於Lua基本知識可以參考如下文章:
Lua簡明教程
lua線上lua學習教程
Lua 5.1 參考手冊
Lua5.3 參考手冊
Nginx Lua API
和一般的Web Server類似,我們需要接收請求、處理並輸出響應。而對於請求我們需要獲取如請求引數、請求頭、Body體等資訊;而對於處理就是呼叫相應的Lua程式碼即可;輸出響應需要進行響應狀態碼、響應頭和響應內容體的輸出。因此我們從如上幾個點出發即可。
接收請求
1、example.conf配置檔案
- location ~ /lua_request/(\d+)/(\d+) {
- #設定nginx變數
- set $a $1;
- set $b $host;
- default_type "text/html";
- #nginx內容處理
- content_by_lua_file /usr/example/lua/test_request.lua;
- #內容體處理完成後呼叫
- echo_after_body "ngx.var.b $b";
- }
2、test_request.lua
- --nginx變數
- local var = ngx.var
- ngx.say("ngx.var.a : ", var.a, "<br/>")
- ngx.say("ngx.var.b : ", var.b, "<br/>")
- ngx.say("ngx.var[2] : ", var[2], "<br/>")
- ngx.var.b = 2;
- ngx.say("<br/>")
- --請求頭
- local headers = ngx.req.get_headers()
- ngx.say("headers begin", "<br/>")
- ngx.say("Host : ", headers["Host"], "<br/>")
- ngx.say("user-agent : ", headers["user-agent"], "<br/>")
- ngx.say("user-agent : ", headers.user_agent, "<br/>")
- for k,v in pairs(headers) do
- if type(v) == "table" then
- ngx.say(k, " : ", table.concat(v, ","), "<br/>")
- else
- ngx.say(k, " : ", v, "<br/>")
- end
- end
- ngx.say("headers end", "<br/>")
- ngx.say("<br/>")
- --get請求uri引數
- ngx.say("uri args begin", "<br/>")
- local uri_args = ngx.req.get_uri_args()
- for k, v in pairs(uri_args) do
- if type(v) == "table" then
- ngx.say(k, " : ", table.concat(v, ", "), "<br/>")
- else
- ngx.say(k, ": ", v, "<br/>")
- end
- end
- ngx.say("uri args end", "<br/>")
- ngx.say("<br/>")
- --post請求引數
- ngx.req.read_body()
- ngx.say("post args begin", "<br/>")
- local post_args = ngx.req.get_post_args()
- for k, v in pairs(post_args) do
- if type(v) == "table" then
- ngx.say(k, " : ", table.concat(v, ", "), "<br/>")
- else
- ngx.say(k, ": ", v, "<br/>")
- end
- end
- ngx.say("post args end", "<br/>")
- ngx.say("<br/>")
- --請求的http協議版本
- ngx.say("ngx.req.http_version : ", ngx.req.http_version(), "<br/>")
- --請求方法
- ngx.say("ngx.req.get_method : ", ngx.req.get_method(), "<br/>")
- --原始的請求頭內容
- ngx.say("ngx.req.raw_header : ", ngx.req.raw_header(), "<br/>")
- --請求的body內容體
- ngx.say("ngx.req.get_body_data() : ", ngx.req.get_body_data(), "<br/>")
- ngx.say("<br/>")
ngx.var : nginx變數,如果要賦值如ngx.var.b = 2,此變數必須提前宣告;另外對於nginx location中使用正則捕獲的捕獲組可以使用ngx.var[捕獲組數字]獲取;
ngx.req.get_headers:獲取請求頭,預設只獲取前100,如果想要獲取所以可以呼叫ngx.req.get_headers(0);獲取帶中劃線的請求頭時請使用如headers.user_agent這種方式;如果一個請求頭有多個值,則返回的是table;
ngx.req.get_uri_args:獲取url請求引數,其用法和get_headers類似;
ngx.req.get_post_args:獲取post請求內容體,其用法和get_headers類似,但是必須提前呼叫ngx.req.read_body()來讀取body體(也可以選擇在nginx配置檔案使用lua_need_request_body on;開啟讀取body體,但是官方不推薦);
ngx.req.raw_header:未解析的請求頭字串;
ngx.req.get_body_data:為解析的請求body體內容字串。
使用如下指令碼測試
- wget --post-data 'a=1&b=2' 'http://127.0.0.1/lua_request/1/2?a=3&b=4' -O -
輸出響應
1.1、example.conf配置檔案
- location /lua_response_1 {
- default_type "text/html";
- content_by_lua_file /usr/example/lua/test_response_1.lua;
- }
1.2、test_response_1.lua
- --寫響應頭
- ngx.header.a = "1"
- --多個響應頭可以使用table
- ngx.header.b = {"2", "3"}
- --輸出響應
- ngx.say("a", "b", "<br/>")
- ngx.print("c", "d", "<br/>")
- --200狀態碼退出
- return ngx.exit(200)
ngx.header:輸出響應頭;
ngx.print:輸出響應內容體;
ngx.say:通ngx.print,但是會最後輸出一個換行符;
ngx.exit:指定狀態碼退出。
2.1、example.conf配置檔案
- location /lua_response_2 {
- default_type "text/html";
- content_by_lua_file /usr/example/lua/test_response_2.lua;
- }
2.2、test_response_2.lua
- ngx.redirect("http://jd.com", 302)
ngx.redirect:重定向;
ngx.status=狀態碼,設定響應的狀態碼;ngx.resp.get_headers()獲取設定的響應狀態碼;ngx.send_headers()傳送響應狀態碼,當呼叫ngx.say/ngx.print時自動傳送響應狀態碼;可以通過ngx.headers_sent=true判斷是否傳送了響應狀態碼。
其他API
1、example.conf配置檔案
- location /lua_other {
- default_type "text/html";
- content_by_lua_file /usr/example/lua/test_other.lua;
- }
2、test_other.lua
- --未經解碼的請求uri
- local request_uri = ngx.var.request_uri;
- ngx.say("request_uri : ", request_uri, "<br/>");
- --解碼
- ngx.say("decode request_uri : ", ngx.unescape_uri(request_uri), "<br/>");
- --MD5
- ngx.say("ngx.md5 : ", ngx.md5("123"), "<br/>")
- --http time
- ngx.say("ngx.http_time : ", ngx.http_time(ngx.time()), "<br/>")
ngx.escape_uri/ngx.unescape_uri : uri編碼解碼;
ngx.encode_args/ngx.decode_args:引數編碼解碼;
ngx.encode_base64/ngx.decode_base64:BASE64編碼解碼;
ngx.re.match:nginx正則表示式匹配;
Nginx全域性記憶體
使用過如Java的朋友可能知道如Ehcache等這種程序內本地快取,Nginx是一個Master程序多個Worker程序的工作方式,因此我們可能需要在多個Worker程序中共享資料,那麼此時就可以使用ngx.shared.DICT來實現全域性記憶體共享。
1、首先在nginx.conf的http部分分配記憶體大小
- #共享全域性變數,在所有worker間共享
- lua_shared_dict shared_data 1m;
2、example.conf配置檔案
- location /lua_shared_dict {
- default_type "text/html";
- content_by_lua_file /usr/example/lua/test_lua_shared_dict.lua;
- }
3、 test_lua_shared_dict.lua
- --1、獲取全域性共享記憶體變數
- local shared_data = ngx.shared.shared_data
- --2、獲取字典值
- local i = shared_data:get("i")
- if not i then
- i = 1
- --3、惰性賦值
- shared_data:set("i", i)
- ngx.say("lazy set i ", i, "<br/>")
- end
- --遞增
- i = shared_data:incr("i", 1)
- ngx.say("i=", i, "<br/>")
到此基本的Nginx Lua API就學完了,對於請求處理和輸出響應如上介紹的API完全夠用了,更多API請參考官方文件。
Nginx與Lua編寫指令碼的基本構建塊是指令。 指令用於指定何時執行使用者Lua程式碼以及如何使用結果。 下面是顯示指令執行順序的圖。
Nginx Lua模組指令
Nginx共11個處理階段,而相應的處理階段是可以做插入式處理,即可插拔式架構;另外指令可以在http、server、server if、location、location if幾個範圍進行配置:
指令 |
所處處理階段 |
使用範圍 |
解釋 |
init_by_lua init_by_lua_file |
loading-config |
http |
nginx Master程序載入配置時執行; 通常用於初始化全域性配置/預載入Lua模組 |
init_worker_by_lua init_worker_by_lua_file |
starting-worker |
http |
每個Nginx Worker程序啟動時呼叫的計時器,如果Master程序不允許則只會在init_by_lua之後呼叫; 通常用於定時拉取配置/資料,或者後端服務的健康檢查 |
set_by_lua set_by_lua_file |
rewrite |
server,server if,location,location if |
設定nginx變數,可以實現複雜的賦值邏輯;此處是阻塞的,Lua程式碼要做到非常快; |
rewrite_by_lua rewrite_by_lua_file |
rewrite tail |
http,server,location,location if |
rrewrite階段處理,可以實現複雜的轉發/重定向邏輯; |
access_by_lua access_by_lua_file |
access tail |
http,server,location,location if |
請求訪問階段處理,用於訪問控制 |
content_by_lua content_by_lua_file |
content |
location,location if |
內容處理器,接收請求處理並輸出響應 |
header_filter_by_lua header_filter_by_lua_file |
output-header-filter |
http,server,location,location if |
設定header和cookie |
body_filter_by_lua body_filter_by_lua_file |
output-body-filter |
http,server,location,location if |
對響應資料進行過濾,比如截斷、替換。 |
log_by_lua log_by_lua_file |
log |
http,server,location,location if |
log階段處理,比如記錄訪問量/統計平均響應時間 |
init_by_lua
每次Nginx重新載入配置時執行,可以用它來完成一些耗時模組的載入,或者初始化一些全域性配置;在Master程序建立Worker程序時,此指令中載入的全域性變數會進行Copy-OnWrite,即會複製到所有全域性變數到Worker程序。
1、nginx.conf配置檔案中的http部分新增如下程式碼
- #共享全域性變數,在所有worker間共享
- lua_shared_dict shared_data 1m;
- init_by_lua_file /usr/example/lua/init.lua;
2、init.lua
- --初始化耗時的模組
- local redis = require 'resty.redis'
- local cjson = require 'cjson'
- --全域性變數,不推薦
- count = 1
- --共享全域性記憶體
- local shared_data = ngx.shared.shared_data
- shared_data:set("count", 1)
3、test.lua
- count = count + 1
- ngx.say("global variable : ", count)
- local shared_data = ngx.shared.shared_data
- ngx.say(", shared memory : ", shared_data:get("count"))
- shared_data:incr("count", 1)
- ngx.say("hello world")
4、訪問如http://192.168.1.2/lua 會發現全域性變數一直不變,而共享記憶體一直遞增
global variable : 2 , shared memory : 8 hello world
另外注意一定在生產環境開啟lua_code_cache,否則每個請求都會建立Lua VM例項。
init_worker_by_lua
用於啟動一些定時任務,比如心跳檢查,定時拉取伺服器配置等等;此處的任務是跟Worker程序數量有關係的,比如有2個Worker程序那麼就會啟動兩個完全一樣的定時任務。
1、nginx.conf配置檔案中的http部分新增如下程式碼
- init_worker_by_lua_file /usr/example/lua/init_worker.lua;
2、init_worker.lua
- local count = 0
- local delayInSeconds = 3
- local heartbeatCheck = nil
- heartbeatCheck = function(args)
- count = count + 1
- ngx.log(ngx.ERR, "do check ", count)
- local ok, err = ngx.timer.at(delayInSeconds, heartbeatCheck)
- if not ok then
- ngx.log(ngx.ERR, "failed to startup heartbeart worker...", err)
- end
- end
- heartbeatCheck()
ngx.timer.at:延時呼叫相應的回撥方法;ngx.timer.at(秒單位延時,回撥函式,回撥函式的引數列表);可以將延時設定為0即得到一個立即執行的任務,任務不會在當前請求中執行不會阻塞當前請求,而是在一個輕量級執行緒中執行。
另外根據實際情況設定如下指令
lua_max_pending_timers 1024; #最大等待任務數
lua_max_running_timers 256; #最大同時執行任務數
set_by_lua
設定nginx變數,我們用的set指令即使配合if指令也很難實現負責的賦值邏輯;
1.1、example.conf配置檔案
- location /lua_set_1 {
- default_type "text/html";
- set_by_lua_file $num /usr/example/lua/test_set_1.lua;
- echo $num;
- }
set_by_lua_file:語法set_by_lua_file $var lua_file arg1 arg2...; 在lua程式碼中可以實現所有複雜的邏輯,但是要執行速度很快,不要阻塞;
1.2、test_set_1.lua
- local uri_args = ngx.req.get_uri_args()
- local i = uri_args["i"] or 0
- local j = uri_args["j"] or 0
- return i + j
得到請求引數進行相加然後返回。
訪問如http://192.168.1.2/lua_set_1?i=1&j=10進行測試。 如果我們用純set指令是無法實現的。
再舉個實際例子,我們實際工作時經常涉及到網站改版,有時候需要新老並存,或者切一部分流量到新版
2.1、首先在example.conf中使用map指令來對映host到指定nginx變數,方便我們測試
- ############ 測試時使用的動態請求
- map $host $item_dynamic {
- default "0";
- item2014.jd.com "1";
- }
如繫結hosts
192.168.1.2 item.jd.com;
192.168.1.2 item2014.jd.com;
此時我們想訪問item2014.jd.com時訪問新版,那麼我們可以簡單的使用如
- if ($item_dynamic = "1") {
- proxy_pass http://new;
- }
- proxy_pass http://old;
但是我們想把商品編號為為8位(比如品類為圖書的)沒有改版完成,需要按照相應規則跳轉到老版,但是其他的到新版;雖然使用if指令能實現,但是比較麻煩,基本需要這樣
- set jump "0";
- if($item_dynamic = "1") {
- set $jump "1";
- }
- if(uri ~ "^/6[0-9]{7}.html") {
- set $jump "${jump}2";
- }
- #非強制訪問新版,且訪問指定範圍的商品
- if (jump == "02") {
- proxy_pass http://old;
- }
- proxy_pass http://new;
以上規則還是比較簡單的,如果涉及到更復雜的多重if/else或巢狀if/else實現起來就更痛苦了,可能需要到後端去做了;此時我們就可以藉助lua了:
- set_by_lua $to_book '
- local ngx_match = ngx.re.match
- local var = ngx.var
- local skuId = var.skuId
- local r = var.item_dynamic ~= "1" and ngx.re.match(skuId, "^[0-9]{8}$")
- if r then return "1" else return "0" end;
- ';
- set_by_lua $to_mvd '
- local ngx_match = ngx.re.match
- local var = ngx.var
- local skuId = var.skuId
- local r = var.item_dynamic ~= "1" and ngx.re.match(skuId, "^[0-9]{9}$")
- if r then return "1" else return "0" end;
- ';
- #自營圖書
- if ($to_book) {
- proxy_pass http://127.0.0.1/old_book/$skuId.html;
- }
- #自營音像
- if ($to_mvd) {
- proxy_pass http://127.0.0.1/old_mvd/$skuId.html;
- }
- #預設
- proxy_pass http://127.0.0.1/proxy/$skuId.html;
rewrite_by_lua
執行內部URL重寫或者外部重定向,典型的如偽靜態化的URL重寫。其預設執行在rewrite處理階段的最後。
1.1、example.conf配置檔案
- location /lua_rewrite_1 {
- default_type "text/html";
- rewrite_by_lua_file /usr/example/lua/test_rewrite_1.lua;
- echo "no rewrite";
- }
1.2、test_rewrite_1.lua
- if ngx.req.get_uri_args()["jump"] == "1" then
- return ngx.redirect("http://www.jd.com?jump=1", 302)
- end
當我們請求http://192.168.1.2/lua_rewrite_1時發現沒有跳轉,而請求http://192.168.1.2/lua_rewrite_1?jump=1時發現跳轉到京東首頁了。 此處需要301/302跳轉根據自己需求定義。
2.1、example.conf配置檔案
- location /lua_rewrite_2 {
- default_type "text/html";
- rewrite_by_lua_file /usr/example/lua/test_rewrite_2.lua;
- echo "rewrite2 uri : $uri, a : $arg_a";
- }
2.2、test_rewrite_2.lua
- if ngx.req.get_uri_args()["jump"] == "1" then
- ngx.req.set_uri("/lua_rewrite_3", false);
- ngx.req.set_uri("/lua_rewrite_4", false);
- ngx.req.set_uri_args({a = 1, b = 2});
- end
ngx.req.set_uri(uri, false):可以內部重寫uri(可以帶引數),等價於 rewrite ^ /lua_rewrite_3;通過配合if/else可以實現 rewrite ^ /lua_rewrite_3 break;這種功能;此處兩者都是location內部url重寫,不會重新發起新的location匹配;
ngx.req.set_uri_args:重寫請求引數,可以是字串(a=1&b=2)也可以是table;
訪問如http://192.168.1.2/lua_rewrite_2?jump=0時得到響應
rewrite2 uri : /lua_rewrite_2, a :
訪問如http://192.168.1.2/lua_rewrite_2?jump=1時得到響應
rewrite2 uri : /lua_rewrite_4, a : 1
3.1、example.conf配置檔案
- location /lua_rewrite_3 {
- default_type "text/html";
- rewrite_by_lua_file /usr/example/lua/test_rewrite_3.lua;
- echo "rewrite3 uri : $uri";
- }
3.2、test_rewrite_3.lua
- if ngx.req.get_uri_args()["jump"] == "1" then
- ngx.req.set_uri("/lua_rewrite_4", true);
- ngx.log(ngx.ERR, "=========")
- ngx.req.set_uri_args({a = 1, b = 2});
- end
ngx.req.set_uri(uri, true):可以內部重寫uri,即會發起新的匹配location請求,等價於 rewrite ^ /lua_rewrite_4 last;此處看error log是看不到我們記錄的log。
所以請求如http://192.168.1.2/lua_rewrite_3?jump=1會到新的location中得到響應,此處沒有/lua_rewrite_4,所以匹配到/lua請求,得到類似如下的響應
global variable : 2 , shared memory : 1 hello world
即
rewrite ^ /lua_rewrite_3; 等價於 ngx.req.set_uri("/lua_rewrite_3", false);
rewrite ^ /lua_rewrite_3 break; 等價於 ngx.req.set_uri("/lua_rewrite_3", false); 加 if/else判斷/break/return
rewrite ^ /lua_rewrite_4 last; 等價於 ngx.req.set_uri("/lua_rewrite_4", true);
注意,在使用rewrite_by_lua時,開啟rewrite_log on;後也看不到相應的rewrite log。
access_by_lua
用於訪問控制,比如我們只允許內網ip訪問,可以使用如下形式
- allow 127.0.0.1;
- allow 10.0.0.0/8;
- allow 192.168.0.0/16;
- allow 172.16.0.0/12;
- deny all;
1.1、example.conf配置檔案
- location /lua_access {
- default_type "text/html";
- access_by_lua_file /usr/example/lua/test_access.lua;
- echo "access";
- }
1.2、test_access.lua
- if ngx.req.get_uri_args()["token"] ~= "123" then
- return ngx.exit(403)
- end
即如果訪問如http://192.168.1.2/lua_access?token=234將得到403 Forbidden的響應。這樣我們可以根據如cookie/使用者token來決定是否有訪問許可權。
content_by_lua
此指令之前已經用過了,此處就不講解了。
另外在使用PCRE進行正則匹配時需要注意正則的寫法,具體規則請參考http://wiki.nginx.org/HttpLuaModule中的Special PCRE Sequences部分。還有其他的注意事項也請閱讀官方文件。