OpenResty不完全指南



OpenResty不完全指南

2018.05.27 17:57:33字數1443閱讀532

OpenResty 簡介

OpenResty® 是一個基於 Nginx 與 Lua 的高性能 Web 平臺。我們知道開發 Nginx 的模塊需要用 C ,同時還要熟悉它的源碼,成本和門檻比較高。國人章亦春把 LuaJIT VM 嵌入到了 Nginx 中,使得可以直接 Lua 在 Nginx 上進行編程,同時還提供了大量的類庫(如:lua-resty-mysql lua-resty-redis 等),直接把一個 Nginx 這個 Web Server 擴展成了一個 Web 框架,藉助於 Nginx 的高性能, 能夠快速地構造出一個足以勝任 10K 乃至 1000K 以上單機併發連接的高性能 Web 應用系統。

Nginx 採用的是 master-worker 模型,一個 master 進程管理多個 worker 進程,worker 真正負責對客戶端的請求處理,master 僅負責一些全局初始化,以及對 worker 進行管理。在 OpenResty 中,每個 worker 中有一個 Lua VM, 當一個請求被分配到 worker 時,worker 中的 Lua VM 裏創建一個 coroutine(協程) 來負責處理。協程之間的數據隔離,每個協程具有獨立的全局變量 _G

1149

ngx_lua works.png

OpenResty 處理請求的流程

由於 Nginx 把一個請求分成了很多階段,第三方模塊就可以根據自己的行爲,掛載到不同階段處理達到目的。OpenResty 也應用了同樣的特性。不同的階段,有不同的處理行爲,這是 OpenResty 的一大特色。OpenResty 處理一個請求的流程參考下圖(從 Request start 開始):


1005

77d1c09e-1a37-11e6-97ef-d9767035fc3e.png

指令使用範圍解釋
int_by_lua* init_worker_by_lua*http初始化全局配置/預加載Lua模塊
set_by_lua*server,server if,location,location if設置nginx變量,此處是阻塞的,Lua代碼要做到非常快
rewrite_by_lua*http,server,location,location ifrewrite階段處理,可以實現複雜的轉發/重定向邏輯
access_by_lua*http,server,location,location if請求訪問階段處理,用於訪問控制
content_by_lua*location, location if內容處理器,接收請求處理並輸出響應
header_filter_by_lua*http,server,location,location if設置 heade 和 cookie
body_filter_by_lua*http,server,location,location if對響應數據進行過濾,比如截斷、替換
log_by_luahttp,server,location,location iflog階段處理,比如記錄訪問量/統計平均響應時間

更多詳情請參考官方文檔

配置 OpenResty

OpenResty 的 Lua 代碼是提現在 nginx.conf 的配置文件之中的,可以與配置文件寫在一起,也可以把 Lua 腳本放在一個文件中進行加載:

內聯在 nginx.conf 中:

server {
    ...
    location /lua_content {
         # MIME type determined by default_type:
         default_type 'text/plain';

         content_by_lua_block {
             ngx.say('Hello,world!')
         }
    }
    ....
}

通過加載 lua 腳本的方式:

server {
    ...
    location = /mixed {
         rewrite_by_lua_file /path/to/rewrite.lua;
         access_by_lua_file /path/to/access.lua;
         content_by_lua_file /path/to/content.lua;
     }
    ....
}

OpenResty 變量的共享範圍

全局變量

在 OpenResty 中,只有在 init_by_lua*init_worker_by_lua* 階段才能定義真正的全局變量。因爲在其他階段,OpenResty 會設置一個隔離的全局變量表,以免在處理過程中污染了其他請求。即使在上述兩個階段可以定義全局變量,也儘量避免這麼做。全局變量能解決的問題,用模塊變量也能解決,而且會更清晰,乾淨。

模塊變量

這裏將定義在 Lua 模塊中的變量稱爲模塊變量。Lua VM 會將 require 進來的模塊換成到 package.loaded table 裏,模塊裏的變量都會被緩存起來,在同一個 Lua VM下,模塊中的變量在每個請求中是共享的,這樣就可以避免使用全局變量來實現共享了,看下面一個例子:

nginx.conf

worker_processes  1;...location {
    ...
    lua_code_cache on;
    default_type "text/html";
    content_by_lua_file 'lua/test_module_1.lua'}

lua/test_module_1.lua

local module1 = require("module1")

module1.hello()

lua/module1.lua

local count = 0
local function hello() 
    count = count + 1
    ngx.say("count: ", count)
end

local _M  = {
    hello = hello
}   

return _M

當通過瀏覽器訪問時,可以看到 count 輸出是一個遞增的,這也說明了在 lua/module1.lua 的模塊變量在每個請求中時共享的:

count: 1
count: 2
.....

另外,如果 worker_processes 的數量大於 1 時呢,得到的結果可能就不一樣了。因爲每個 worker 中都有一個 Lua VM 了,模塊變量僅在同一個 VM 下,所有的請求共享。如果要在多個 Worker 進程間共享請考慮使用 ngx.shared.DICT 或如 Redis 存儲了。

本地變量

跟全局變量,模塊變量相對,我們這裏姑且把 *_by_lua* 裏定義的變量稱爲本地變量。本地變量僅在當前階段有效,如果需要跨階段使用,需要藉助 ngx.ctx 或者附加到模塊變量裏。

這裏我們使用了 ngx.ctx 表在三個不同的階段來傳遞使用變量 foo

location /test {
     rewrite_by_lua_block {
         ngx.ctx.foo = 76
     }
     access_by_lua_block {
         ngx.ctx.foo = ngx.ctx.foo + 3
     }
     content_by_lua_block {
         ngx.say(ngx.ctx.foo)
     }
 }

額外注意,每個請求,包括子請求,都有一份自己的 ngx.ctx 表。例如:

 location /sub {
     content_by_lua_block {
         ngx.say("sub pre: ", ngx.ctx.blah)
         ngx.ctx.blah = 32
         ngx.say("sub post: ", ngx.ctx.blah)
     }
 }

 location /main {
     content_by_lua_block {
         ngx.ctx.blah = 73
         ngx.say("main pre: ", ngx.ctx.blah)
         local res = ngx.location.capture("/sub")
         ngx.print(res.body)
         ngx.say("main post: ", ngx.ctx.blah)
     }
 }

訪問 GET /main 輸出:

main pre: 73sub pre: nil  # 子請求中並沒有獲取到父請求的變量 $pre
sub post: 32main post: 73

性能開關 lua_code_cache

開啓或關閉在 *_by_lua_file(如:set_by_lua_file, content_by_lua_file) 指令中以及 Lua 模塊中 Lua 代碼的緩存。

若關閉,ngx_lua 會爲每個請求創建一個獨立的 Lua VM,所有 *_by_lua_file 指令中的代碼將不會被緩存到內存中,並且所有的 Lua 模塊每次都會從頭重新加載。在開發模式下,這給我們帶來了不需要 reload nginx 就能調試的便利性,但是在生成環境下,強烈建議開啓。 若關閉,即使是一個簡單的 Hello World 都會慢上一個數量級(每次 IO 讀取和編譯消耗很大)。

但是,那些直接寫在 nginx.conf 配置文件中的 *_by_lua_block 指令下的代碼不會在你編輯下實時更新,只有發送 HUP 信號給 Nginx 才能能夠重新。

小案例

通過 OpenResty + Redis 實現動態路由

該方案是將原來定義 upstream 中的 server_ip 存放在 redis 中。

827

image

  1. 使用 ngx_redis2 模塊來讀取 redis 實現讀取 redis 的接口,並在 location 中配置 internal 保護這個接口只運行內部調用。

location = /redis {
    internal;
    set_unescape_uri $key $arg_key;
    redis2_query get $key;
    redis2_pass 192.168.4.182:6379;
}
  1. 使用 ngx.location.capture 來調用內部 redis 的接口,它可以發起非阻塞的內部請求訪問目標 location。

location = /app1 {
    resolver 114.114.114.114;
    set $target '';
    default_type "text/html";

    access_by_lua_block {
        local rds_key = "app1"
        # 從 redis 中獲取 key 爲 app1 對應的 server_ip
        local res = ngx.location.capture('/get_redis', { args = {key = rds_key}})

        local parser = require("redis.parser")
        local server, typ = parser.parse_reply(res.body)
        if typ ~= parser.BULK_REPLY or not server then
            ngx.log(ngx.ERR, "bad redis response: ", res.body)
            ngx.exit(500)
        end

        ngx.var.target = server
    }

    proxy_pass http://$target;
}

後續優化,可以使用本地環境 + Redis 環境的方案來提升性能。

最後,推薦兩個基於 OpenResty 的比較實用的兩個開源項目:


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