OpenResty 的兩個基石:NGINX 和 LuaJIT。
NGINX基礎
在 OpenResty 的開發中,我們需要注意下面幾點:
- 要盡可能少地配置 nginx.conf;
- 避免使用if、set 、rewrite 等多個指令的配合;
- 能通過 Lua 代碼解決的,就別用 NGINX 的配置、變量和模塊來解決。
這樣可以最大限度地提高可讀性、可維護性和可擴展性。
下面這段 NGINX 配置,就是一個典型的反例,可以說是把配置項當成了代碼來使用,在使用 OpenResty 進行開發時需要避免。
location ~ ^/mobile/(web/app.htm) {
set $type $1;
set $orig_args $args;
if ( $http_user_Agent ~ "(iPhone|iPad|Android)" ) {
rewrite ^/mobile/(.*) http://touch.foo.com/mobile/$1 last;
}
proxy_pass http://foo.com/$type?$orig_args;
}
NGINX 配置
NGINX 通過配置文件來控制自身行為,它的配置可以看作是一個簡單 的 DSL。NGINX 在進程啟動的時候讀取配置,並加載到內存中。如果修改了配置文件,需要重啟或者重載 NGINX,再次讀取后才能生效。只有 NGINX 的商業版本,才會在運行時, 以 API 的形式提供部分動態的能力。
如下為一段簡單的nginx配置文件:
worker_processes auto;
pid logs/nginx.pid;
error_log logs/error.log notice;
worker_rlimit_nofile 65535;
events {
worker_connections 16384;
}
http {
server {
listen 80;
listen 443 ssl;
location / {
proxy_pass https://foo.com;
}
}
}
stream {
server {
listen 53 udp;
}
基礎概念:
1.每個指令都有自己適用的上下文(Context),也就是NGINX配置文件中指令的作用域。
最上層的是 main,里面是和具體業務無關的一些指令,比如上面出現的 worker_processes、pid 和 error_log,都屬於 main 這個上下文。另外,上下文是有層級關系的,比如 location 的上下文是 server, server 的上下文是 http,http 的上下文是 main。
指令不能運行在錯誤的上下文中,NGINX 在啟動時會檢測 nginx.conf 是否合法。比如把 listen 80; 從 server 上下文換到 main 上下文,然后啟動 NGINX 服務,會看到類似這樣的報錯:
"listen" directive is not allowed here ......
2.NGINX 不僅可以處理 HTTP 請求 和 HTTPS 流量,還可以處理 UDP 和 TCP 流量。
其中,七層的放在 HTTP 中,四層的放在 stream中。在 OpenResty 里面, lua-nginx-module 和 streamlua-nginx-module 分別和這倆對應。
NGINX 支持的功能,OpenResty 並不一定支持,需要看 OpenResty 的版本號。
OpenResty 的版本號是和 NGINX 保持一致的,所以很容易識別。比如 NGINX 在 2018 年 3 月份發布的 1.13.10 版本中,增加了對 gRPC 的支持,但 OpenResty 在 2019 年 4 月份時的最新版本是 1.13.6.2,由此 可以推斷 OpenResty 還不支持 gRPC。
MASTER-WORKER 模式
NGINX 啟動后,會有一個 Master 進程和多個 Worker 進程(也可以只有一個 Worker 進程)。

Master 進程,扮演“管理者”的角色,並不負責處理終端的請求,它是用來管理 Worker 進程的,包括接受管理員發送的信號量、監控 Worker 的運行狀態。當 Worker 進程異常退出時, Master 進程會重新啟動一個新的 Worker 進程。
Worker 進程則是“一線員工”,用來處理終端用戶的請求。它是從 Master 進程 fork 出來的,彼此之間相互獨立,互不影響。多進程的模式比 Apache多線程的模式要先進很多,沒有線程間加鎖,也方便調試。即使某個進程崩潰退出了,也不會影響其他 Worker 進程正常工作。
OpenResty 在 NGINX Master-Worker 模式的前提下,又增加了獨有的特權進程(privileged agent)。 這個進程並不監聽任何端口,和 NGINX 的 Master 進程擁有同樣的權限,所以可以做一些需要高權限才能 完成的任務,比如對本地磁盤文件的一些寫操作等。
如果特權進程與 NGINX 二進制熱升級的機制互相配合,OpenResty 就可以實現自我二進制熱升級的整個流程,而不依賴任何外部的程序。
減少對外部程序的依賴,盡量在 OpenResty 進程內解決問題,不僅方便部署、降低運維成本,也可以降低程序出錯的概率。可以說,OpenResty 中的特權進程、ngx.pipe 等功能,都是出於這個目的。
執行階段
執行階段也是 NGINX 重要的特性,與 OpenResty 的具體實現密切相關。NGINX 有 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 階段的關系如下圖所示(圖片來 自 lua-nginx-module 文檔):

其中, init_by_lua 只會在 Master 進程被創建時執行,init_worker_by_lua 只會在每個 Worker 進程被創建時執行。其他的 *_by_lua 指令則是由終端請求觸發,會被反復執行。
所以在 init_by_lua 階段,我們可以預先加載 Lua 模塊和公共的只讀數據,這樣可以利用操作系統的 COW(copy on write)特性,來節省一些內存。
對於業務代碼來說,其實大部分的操作都可以在 content_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:日志記錄。
舉一個例子來說明這樣拆分的好處。假設對外提供了很多明文 API,現在需要增加自定義的加密 和解密邏輯。那么需要修改所有 API 的代碼嗎?
# 明⽂協議版本
location /mixed {
content_by_lua '...'; # 處理請求
}
當然不用。事實上,利用階段的特性,我們只需要簡單地在 access 階段解密,在 body filter 階段加密就可以了,原來 content 階段的代碼是不用做任何修改的:
# 加密協議版本
location /mixed {
access_by_lua '...'; # 請求體解密
content_by_lua '...'; # 處理請求,不需要關⼼通信協議
body_filter_by_lua '...'; # 應答體加密
}
二進制熱升級
在修改完 NGINX 的配置文件后,還需要重啟才能生效。但在 NGINX 升級自身版本的時候,卻可以做到熱升級。這看上去有點兒本末倒置,不過,考慮到 NGINX 是從傳統靜態的負載均衡、反向代理、文件緩存起家的,這倒也可以理解。
熱升級通過向舊的 Master 進程發送 USR2 和 WINCH 信號量來完成。對於這兩步,前者的作用,是啟動新的 Master 進程;后者的作用,是逐步關閉Worker 進程。
執行完這兩步后,新的 Master 和新的 Worker 就已經啟動了。不過此時,舊的 Master 並沒有退出。不退出的原因也很簡單,如果你需要回退,依舊可以給舊的 Master 發送 HUP 信號量。當然,如果你已經確定不需要回退,就可以給舊 Master 發送 KILL 信號量來退出。
至此,二進制的熱升級就完成了。
在 OpenResty 中用到的都是 Nginx 的基礎知識,主要涉及到配置、主從進程、執行階段等。而 其他能用 Lua 代碼解決的,盡量用代碼來解決,而非使用Nginx 的模塊和配置,這是在學習 OpenResty 中的一個思路轉變。
LUA基礎
Lua是 OpenResty 中使用的腳本語言。Lua 在設計之初,就把自己定位為一個簡單、輕量、可嵌入的膠水語言,沒有走大而全的路線。雖然平常工作中可能沒有直接編寫 Lua 代碼,但 Lua 的使用其實非常廣泛。很多的網 游,比如魔獸世界,都會采用 Lua 來編寫插件;而鍵值數據庫 Redis 則是內置了 Lua 來控制邏輯。
另一方面,雖然 Lua 自身的庫比較簡單,但它可以方便地調用 C 庫,大量成熟的 C 代碼都可以為其所用。 比如在 OpenResty 中,很多時候都需要你調用 NGINX 和 OpenSSL 的 C 函數,而這都得益於 Lua 和 LuaJIT 這種方便調用 C 庫的能力。
環境和 hello world
不用專門去安裝標准 Lua 5.1 之類的環境,因為 OpenResty 已經不再支持標准 Lua,而只支持 LuaJIT。 這里介紹的 Lua 語法,也是和 LuaJIT 兼容的部分,而不是基於最新的 Lua 5.3。
在 OpenResty 的安裝目錄下,可以找到 LuaJIT 的目錄和可執行文件。
$ which luajit /usr/local/Cellar/openresty/1.15.8.3_1/luajit/bin/luajit $ luajit -v LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/
使用resty運行lua腳本,它最終也是用 LuaJIT 來執行的:
$ resty -e 'print("hello world")'
hello world
數據類型
Lua 中的數據類型不多,你可以通過 type 函數來返回一個值的類型,比如下面這樣的操作:
$ resty -e 'print(type("hello world"))
> print(type(print))
> print(type(true))
> print(type(360.0))
> print(type({}))
> print(type(nil))
> '
string
function
boolean
number
table
nil
這幾種就是 Lua 中的基本數據類型了。下面來簡單介紹一下:
字符串
在 Lua 中,字符串是不可變的值,如果你要修改某個字符串,就等於創建了一個新的字符串。這種做法顯然有利有弊:好處是即使同一個字符串出現很多次,在內存中也只有一份;但劣勢也很明顯,如果想修改、拼接字符串,會額外地創建很多不必要的字符串。
如下是把 1 到 10 這些數字當作字符串拼接起來。在 Lua 中,使用兩個點號來表示字符串的相加:
$ resty -e ' > local s = "" > for i = 1,10 do > s = s..tostring(i) > end > print(s) > ' 12345678910
在 Lua 中,你有三種方式可以表達一個字符串:單引號、雙引號,以及長括號(兩對中括號)[[]]。長括號中的字符串不會做任何的轉義處理。
$ resty -e 'print([[string has \n and \r]])' string has \n and \r
如果上面那段字符串中包括了長括號本身,在長括號中間增加一個或者多個 = 符號:
$ resty -e 'print([=[ string has a [[]]. ]=])' string has a [[]].
布爾值
true 和 false。但在 Lua 中,只有 nil 和 false 為假,其他都為真,包括 0 和空字符串也為真。
$ resty -e 'local a = 0
> if a then
> print("true")
> end
> a = ""
> if a then
> print("true")
> end'
true
true
這種判斷方式和很多常見的開發語言並不一致,所以,為了避免在這種問題上出錯,可以顯式地寫明比較 的對象,比如下面這樣:
$ resty -e 'local a = 0
if a == false then
print("true")
end
'
數字
Lua 的 number 類型,是用雙精度浮點數來實現的。LuaJIT 支持 dual-number(雙數)模 式,也就是說, LuaJIT 會根據上下文來用整型來存儲整數,而用雙精度浮點數來存放浮點數。
LuaJIT 還支持⻓⻓整型的大整數,比如下面的例子:
$ resty -e 'print(9223372036854775807LL - 1)' 9223372036854775806LL
函數
函數在 Lua 中是一等公民,可以把函數存放在一個變量中,也可以當作另外一個函數的入參和出參。
下面兩個函數的聲明是完全等價的:
function foo() end foo = function () end
table
table 是 Lua 中唯一的數據結構
$ resty -e 'local color = {first = "red"}
> print(color["first"])'
red
空值
在 Lua 中,空值就是 nil。如果定義了一個變量,但沒有賦值,它的默認值就是 nil:
$ resty -e 'local a > print(type(a))' nil
真正進入 OpenResty 體系中后,會發現很多種空值,比如 ngx.null 等等.
常用標准庫
Lua 比較小巧,內置的標准庫並不多。而且,在 OpenResty 的環境中,Lua 標准庫的優先級是很低的。對 於同一個功能,更推薦優先使用 OpenResty 的 API 來解決,然后是 LuaJIT 的庫函數,最后才是標准 Lua 的函數。
OpenResty的API > LuaJIT的庫函數 > 標准Lua的函數,這個優先級會對性能產生非常大的影響。
幾個比較常用的Lua標准庫:
string 庫
字符串操作是最常用到的,也是坑最多的地方。有一個簡單的原則,那就是如果涉及到正則表達式的, 請一定要使用 OpenResty 提供的 ngx.re.* 來解決,不要用 Lua 的 string.* 處理。這是因為,Lua 的正 則獨樹一幟,不符合 PCRE 的規范。
其中 string.byte(s [, i [, j ]]),是比較常用到的一個 string 庫函數,它返回字符 s[i]、s[i + 1]、 s[i + 2]、······、s[j] 所對應的 ASCII 碼。i 的默認值為 1,即第一個字節,j 的默認值為 i。
$ resty -e 'print(string.byte("abc", 1, 3))
> print(string.byte("abc", 3)) -- 缺少第三個參數,第三個參數默認與第⼆個相同,此時為 3
> print(string.byte("abc")) -- 缺少第⼆個和第三個參數,此時這兩個參數都默認為 1
> '
979899
99
97
table 庫
在 OpenResty 的上下文中,對於Lua 自帶的 table 庫,除了 table.concat 、table.sort 等少數幾個函 數,大部分都不推薦使用。
table.concat一般用在字符串拼接的場景下,可以避免生成很多無用的字符串。
$ resty -e 'local a = {"A","B","C"} print(table.concat(a))'
ABC
math 庫
Lua math 庫由一組標准的數學函數構成。數學庫的引入,既豐富了 Lua 編程語言的功能,同時也方便了程序的編寫。
在 OpenResty 的實際項目中,我們很少用 Lua 去做數學方面的運算,其中和隨機數相關的 math.random() 和 math.randomseed() 兩個函數比較常用,比如下面的這段代碼可以在指 定的范圍內,隨機地生成兩個數字。
$ resty -e 'math.randomseed (os.time()) > print(math.random()) > print(math.random(100))' 0.6389552204975 39
虛變量
設想這么一個場景,當一個函數返回多個值的時候,有些返回值我們並不需要,這時候,應該怎么接收這些值呢?
Lua 提供了一個虛變量(dummy variable)的概念, 按照慣例以一個下划線來命名,用來表示丟棄不需要的數值,僅僅起到占位的作用。
以 string.find 這個標准庫函數為例,來看虛變量的用法。這個標准庫函數會返回兩個值,分別代表開始和結束的下標。
如果我們只需要獲取開始的下標,只聲明一個變量來接收 string.find 的返回值即可:
$ resty -e 'local s = string.find("hello","he") print(s)'
1
如果只想獲取結束的下標,那就必須使用虛變量了:
$ resty -e 'local _, end_pos = string.find("hello", "he")
> print(end_pos)'
2
除了在返回值里使用,虛變量還經常用於循環中,
$ resty -e 'for _, v in ipairs({4,5,6}) do
> print(v)
> end'
4
5
6
而當有多個返回值需要忽略時,你可以重復使用同一個虛變量。
LuaJIT
LuaJIT是OpenResty 的另一塊基石。在 OpenResty 中,寫出正確的 LuaJIT 代碼的門檻並不高,但要寫出高效的 LuaJIT 代碼絕非易事。
LuaJIT 在 OpenResty 整體架構中的位置:

OpenResty 的 worker 進程都是 fork master 進程而得到的, 其實, master 進程中的 LuaJIT 虛擬機也會一起 fork 過來。在同一個 worker 內的所有協程,都會共享這個 LuaJIT 虛擬機,Lua 代 碼的執行也是在這個虛擬機中完成的。這可以算是 OpenResty 的基本原理。
標准 Lua 和 LuaJIT 的關系
標准 Lua 和 LuaJIT 是兩回事兒,LuaJIT 只是兼容了 Lua 5.1 的語法,並對 Lua 5.2 和 5.3 做了選擇性支持。
在 OpenResty 幾年前的老版本中, 編譯的時候,可以選擇使用標准 Lua VM ,或者 LuaJIT VM 來作為執行環境,不過,現在已經去掉了對標 准 Lua 的支持,只支持 LuaJIT。
OpenResty 並沒有直接使用 LuaJIT 官方提供的 2.1.0-beta3 版本,而是在此基礎上,擴展了 自己的 fork: [openresty-luajit2]
為什么選擇 LuaJIT
不直接使用Lua,而是要用open resty維護的LuaJIT 最主要的原因,還是LuaJIT的性能優勢。
其實標准 Lua 出於性能考慮,也內置了虛擬機,所以 Lua 代碼並不是直接被解釋執行的,而是先由 Lua 編 譯器編譯為字節碼(Byte Code),然后再由 Lua 虛擬機執行。
而 LuaJIT 的運行時環境,除了一個匯編實現的 Lua 解釋器外,還有一個可以直接生成機器代碼的 JIT 編譯 器。開始的時候,LuaJIT和標准 Lua 一樣,Lua 代碼被編譯為字節碼,字節碼被 LuaJIT 的解釋器解釋執行。
但不同的是,LuaJIT的解釋器會在執行字節碼的同時,記錄一些運行時的統計信息,比如每個 Lua 函數調用 入口的實際運行次數,還有每個 Lua 循環的實際執行次數。當這些次數超過某個隨機的閾值時,便認為對 應的 Lua 函數入口或者對應的 Lua 循環足夠熱,這時便會觸發 JIT 編譯器開始工作。
JIT 編譯器會從熱函數的入口或者熱循環的某個位置開始,嘗試編譯對應的 Lua 代碼路徑。編譯的過程,是 把 LuaJIT 字節碼先轉換成LuaJIT 自己定義的中間碼(IR),然后再生成針對目標體系結構的機器碼。
所謂 LuaJIT 的性能優化,本質上就是讓盡可能多的 Lua 代碼可以被 JIT 編譯器生成機器碼,而不是回 退到 Lua 解釋器的解釋執行模式。
Lua 特別之處
Lua 的下標從 1 開始
$ resty -e 'local t={100};ngx.say(t[1])'
100
使用 .. 來拼接字符串
$ resty -e "ngx.say('hello' .. ', world')"
hello, world
只有 table 這一種數據結構
不同於 Python 這種內置數據結構豐富的語言,Lua 中只有一種數據結構,那就是 table,它里面可以包括數組和哈希表:
$ resty -e '
> local color = {first = "red", "blue", third = "green", "yellow"}
> print(color["first"])
> print(color[1])
> print(color["third"])
> print(color[2])
> print(color[3])
> '
red
blue
green
yellow
如果不顯式地用_鍵值對_的方式賦值,table 就會默認用數字作為下標,從 1 開始。所以 color[1] 就是 blue。
另外,想在 table 中獲取到正確長度,也是一件不容易的事情,我們來看下面這些例子:
$ resty -e 'local t1 = {1,2,3};print("t1 length:" .. table.getn(t1))'
t1 length:3
$ resty -e 'local t2 = { 1, a = 2, 3 };print("t2 length: " .. table.getn(t2))'
t2 length: 2
$ resty -e 'local t3 = { 1, nil };print("t3 length: " .. table.getn(t3))'
t3 length: 1
$ resty -e 'local t4 = { 1, nil, 2 };print("t4 length: " .. table.getn(t4))'
t4 length: 1
想 要在Lua 中獲取 table 長度,必須注意到,只有在 table 是 序列 的時候,才能返回正確的值。
先序列是數組(array)的子集,也就是說,table 中的元素都可以用正整數下標訪問到,不存在鍵值對的情況。對應到上面的代碼中,除了 t2 外,其他的 table 都是 array。
序列中不包含空洞(hole),即 nil。綜合這兩點來看,上面的 table 中, t1 是一個序列,而 t3 和 t4 是 array,卻不是序列(sequence)。
t4 的長度是 1 是因為,在遇到 nil 時,獲取長度的邏輯就不繼續往下運行,而是直接返回了。
默認是全局變量
除非相當確定,否則在 Lua 中聲明變量時,前面都要加上 local,是因為在 Lua 中,變量默認是全局的,會被放到名為 _G 的 table 中。不加 local 的變量會在全局表中查 找,這是昂貴的操作。如果再加上一些變量名的拼寫錯誤,就會造成難以定位的 bug。
所以,在 OpenResty 編程中,應該總是使用 local 來聲明變量,即使在 require module 的時候也是一樣:
-- Recommended
local xxx = require('xxx')
-- Avoid
require('xxx')
LuaJIT
除了兼容 Lua 5.1 的語法並支持 JIT 外,LuaJIT 還緊密結 合了 FFI(Foreign Function Interface),可以直接在 Lua 代碼中調用外部的 C 函數和使用 C 的數據結構。
$ resty -e '
local ffi = require("ffi");
ffi.cdef[[
int printf(const char *fmt, ...);
]];
ffi.C.printf("Hello %s! \n", "world");
'
Hello world!
這幾行代碼,就可以直接在 Lua 中調用 C 的 printf 函數,打印出 Hello world!。
類似的,我們可以用 FFI 來調用 NGINX、OpenSSL 的 C 函數,來完成更多的功能。實際上,FFI 方式比傳 統的 Lua/C API 方式的性能更優,這也是 lua-resty-core 項目存在的意義。
出於性能方面的考慮,LuaJIT 還擴展了 table 的相關函數:table.new 和 table.clear。這是兩 個在性能優化方面非常重要的函數。
為什么lua-resty-core性能更高一些
在 Lua 中,可以用 Lua C API 來調用 C 函數,而在 LuaJIT 中還可以使用 FFI。對 OpenResty 而言:
- 在核心的 lua-nginx-module 中,調用 C 函數的 API,都是使用 Lua C API 來完成的;
- 而在 lua-resty-core 中,則是把 lua-nginx-module 已有的部分 API,使用 FFI 的模式重新實現了一遍。
以 ngx.base64_decode 這個很簡單的 API 為例,看下 Lua C API 和 FFI 的實現有何不同 之處。
Lua CFunction
先來看下, lua-nginx-module 中用 Lua C API 是如何實現的。在項目的代碼中搜索 decode_base64,可以找到它的代碼實現在 ngx_http_lua_string.c 中
lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64); lua_setfield(L, -2, "decode_base64");
這里注冊了一個 CFunction:ngx_http_lua_ngx_decode_base64, 而它與 ngx.base64_decode 這個對外暴露的 API 是對應關系。
在這個 C 文件中搜索 ngx_http_lua_ngx_decode_base64,它定義在文件的開始位置:
static int ngx_http_lua_ngx_decode_base64(lua_State *L)
對於那些能夠被 Lua 調用的 C 函數來說,它的接口必須遵循 Lua 要求的形式,也就是 typedef int (*lua_CFunction)(lua_State* L)。它包含的參數是 lua_State 類型的指針 L ;它的返回值類型是 一個整型,表示返回值的數量,而非返回值自身。
它的實現如下:
static int
ngx_http_lua_ngx_decode_base64(lua_State *L)
{
ngx_str_t p, src;
src.data = (u_char *) luaL_checklstring(L, 1, &src.len);
p.len = ngx_base64_decoded_length(src.len);
p.data = lua_newuserdata(L, p.len);
if (ngx_decode_base64(&p, &src) == NGX_OK) {
lua_pushlstring(L, (char *) p.data, p.len);
} else {
lua_pushnil(L);
}
return 1;
}
這段代碼中,最主要的是 ngx_base64_decoded_length 和 ngx_decode_base64, 它們都是 NGINX 自身提供的 C 函數。
用 C 編寫的函數,無法把返回值傳給 Lua 代碼,而是需要通過棧,來傳遞 Lua 和 C 之間的調用 參數和返回值。同時,這些代碼也不能被 JIT 跟蹤到, 所以對於 LuaJIT 而言,這些操作是處於黑盒中的,沒法進行優化。
LuaJIT FFI
而 FFI 則不同。FFI 的交互部分是用 Lua 實現的,這部分代碼可以被 JIT 跟蹤到,並進行優化;當然,代碼 也會更加簡潔易懂。
還是以 base64_decode為例,它的 FFI 實現分散在兩個倉庫中: lua-resty-core 和 lua-nginx-module。
先來看下前者里面實現的代碼:
ngx.decode_base64 = function (s)
local slen = #s
local dlen = base64_decoded_length(slen)
local dst = get_string_buf(dlen)
local pdlen = get_size_ptr()
local ok = C.ngx_http_lua_ffi_decode_base64(s, slen, dst, pdlen)
if ok == 0 then
return nil
end
return ffi_string(dst, pdlen[0])
end
OpenResty 中的函數都是有命名規范的:
- ngx_http_lua_ffi_ ,是用 FFI 來處理 NGINX http 請求的 Lua 函數;
- ngx_http_lua_ngx_ ,是用 Cfunction 來處理 NGINX http 請求的 Lua 函數;
- 其他 ngx_ 和 lua_ 開頭的函數,則分別屬於 NGINX 和 Lua 的內置函數。
LuaJIT FFI GC
在 FFI 中申請的內存,到底由誰來管理呢?是應該我們在 C 里面手動釋 放,還是 LuaJIT 自動回收呢?
這里有個簡單的原則:LuaJIT 只負責由自己分配的資源;而 ffi.C 是 C 庫的命名空間,所以,使用 ffi.C 分配的空間不由 LuaJIT 負責,需要你自己手動釋放。
舉個例子,比如你使用 ffi.C.malloc 申請了一塊內存,那你就需要用配對的 ffi.C.free 來釋放。 LuaJIT 的官方文檔中有一個對應的示例:
local p = ffi.gc(ffi.C.malloc(n), ffi.C.free) ... p = nil -- Last reference to p is gone. -- GC will eventually run finalizer: ffi.C.free(p)
這段代碼中,ffi.C.malloc(n) 申請了一段內存,同時 ffi.gc 就給它注冊了一個析構的回調函數 ffi.C.free。這樣一來,p 這個 cdata 在被 LuaJIT GC 的時候,就會自動調用 ffi.C.free,來釋放 C 級別的內存。而 cdata 是由 LuaJIT 負責 GC的 ,所以上述代碼中的 p 會被 LuaJIT 自動釋放。 這里要注意,如果你要在 OpenResty 中申請大塊的內存,更推薦用 ffi.C.malloc 而不是 ffi.new。原因也很明顯:
- ffi.new 返回的是一個 cdata,這部分內存由 LuaJIT 管理;
- LuaJIT GC 的管理內存是有上限的,OpenResty 中的 LuaJIT 並未開啟 GC64 選項,所以單個 worker 內存 的上限只有2G。一旦超過 LuaJIT 的內存管理上限,就會導致報錯。
在使用 FFI 的時候,我們還需要特別注意內存泄漏的問題。
lua-resty-core
FFI 的方式不僅代碼更簡潔,而且可以被 LuaJIT 優化,顯然是更優的選 擇。其實現實也是如此,實際上,CFunction 的實現方式已經被 OpenResty 廢棄,相關的實現也從代碼庫中移除了。現在新的 API,都通過 FFI 的方式,在 lua-resty-core 倉庫中實現。
在 OpenResty 2019 年 5 月份發布的 1.15.8.1 版本前,lua-resty-core 默認是不開啟的,而這不僅會帶來性能損失,更嚴重的是會造成潛在的 bug。所以,還在使用歷史版本的用戶,都手動開啟 lua-resty-core。需要在 init_by_lua 階段,增加一行代碼就可以了
require "resty.core"
1.15.8.1 版本中,已經增加了 lua_load_resty_core 指令,默認開啟了 luaresty-core。
lua-resty-core 中不僅重新實現了部分 lua-nginx-module 項目中的 API,比如 ngx.re.match、ngx.md5 等,還實現了不少新的 API,比如 ngx.ssl、ngx.base64、ngx.errlog、 ngx.process、ngx.re.split、ngx.resp.add_header、ngx.balancer、ngx.semaphore 等等。
FFI 雖然好,卻也並不是性能銀彈。它之所以高效,主要原因就是可以被 JIT 追蹤並優化。如果你寫的 Lua 代碼不能被 JIT,而是需要在解釋模式下執行,那么 FFI 的效率反而會更低。
