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 版本不支持这么用。