内容简介:对 ngx.ctx 的一次 Hack
ngx.ctx
是 lua-nginx-module
提供的一个充满魔力的 Lua table,它可以存放任何我们想要存放的内容,生命周期贯穿整个 location
,也正因为生命周期局限在单个 location
里,所以当发生内部跳转(例如通过 ngx.exec
)之后,之前的 ngx.ctx
将被销毁。所以很多时候,我们不得不转而使用 ngx.var.VARIABLE
来替代 ngx.ctx
,例如我们需要在 log
阶段的时候收集之前准备好的字段,然后发送到日志服务器或者 nsq
等组件。
然而,事物总是具有两面性, ngx.var.VARIABLE
生命周期虽然贯穿于一个请求,但是其代价却更加昂贵,它具有计算 hash
值,查找 hash
表,分配内存等等操作,这相比于 ngx.ctx
实在是繁重得多了。通过观察火焰图,大量的使用 ngx.var.VARIABLE
已经成为了一个瓶颈。于是才有了对 ngx.ctx
,或者说 ngx.exec
的一次 hack 过程。
ngx.ctx
既然要对 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
这个成员变量。
为什么经过内部跳转,ngx.ctx 会被销毁
Nginx 核心在进行内部跳转的时候,会把对应请求所有的模块上下文全部清除,可以参考函数 ngx_http_internal_redirect
,所以 lua-nginx-module
的 ctx_ref
也会被销毁。在 lua-nginx-module
关于 ngx.exec
的源码里也可以看到对 ngx.ctx
的解索引过程。
Hack it
了解了它的机制之后,我们可以试着来绕过这种限制,既然 lua-nginx-module
利用一个数字来索引 ngx.ctx
,我们也可以主动创建一个索引,将它存在一个介质里,只要这个介质不随着内部跳转而消失即可(例如 Nginx 变量就是一个非常好的选择),等到内部跳转完成之后,第一时间将 ngx.ctx
恢复出来即可,下面来介绍下这个过程。
首先我们需要一个变量
set ctx_ref "";
设计一个函数,创建一个新的索引
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 的注册表,通过下面的方法获得。
local debug = require "debug" local registry = debug.getregistry()
所有请求的 ngx.ctx
放置在一张表里,这张表存放在注册表里,key 就是 "ngx_http_lua_ctx_tables"
,所以上述代码里的 ctxs
就是存放所有请求的 ngx.ctx
的那张表了。
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)。
以上所述就是小编给大家介绍的《对 ngx.ctx 的一次 Hack》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。