本文目的是學(xué)習(xí) Nginx+Lua 開發(fā),對于 Nginx 基本知識可以參考如下文章:
nginx 啟動、關(guān)閉、重啟
http://www.cnblogs.com/derekchen/archive/2011/02/17/1957209.html
agentzh 的 Nginx 教程
http://openresty.org/download/agentzh-nginx-tutorials-zhcn.html
Nginx+Lua 入門
http://17173ops.com/2013/11/01/17173-ngx-lua-manual.shtml
nginx 配置指令的執(zhí)行順序
http://zhongfox.github.io/blog/server/2013/05/15/nginx-exec-order/
nginx 與 lua 的執(zhí)行順序和步驟說明
http://www.mrhaoting.com/?p=157
Nginx 配置文件 nginx.conf 中文詳解
Tengine 的 Nginx 開發(fā)從入門到精通
http://tengine.taobao.org/book/
官方文檔
http://wiki.nginx.org/Configuration
本文目的是學(xué)習(xí) Nginx+Lua 開發(fā),對于 Lua 基本知識可以參考如下文章:
Lua 簡明教程
http://coolshell.cn/articles/10739.html
lua 在線 lua 學(xué)習(xí)教程
Lua 5.1 參考手冊
http://www.codingnow.com/2000/download/lua_manual.html
Lua 5.3 參考手冊
http://cloudwu.github.io/lua53doc/
和一般的 Web Server 類似,我們需要接收請求、處理并輸出響應(yīng)。而對于請求我們需要獲取如請求參數(shù)、請求頭、Body 體等信息;而對于處理就是調(diào)用相應(yīng)的 Lua 代碼即可;輸出響應(yīng)需要進(jìn)行響應(yīng)狀態(tài)碼、響應(yīng)頭和響應(yīng)內(nèi)容體的輸出。因此我們從如上幾個點出發(fā)即可。
Java 代碼 收藏代碼
location ~ /lua_request/(\d+)/(\d+) {
#設(shè)置nginx變量
set $a $1;
set $b $host;
default_type "text/html";
#nginx內(nèi)容處理
content_by_lua_file /usr/example/lua/test_request.lua;
#內(nèi)容體處理完成后調(diào)用
echo_after_body "ngx.var.b $b";
}
Java 代碼 收藏代碼
--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參數(shù)
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請求參數(shù)
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協(xié)議版本
ngx.say("ngx.req.http_version : ", ngx.req.http_version(), "<br/>")
--請求方法
ngx.say("ngx.req.get_method : ", ngx.req.get_method(), "<br/>")
--原始的請求頭內(nèi)容
ngx.say("ngx.req.raw_header : ", ngx.req.raw_header(), "<br/>")
--請求的body內(nèi)容體
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 [捕獲組數(shù)字]獲??;
ngx.req.get_headers:獲取請求頭,默認(rèn)只獲取前100,如果想要獲取所以可以調(diào)用ngx.req.get_headers(0);獲取帶中劃線的請求頭時請使用如 headers.user_agent 這種方式;如果一個請求頭有多個值,則返回的是 table;
ngx.req.get_uri_args:獲取 url 請求參數(shù),其用法和 get_headers 類似;
ngx.req.get_post_args:獲取 post 請求內(nèi)容體,其用法和 get_headers 類似,但是必須提前調(diào)用 ngx.req.read_body() 來讀取 body 體(也可以選擇在 nginx 配置文件使用lua_need_request_body on;開啟讀取 body 體,但是官方不推薦);
ngx.req.raw_header:未解析的請求頭字符串;
ngx.req.get_body_data:為解析的請求 body 體內(nèi)容字符串。
如上方法處理一般的請求基本夠用了。另外在讀取 post 內(nèi)容體時根據(jù)實際情況設(shè)置 client_body_buffer_size 和 client_max_body_size 來保證內(nèi)容在內(nèi)存而不是在文件中。
使用如下腳本測試
Java 代碼 收藏代碼
wget --post-data 'a=1&b=2' 'http://127.0.0.1/lua_request/1/2?a=3&b=4' -O -
Java 代碼 收藏代碼
location /lua_response_1 {
default_type "text/html";
content_by_lua_file /usr/example/lua/test_response_1.lua;
}
Java 代碼 收藏代碼
--寫響應(yīng)頭
ngx.header.a = "1"
--多個響應(yīng)頭可以使用table
ngx.header.b = {"2", "3"}
--輸出響應(yīng)
ngx.say("a", "b", "<br/>")
ngx.print("c", "d", "<br/>")
--200狀態(tài)碼退出
return ngx.exit(200)
ngx.header:輸出響應(yīng)頭;
ngx.print:輸出響應(yīng)內(nèi)容體;
ngx.say:通ngx.print,但是會最后輸出一個換行符;
ngx.exit:指定狀態(tài)碼退出。
Java 代碼 收藏代碼
location /lua_response_2 {
default_type "text/html";
content_by_lua_file /usr/example/lua/test_response_2.lua;
}
Java 代碼 收藏代碼
ngx.redirect("http://jd.com", 302)
ngx.redirect:重定向;
ngx.status= 狀態(tài)碼,設(shè)置響應(yīng)的狀態(tài)碼;ngx.resp.get_headers() 獲取設(shè)置的響應(yīng)狀態(tài)碼;ngx.send_headers() 發(fā)送響應(yīng)狀態(tài)碼,當(dāng)調(diào)用 ngx.say/ngx.print 時自動發(fā)送響應(yīng)狀態(tài)碼;可以通過 ngx.headers_sent=true 判斷是否發(fā)送了響應(yīng)狀態(tài)碼。
Java 代碼 收藏代碼
location /lua_other {
default_type "text/html";
content_by_lua_file /usr/example/lua/test_other.lua;
}
Java 代碼 收藏代碼
--未經(jīng)解碼的請求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/>")
更多 Nginx Lua API 請參考 http://wiki.nginx.org/HttpLuaModule#Nginx_API_for_Lua。
使用過如 Java 的朋友可能知道如 Ehcache 等這種進(jìn)程內(nèi)本地緩存,Nginx 是一個 Master 進(jìn)程多個 Worker 進(jìn)程的工作方式,因此我們可能需要在多個 Worker 進(jìn)程中共享數(shù)據(jù),那么此時就可以使用 ngx.shared.DICT 來實現(xiàn)全局內(nèi)存共享。
Java 代碼 收藏代碼
\#共享全局變量,在所有worker間共享
lua_shared_dict shared_data 1m;
Java 代碼 收藏代碼
location /lua_shared_dict {
default_type "text/html";
content_by_lua_file /usr/example/lua/test_lua_shared_dict.lua;
}
Java 代碼 收藏代碼
--1、獲取全局共享內(nèi)存變量
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/>")
更多 API 請參考 http://wiki.nginx.org/HttpLuaModule#ngx.shared.DICT。
到此基本的 Nginx Lua API 就學(xué)完了,對于請求處理和輸出響應(yīng)如上介紹的 API 完全夠用了,更多 API 請參考官方文檔。
Nginx 共11個處理階段,而相應(yīng)的處理階段是可以做插入式處理,即可插拔式架構(gòu);另外指令可以在 http、server、server if、location、location if 幾個范圍進(jìn)行配置:
|
指令 |
所處處理階段 |
使用范圍 |
解釋 |
|
init_by_lua init_by_lua_file |
loading-config |
http |
nginx Master進(jìn)程加載配置時執(zhí)行; 通常用于初始化全局配置/預(yù)加載Lua模塊 |
|
init_worker_by_lua init_worker_by_lua_file |
starting-worker |
http |
每個Nginx Worker進(jìn)程啟動時調(diào)用的計時器,如果Master進(jìn)程不允許則只會在init_by_lua之后調(diào)用; 通常用于定時拉取配置/數(shù)據(jù),或者后端服務(wù)的健康檢查 |
|
set_by_lua set_by_lua_file |
rewrite |
server,server if,location,location if |
設(shè)置nginx變量,可以實現(xiàn)復(fù)雜的賦值邏輯;此處是阻塞的,Lua代碼要做到非???; |
|
rewrite_by_lua rewrite_by_lua_file |
rewrite tail |
http,server,location,location if |
rrewrite階段處理,可以實現(xiàn)復(fù)雜的轉(zhuǎn)發(fā)/重定向邏輯; |
|
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 |
內(nèi)容處理器,接收請求處理并輸出響應(yīng) |
|
header_filter_by_lua header_filter_by_lua_file |
output-header-filter |
http,server,location,location if |
設(shè)置header和cookie |
|
body_filter_by_lua body_filter_by_lua_file |
output-body-filter |
http,server,location,location if |
對響應(yīng)數(shù)據(jù)進(jìn)行過濾,比如截斷、替換。 |
|
log_by_lua log_by_lua_file |
log |
http,server,location,location if |
log階段處理,比如記錄訪問量/統(tǒng)計平均響應(yīng)時間 |
更詳細(xì)的解釋請參考 http://wiki.nginx.org/HttpLuaModule#Directives。如上指令很多并不常用,因此我們只拿其中的一部分做演示。
每次 Nginx 重新加載配置時執(zhí)行,可以用它來完成一些耗時模塊的加載,或者初始化一些全局配置;在 Master 進(jìn)程創(chuàng)建 Worker 進(jìn)程時,此指令中加載的全局變量會進(jìn)行 Copy-OnWrite,即會復(fù)制到所有全局變量到 Worker 進(jìn)程。
Java 代碼 收藏代碼
\#共享全局變量,在所有worker間共享
lua_shared_dict shared_data 1m;
init_by_lua_file /usr/example/lua/init.lua;
Java 代碼 收藏代碼
--初始化耗時的模塊
local redis = require 'resty.redis'
local cjson = require 'cjson'
--全局變量,不推薦
count = 1
--共享全局內(nèi)存
local shared_data = ngx.shared.shared_data
shared_data:set("count", 1)
Java 代碼 收藏代碼
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")
global variable : 2 , shared memory : 8 hello world
另外注意一定在生產(chǎn)環(huán)境開啟 lua_code_cache,否則每個請求都會創(chuàng)建 Lua VM 實例。
用于啟動一些定時任務(wù),比如心跳檢查,定時拉取服務(wù)器配置等等;此處的任務(wù)是跟 Worker 進(jìn)程數(shù)量有關(guān)系的,比如有2個 Worker 進(jìn)程那么就會啟動兩個完全一樣的定時任務(wù)。
Java 代碼 收藏代碼
init_worker_by_lua_file /usr/example/lua/init_worker.lua;
Java 代碼 收藏代碼
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:延時調(diào)用相應(yīng)的回調(diào)方法;ngx.timer.at(秒單位延時,回調(diào)函數(shù),回調(diào)函數(shù)的參數(shù)列表);可以將延時設(shè)置為0即得到一個立即執(zhí)行的任務(wù),任務(wù)不會在當(dāng)前請求中執(zhí)行不會阻塞當(dāng)前請求,而是在一個輕量級線程中執(zhí)行。
另外根據(jù)實際情況設(shè)置如下指令
set_by_lua
設(shè)置 nginx 變量,我們用的 set 指令即使配合 if 指令也很難實現(xiàn)負(fù)責(zé)的賦值邏輯;
Java 代碼 收藏代碼
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代碼中可以實現(xiàn)所有復(fù)雜的邏輯,但是要執(zhí)行速度很快,不要阻塞;
Java 代碼 收藏代碼
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
得到請求參數(shù)進(jìn)行相加然后返回。
訪問如 http://192.168.1.2/lua_set_1?i=1&j=10 進(jìn)行測試。 如果我們用純 set 指令是無法實現(xiàn)的。
再舉個實際例子,我們實際工作時經(jīng)常涉及到網(wǎng)站改版,有時候需要新老并存,或者切一部分流量到新版
Java 代碼 收藏代碼
############ 測試時使用的動態(tài)請求
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 時訪問新版,那么我們可以簡單的使用如
Java 代碼 收藏代碼
if ($item_dynamic = "1") {
proxy_pass http://new;
}
proxy_pass http://old;
但是我們想把商品編號為 8 位(比如品類為圖書的)沒有改版完成,需要按照相應(yīng)規(guī)則跳轉(zhuǎn)到老版,但是其他的到新版;雖然使用 if 指令能實現(xiàn),但是比較麻煩,基本需要這樣
Java 代碼 收藏代碼
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;
以上規(guī)則還是比較簡單的,如果涉及到更復(fù)雜的多重 if/else 或嵌套 if/else 實現(xiàn)起來就更痛苦了,可能需要到后端去做了;此時我們就可以借助 lua 了:
Java 代碼 收藏代碼
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;
}
\#默認(rèn)
proxy_pass http://127.0.0.1/proxy/$skuId.html;
執(zhí)行內(nèi)部 URL 重寫或者外部重定向,典型的如偽靜態(tài)化的 URL 重寫。其默認(rèn)執(zhí)行在 rewrite 處理階段的最后。
Java 代碼 收藏代碼
location /lua_rewrite_1 {
default_type "text/html";
rewrite_by_lua_file /usr/example/lua/test_rewrite_1.lua;
echo "no rewrite";
}
Java 代碼 收藏代碼
if ngx.req.get_uri_args()["jump"] == "1" then
return ngx.redirect("http://www.jd.com?jump=1", 302)
end
當(dāng)我們請求 http://192.168.1.2/lua_rewrite_1 時發(fā)現(xiàn)沒有跳轉(zhuǎn),而請求 http://192.168.1.2/lua_rewrite_1?jump=1 時發(fā)現(xiàn)跳轉(zhuǎn)到京東首頁了。 此處需要301/302跳轉(zhuǎn)根據(jù)自己需求定義。
Java 代碼 收藏代碼
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";
}
Java 代碼 收藏代碼
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):可以內(nèi)部重寫 uri(可以帶參數(shù)),等價于 rewrite ^ /lua_rewrite_3;通過配合 if/else 可以實現(xiàn) rewrite ^ /lua_rewrite_3 break;這種功能;此處兩者都是 location 內(nèi)部 url 重寫,不會重新發(fā)起新的 location 匹配;
ngx.req.set_uri_args:重寫請求參數(shù),可以是字符串(a=1&b=2)也可以是 table;
訪問如 http://192.168.1.2/lua_rewrite_2?jump=0 時得到響應(yīng)
rewrite2 uri : /lua_rewrite_2, a :
訪問如 http://192.168.1.2/lua_rewrite_2?jump=1 時得到響應(yīng)
rewrite2 uri : /lua_rewrite_4, a : 1
Java 代碼 收藏代碼
location /lua_rewrite_3 {
default_type "text/html";
rewrite_by_lua_file /usr/example/lua/test_rewrite_3.lua;
echo "rewrite3 uri : $uri";
}
Java 代碼 收藏代碼
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):可以內(nèi)部重寫 uri,即會發(fā)起新的匹配 location 請求,等價于 rewrite ^ /lua_rewrite_4 last;此處看 error log 是看不到我們記錄的log。
所以請求如 http://192.168.1.2/lua_rewrite_3?jump=1 會到新的 location 中得到響應(yīng),此處沒有 /lua_rewrite_4,所以匹配到 /lua 請求,得到類似如下的響應(yīng) 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;后也看不到相應(yīng)的 rewrite log。
用于訪問控制,比如我們只允許內(nèi)網(wǎng) ip 訪問,可以使用如下形式
Java 代碼 收藏代碼
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;
Java 代碼 收藏代碼
location /lua_access {
default_type "text/html";
access_by_lua_file /usr/example/lua/test_access.lua;
echo "access";
}
Java 代碼 收藏代碼
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 的響應(yīng)。這樣我們可以根據(jù)如 cookie/ 用戶 token 來決定是否有訪問權(quán)限。
此指令之前已經(jīng)用過了,此處就不講解了。
另外在使用 PCRE 進(jìn)行正則匹配時需要注意正則的寫法,具體規(guī)則請參考 http://wiki.nginx.org/HttpLuaModule中的Special PCRE Sequences部 分。還有其他的注意事項也請閱讀官方文檔。