资讯专栏INFORMATION COLUMN

对 ngx.ctx 的一次 hack

qc1iu / 2637人阅读

摘要:于是才有了对,或者说的一次过程。另外,这类基础的操作,不适合存放在业务态,由调用者自己控制,因为这两个函数必须成对调用,否则就会造成内存泄漏。使用之后,强烈建议进行压测,确认没有内存泄漏的隐患。

缘起

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 过程。

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)。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/40543.html

相关文章

  • 【Nginx源码研究】Master进程浅析

    摘要:内核代表进程来执行信号处理器函数,当处理器返回时,主程序会在处理器被中断的位置恢复执行。进程信号掩码内核会为每个进程维护一个信号掩码。这个竞态条件发生在主程序和信号处理器对同一个被解除信号的竞争关系。 运营研发团队 季伟滨 一、前言 众所周如,Nginx是多进程架构。有1个master进程和N个worker进程,一般N等于cpu的核数。另外, 和文件缓存相关,还有cache mana...

    wupengyu 评论0 收藏0
  • Nginx 源码分析:从模块到配置(上)

    摘要:结构体数组,用来表示该模块可以在配置文件中配置的项目,及其操作指令。 源文件路径 srccore gx_conf_file.h srccore gx_conf_file.c 主要内容 本篇的主要目的在于分析Nginx的配置功能。由于Nginx的配置基本就是对模块的配置,因此,在讨论配置功能之前,需要先分析Nginx的模块功能。 对于模块功能,这里的重点不在于某个模块的细节,而...

    gotham 评论0 收藏0
  • 【Nginx源码研究】nginx限流模块详解

    摘要:限流算法最简单粗暴的限流算法就是计数器法了,而比较常用的有漏桶算法和令牌桶算法计数器计数器法是限流算法里最简单也是最容易实现的一种算法。 运营研发团队 李乐 高并发系统有三把利器:缓存、降级和限流; 限流的目的是通过对并发访问/请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页)、排队等待(秒杀)、降级(返回兜底数据或默认数据); 高并发系统常见的限流有:限制总并发...

    voyagelab 评论0 收藏0
  • ngx_http_limit_req_module 源码分析

    摘要:如果当前需要延迟处理,又会把请求放到定时器中,等到定时器过期以后,执行写事件回调,这个函数里会执行,重新进行的个阶段。 ngx_http_limit_req_module 是 Nginx 官方提供的一个 http 模块,它工作在 NGX_HTTP_PREACCESS_PHASE 阶段,通过在 nginx.conf 中进行简单地配置,我们可以轻易地对请求速率进行限制。 配置指令 官方文档...

    lentrue 评论0 收藏0
  • 不再依靠巧合编写 Nginx 配置

    摘要:找到这个模块的指令后,则会调用这个指令的解析回调函数即结构体的第三个参数来进行处理。调用他们上面提到的中的回调函数来申请和初始化对应模块的配置结构体。需要注意的是,即时当前是直接在块级别,这三个回调函数都会被调用。拒绝暴力枚举式编写配置文件 原博:https://blog.coordinate35.cn/... 热身 首先来看下这几个小例子: 第一个例子: server { l...

    tulayang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<