爲何你應該在 OpenResty 項目中使用 lua-resty-core

lua-resty-core 是什麼?

lua-resty-core 是 OpenResty 組件的一部分。它由兩部分組成,一部分是 resty.core.*,提供了對 lua-nginx-module Lua 接口的替換實現;另外一部分是 ngx.*,OpenResty 新的接口通常都會放到這裏。
跟其餘 lua-resty 開頭的庫同樣,lua-resty-core 也是用 Lua 實現的。說到這有人可能會問,既然 lua-nginx-module 已經有了一套 API,爲何還要在 lua-resty-core 裏面從新實現一次,並且仍是用 Lua?nginx

須要澄清下,lua-nginx-module 提供的 API,並非徹底意義上的用 C 實現的。準確來講,是經過 C 實現,並經過 Lua CFunction 暴露出來的。而 lua-resty-core 提供的 API,也不是表面看上去那樣用 Lua 實現的。準確來講,是經過在 lua-nginx-module 裏面的以 *_lua_ffi_* 形式命名的 C 函數實現的,並在 lua-resty-core 裏面經過 LuaJIT FFI 暴露出來的。因此其實二者都是 C 實現。二者的比較,應該是 Lua CFunction 和 LuaJIT FFI 的比較。git

那 LuaJIT FFI 有着怎樣的優勢,值得在已有一套的基於 Lua CFunction 的接口的前提下,去費大力氣從新實現一遍?github

FFI + JIT

LuaJIT FFI 的實現深深地根植於解釋器自身。若是當前 LuaJIT 正處於 JIT 模式,它會在 FFI 調用時優化 Lua 領域和 C 領域間傳參和返回的過程,所以採用 FFI 要比直接調用 Lua CFunction 要快。至於能快多少,則取決於調用時兩個領域間數據交換頻繁狀況。curl

舉個例子,函數

init_by_lua_block {
    -- 註釋下面一行來禁用 lua-resty-core
    require 'resty.core'
}

location /foo {
    content_by_lua_block {
        local s = ("test"):rep(256)
        local start = ngx.now()
        for _ = 1, 1e6 do
            ngx.md5(s)
        end
        ngx.update_time()
        ngx.say(ngx.now() - start)
    }
}

在啓用了 lua-resty-core 的狀況下(走 FFI 路徑),用時是oop

¥ curl localhost:8888/foo
2.6159999370575

禁用 lua-resty-core 後(走 CFunction 路徑),用時是性能

¥ curl localhost:8888/foo
2.664999961853

二者並沒有明顯區別。優化

不過換個須要跟 C 領域頻繁交互的調用,ui

local s = ("test"):rep(256)
local start = ngx.now()
for _ = 1, 1e8 do
    ngx.ctx.test = s
    local r = ngx.ctx.test
end
ngx.update_time()
ngx.say(ngx.now() - start)

啓用了 lua-resty-core,用時google

¥ curl localhost:8888/foo
1.800999879837

禁用後用時

¥ curl localhost:8888/foo
38.345999956131

二者便有天壤之別。

ngx.ctx 同樣,會收益於 FFI + JIT 的接口,還有 ngx.shared.dictngx.re 這樣兩類。(固然對它們的加成相對沒有那麼顯著)

前面在提到 FFI 優化的時候,我特地強調了「當前 LuaJIT 正處於 JIT 模式」。若是當前 LuaJIT 處於解析器模式,很不幸,FFI 調用會比 CFunction 的形式慢。

在繼續以前,先跳出 FFI 的話題,介紹下 LuaJIT 的 JIT 原理。

LuaJIT 是 tracing JIT Compiler。它的 JIT 是基於分支(循環或者函數)的。對於每一個 tracing 的分支,它會維護一個計數器。一旦某個分支足夠熱,LuaJIT 會把該分支編譯掉,並用編譯掉的結果替換原來的代碼。這要求一點:整個分支都須要是可被編譯的。若是分支中有不能編譯的語句,LuaJIT 會中斷 tracing,該分支也就一直無法被 JIT 掉。這種不能被編譯的語句,在 LuaJIT 裏面叫 NYI。能夠在 http://wiki.luajit.org/NYI 查看當前的 NYI 列表。

查看 JIT trace 結果很容易,僅需在 init_by_lua_block 裏添加下面兩行:

local v = require "jit.v"
v.on("/tmp/dump")
require "resty.core" -- 確保 lua-resty-core 是啓用的

運行以後就能在 /tmp/dump 裏查看 trace 狀況了。在咱們的例子裏,結果只有一行:

[TRACE   1 content_by_lua(nginx.conf:21):4 loop]

它表示 content_by_lua 第 4 行有一個循環,可以被完整地 trace 掉。

若是想了解更詳細的狀況,能夠改用下面兩行:

local dump = require "jit.dump"
dump.on(nil, "/tmp/dump")

這時候它會記錄更詳細的內容,包括 trace 的過程、IR 和 mcode 的生成狀況。當 LuaJIT 中斷 tracing 時,你能夠憑 dump 下來的內容找出它是在哪裏中斷的。

迴歸正題。讓咱們找個 NYI 語句,插入到循環中,好比下面這樣:

local t = {}
for _ = 1, 1e6 do
    ngx.md5(s)
    next(t)
end

從新跑下,用時

¥ curl localhost:8888/foo
2.9719998836517

比調用 Lua CFunction 時要慢一些。

欲抑先揚,欲揚先抑。即便解釋器模式下 FFI 會明顯地慢,但有些時候仍是比 CFunction 快一些。好比前面的 ngx.ctx 這個例子,在解釋器模式下,它的用時是:

¥ curl localhost:8888/foo
19.00200009346

慢得要命,但仍是 CFunction 版本的兩倍。

若是擔憂項目支持的 NYI 語句太多,啓用 lua-resty-core 會致使性能不升反降,那麼我插一句:Lua CFunction 調用就是一種 NYI 語句,而 FFI 調用是能夠 JIT 的。也便是說,啓用 lua-resty-core 會減小項目中一類 NYI 語句的存在。這算是切換到 lua-resty-core 的另外一個理由了。

lua-resty-core 寄託着 OpenResty 的將來

你可能會以爲,JIT 啊、NYI 啊什麼的離我太遠了,咱們的項目不須要什麼性能上的優化,因此也無需引入 lua-resty-core。

OK,即便不考慮性能,你也應該引入 lua-resty-core。春哥(OpenResty 的做者)曾經公開說過,有計劃淘汰掉現有的一套 Lua CFunction 接口。因此早晚你也會用上 lua-resty-core 所暴露的接口。這是其一。

其二,OpenResty 目前新的功能開發,都是放到 lua-resty-core 上的。畢竟舊的接口要淘汰了嘛。對 FFI 的偏好並不只僅體如今新功能開發上。若是改用 FFI 能解決 CFunction 接口的 bug,OpenResty 開發者會認爲這個問題已經解決了。(參見 BUG Report 嚴重(特別是使用了lua-resty-lock庫的服務,有必定機率workers死鎖,可重現)

最後,一樣的方法,來自 lua-resty-core 的版本除了性能外,還會有其餘優點。舉個例子,由於內部實現上的差別,lua-resty-core 中的 ngx.re 能夠用在 init_by_lua* 階段,而原來的 Lua CFunction 版本不支持這麼用。

相關文章
相關標籤/搜索