1. 程式人生 > >Nginx Lua的執行階段

Nginx Lua的執行階段

對剛接觸Ngx_lua的讀者來說,可能會存在下面兩個困惑。

1、Lua在Nginx的哪些階段可以執行程式碼?
Lua在Nginx的每個階段可以執行哪些操作?

2、只有理解了這兩個問題,才能在業務中巧妙地利用Ngx_Lua來完成各項需求。

Nginx的11個執行階段,每個階段都有自己能夠執行的指令,並可以實現不同的功能。Ngx_Lua的功能大部分是基於Nginx這11個執行階段開發和配置的,Lua程式碼在這些指令塊中執行,並依賴於它們的執行順序。本章將對Ngx_Lua的執行階段進行一一講解。

一、 init_by_lua_block

init_by_lua_block是init_by_lua的替代版本,在OpenResty 1.9.3.1或Lua-Nginx-Modulev 0.9.17之前使用的都是init_by_lua。init_by_lua_block比init_by_lua更靈活,所以建議優先選用init_by_lua_block。
本章中的執行階段都採用*_block格式的指令,後續不再說明。

1.1 階段說明
語法:init_by_lua_block {lua-script-str}
配置環境:http
階段:loading-config
含義:當Nginx的master程序載入Nginx配置檔案(載入或重啟Nginx程序)時,會在全域性的Lua VM(Virtual Machine,虛擬機器)層上執行<lua-script-str> 指定的程式碼,每次當Nginx獲得HUP(即Hangup)過載訊號載入程序時,程式碼都會被重新執行。

1.2 初始化配置
在loading-config階段一般會執行如下操作。
1.初始化Lua全域性變數,特別適合用來處理在啟動master程序時就要求存在的資料,對CPU消耗較多的功能也可以在此處處理。
2.預載入模組。
3.初始化lua_shared_dict共享記憶體的資料(關於共享記憶體詳見第10章)。
示例如下:
user webuser webuser;
worker_processes 1;
worker_rlimit_nofile 10240;

events {
use epoll;
worker_connections 10240;
}

http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr-$remote_user[$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" "$request_time" "$upstream_addr $upstream_status $upstream_response_time" "upstream_time_sum:$upstream_time_sum" "jk_uri:$jk_uri"';
access_log logs/access.log main;
sendfile on;
keepalive_timeout 65;

lua_package_path "/usr/local/nginx_1.12.2/conf/lua_modules/?.lua;;";
lua_package_cpath "/usr/local/nginx_1.12.2/conf/lua_modules/c_package/?.so;;";

lua_shared_dict dict_a 100k;  --宣告一個Lua共享記憶體,dict_a為100KB

init_by_lua_block {
-- cjson.so檔案需要手動存放在lua_package_cpath搜尋路徑下,如果是OpenResty,就不必了,因為它預設支援該操作
cjson = require "cjson";
local dict_a = ngx.shared.dict_a;
dict_a:set("abc", 9)
}

server {
    listen       80;
    server_name  testnginx.com;
    location / {
       content_by_lua_block {
           ngx.say(cjson.encode({a = 1, b = 2}))
           local dict_a = ngx.shared.dict_a;
           ngx.say("abc=",dict_a:get("abc"))
       }
    }
}

執行結果如下:

# curl -I http://testnginx.com/
{"a":1,"b":2}
abc=9

1.3 控制初始值
在init_by_lua_block階段設定的初始值,即使在其他執行階段被修改過,當Nginx過載配置時,這些值就又會恢復到初始狀態。如果在過載Nginx配置時不希望再次改動這些初始值,可以在程式碼中做如下調整。

init_by_lua_block {
      local cjson = require "cjson";
      local dict_a = ngx.shared.dict_a;
      local v = dict_a:get("abc");  --判斷初始值是否已經被set
      if not v then                 --如果沒有,就執行初始化操作
        dict_a:set("abc", 9)
      end
}

1.4 init_by_lua_file
init_by_lua_file和init_by_lua_block的作用一樣,主要用於將init_by_lua_block的內容轉存到指定的檔案中,這樣Lua程式碼就不必全部寫在Nginx配置裡了,易讀性更強。
init_by_lua_file支援配置絕對路徑和相對路徑。相對路徑是在啟動Nginx時由-p PATH 決定的,如果啟動Nginx時沒有配置-p PATH,就會使用編譯時--prefix的值,該值一般存放在Nginx的$prefix(也可以用${prefix}來表示)變數中。init_by_lua_file和Nginx的include指令的相對路徑一致。
舉例如下:

init_by_lua_file conf/lua/init.lua;  --相對路徑
init_by_lua_file /usr/local/nginx/conf/lua/init.lua; --絕對路徑
init.lua檔案的內容如下:
cjson = require "cjson"
local dict_a = ngx.shared.dict_a
local v = dict_a:get("abc")
if not v then
   dict_a:set("abc", 9)
end  

1.5 可使用的Lua API指令
init_by_lua是Nginx配置載入的階段,很多Nginx API for Lua命令是不支援的。目前已知支援的Nginx API for Lua的命令有ngx.log、ngx.shared.DICT、print。
注意:init_by_lua
中的表示萬用字元,init_by_lua即所有以init_by_lua開頭的API。後續的萬用字元亦是如此,不再另行說明。

二、init_worker_by_lua_block

2.1 階段說明
語法:init_worker_by_lua_block {lua-script-str}
配置環境:http
階段:starting-worker
含義:當master程序被啟動後,每個worker程序都會執行Lua程式碼。如果Nginx禁用了master程序,init_by_lua*將會直接執行。

2.2 啟動Nginx的定時任務
在init_worker_by_lua_block執行階段最常見的功能是執行定時任務。示例如下:

user  webuser webuser;
worker_processes  3;
worker_rlimit_nofile 10240;

events {
    use epoll;
    worker_connections  10240;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    upstream  test_12 {
        server 127.0.0.1:81  weight=20  max_fails=300000 fail_timeout=5s;
        server 127.0.0.1:82  weight=20  max_fails=300000 fail_timeout=5s;
    }

    lua_package_path "${prefix}conf/lua_modules/?.lua;;";
    lua_package_cpath "${prefix}conf/lua_modules/c_package/?.so;;";

    init_worker_by_lua_block  {
        local delay = 3  --3秒
        local cron_a   
    --定時任務的函式
        cron_a = function (premature)   
            if not  premature then  --如果執行函式時沒有傳參,則該任務會一直被觸發執行
                ngx.log(ngx.ERR, "Just do it !")
            end

        end
    --每隔delay引數值的時間,就執行一次cron_a函式
        local ok, err = ngx.timer.every(delay, cron_a)   
        if not ok then
            ngx.log(ngx.ERR, "failed to create the timer: ", err)
            return
        end
    }

2.3 動態進行後端健康檢查
在init_worker_by_lua_block階段,也可以實現後端健康檢查的功能,用於檢查後端HTTP服務是否正常,類似於Nginx商業版中的health_check功能。
如果使用OpenResty 1.9.3.2及以上的版本,預設已支援此模組;如果使用Nginx,則首先需要安裝此模組,安裝方式如下:

# git clone https://github.com/openresty/lua-upstream-nginx-module.git
# cd nginx-1.12.2
# ./configure --prefix=/opt/nginx \
    --with-ld-opt="-Wl,-rpath,$LUAJIT_LIB" \
    --add-module=/path/to/lua-nginx-module \
    --add-module=/path/to/lua-upstream-nginx-module 
# make && make install

注意:安裝完成後,重新編譯Nginx時,請先確認之前安裝模組的數量,避免遺忘某個模組。
然後,將lua-resty-upstream-healthcheck模組中的lib檔案複製到lua_package_path指定的位置,示例如下:

# git clone https://github.com/openresty/lua-resty-upstream-healthcheck.git
# cp lua-resty-upstream-healthcheck/lib/resty/upstream/healthcheck.lua   /usr/local/nginx_1.12.2/conf/lua_modules/resty/

實現動態進行後端健康檢查的功能,配置如下:

user  webuser webuser;
worker_processes  3;
worker_rlimit_nofile 10240;

events {
    use epoll;
    worker_connections  10240;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    upstream  test_12 {
        server 127.0.0.1:81  weight=20  max_fails=10 fail_timeout=5s;
        server 127.0.0.1:82  weight=20  max_fails=10 fail_timeout=5s;
        server 127.0.0.1:8231  weight=20  max_fails=10 fail_timeout=5s;
    }
    lua_shared_dict healthcheck 1m;  # 存放upstream servers的共享記憶體,後端伺服器組越多,配置就越大
    lua_socket_log_errors off;  # 當TCP傳送失敗時,會發送error日誌到error.log中,該過程會增加效能開銷,建議關閉,以避免在健康檢查過程中出現多臺伺服器宕機的情況,異常情況請使用ngx.log來記錄
    lua_package_path "${prefix}conf/lua_modules/?.lua;;";
    lua_package_cpath "${prefix}conf/lua_modules/c_package/?.so;;";

    init_worker_by_lua_block  {
        local hc = require "resty.upstream.healthcheck"
        local ok, err = hc.spawn_checker{
            shm = "healthcheck",  -- 使用共享記憶體 
            upstream = "test_12", -- 進行健康檢查的upstream名字
            type = "http",        -- 檢查型別是http
            http_req = "GET /status HTTP/1.0\r\nHost: testnginx.com\r\n\r\n",
                    -- 用來發送HTTP請求的格式和資料,核實服務是否正常 
            interval = 3000,  -- 設定檢查的頻率為每3s一次
            timeout = 1000,   -- 設定請求的超時時間為1s
            fall = 3,  --設定連續失敗3次後就把後端服務改為down 
            rise = 2,  --設定連續成功2次後就把後端服務改為up 
        valid_statuses = {200, 302},  --設定請求成功的響應狀態碼是200和302
            concurrency = 10,  --設定傳送請求的併發等級
        }
        if not ok then
            ngx.log(ngx.ERR, "failed to spawn health checker: ", err)
            return
        end
    }
    server {
        listen       80;
        server_name  testnginx.com;
        location / {
           proxy_pass http://test_12;
        }
        # /status 定義了後端健康檢查結果的輸出頁面
        location = /status {
            default_type text/plain;
            content_by_lua_block {
                local hc = require "resty.upstream.healthcheck"
                --輸出當前檢查結果是哪個worker程序的
                ngx.say("Nginx Worker PID: ", ngx.worker.pid())
--status_page()輸出後端伺服器的詳細情況
                ngx.print(hc.status_page())
            }
        }
    }

訪問http://testnginx.com/status檢視檢查的結果,圖8-1所示為健康檢查資料結果。
Nginx Lua的執行階段
圖8-1 健康檢查資料結果
如果要檢查多個upstream,則配置如下(只有黑色加粗位置的配置有變化):

local ok, err = hc.spawn_checker{
    shm = "healthcheck",
    upstream = "test_12",
    type = "http",

    http_req = "GET /status HTTP/1.0\r\nHost: testnginx.com\r\n\r\n",

    interval = 3000,
    timeout = 1000,
    fall = 3,
    rise = 2,
    valid_statuses = {200, 302},  
    concurrency = 10, 
}
local ok, err = hc.spawn_checker{
    shm = "healthcheck",
    upstream = "test_34",
    type = "http",

    http_req = "GET /test HTTP/1.0\r\nHost: testnginx.com\r\n\r\n",
    interval = 3000,
    timeout = 1000,
    fall = 3,
    rise = 2,
    valid_statuses = {200, 302}, 
    concurrency = 10,  

如果把lua_socket_log_errors設定為on,那麼當有異常出現時,例如出現了超時,就會往error.log裡寫日誌,日誌記錄如圖8-2所示。
Nginx Lua的執行階段
圖8-2 日誌記錄
經過lua-resty-upstream-healthcheck的健康檢查發現異常的伺服器後,Nginx會動態地將異常伺服器在upstream中禁用,以實現更精準的故障轉移。

三、set_by_lua_block

3.1 階段說明
語法:set_by_lua_block $res {lua-script-str}
配置環境:server,server if,location,location if
階段:rewrite
含義:執行<lua-script-str>程式碼,並將返回的字串賦值給$res。

3.2 變數賦值
本指令一次只能返回一個值,並賦值給變數$res(即只有一個$res被賦值),示例如下:

server {
    listen       80;
    server_name  testnginx.com;
    location / {
    set $a '';
    set_by_lua_block $a {
         local t = 'tes'
         return t
    }
    return 200 $a;
}

執行結果如下:

#  curl http://testnginx.com/
test

那如果希望返回多個變數,該怎麼辦呢?可以使用ngx.var.VARIABLE,示例如下:

server {
    listen       80;
    server_name  testnginx.com;
    location / {
    #使用ngx.var.VARIABLE前需先定義變數
    set $b '';
    set_by_lua_block $a {
        local t = 'test'
        ngx.var.b = 'test_b'
        return t
    }
    return 200 $a,$b;
    }
}

執行結果如下:

#  curl http://testnginx.com/test
test,test_b

3.2 Rewrite階段的混用模式
因為set_by_lua_block處在rewrite階段,所以它可以和ngx_http_rewrite_module、set-misc-nginx-module,以及array-var-nginx-module一起使用,在程式碼執行時,按照配置檔案的順序從上到下執行,示例如下:

server {
    listen       80;
    server_name  testnginx.com;
    location / {

    set $a '123';
    set_by_lua_block $b {
        local t = 'bbb'
        return t
    }
    set_by_lua_block $c {
        local t = 'ccc'  .. ngx.var.b
        return t
    }
    set $d "456$c"; 
    return 200 $a,$b,$c,$d;
    }

}

從執行結果可以看出資料是從上到下執行的,如下所示:

# curl  http://testnginx.com/test
123,bbb,cccbbb,456cccbbb

3.3 阻塞事件
set_by_lua_block指令塊在Nginx中執行的指令是阻塞型操作,因此應儘量在這個階段執行輕、快、短、少的程式碼,以避免耗時過多。set_by_lua_block不支援非阻塞I/O,所以不支援yield當前Lua的輕執行緒。
3.4 被禁用的Lua API指令
在set_by_lua_block階段的上下文中,下面的Lua API是被禁止的(只羅列部分)。
輸出型別的API函式(如ngx.say和ngx.send_headers)。
控制型別的API函式(如ngx.exit)。
子請求的API函式(如ngx.location.capture和ngx.location.capture_multi)。
Cosocket API函式(如ngx.socket.tcp和ngx.req.socket)。
休眠API函式ngx.sleep。

四、rewrite_by_lua_block

4.1 階段說明
語法:rewrite_by_lua_block {lua-script-str}
配置環境:http,server,location,location if
階段:rewrite tail
含義:作為一個重寫階段的處理程式,對每個請求執行<lua-script-str>指定的Lua程式碼。
這些Lua程式碼可以呼叫所有的Lua API,並且執行在獨立的全域性環境(類似於沙盒)中,以新的協程來執行。因為可以呼叫所有的Lua API,所以此階段可以實現很多功能,例如對請求的URL進行重定向、讀取MySQL或Redis資料、傳送子請求、控制請求頭等。

4.2 利用rewrite_by_lua_no_postpone改變執行順序
rewrite_by_lua_block命令預設在ngx_http_rewrite_module之後執行,示例如下:

server {
    listen       80;
    server_name  testnginx.com;
    location / {
     set $b  '1';
     rewrite_by_lua_block { ngx.var.b = '2'}
     set $b  '3';
     echo $b;
    }
}

從程式碼來看,預計應輸出的結果是3,但輸出的結果卻是2,如下所示:

#  curl  http://testnginx.com/test
2

上述配置的操作結果說明rewrite_by_lua_block始終都在rewrite階段的後面執行,如果要改變這個順序,需使用rewrite_by_lua_no_postpone指令。
語法:rewrite_by_lua_no_postpone on|off
預設:rewrite_by_lua_no_postpone off
配置環境:http
含義:在rewrite請求處理階段,控制rewrite_by_lua*的所有指令是否被延遲執行,預設為off,即延遲到最後執行;如果設定為on,則會根據配置檔案的順序從上到下執行。
示例:

rewrite_by_lua_no_postpone on;   #只能在http階段配置
server {
    listen       80;
    server_name  testnginx.com;
    location / {
     set $b  '1';
     rewrite_by_lua_block { ngx.var.b = '2'}
     set $b  '3';
     echo $b;
    }
}

執行結果如下:

#  curl -i http://testnginx.com/test
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Mon, 28 May 2018 12:47:37 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

3

注意:if 語句是在rewrite_by_lua_block階段之前執行的,所以在運用if語句時要特別留意執行順序,避免出現意想不到的結果。

4.3 階段控制
在rewrite_by_lua_block階段,當呼叫ngx.exit(ngx.OK)之後,請求會退出當前的執行階段,繼續下一階段的內容處理(如果想了解更多關於ngx.exit的使用方式,請參考7.16.3節)。

五、access_by_lua_block

5.1 階段說明
語法:access_by_lua_block {lua-script-str}
配置環境:http,server,location,location if
階段:access tail
含義:在Nginx的access階段,對每個請求執行<lua-script-str>的程式碼,和rewrite_bylua block一樣,這些Lua程式碼可以呼叫所有的Lua API,並且執行在獨立的全域性環境(類似於沙盒)中,以新的協程來執行。此階段一般用來進行許可權檢查和黑白名單配置。

5.2 利用access_by_lua_no_postpone改變執行順序
access_by_lua_block預設在ngx_http_access_module之後。但把access_by_lua_no_postpone設定為on可以改變執行順序,變成根據配置檔案的順序從上到下執行。access_by_luano postpone的用法和rewrite_by_lua_no_postpone類似。

5.3 階段控制
access_by_lua_block和rewrite_by_lua_block一樣,都可以通過ngx.exit來結束當前的執行階段。

5.4 動態配置黑白名單
Nginx提供了allow、deny等指令來控制IP地址訪問服務的許可權,但如果每次新增的IP地址都需要過載配置檔案就不太靈活了,而且反覆過載Nginx也會導致服務不穩定。此時,可以通過access_by_lua_block來動態新增黑白名單。
下面是兩種常見的動態配置黑白名單的方案。

1.將黑白名單存放在Redis中,然後使用Nginx+Lua讀取Redis的資料,通過修改Redis中的資料來動態配置黑白名單。這種方案的缺點是增加了對網路I/O的操作,相比使用共享記憶體的方式,效能稍微差了些。

2.將黑白名單存放在Ngx_Lua提供的共享記憶體中,每次請求時都去讀取共享記憶體中的黑白名單,通過修改共享記憶體的資料達到動態配置黑白名單的目的。本方案的缺點是在Nginx 重啟的過程中資料會丟失。

8.6 content_by_lua_block

6.1 階段說明
語法:content_by_lua_block {lua-script-str}
配置環境:location,location if
階段:content
含義:content_by_lua_block 作為內容處理階段,對每個請求執行<lua-script-str>的程式碼。和rewrite_by_lua_block一樣,這些Lua程式碼可以呼叫所有的Lua API,並且執行在獨立的全域性環境(類似於沙盒)中,以新的協程來執行。
content_by_lua_block指令不可以和其他內容處理階段的模組如echo、return、proxy_pass等放在一起,示例如下:

server {
    listen       80;
    server_name  testnginx.com;

    location / {
        content_by_lua_block {
             ngx.say("content_by_lua_block")
        }
        echo 'ok';
    }
}

輸出結果只有ok,並未執行content_by_lua_block指令。

6.2 動態調整執行檔案的路徑
指令content_by_lua_file可以用來動態地調整執行檔案的路徑,它後面跟的Lua執行檔案路徑可以是變數,該變數可以是不同的引數或URL等,示例如下:

location / {
    #content_by_lua_file可以直接獲取URL中引數file_name的值
    content_by_lua_file conf/lua/$arg_file_name;
}

#七、balancer_by_lua_block

7.1 階段說明
語法:balancer_by_lua_block { lua-script }
配置環境:upstream
階段:content
含義:在upstream的配置中執行並控制後端伺服器的地址,它會忽視原upstream中預設的配置。
示例:

upstream foo {
    server 127.0.0.1;  #會忽視這個配置
    balancer_by_lua_block {
        #真實的IP地址在這裡配置
    }
}

7.2 被禁用的Lua API指令
在balancer_by_lua_block階段,Lua程式碼的執行環境不支援yield,因此需禁用可能會產生yield的Lua API指令(如cosockets和light threads)。但可以利用ngx.ctx建立一個擁有上下文的變數,在本階段前面的某個執行階段(如rewrite_by_lua*階段)將資料生成後傳入upstream中。

八、header_filter_by_lua_block

8.1 階段說明
語法:header_filter_by_lua_block { lua-script }
配置環境:http,server,location,location if
階段:output-header-filter
含義:在header_filter_by_lua_block階段,對每個請求執行<lua-script-str>的程式碼,以此對響應頭進行過濾。常用於對響應頭進行新增、刪除等操作。
例如,新增一個響應頭test,值為nginx-lua,示例如下:

location / {
    header_filter_by_lua_block {
       ngx.header.test = "nginx-lua";
    }
    echo 'ok';
}

8.2 被禁用的Lua API指令
在header_filter_by_lua_block階段中,下列Lua API是被禁止使用的。
輸出型別的API函式(如ngx.say和ngx.send_headers)。
控制型別的API函式(如ngx.redirect和ngx.exec)。
子請求的API函式(如ngx.location.capture和ngx.location.capture_multi)。
cosocket API函式(如ngx.socket.tcp和ngx.req.socket)。

九、body_filter_by_lua_block

9.1 階段說明
語法:body_filter_by_lua_block { lua-script-str }
配置環境:http,server,location,location if
階段:output-body-filter
含義:在body_filter_by_lua_block階段執行<lua-script-str>的程式碼,用於設定輸出響應體的過濾器。在此階段可以修改響應體的內容,如修改字母的大小寫、替換字串等。

9.2 控制響應體資料
通過ngx.arg[1](ngx.arg[1]是Lua的字串型別的資料)輸入資料流,結束標識eof是響應體資料的最後一位ngx.arg[2](ngx.arg[2]是Lua的布林值型別的資料)。
由於Nginx chain緩衝區的存在,資料流不一定都是一次性完成的,可能需要傳送多次。在這種情況下,結束標識eof僅僅是Nginx chain緩衝區的last_buf(主請求)或last_in_chain(子請求),因此Nginx輸出過濾器在一個單獨的請求中會被多次呼叫,此階段的Lua指令也會被執行多次,示例如下:

server {
    listen       80;
    server_name  testnginx.com;

    location / {
        #將響應體全部轉換為大寫
        body_filter_by_lua_block { ngx.arg[1] = string.upper(ngx. arg[1]) }
        echo 'oooKkk';
        echo 'oooKkk';
        echo 'oooKkk';
    }
}

執行結果如下:

#  curl -i http://testnginx.com/?file_name=1.lua
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Tue, 29 May 2018 11:36:54 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

OOOKKK
OOOKKK
OOOKKK
使用return ngx.ERROR可以截斷響應體,但會導致資料不完整,請求無效:
location / {
    body_filter_by_lua_block {
        ngx.arg[1] = string.upper(ngx.arg[1]);
        return ngx.ERROR
    }
    echo 'oooKkk';
    echo 'oooKkk';
    echo 'oooKkk';
}

執行結果如下:

#  curl -i http://testnginx.com/
curl: (52) Empty reply from server

ngx.arg[2]是一個布林值,如果把它設為true,可以用來截斷響應體的資料,和return ngx.ERROR不一樣,此時被截斷的資料仍然可以輸出內容,是有效的請求,示例如下:

server {
    listen       80;
    server_name  testnginx.com;
    location / {
        body_filter_by_lua_block {
           local body_chunk = ngx.arg[1]
           --如果響應體匹配到了2ooo,就讓ngx.arg[2]=true
           if string.match(body_chunk, "2ooo") then
               ngx.arg[2] = true 
               return
           end
--設定ngx.arg[1] = nil,表示此響應體不會出現在輸出內容中
           ngx.arg[1] = nil

        }
        echo '1oooKkk';
        echo '2oooKkk';
        echo '3oooKkk';
    }
}

執行結果如下:

#  curl -i http://testnginx.com/?file_name=1.lua
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Tue, 29 May 2018 11:48:52 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

2oooKkk

從輸出結果可以得出如下結論。
1.沒有輸出1oooKkk,是因為它在if語句中匹配不成功,設定 ngx.arg[1] = nil將沒有匹配成功的資料遮蔽了。
2.沒有輸出3oooKkk,是因為2oooKkk被匹配成功了,使用ngx.arg[2] = true終止了後續的操作,剩下的內容就不會再被輸出了。

9.3 被禁用的Lua API指令
在body_filter_by_lua_block階段中,下列Lua API是被禁止的。
輸出型別的API函式(如ngx.say和ngx.send_headers)。
控制型別的API函式(如ngx.redirect和ngx.exec)。
子請求的API函式(如ngx.location.capture和ngx.location.capture_multi)。
cosocket API函式(如ngx.socket.tcp和ngx.req.socket)。

#十、log_by_lua_block

10.1 階段說明
語法:log_by_lua_block { lua-script }
配置環境:http,server,location,location if
階段:log
含義:在日誌請求處理階段執行的{ lua-script }程式碼。它不會替換當前access請求的日誌,而會執行在access的前面。
log_by_lua_block階段非常適合用來對日誌進行定製化處理,且可以實現日誌的叢集化維護(詳見第12章)。另外,此階段屬於log階段,這時,請求已經返回到了客戶端,對Ngx_Lua程式碼的異常影響要小很多。
示例:

server {
    listen       80;
    server_name  testnginx.com;

    location / {
        log_by_lua_block {
            local ngx = require "ngx";
            xxxsada  --一個明顯的語法錯誤
            ngx.log(ngx.ERR, 'x');
        }
        echo 'ok';
    }
}

上述程式碼的error_log雖有報錯,但執行結果仍然會輸出“ok”。

10.2 被禁用的Lua API指令
在log_by_lua_block階段中,下列Lua API是被禁止的。
輸出型別的API函式(如ngx.say和ngx.send_headers)。
控制型別的API函式(如ngx.redirect和ngx.exec)。
子請求的API函式(如ngx.location.capture和ngx.location.capture_multi)。
cosocket API函式(如ngx.socket.tcp和ngx.req.socket)。
休眠API函式ngx.sleep。

#十一、Lua和ssl

Lua API還可以對HTTPS協議進行處理,可以使用ssl_certificate_by_lua_block、ssl_session_fetch_by_lua_block、ssl_session_store_by_lua_block這3個指令塊進行配置,由於都涉及lua-resty-core模組的ngx.ssl指令(這已經超出了本章的內容),有興趣的讀者可以去檢視lua-resty-core的相關資料。

#十二、Ngx_Lua執行階段

通過前面章節的學習,讀者瞭解了Ngx_lua模組各個執行階段的作用,現在將這些執行階段彙總到一起來觀察一下整體的執行流程。
圖8-3所示為Ngx_lua的執行順序,它引用自Lua-Nginx-Module的官方Wiki,很清晰地展示了Ngx_lua在Nginx中的執行順序。下面舉一個例子來驗證一下這個執行順序。
Nginx Lua的執行階段
圖8-3 Ngx_Lua的執行順序

1.測試程式碼使用 *_by_lua_file指令來配置,首先,建立如下的test.lua檔案:

# vim /usr/local/nginx_1.12.2/conf/lua/test.lua
 local ngx  = require "ngx"
-- ngx.get_phase()可以獲取Lua程式碼在執行時屬於哪個執行階段
 local phase = ngx.get_phase()
 ngx.log(ngx.ERR, phase, ':   Hello,world! ')

2.在Nginx配置中載入如下的檔案:

# init_by_lua_file 和 init_worker_by_lua_file只能在http指令塊執行
init_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;
init_worker_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;

server {
    listen       80;
    server_name  testnginx.com;

    location / {
       #檔案的順序是隨意擺放的,觀察日誌,留意檔案順序是否對執行順序有干擾
       body_filter_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;
       header_filter_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;
       rewrite_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;
       access_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;
       set_by_lua_file $test /usr/local/nginx_1.12.2/conf/lua/test.lua;
       log_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;
       # content_by_lua_file 和 balancer_by_lua_block 不能存在於同一個location,所以只用content_by_lua_file示例
       content_by_lua_file  /usr/local/nginx_1.12.2/conf/lua/test.lua;
    }
}

3.過載Nginx配置讓程式碼生效,此時不要訪問Nginx,可以觀察到error.log在沒有請求訪問的情況下也會有內容輸出。
如果輸出“init”,表示此階段屬於init_by_lua_block階段;如果輸出“init_worker”,表示此階段屬於init_worker_by_lua_block階段。
有些讀者可能會發現init_worker的輸出有3行,那是因為此階段是在Nginx的worker程序上執行的,每個worker程序都會獨立執行一次Lua程式碼,因此可以看出此時Nginx啟動了3個worker程序,如下所示:

2018/06/04 19:00:23 [error] 12034#12034: [lua] test.lua:3: init:   Hello,world!
2018/06/04 19:00:23 [error] 21019#21019: *914 [lua] test.lua:3: init_worker:   Hello,world!, context: init_worker_by_lua*
2018/06/04 19:00:23 [error] 21020#21020: *915 [lua] test.lua:3: init_worker:   Hello,world!, context: init_worker_by_lua*
2018/06/04 19:00:23 [error] 21018#21018: *916 [lua] test.lua:3: init_worker:   Hello,world!, context: init_worker_by_lua*

4.傳送請求:

curl -i http://testnginx.com/
HTTP/1.1 200 OK
Server: nginx/1.12.2
Date: Mon, 04 Jun 2018 11:00:29 GMT
Content-Type: application/octet-stream
Transfer-Encoding: chunked
Connection: keep-alive

5.觀察access.log日誌,能夠看到每個執行階段的優先順序順序:

2018/06/04 19:16:20 [error] 21019#21019: *920 [lua] test.lua:3: set:   Hello,world!, client: 10.19.64.210, server: testnginx.com, request: "GET /test?ss HTTP/1.1", host: "testnginx.com"
2018/06/04 19:16:20 [error] 21019#21019: *920 [lua] test.lua:3: rewrite:   Hello,world!, client: 10.19.64.210, server: testnginx.com, request: "GET /test?ss HTTP/1.1", host: "testnginx.com"
2018/06/04 19:16:20 [error] 21019#21019: *920 [lua] test.lua:3: access:   Hello,world!, client: 10.19.64.210, server: testnginx.com, request: "GET /test?ss HTTP/1.1", host: "testnginx.com"
2018/06/04 19:16:20 [error] 21019#21019: *920 [lua] test.lua:3: content:   Hello,world!, client: 10.19.64.210, server: testnginx.com, request: "GET /test?ss HTTP/1.1", host: "testnginx.com"
2018/06/04 19:16:20 [error] 21019#21019: *920 [lua] test.lua:3: header_filter:   Hello,world!, client: 10.19.64.210, server: testnginx.com, request: "GET /test?ss HTTP/1.1", host: "testnginx.com"
2018/06/04 19:16:20 [error] 21019#21019: *920 [lua] test.lua:3: body_filter:   Hello,world!, client: 10.19.64.210, server: testnginx.com, request: "GET /test?ss HTTP/1.1", host: "testnginx.com"
2018/06/04 19:16:20 [error] 21019#21019: *920 [lua] test.lua:3: log:   Hello,world! while logging request, client: 10.19.64.210, server: testnginx.com, request: "GET /test?ss HTTP/1.1", host: "testnginx.com"

通過access.log日誌截圖(如圖8-4所示)可以看得更清晰。
Nginx Lua的執行階段
圖8-4 access.log日誌截圖
觀察Ngx_Lua整體的執行流程,可得出如下結論。
Ngx_Lua執行順序如下:

init_by_lua_block
init_worker_by_lua_block
set_by_lua_block
rewrite_by_lua_block
access_by_lua_block
content_by_lua_block
header_filter_by_lua_block
body_filter_by_lua_block
log_by_lua_block

指令在配置檔案中的順序不會影響執行的順序,這一點和Nginx一樣。
http階段的init_by_lua_block和init_worker_by_lua_block只會在配置過載時執行,只進行HTTP請求時,不會被執行。
注意:init_by_lua_file 在lua_code_cache 為off 的情況下,每次執行HTTP請求都會執行過載配置階段的Lua程式碼。

13、小結

本章對Ngx_Lua的執行階段進行了講解,掌握每個執行階段的用途會讓大家在實際開發中更加得心應手。