得物技術初探OpenResty

簡介

Nginx 的高性能是業界公認的,近年來在全球服務器市場上的佔比份額也在逐年增長,在國內知名互聯網公司也有普遍的應用,阿里還基於Nginx進行擴展打造了著名的Tengine。而OpenResty是由國人章亦春基於Nginx和LuaJIT打造的動態web平臺,LuaJIT是Lua編程語言的即時編譯器。Lua是一種強大、動態、輕量級的編程語言。該語言的設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能,OpenResty就是經過使用Lua來擴展Nginx來實現的可擴展Web平臺。目前OpenResty 大多用在 API 網關的開發中,固然也能夠用來替代Nginx,用於反向代理和負載均衡的場景。nginx

OpenResty 的架構組成

如前所述,OpenResty 底層是基於Nginx 和 LuaJIT 的,因此 OpenResty 繼承了 Nginx 的多進程架構, 每個 Worker 進程都是 fork Master 進程而獲得的, 其實, Master 進程中的 LuaJIT 虛擬機也會一塊兒 fork 過來。在同一個 Worker 內的全部協程,都會共享這個 LuaJIT 虛擬機,Lua 代碼的執行也是在這個虛擬機中完成的。而在同一個時間點上,每一個 Worker 進程只能處理一個用戶的請求,也就是隻有一個協程在運行。git

Nginx

因爲 Nginx 處理請求採用的是事件驅動模型,因此每個 Worker進程最好獨佔一個CPU。實踐中咱們每每把 Worker 進程的數量配置成與CPU核數相同,此外把每個 Worker 進程與某一個CPU核綁定在一塊兒,這樣能夠更好的使用每個CPU核上的CPU緩存,減小緩存失效的命中率,進而提升請求處理的性能。github

LuaJIT

其實 OpenResty 最初默認使用的是標準Lua,從 1.5.8.1 版本開始才默認使用 LuaJIT,背後的緣由是由於 LuaJIT 相比標準Lua有很大的性能優點。web

首先,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 方式的性能更優。segmentfault

下面是一個簡單的調用示例:瀏覽器

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

文/郭先生

關注得物技術,攜手走向技術的雲端

相關文章
相關標籤/搜索