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
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.dict
和 ngx.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 的另外一個理由了。
你可能會以爲,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 版本不支持這麼用。