第二章 OpenResty(Nginx+Lua)開發入門

Nginx入門

本文目的是學習Nginx+Lua開發,對於Nginx基本知識可以參考如下文章:

nginx啓動、關閉、重啓

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 配置指令的執行順序

http://zhongfox.github.io/blog/server/2013/05/15/nginx-exec-order/

nginx與lua的執行順序和步驟說明

http://www.mrhaoting.com/?p=157

Nginx配置文件nginx.conf中文詳解

http://www.ha97.com/5194.html

Tengine的Nginx開發從入門到精通

http://tengine.taobao.org/book/

官方文檔

http://wiki.nginx.org/Configuration

 

Lua入門

本文目的是學習Nginx+Lua開發,對於Lua基本知識可以參考如下文章:

Lua簡明教程

http://coolshell.cn/articles/10739.html

lua在線lua學習教程

http://book.luaer.cn/

Lua 5.1 參考手冊

http://www.codingnow.com/2000/download/lua_manual.html

Lua5.3 參考手冊

http://cloudwu.github.io/lua53doc/

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體內容字符串。

 

如上方法處理一般的請求基本夠用了。另外在讀取post內容體時根據實際情況設置client_body_buffer_sizeclient_max_body_size來保證內容在內存而不是在文件中。

 

使用如下腳本測試

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 Lua API請參考 http://wiki.nginx.org/HttpLuaModule#Nginx_API_for_Lua

 

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/>")

更多API請參考http://wiki.nginx.org/HttpLuaModule#ngx.shared.DICT。 

 

 

到此基本的Nginx Lua API就學完了,對於請求處理和輸出響應如上介紹的API完全夠用了,更多API請參考官方文檔。

 

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階段處理,比如記錄訪問量/統計平均響應時間

 

更詳細的解釋請參考http://wiki.nginx.org/HttpLuaModule#Directives。如上指令很多並不常用,因此我們只拿其中的一部分做演示。

 

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部分。還有其他的注意事項也請閱讀官方文檔。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章