実行中の OpenResty(LuaJIT)プロセスでリークした Lua テーブルを特定するには、GC ルートから参照パスをたどります:GC roots => registry => _LOADED => <モジュール> => <テーブル>。OpenResty XRay の lj-gco-ref アナライザはこのパスをフレームグラフとして描画し、メモリを保持している当のテーブルを正確に突き止めます——コード変更も再起動も不要です。

本記事では、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 プロセスが大量のメモリを使用している状況を分析する方法をご紹介します。

Lua テーブルのリーク例

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 テーブルのサイズが増加します。

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

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

wrk を使用して 30 秒間ベンチマークを実行した後のプロセスメモリスナップショット:

ベンチマーク後、OpenResty Nginx worker のメモリが 213MB まで増加

Nginx Worker プロセスのメモリ使用量が 213MB に増加しました。メモリ消費が高くなった原因を特定するために、OpenResty XRay コマンドラインツールを使用して Nginx Worker プロセスのメモリを分析しました。

LuaJIT GC オブジェクト種別ごとにプロセスメモリを分解する

以下のコマンドを使用して、プロセス上で resty-memory アナライザを実行しました(ターゲットプロセスの PID が 7646 であると仮定):

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

コマンド出力の URL をブラウザで開いて、分析結果を確認します。

アプリケーションレベルのメモリ使用状況を見ると、HTTP LuaJIT Allocator のメモリが総メモリの 169.89MB(85.3%)を占めていることがわかります。

アプリケーションレベルのメモリ内訳:HTTP LuaJIT Allocator が 169.89MB・総メモリの 85.3% を占有

「HTTP LuaJIT Allocator managed Memory」 の円グラフをクリックすると、以下の詳細が表示されます:

OpenResty XRay における HTTP LuaJIT Allocator 管理メモリの内訳

「GC-managed (HTTP including all chunks)」セクションが 169.80MB のメモリを占めています。さらに詳しく調べると、以下の LuaJIT GC オブジェクトの分布が明らかになります:

種別ごとの LuaJIT GC オブジェクトサイズ:文字列 105.67MB、テーブル 32.03MB、グローバル文字列テーブル 32MB

分析結果によると、文字列オブジェクトが最大のメモリ消費者で、105.67MB(62.2%)を占めており、次いでテーブルオブジェクトが 32.03MB(18.9%)、グローバル文字列テーブルが 32MB(18.8%)を占めています。

lj-gco-ref フレームグラフの読み方:registry、_LOADED、package.loaded

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/targets/1355/history/4375500985 for charts

ここで提示された URL をブラウザで開いて、対応するフレームグラフを確認します:

lj-gco-ref フレームグラフ:GC 参照パス registry、_LOADED、demo、foo をたどる

フレームグラフのデータが示すように、最大のメモリオブジェクトは 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 テーブルに多くのメモリオブジェクトが格納されていることがわかりました。この結果に基づいて、ターゲットを絞った最適化を行うことができます。

この Lua GC オブジェクト参照ビューはまさに、実際の本番アプリケーションでLRU キャッシュのメモリリークをキャッシュされた SSL 証明書まで突き止めた際に使ったものです。

完全自動の Lua メモリリーク分析

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

GC オブジェクト参照パスを含む OpenResty XRay 自動メモリ分析レポート

結論

本記事では、Lua GC と Lua オブジェクトの増加がプロセス内のメモリ使用率の高さにつながる仕組みを説明し、OpenResty XRay コマンドラインツールを使用して分析を行いました。また、lj-gco-ref アナライザを単独で使用する方法についても紹介しました。リークが Nginx 自身のより深い場所に潜んでいるケースについては、再起動なしで静かなメモリリークを突き止めた事例もご覧ください。

よくある質問

Lua の debug.getregistry() は何を返しますか?

debug.getregistry() は LuaJIT 内部の registry テーブルを返します。これは GC ルートの一つであり、ガベージコレクタがトレースを始める起点です。GC ルートであるため、OpenResty XRay はある Lua オブジェクトがなぜ回収されないのかを追跡する際、ここを起点にたどります。本例での参照パスは GC roots => registry => _LOADED => demo => foo でした。

_LOADEDpackage.loaded はどのような関係ですか?

debug.getregistry()._LOADEDpackage.loaded と同じテーブルです。require() で読み込んだモジュールはすべてここにキャッシュされるため、リークしているモジュールレベルのテーブルはこれを通じて到達可能なまま残り、ガベージコレクションで回収されません。だからこそ debug.getregistry()._LOADED.demo.foopackage.loaded.demo.foo は同一の foo テーブルを指します。

どの Lua テーブルがメモリをリークしているか、どう見つけますか?

稼働中のプロセスに対して OpenResty XRay の lj-gco-ref アナライザを実行します。Lua GC オブジェクトの参照関係をフレームグラフとして描画し、下から上へ読むと正確な保持パス——本例では registry => _LOADED => demo => foo——が得られます。これにより、コード変更も再起動もなしに、修正すべきテーブルを的確に特定できます。

lj-gco-ref アナライザとは何ですか?

lj-gco-ref は OpenResty XRay のアナライザで、稼働中の組み込み LuaJIT プロセスを動的に検査し、メモリを最も消費している Lua GC オブジェクトと、その GC ルートからの参照パスを特定します。どの Lua テーブル・文字列・その他のオブジェクトが Lua アプリケーションのメモリを肥大化させているかを突き止めるのに最適です。

著者について

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

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

翻訳

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