Lua 是一種輕量、簡潔、可擴充套件的指令碼語言。它有一個相對簡單的 C API,易於嵌入應用程式。許多應用程式使用 Lua 作為它們的嵌入式指令碼語言,以實現可配置性和可擴充套件性。這包括我們的 OpenResty。

在本文中,我們將演示如何使用 OpenResty XRay 的命令列工具來快速定位正在執行的 OpenResty Lua 應用程式中洩漏的 Lua table。

LuaJIT 如何管理記憶體

LuaJIT 裡的垃圾回收器(GC),是其記憶體管理的一部分。它是基於 mark-sweep 演算法的增量 GC,旨在釋放未使用的記憶體物件。它定期收集所有 Lua GC 物件,這些未使用的物件從 GC root 不可達。GC root 是特殊物件,它們作為 GC 跟蹤和標記記憶體圖中的活動物件的起點。在 Lua 中,GC root 包括登錄檔、全域性字串表、全域性變數等。換句話說,Lua GC 物件如果從 GC root 不可達,則會被 GC 清理。在 Lua 的世界中,如果一個 Lua GC 物件有來自 GC root 的直接或間接引用,則處於 “存活” 狀態;否則,它處於 “死亡” 狀態。隨著 GC 的工作,這個物件最終會被清理並釋放。Lua 參與 GC 的物件包括 table、函式、模組、執行緒(協程)、字串等。

LuaJIT 中,以下內容被視為“GC 物件”:

  • string:Lua 字串
  • upvalue:Lua Upvalue
  • thread:Lua 執行緒(即 Lua 協程)
  • proto:Lua 函式原型
  • function:Lua 函式(Lua 閉包)和 C 函式
  • cdata:由 Lua 中的 FFI API 建立的 cdata
  • table:Lua 表

OpenResty XRay 的命令列工具

OpenResty XRay 有一個名為 orxray 的命令列工具。如果您還沒有安裝這個工具,可以參照 OpenResty XRay™ CLI 使用者手冊中的安裝部分中的步驟進行操作。

當您面臨記憶體方面的問題時,OpenResty XRay 為您提供了綜合分析解決方案。在本文中,我們將向您展示如何分析 OpenResty Nginx Worker 程序由於在 Lua 中建立了太多的 Lua 物件而使用了大量記憶體的情況。

洩漏示例

我們將使用一個簡單的 OpenResty Lua 模組來演示 OpenResty Lua 的記憶體如何不斷增長,從而導致 Nginx Worker 程序佔用大量記憶體。我們將使用的 demo.lua 模組的原始碼如下:

local _M = {}

local foo = {}

function _M.go(self)
    foo[#foo + 1] = "hello " .. #foo
end

return _M

nginx.conf配置檔案片段如下。

location = /t {
    content_by_lua_block {
        local demo = require("demo")
        demo:go()
        ngx.say("ok")
    }
}

該模組有一個 demo:go() 方法,每次呼叫時都會增加模組中的 foo table 的大小。

基準測試前的程序記憶體快照:

leak-tables-before-sh

使用 wrk 進行 30 秒基準測試後的程序記憶體快照:

leak-tables-after-sh

Nginx Worker 程序的記憶體佔用量增加到了 213MB。為了確定記憶體消耗高的原因,使用 OpenResty XRay 命令列工具分析了 Nginx Worker 程序記憶體。

分析過程

使用以下命令在程序上執行了 resty-memory 分析器(假設目標程序 PID 是 7646):

$ orxray analyzer run resty-memory -p 7646
Goto https://8pu4z6.xray.openresty.com.cn/targets/1355/history/4375500223 for charts

在瀏覽器中開啟命令輸出的URL,以檢視分析結果。

應用程式記憶體使用情況顯示,HTTP LuaJIT Allocator 記憶體佔用了總記憶體的 169.89MB(85.3%)。

application-level-memory

單擊 “HTTP LuaJIT Allocator managed Memory” 餅圖顯示了以下詳細資訊:

http-luajit-allocator-managed-memory

“GC-managed (HTTP including all chunks)” 部分佔用了 169.80MB 的記憶體。進一步的檢查顯示了以下LuaJIT GC 物件的分佈:

luajit-gc-object-total-s

分析結果表明,字串物件是最大的記憶體消耗者,佔用了 105.67MB(62.2%),其次是 table 物件,佔用了 32.03MB(18.9%),全域性字串表佔用了 32MB(18.8%)。

lj-gco-ref 分析器

OpenResty XRay 提供了 lj-gco-ref 分析器,用於動態分析嵌入式 LuaJIT 程式程序,例如在 OpenResty Nginx Worker 程序中轉儲的 Lua 物件,並快速定位佔用大量記憶體的 Lua 物件的路徑。

lj-gco-ref 分析器非常適合查詢臃腫的嵌入式 LuaJIT 程序中最大的 Lua 物件。我們可以像這樣執行它(假設 nginx worker 目標程序的 PID 為 7646):

$ orxray analyzer run lj-gco-ref -p 7646
Goto https://8pu4z6.xray.openresty.com.cn/targets/1355/history/4375500985 for charts

在瀏覽器中開啟此處提示的 URL,以檢視相應的火焰圖:

如火焰圖中的資料所示,最大的記憶體物件是 Lua 字串。

從火焰圖底部到頂部進行跟蹤,我們得到以下路徑:

GC roots => registry => ._LOADED => .demo => .foo

相應的 Lua 虛擬碼:

debug.getregistry()._LOADED.demo.foo

其中 debug.getregistry()._LOADED 對應於 package.loaded table,而由 require() 載入的 lua 模組被快取在 package.loaded table 中。

轉換為 Lua 原始碼:

package.loaded.demo.foo

最終,我們瞭解到在 demo 模組中的 foo table 存有太多記憶體物件。基於此結果,我們可以進行針對性的最佳化。

全自動分析

上面,我們演示瞭如何透過命令列手動呼叫 lj-gco-ref 分析器來分析 LuaJIT GC 相關的記憶體使用問題。實際上,OpenResty XRay 可以按需自動呼叫該分析器對目標程序進行取樣,並自動分析生成的火焰圖,最終獲得那些記憶體使用率最高的 GC 物件的資料引用路徑,並以人類可讀的形式在自動分析報告中給出結論。這極大地解放了我們的使用者,他們不必等待伺服器上出現記憶體問題,也不必手動執行相應的分析器,甚至不必自行解讀分析器取樣的結果。

OpenResty XRay Memory Report

結論

我們在上文中解釋了 Lua GC 和 Lua 物件增長如何導致程序中的記憶體使用率偏高,並使用 OpenResty XRay 命令列工具進行了分析。我們還介紹瞭如何單獨使用 lj-gco-ref 分析器。

關於作者

章亦春是開源 OpenResty® 專案創始人兼 OpenResty Inc. 公司 CEO 和創始人。

章亦春(Github ID: agentzh),生於中國江蘇,現定居美國灣區。他是中國早期開源技術和文化的倡導者和領軍人物,曾供職於多家國際知名的高科技企業,如 Cloudflare、雅虎、阿里巴巴, 是 “邊緣計算“、”動態追蹤 “和 “機器程式設計 “的先驅,擁有超過 22 年的程式設計及 16 年的開源經驗。作為擁有超過 4000 萬全球域名使用者的開源專案的領導者。他基於其 OpenResty® 開源專案打造的高科技企業 OpenResty Inc. 位於美國矽谷中心。其主打的兩個產品 OpenResty XRay(利用動態追蹤技術的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最適合微服務和分散式流量的全能型閘道器軟體),廣受全球眾多上市及大型企業青睞。在 OpenResty 以外,章亦春為多個開源專案貢獻了累計超過百萬行程式碼,其中包括,Linux 核心、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,並編寫過 60 多個開源軟體庫。

關注我們

如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:

我們的微信公眾號

翻譯

我們提供了英文版原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!