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
。
ngx_lua works.png
OpenResty 處理請求的流程
由於 Nginx 把一個請求分成了很多階段,第三方模塊就可以根據自己的行爲,掛載到不同階段處理達到目的。OpenResty 也應用了同樣的特性。不同的階段,有不同的處理行爲,這是 OpenResty 的一大特色。OpenResty 處理一個請求的流程參考下圖(從 Request start 開始):
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 if | rewrite階段處理,可以實現複雜的轉發/重定向邏輯 |
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_lua | http,server,location,location if | log階段處理,比如記錄訪問量/統計平均響應時間 |
更多詳情請參考官方文檔
配置 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 中。
image
使用 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; }
使用 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 的比較實用的兩個開源項目:
基於動態策略的灰度發佈系統 ABTestingGateway
基於ngx_lua的web應用防火牆 ngx_lua_waf