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

相關文章
相關標籤/搜索