簡介
Nginx 的高性能是業界公認的,近年來在全球服務器市場上的佔比份額也在逐年增加,在國內知名互聯網公司也有廣泛的應用,阿里還基於Nginx進行擴展打造了著名的Tengine。而OpenResty是由國人章亦春基於Nginx和LuaJIT打造的動態web平台,LuaJIT是Lua編程語言的即時編譯器。Lua是一種強大、動態、輕量級的編程語言。該語言的設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定製功能,OpenResty就是通過使用Lua來擴展Nginx來實現的可擴展Web平台。目前OpenResty 大多用在 API 網關的開發中,當然也可以用來替代Nginx,用於反向代理和負載均衡的場景。
OpenResty 的架構組成
如前所述,OpenResty 底層是基於Nginx 和 LuaJIT 的,所以 OpenResty 繼承了 Nginx 的多進程架構, 每一個 Worker 進程都是 fork Master 進程而得到的, 其實, Master 進程中的 LuaJIT 虛擬機也會一起 fork 過來。在同一個 Worker 內的所有協程,都會共享這個 LuaJIT 虛擬機,Lua 代碼的執行也是在這個虛擬機中完成的。而在同一個時間點上,每個 Worker 進程只能處理一個用户的請求,也就是隻有一個協程在運行。
Nginx
由於 Nginx 處理請求採用的是事件驅動模型,所以每一個 Worker進程最好獨佔一個CPU。實踐中我們往往把 Worker 進程的數量配置成與CPU核數相同,此外把每一個 Worker 進程與某一個CPU核綁定在一起,這樣可以更好的使用每一個CPU核上的CPU緩存,減少緩存失效的命中率,進而提高請求處理的性能。
LuaJIT
其實 OpenResty 最初默認使用的是標準Lua,從 1.5.8.1 版本開始才默認使用 LuaJIT,背後的原因是因為 LuaJIT 相比標準Lua有很大的性能優勢。
首先,LuaJIT 的運行時環境除了一個彙編實現的 Lua 解釋器外,還有一個可以直接生成機器代碼的 JIT 編譯器。開始的時候,LuaJIT 和標準 Lua 一樣,Lua 代碼被編譯為字節碼,字節碼被 LuaJIT 的解釋器解釋執行。但不同的是,LuaJIT 的解釋器會在執行字節碼的同時,記錄一些運行時的統計信息,比如每個 Lua 函數調用入口的實際運行次數,還有每個 Lua 循環的實際執行次數。當這些次數超過某個隨機的閾值時,便認為對應的 Lua 函數入口或者對應的 Lua 循環足夠熱,這時便會觸發 JIT 編譯器開始工作。JIT 編譯器會從熱函數的入口或者熱循環的某個位置開始,嘗試編譯對應的 Lua 代碼路徑。編譯的過程,是把 LuaJIT 字節碼先轉換成 LuaJIT 自己定義的中間碼(IR),然後再生成目標機器的機器碼。這個過程跟Java中JIT編譯器工作原理類似,其實它們都是為了提高程序運行效率而採取的同一類優化手段,正所謂底層技術都是相通的,可以類比學習。
其次,LuaJIT 還緊密結合了 FFI(Foreign Function Interface,它不能作為單獨的模塊使用),可以讓你直接在 Lua 代碼中調用外部的 C 函數和使用 C 的數據結構。FFI 通過解析普通的C聲明,就完成 Lua/C 的綁定工作。JIT 編譯器從Lua代碼訪問C數據結構而生成的代碼與C編譯器生成的代碼相同。與通過經典Lua/C API綁定的函數調用不同,對C函數的調用可以內聯在 JIT 編譯的代碼中,所以FFI 方式不僅簡單,而且比傳統的 Lua/C API 方式的性能更優。
下面是一個簡單的調用示例:
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")
短短這幾行代碼,就可以直接在 Lua 中調用 C 的 printf 函數,打印出 Hello world!。類似的,我們可以用 FFI 來調用 NGINX、OpenSSL 的 C 函數,來完成更多的功能。
OpenResty 的工作原理
OpenResty 是基於Nginx的高性能Web平台,所以其高效運行與Nginx密不可分。
Nginx 處理HTTP請求有11個執行階段,我們可以從 ngx_http_core_module.h 的源碼中看到:
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE,
NGX_HTTP_FIND_CONFIG_PHASE,
NGX_HTTP_REWRITE_PHASE,
NGX_HTTP_POST_REWRITE_PHASE,
NGX_HTTP_PREACCESS_PHASE,
NGX_HTTP_ACCESS_PHASE,
NGX_HTTP_POST_ACCESS_PHASE,
NGX_HTTP_PRECONTENT_PHASE,
NGX_HTTP_CONTENT_PHASE,
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
巧合的是,OpenResty 也有 11 個 *_by_lua 指令,它們和 NGINX 的11個執行階段有很大的關聯性。指令是使用Lua編寫Nginx腳本的基本構建塊,用於指定用户編寫的Lua代碼何時運行以及運行結果如何使用等。下圖顯示了不同指令的執行順序,這張圖可以幫助理清我們編寫的腳本是按照怎樣的邏輯運行的。
其中, init_by_lua 只會在 Master 進程被創建時執行,init_worker_by_lua 只會在每個 Worker 進程被創建時執行。其他的 *_by_lua 指令則是由終端請求觸發,會被反覆執行。
下面對每一個OpenResty 指令的執行時機和使用進行説明。
在 Nginx 啓動過程中嵌入Lua 代碼
init_by_lua :在 Nginx 解析配置文件(Master進程)時在 Lua VM 層面立即調用的 Lua 代碼。一般在 init_by_lua 階段,我們可以預先加載 Lua 模塊和公共的只讀數據,這樣可以利用操作系統的 COW(copy on write)特性,來節省一些內存。不過,init_by_lua 階段無法執行http請求獲取遠程配置信息,對初始化工作多少有些不便。
init_worker_by_lua :在 Nginx Worker 進程啓動時調用,一般在init_worker_by_lua階段,我們會執行一些定時任務,比如上游服務節點擴所容動態感知和健康檢查等,對於init_by_lua*階段無法執行http請求的問題,也可以在此階段的定時任務中進行。
在 OpenSSL 處理 SSL 協議時嵌入Lua代碼
ssl_certificate_by_lua* :利用 OpenSSL 庫(要求1.0.2e版本以上)的SSL_CTX_set_cert_cb特性,將 Lua代碼添加到驗證下游客户端SSL證書的代碼前,可用於為每個請求設置 SSL 證書鏈和相應的私鑰以及在這種上下文中無阻塞地進行SSL握手流量控制。
在11個HTTP階段中嵌入Lua代碼
set_by_lua* :將Lua代碼添加到Nginx官方 ngx_http_rewrite_module 模塊中的腳本指令中執行,因為 ngx_http_rewrite_module在它的指令中不支持非阻塞I/O,所以需要生成當前Lua "light threads" 的Lua API不能在這個階段中工作。由於Nginx事件循環在此階段代碼執行過程中將被阻塞,故需要避免在此階段中執行耗時操作,一般用於執行比較快和少的代碼來設置變量。
rewrite_by_lua* :將Lua代碼添加到11個階段中的 rewrite階段中,作為獨立模塊為每個請求執行相應的 Lua代碼。此階段的Lua代碼可以進行API調用,並在獨立的全局環境(即沙箱)中作為一個新生成的協程執行。此階段可以實現很多功能,比如調用外部服務、轉發和重定向處理等。
access_by_lua :將Lua代碼添加到11個階段中的 access 階段中執行,與rewrite_by_lua類似,也是作為獨立模塊為每個請求執行相應的 Lua代碼。此階段的Lua代碼可以進行API調用,並在獨立的全局環境(即沙箱)中作為一個新生成的協程執行。一般用於訪問控制、權限校驗等。
content_by_lua* :在 11 個階段的 content 階段以獨佔方式為每個請求執行相應的 Lua 代碼,用於生成返回內容。需要注意的是,不要在同一 location 中使用此指令和其他內容處理指令。例如,這個指令和 proxy_pass 指令不應該在同一個 location 中使用。
log_by_lua* :將Lua代碼添加到11個階段中的log階段中執行,它不會替換當前請求的access日誌,但會在其之前運行,一般用於請求的統計及日誌記錄。
在負載均衡時嵌入Lua代碼
balance_by_lua :將Lua代碼添加到反向代理模塊、生成上游服務地址的 init_upstream 回調方法中,用於 upstream 負載均衡控制。這個Lua代碼執行上下文不支持 yield,因此在這個上下文中禁用可能 yield 的 Lua API (比如 cosockets 和 "light threads")。不過我們一般可以通過在早期的處理階段(如 access_by_lua )中執行這樣的操作,並通過 ngx.ctx 將結果傳遞到這個上下文中來繞過這個限制。
在過濾響應時嵌入Lua代碼
header_filter_by_lua* :將Lua代碼嵌入到響應頭部過濾階段中,用於應答頭過濾處理。
body_filter_by_lua* :將Lua代碼嵌入到響應包體過濾階段中,用於應答體過濾處理。需要注意的是,此階段可能在一個請求中被調用多次,因為響應體可能以塊的形式傳遞。因此,該指令中指定的Lua代碼也可以在單個HTTP請求的生命週期內運行多次。
OpenResty 快速體驗
在瞭解了OpenResty 的架構組成和基本工作原理後,我們通過一個簡單的例子來上手OpenResty,以我們工作用的Mac系統來進行。
安裝OpenResty
$ brew tap openresty/brew
$ brew install openresty
創建工作目錄
$ mkdir ordemo
$ cd ordemo
$ mkdir logs/ conf/
創建nginx配置文件
在 conf 工作目錄下,創建 nginx配置文件 nginx.conf ,配置內容如下:
error_log logs/error.log debug;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
access_log logs/access.log
server {
listen 8080;
location / {
content_by_lua '
ngx.say("Welcome to OpenResty!")
';
}
}
}
啓動服務
$ cd ordemo
$ openresty -p `pwd` -c conf/nginx.conf
# 停止服務
$ openresty -p `pwd` -c conf/nginx.conf -s stop
沒有報錯的話,説明 OpenResty 已經啓動成功了。可以通過瀏覽器或者 curl 命令發起請求:
$ curl -i 127.0.0.1:8080
HTTP/1.1 200 OK
Server: openresty/1.19.3.1
Date: Tue, 29 Jun 2021 08:55:51 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Connection: keep-alive
Welcome to OpenResty!
這就是一個最簡單的基於 OpenResty 的服務開發過程,只在 Nginx HTTP 請求的11個階段中的 content 階段嵌入了 Lua 代碼,直接生成了請求響應體。
OpenResty 在得物的應用
當前基礎架構團隊基於 OpenResty 開發了流量路由組件(API-ROUTE)用於異地多活和小得物項目,該組件主要通過識別請求中的用户ID,根據路由規則進行動態路由,也實現了基於客户端IP和用户ID的灰度導流,後續根據規劃將承擔更多角色。
上面那個簡單的Demo是不是挺簡單,有沒有想起編程語言入門Demo Hello World?Hello World 看似簡單,但其隱藏在背後的執行過程可沒那麼簡單!同樣的,OpenResty 也沒我們看到的那麼單純!它的背後隱藏了非常多的文化和技術細節。。懂得都懂。。
最後歡迎對OpenResty有興趣的同學一起交流學習進步。
參考及學習列表
Nginx核心知識150講
OpenResty從入門到實戰
OpenResty 官網
OpenResty API
awesome-resty
文/言甚
關注得物技術,攜手走向技術的雲端