Lua は軽量で簡潔、拡張性の高いスクリプト言語です。比較的シンプルな C API を持ち、アプリケーションに容易に組み込むことができます。多くのアプリケーションが設定可能性と拡張性を実現するために、Lua を組み込みスクリプト言語として使用しています。これには我々の OpenResty も含まれます。

本記事では、OpenResty XRay のコマンドラインツールを使用して、実行中の OpenResty Lua アプリケーションでメモリリークしている Lua テーブルを迅速に特定する方法をご紹介します。

LuaJIT のメモリ管理方法

LuaJIT のガベージコレクタ(GC)は、そのメモリ管理の一部です。これは mark-sweep アルゴリズムに基づく増分 GC で、未使用のメモリオブジェクトを解放することを目的としています。定期的に全ての Lua GC オブジェクトを収集し、GC ルートから到達不可能な未使用オブジェクトを解放します。GC ルートは特別なオブジェクトで、GC がメモリグラフ内のアクティブなオブジェクトをトレースしマークするための起点となります。Lua では、GC ルートにはレジストリ、グローバル文字列テーブル、グローバル変数などが含まれます。言い換えれば、Lua GC オブジェクトが GC ルートから到達不可能な場合、GC によってクリーンアップされます。Lua の世界では、Lua GC オブジェクトが GC ルートから直接または間接的な参照を持っている場合、それは「生存」状態にあります。そうでない場合、それは「死亡」状態にあります。GC の動作により、このオブジェクトは最終的にクリーンアップされ、解放されます。Lua の GC に参加するオブジェクトには、テーブル、関数、モジュール、スレッド(コルーチン)、文字列などが含まれます。

LuaJIT では、以下のものが「GC オブジェクト」とみなされます:

  • string:Lua 文字列
  • upvalue:Lua Upvalue
  • thread:スレッド(つまり Lua コルーチン)
  • proto:Lua 関数プロトタイプ
  • function:Lua 関数(Lua クロージャ)と C 関数
  • cdata:Lua の FFI API によって作成された cdata
  • table:Lua テーブル

OpenResty XRay のコマンドラインツール

OpenResty XRay には orxray というコマンドラインツールがあります。このツールをまだインストールしていない場合は、OpenResty XRay™ CLI ユーザーマニュアルインストールセクションの手順に従ってインストールしてください。

メモリに関する問題に直面した場合、OpenResty XRay は包括的な分析ソリューションを提供します。本記事では、Lua で多くの Lua オブジェクトを作成したことにより、OpenResty Nginx Worker プロセスが大量のメモリを使用している状況を分析する方法をご紹介します。

リークの例

OpenResty Lua のメモリが継続的に増加し、Nginx Worker プロセスが大量のメモリを占有するようになる様子を示すために、シンプルな OpenResty Lua モジュールを使用します。使用する 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 テーブルのサイズが増加します。

ベンチマーク前のプロセスメモリスナップショット:

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%)を占めており、次いでテーブルオブジェクトが 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()._LOADEDpackage.loaded テーブルに対応し、require() によってロードされた lua モジュールは package.loaded テーブルにキャッシュされます。

Lua ソースコードに変換すると:

package.loaded.demo.foo

最終的に、demo モジュール内の foo テーブルに多くのメモリオブジェクトが格納されていることがわかりました。この結果に基づいて、ターゲットを絞った最適化を行うことができます。

完全自動分析

上記では、コマンドラインから手動で lj-gco-ref アナライザを呼び出して LuaJIT GC 関連のメモリ使用問題を分析する方法を示しました。実際には、OpenResty XRay は必要に応じてこのアナライザを自動的に呼び出してターゲットプロセスをサンプリングし、生成されたフレームグラフを自動的に分析して、最終的にメモリ使用率が最も高い GC オブジェクトのデータ参照パスを取得し、人間が読める形式で自動分析レポートに結論を提示することができます。これにより、ユーザーは大幅に負担が軽減され、サーバー上でメモリ問題が発生するのを待つ必要もなく、対応するアナライザを手動で実行する必要もなく、さらにはアナライザのサンプリング結果を自分で解釈する必要もありません。

OpenResty XRay Memory Report

結論

本記事では、Lua GC と Lua オブジェクトの増加がプロセス内のメモリ使用率の高さにつながる仕組みを説明し、OpenResty XRay コマンドラインツールを使用して分析を行いました。また、lj-gco-ref アナライザを単独で使用する方法についても紹介しました。

著者について

章亦春(Zhang Yichun)は、オープンソースの OpenResty® プロジェクトの創始者であり、OpenResty Inc. の CEO および創業者です。

章亦春(GitHub ID: agentzh)は中国江蘇省生まれで、現在は米国ベイエリアに在住しております。彼は中国における初期のオープンソース技術と文化の提唱者およびリーダーの一人であり、Cloudflare、Yahoo!、Alibaba など、国際的に有名なハイテク企業に勤務した経験があります。「エッジコンピューティング」、「動的トレーシング」、「機械プログラミング」 の先駆者であり、22 年以上のプログラミング経験と 16 年以上のオープンソース経験を持っております。世界中で 4000 万以上のドメイン名を持つユーザーを抱えるオープンソースプロジェクトのリーダーとして、彼は OpenResty® オープンソースプロジェクトをベースに、米国シリコンバレーの中心部にハイテク企業 OpenResty Inc. を設立いたしました。同社の主力製品である OpenResty XRay動的トレーシング技術を利用した非侵襲的な障害分析および排除ツール)と OpenResty XRay(マイクロサービスおよび分散トラフィックに最適化された多機能ゲートウェイソフトウェア)は、世界中の多くの上場企業および大企業から高い評価を得ております。OpenResty 以外にも、章亦春は Linux カーネル、Nginx、LuaJITGDBSystemTapLLVM、Perl など、複数のオープンソースプロジェクトに累計 100 万行以上のコードを寄与し、60 以上のオープンソースソフトウェアライブラリを執筆しております。

翻訳

英語版の原文と日本語訳版(本文)をご用意しております。読者の皆様による他の言語への翻訳版も歓迎いたします。全文翻訳で省略がなければ、採用を検討させていただきます。心より感謝申し上げます!