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. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!