对 ngx.ctx 的一次 hack

1

缘起

ngx.ctxlua-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 过程。

<!-- more -->

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-modulectx_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-corebase.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)。

你可能感兴趣的

载入中...