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?
需要澄清下,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 的比较。
那 LuaJIT FFI 有着怎样的优点,值得在已有一套的基于 Lua CFunction 的接口的前提下,去费大力气重新实现一遍?
FFI + JIT
LuaJIT FFI 的实现深深地根植于解释器自身。如果当前 LuaJIT 正处于 JIT 模式,它会在 FFI 调用时优化 Lua 领域和 C 领域间传参和返回的过程,因此采用 FFI 要比直接调用 Lua CFunction 要快。至于能快多少,则取决于调用时两个领域间数据交换频繁情况。
举个例子,
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 路径),用时是
¥ curl localhost:8888/foo
2.6159999370575
禁用 lua-resty-core 后(走 CFunction 路径),用时是
¥ curl localhost:8888/foo
2.664999961853
两者并无明显区别。
不过换个需要跟 C 领域频繁交互的调用,
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,用时
¥ 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 的另一个理由了。
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 版本不支持这么用。