ngx.ctx
是 lua-nginx-module
提供的一个充满魔力的 Lua table,它能够存听任何咱们想要存放的内容,生命周期贯穿整个 location
,也正由于生命周期局限在单个 location
里,因此当发生内部跳转(例如经过 ngx.exec
)以后,以前的 ngx.ctx
将被销毁。因此不少时候,咱们不得不转而使用 ngx.var.VARIABLE
来替代 ngx.ctx
,例如咱们须要在 log
阶段的时候收集以前准备好的字段,而后发送到日志服务器或者 nsq
等组件。nginx
然而,事物老是具备两面性,`ngx.var.VARIABLE` 生命周期虽然贯穿于一个请求,可是其代价却更加昂贵,它具备计算 `hash` 值,查找 `hash` 表,分配内存等等操做,这相比于 `ngx.ctx` 实在是繁重得多了。经过观察火焰图,大量的使用 `ngx.var.VARIABLE` 已经成为了一个瓶颈。因而才有了对 `ngx.ctx`,或者说 `ngx.exec` 的一次 hack 过程。
<!-- more -->服务器
既然要对 ngx.ctx
进行 hack,首先须要了解 ngx.ctx
的机制,事实上,ngx.ctx
就是一个普通的 Lua table,lua-nginx-module
建立一个 table 以后,将其存放在 Lua 的注册表里,利用 luaL_ref
来索引每一个 ngx.ctx
,利用 luaL_unref
来解除索引。这个索引,是被存放在 lua-nginx-module
的模块上下文里的,也就是 ngx_http_lua_ctx_s::ctx_ref
这个成员变量。
app
为何通过内部跳转,ngx.ctx 会被销毁ide
Nginx 核心在进行内部跳转的时候,会把对应请求全部的模块上下文所有清除,能够参考函数 ngx_http_internal_redirect
,因此 lua-nginx-module
的 ctx_ref
也会被销毁。在 lua-nginx-module
关于 ngx.exec
的源码里也能够看到对 ngx.ctx
的解索引过程。函数
了解了它的机制以后,咱们能够试着来绕过这种限制,既然 lua-nginx-module
利用一个数字来索引 ngx.ctx
,咱们也能够主动建立一个索引,将它存在一个介质里,只要这个介质不随着内部跳转而消失便可(例如 Nginx 变量就是一个很是好的选择),等到内部跳转完成以后,第一时间将 ngx.ctx
恢复出来便可,下面来介绍下这个过程。性能
首先咱们须要一个变量测试
set ctx_ref "";
设计一个函数,建立一个新的索引ui
function _M.stash_ngx_ctx() local ctxs = registry.ngx_lua_ctx_tables local ctx_ref = base.ref_in_table(ctxs, ngx.ctx) ngx.var.ctx_ref = tostring(ctx_ref) end
registry 就是 Lua 的注册表,经过下面的方法得到。lua
local debug = require "debug" local registry = debug.getregistry()
全部请求的 ngx.ctx
放置在一张表里,这张表存放在注册表里,key 就是 "ngx_http_lua_ctx_tables"
,因此上述代码里的 ctxs
就是存放全部请求的 ngx.ctx
的那张表了。idea
local ctx_ref = base.ref_in_table(ctxs, ngx.ctx)
这行代码给 ngx.ctx
建立了一个新的索引,关于具体的细节,你们有兴趣能够查看 lua-resty-core
的 base.ref_in_table
,这个函数的原理和 luaL_ref
一致。
拿到索引以后,将它存放到咱们的变量便可。至此,当前请求的 ngx.ctx
就存在 2 个索引了(一个索引由 lua-nginx-module
管理,另一个则由咱们本身管理)。
执行完内部跳转后,恢复跳转前的
ngx.ctx
function _M.apply_ngx_ctx() local ctx_ref = tonumber(ngx.var.ctx_ref) if not ctx_ref then return end local ctxs = registry.ngx_lua_ctx_tables local origin_ngx_ctx = ctxs[ctx_ref] ngx.ctx = origin_ngx_ctx local FREE_LIST_REF = 0 ctxs[ctx_ref] = ctxs[FREE_LIST_REF] ctxs[FREE_LIST_REF] = ctx_ref ngx.var.ctx_ref = "" end
咱们经过存放在变量的 ctx_ref 来获得执行内部跳转前的 ngx.ctx
表,接着须要把咱们本身管理的这个索引解除,不然会形成严重的内存泄漏!
local FREE_LIST_REF = 0 ctxs[ctx_ref] = ctxs[FREE_LIST_REF] ctxs[FREE_LIST_REF] = ctx_ref
这三行代码即完成了解索引(和 LuaL_unref 一直),这里简单解释下, LuaL_unref
管理索引的时候,用 0 这个 index 记录上一次解索引的 index(为 nil
则表示目前尚未过解索引的操做),因此上述两行代码,实际上就是在当前须要解索引的 index 处记录了上一次解索引的 index,而后在 0 下标处记录当前最新的 index,有点像链表。这样操做有什么好处呢?当下次须要产生索引的时候,能够首先检查 0 下标,看看是否有解过索引的位置,若是有,复用便可,不然须要返回 #table + 1
,因此利用这个 “链表”,能够避免不少 Lua table 扩大,致使内存拷贝,影响到性能。
这两个函数的代码已经通过充分测试,目前已经运行在咱们的一个项目当中。
另外,这类基础的 Hack 操做,不适合存放在业务态,由调用者本身控制,由于这两个函数必须成对调用,不然就会形成内存泄漏。
使用以后,强烈建议进行压测,确认没有内存泄漏的隐患。
若是你有更多的 idea,能够给我发送邮件(zchao1995@gmail.com)。