也谈 ngx.ctx 继承问题

适合自己的才是最好的

Posted by ms2008 on November 26, 2018

在前一阵子的 OpenResty Con 2018 上,来自又拍云的 @tokers 分享了他们对 ngx.ctx 的 hack,以确保在发生内部跳转后 ngx.ctx 的信息依旧不会丢失。其实这个 hack 早在去年就被 @tokers 分享到了社区:ngx.ctx inheirt,并且写了一篇文章来详细阐述其思路:对 ngx.ctx 的一次 hack

这回呢,@tokers 重新封装并开源了其实现:lua-resty-ctxdump。关于这个问题,其实 Kong 也遇到过,在 Kong 0.14 版本之前,当发生内部错误时(HTTP status 500),Kong 的 log 阶段插件就无法正常的被执行。无法运行的原因就是,Kong 会将状态码为 4XX5XX 的请求内部跳转到另外一个 location 专门来处理这些异常状态:

error_page 400 404 408 411 412 413 414 417 /kong_error_handler;
error_page 500 502 503 504 /kong_error_handler;

location = /kong_error_handler {
    internal;
    uninitialized_variable_warn off;

    content_by_lua_block {
        kong.handle_error()
    }

    header_filter_by_lua_block {
        kong.header_filter()
    }

    body_filter_by_lua_block {
        kong.body_filter()
    }

    log_by_lua_block {
        kong.log()
    }
}

而 Kong 是将每个 API 启用的插件信息全部保存在 ngx.ctx 中的,所以一旦发生跳转,那么 Kong 自然就无法执行后续的插件了。Kong 针对这个问题也给了两种解决方案(ISSUE-3193):

  1. 去掉 kong_error_handler 禁止内部跳转

  2. 就是利用 @tokers 恢复 ngx.ctx 的方案

最终 Kong 选择了第二种,不过 Kong 的实现和 @tokers 这回开源出来的 lua-resty-ctxdump 有些区别。就是 lua-resty-ctxdump 是将 ngx.ctx 引用在了自身的 memo table 中(Lua Land),也正因为如此,所以其提供的 stash_ngx_ctxapply_ngx_ctx 方法必须成对调用,否则就会产生严重的内存泄漏。

local function ref_in_table(tb, key)
    if key == nil then
        return -1
    end

    local ref = tb[FREE_LIST_REF]
    if ref and ref ~= FREE_LIST_REF then
        tb[FREE_LIST_REF] = tb[ref]
    else
        ref = #tb + 1
    end

    tb[ref] = key

    return ref
end


function _M.stash_ngx_ctx()
    local ctx_ref = ref_in_table(memo, ngx.ctx)
    return ctx_ref
end


function _M.apply_ngx_ctx(ref)
    ref = tonumber(ref)
    if not ref or ref <= FREE_LIST_REF then
        return nil, "bad ref value"
    end

    local old_ngx_ctx = memo[ref]

    -- dereference
    memo[ref] = memo[FREE_LIST_REF]
    memo[FREE_LIST_REF] = ref

    return old_ngx_ctx
end

而 Kong 仅仅是将 ngx.ctx 的引用索引存放在了 ngx.var 中,随后根据这个索引把它从 Lua 的注册表中恢复出来。因为没有额外的索引创建动作,所以也就无需考虑引用释放问题。

function _M.stash_ref()
  local r = getfenv(0).__ngx_req
  if not r then
    ngx.log(ngx.WARN, "could not stash ngx.ctx ref: no request found")
    return
  end

  do
    local ctx_ref = ngx.var.ctx_ref
    if not ctx_ref or ctx_ref ~= "" then
      return
    end

    local _ = ngx.ctx -- load context if not previously loaded
  end

  local ctx_ref = C.ngx_http_lua_ffi_get_ctx_ref(r)
  if ctx_ref == FFI_NO_REQ_CTX then
    ngx.log(ngx.WARN, "could not stash ngx.ctx ref: no ctx found")
    return
  end

  ngx.var.ctx_ref = ctx_ref
end


function _M.apply_ref()
  local r = getfenv(0).__ngx_req
  if not r then
    ngx.log(ngx.WARN, "could not apply ngx.ctx: no request found")
    return
  end

  local ctx_ref = ngx.var.ctx_ref
  if not ctx_ref or ctx_ref == "" then
    return
  end

  ctx_ref = tonumber(ctx_ref)
  if not ctx_ref then
    return
  end

  local orig_ctx = registry.ngx_lua_ctx_tables[ctx_ref]
  if not orig_ctx then
    ngx.log(ngx.WARN, "could not apply ngx.ctx: no ctx found")
    return
  end

  ngx.ctx = orig_ctx
  ngx.var.ctx_ref = ""
end

如果从性能角度来考虑,我觉得 lua-resty-ctxdump 会更有优势,毕竟少了一些获取索引的动作。如果你的业务有大量的内部跳转,建议使用这个方案。同时需要注意的是 stash_ngx_ctxapply_ngx_ctx 方法必须成对调用;如果你的业务和 Kong 类似,只是发生异常才会需要少量跳转,那么建议使用 Kong 的方案,stash_refapply_ref 方法也无需成对调用,也可以省点儿事儿。