对 ngx.ctx 的一次 hack

缘起

ngx.ctxlua-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

既然要对 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-modulectx_ref 也会被销毁。在 lua-nginx-module 关于 ngx.exec 的源码里也能够看到对 ngx.ctx 的解索引过程。函数

Hack it

了解了它的机制以后,咱们能够试着来绕过这种限制,既然 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-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)。

相关文章
相关标签/搜索