作者 | 黃超
杏仁運維工程師,關注容器技術和自動化運維。
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。
_G
由於 Nginx 把一個請求分成了很多階段,第三方模塊就可以根據自己的行為,掛載到不同階段處理達到目的。OpenResty 也應用了同樣的特性。不同的階段,有不同的處理行為,這是 OpenResty 的一大特色。OpenResty 處理一個請求的流程參考下圖(從 Request start 開始):
指令使用範圍解釋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 和 cookiebody_filter_by_lua*http,server,location,location if對響應數據進行過濾,比如截斷、替換log_by_luahttp,server,location,location iflog階段處理,比如記錄訪問量/統計平均響應時間
int_by_lua*init_worker_by_lua*
set_by_lua*
rewrite_by_lua*
access_by_lua*
content_by_lua*
header_filter_by_lua*
body_filter_by_lua*
log_by_lua
OpenResty 的 Lua 代碼是提現在 nginx.conf 的配置文件之中的,可以與配置文件寫在一起,也可以把 Lua 腳本放在一個文件中進行載入:
nginx.conf
內聯在 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 中,只有在 init_by_lua* 和 init_worker_by_lua* 階段才能定義真正的全局變數。因為在其他階段,OpenResty 會設置一個隔離的全局變數表,以免在處理過程中污染了其他請求。即使在上述兩個階段可以定義全局變數,也盡量避免這麼做。全局變數能解決的問題,用模塊變數也能解決,而且會更清晰,乾淨。
init_by_lua*
init_worker_by_lua*
這裡將定義在 Lua 模塊中的變數稱為模塊變數。Lua VM 會將 require 進來的模塊換成到 package.loaded table 里,模塊里的變數都會被緩存起來,在同一個 Lua VM下,模塊中的變數在每個請求中是共享的,這樣就可以避免使用全局變數來實現共享了,看下面一個例子:
require
package.loaded
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 存儲了。
worker_processes
跟全局變數,模塊變數相對,我們這裡姑且把 *_by_lua* 里定義的變數稱為本地變數。本地變數僅在當前階段有效,如果需要跨階段使用,需要藉助 ngx.ctx 或者附加到模塊變數里。
*_by_lua*
ngx.ctx
這裡我們使用了 ngx.ctx 表在三個不同的階段來傳遞使用變數 foo:
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: 73 sub pre: nil # 子請求中並沒有獲取到父請求的變數 $pre sub post: 32 main post: 73
開啟或關閉在 *_by_lua_file(如:set_by_lua_file,content_by_lua_file) 指令中以及 Lua 模塊中 Lua 代碼的緩存。
*_by_lua_file
set_by_lua_file
content_by_lua_file
若關閉,ngx_lua 會為每個請求創建一個獨立的 Lua VM,所有 *_by_lua_file指令中的代碼將不會被緩存到內存中,並且所有的 Lua 模塊每次都會從頭重新載入。在開發模式下,這給我們帶來了不需要 reload nginx 就能調試的便利性,但是在生成環境下,強烈建議開啟。 若關閉,即使是一個簡單的 Hello World 都會慢上一個數量級(每次 IO 讀取和編譯消耗很大)。
reload
但是,那些直接寫在 nginx.conf 配置文件中的 *_by_lua_block 指令下的代碼不會在你編輯下實時更新,只有發送 HUP 信號給 Nginx 才能能夠重新。
*_by_lua_block
HUP
Nginx 經常用來作為反向代理伺服器。通常情況下,我們將後端的服務配置在 Nginx 的 upstream 中,當後端服務有變更時就去修改 upstream 中的配置再通過reload 的方式使其生效。這個操作如果在後端服務經常發生變更的情況下,操作起來就會顯得有些繁瑣了。現在利用 Lua + Redis 的方式將 upstream 中的配置放在 Redis 中,以實現動態配置的效果。
upstream
架構圖
原理:
在求請求訪問階段處理(access_by_lua*)通過指定的規則(這個規則根據自己的需求去設計)從 Redis 中去獲取相對應的後端服務地址去替換 Nginx 配置中的proxy_pass 的地址。
proxy_pass
流程:
$backend_server
server { listen 80; server_name app1.example.com;
location / { ... set $backend_server ; } }
同時在 Redis 中存入後端服務的地址。
set app1 10.10.10.10:8080
# GET /get?key=some_key location = /get { internal; # 保護這個介面只運行內部調用 set_unescape_uri $key $arg_key; # this requires ngx_set_misc redis2_query get $key; redis2_pass foo.com:6379; # redis_server and port }
2. 在求請求訪問階段處理利用 ngx.location.capture 模塊請求去上個階段定義的 Redis 介面,並將結果替換 $backend_server。
location / { ... access_by_lua_block { local rds_key = "app1" # 從 redis 中獲取 key 為 app1 對應的 server_ip local res = ngx.location.capture(/get, { args = {key = rds_key}}) # 解析 redis 結果 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.backend_server = server } }
3.Nginx 轉發階段將請求轉發至後端服務。
location / { ... access_by_lua_block {...}; proxy_pass http://$backend_server; }
最後,推薦兩個基於 OpenResty 的比較實用的兩個開源項目:
全文完
我們正在招聘 Java 工程師,歡迎有興趣的同學投遞簡歷到 rd-hr@xingren.com 。
歡迎搜索關注微信公眾號:杏仁技術站(微信號 xingren-tech)。
推薦閱讀:
TAG:OpenResty | 科技 |