標籤:

OpenResty 不完全指南

作者 | 黃超

杏仁運維工程師,關注容器技術和自動化運維。

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

OpenResty 處理請求流程

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

更多詳情請參考官方文檔

配置 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: 73
sub pre: nil # 子請求中並沒有獲取到父請求的變數 $pre
sub post: 32
main 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 實現動態路由

Nginx 經常用來作為反向代理伺服器。通常情況下,我們將後端的服務配置在 Nginx 的 upstream 中,當後端服務有變更時就去修改 upstream 中的配置再通過reload 的方式使其生效。這個操作如果在後端服務經常發生變更的情況下,操作起來就會顯得有些繁瑣了。現在利用 Lua + Redis 的方式將 upstream 中的配置放在 Redis 中,以實現動態配置的效果。

架構圖

原理:

在求請求訪問階段處理(access_by_lua*)通過指定的規則(這個規則根據自己的需求去設計)從 Redis 中去獲取相對應的後端服務地址去替換 Nginx 配置中的proxy_pass 的地址。

流程:

  1. 在 Nginx 配置中創建後端服務地址的變數 $backend_server

server {
listen 80;
server_name app1.example.com;

location / {
...
set $backend_server ;
}
}

同時在 Redis 中存入後端服務的地址。

set app1 10.10.10.10:8080

  1. 使用 ngx_redis2 模塊來實現一個讀取 Redis 的介面。

# 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 的比較實用的兩個開源項目:

  • 基於動態策略的灰度發布系統 ABTestingGateway
  • 基於ngx_lua的web應用防火牆 ngx_lua_waf

參考

  • OpenResty Best Practices
  • lua-nginx-module
  • Nginx Lua Directives
  • Nginx API for Lua
  • Lua 簡明教程

全文完


我們正在招聘 Java 工程師,歡迎有興趣的同學投遞簡歷到 rd-hr@xingren.com 。

歡迎搜索關注微信公眾號:杏仁技術站(微信號 xingren-tech)。

推薦閱讀:

TAG:OpenResty | 科技 |