LRU キャッシュは、その容量が大きすぎてキャッシュ項目をほとんど破棄しない設定になっていると、メモリリークを引き起こします——キャッシュされたすべてのオブジェクトが実質的に永久に保持されてしまうのです。この実際の事例では、ある OpenResty アプリが解析済みの SSL 証明書を lua-resty-lrucache にキャッシュしており、下層の OpenSSL 構造体が蓄積し続け、worker プロセスの RSS が無限に増加していました。以下では、OpenResty XRay がコードの変更も再起動もなしに、これをどのように根本原因まで追跡したかを解説します。

なぜ低トラフィックの OpenResty アプリがメモリをリークし続けたのか

あるお客様の OpenResty アプリケーションでは、ビジネスのトラフィック量は少ないにもかかわらず、その worker プロセスのメモリ使用量が時間の経過とともに増加し続け、想定をはるかに上回っていました。この典型的なメモリリーク現象は、深刻なリソースの浪費を引き起こしただけでなく、本番業務の安定性に対しても重大なリスクをもたらしていました。有効な分析ツールがなかったため、お客様はリークの根本原因を特定できずにいました。そこで OpenResty XRay チームが介入し、その強力な動的追跡および分析能力を活用して、オンラインで稼働中の OpenResty プロセスに対し、非侵入的な診断を実施しました。

LRU キャッシュのメモリリークを一歩ずつ追跡する全過程

RSS の増加トレンドからリークを確認する

まず、OpenResty XRay のプロセスメモリ傾向グラフを通じて、お客様からのご報告を視覚的に検証いたしました。

OpenResty プロセスのメモリ使用量トレンドグラフ

監視データによると、OpenResty プロセスの RSS (Resident Set Size) メモリが明確な線形増加を示しており、これはメモリリークの典型的な特徴です。

リークを Nginx メモリプールではなく Glibc Arena に絞り込む

どの部分のメモリに問題があるかを特定するため、OpenResty XRay のメモリ分析機能を使用しました。自動分析レポートでは、以下の点が示されました:

  • Glibc のメモリ割り当て が総メモリの約 93% を占めており、これがメモリ消費の主な原因でした。
  • LuaJIT のメモリ割り当て は約 2.4% に過ぎませんでした。

メモリ使用量の自動分析レポート

resty-memory のメモリ内訳グラフを用いて Glibc のメモリ使用状況をさらに詳しく分析したところ、その増加が完全に Glibc Arena に起因していることが判明しました。

Glibc メモリの内訳グラフ

通常、Nginx 自身のメモリプールである Nginx memory pool は Arena 領域を通じて割り当てられますが、弊社が観測したところ、Nginx memory pool の使用量は非常に小さく、かつ安定していました。

Nginx メモリプールの使用状況

Glibc によって割り当てられたメモリが時間とともに線形的に増加しているのに対し、Nginx memory pool のメモリ使用量は非常に小さいままであることがわかります。

Glibc メモリと Nginx メモリプールの使用量比較

このことから、リークの原因は通常のリクエスト処理に起因するものではなく、他のモジュールが Glibc を通じて直接確保したメモリにあることが示唆されました。

フレームグラフで lua-resty-lrucache オブジェクトを特定する

では、どのコードが継続的に Arena メモリを確保し、決して解放していなかったのでしょうか?その答えは、OpenResty XRayメモリリークフレームグラフが示してくれました。

XRay のメモリリークフレームグラフ

メモリリークフレームグラフは、メモリリークが SSL/TLS 証明書と秘密鍵を解析する段階で発生していることを示しています。

しかし、なぜリークが発生したのでしょうか?そこで、Lua GC オブジェクトフレームグラフ を使用し、Lua レベルでのオブジェクト参照関係を分析することにしました。これは、コマンドラインツールでリークした Lua テーブルを特定する際に用いるのと同じオブジェクトレベルのビューです。

Lua GC オブジェクトフレームグラフ

フレームグラフが示すところによれば、_LOADED.dynamic_cert.cert_cache という名前の Lua table がメモリの大部分を占有していることが判明しました。

その内部構造に .free_queu.hashht.key2node.node2key といったフィールドが含まれていることから、これが lua-resty-lrucache オブジェクトであると判断しました。

cert_cache というテーブル名とメモリリークフレームグラフを組み合わせることで、お客様が ssl.parse_pem_certssl.parse_pem_priv_key によって解析した結果を lru cache にキャッシュしていたと推測できます。

これにより、問題の全容が完全に明らかになりました。

  1. お客様はパフォーマンスを最適化するため、動的に読み込まれる SSL 証明書と秘密鍵の解析結果を LRU キャッシュである cert_cache にキャッシュしていました。
  2. 問題は、この LRU キャッシュが作成時に設定された容量が大きすぎたため、キャッシュ項目がほとんど、あるいは全く破棄されない状態になっていた点にあります。
  3. その結果、新しいドメインの証明書が解析されるたびに、その結果オブジェクトが LRU キャッシュ内に「永久に」保存されてしまいました。これらのオブジェクトはキャッシュによって参照され続けるため、Lua の GC (ガベージコレクタ) はそれらを回収できず、その下層にある OpenSSL 証明書構造体が占有するメモリも連鎖的にリークしていました。
  4. 時間の経過とともに、解析される証明書の数が増え続け、メモリ使用量もそれに伴い線形的に増加していきました。

最終結果:無限の増加から安定したメモリへ

多くの企業が、システムのメモリ使用量が継続的に急増するものの、その根本原因をなかなか特定できない、という同様の状況に直面しています。問題自体は些細に見えても、一歩間違えればオンラインサービスのダウンタイムに繋がりかねません——弊社が再起動なしで追跡した、もう一つの OpenResty の潜在的なメモリリークのように。

OpenResty XRay は、わずか一回の分析で、お客様が長年抱えていたこのメモリリークの難問を解決いたしました。

  1. 的確な特定、一撃で核心へOpenResty XRay は、メモリトレンドグラフ、メモリ分解分析、そして独自のメモリリークフレームグラフを通じて、段階的に問題の本質を明らかにしました。現象から根本原因に至るまで、分析プロセス全体が明確に可視化されるため、従来の手法にありがちな当て推量や試行錯誤を回避できます。

  2. 非侵入的な診断、本番環境での安全性:分析の全工程において、コードの変更やサービスの再起動は一切不要であり、システムへの負荷も増加させません。高負荷な本番環境であっても、安全かつ安定したリアルタイム分析が可能です。

  3. お客様にもたらされたビジネス価値

    • メモリ使用率の大幅な削減:持続的な増加から、安定的かつ制御可能な状態へ
    • メモリ枯渇による潜在的なサービス停止リスクの排除
    • システム全体のパフォーマンスと応答速度の向上
    • 運用負荷とリソースの無駄を大幅に削減し、企業のコスト削減に直接貢献

この事例が示すように、一見単純に見えるメモリリークの問題であっても、専門的なツールの支援がなければ、技術チームにとって大きな障害となり得ます。OpenResty XRay は、その強力な可観測性 (observability) と分析能力により、迅速に問題の核心に迫り、お客様に明確な解決策を提示することで、技術的価値からビジネス価値への見事な転換を実現します。

よくある質問

なぜ LRU キャッシュがメモリリークを引き起こすのですか?

LRU キャッシュがメモリを解放するのは、キャッシュ項目を破棄するときだけです。容量が実際のワーキングセットよりはるかに大きく設定されていると、ほとんど何も破棄されず、挿入したすべてのオブジェクトがプロセスの生存期間中ずっと参照され続けます。本事例では、cert_cache という LRU キャッシュが決して破棄されない解析済み証明書を保持していたため、新しいドメインが解析されるたびにメモリが線形的に増加しました。

SSL 証明書をキャッシュするとメモリリークになりますか?

はい。ssl.parse_pem_certssl.parse_pem_priv_key の解析結果をキャッシュすると、下層の OpenSSL 証明書・秘密鍵構造体が生き続けます。これらの解析結果が、破棄されない過大な LRU キャッシュに置かれると、OpenSSL 構造体が Glibc Arena に蓄積し、RSS が上昇し続けます。

なぜ Lua のガベージコレクタはキャッシュされたオブジェクトを回収できないのですか?

Lua のガベージコレクタは、到達不能になったオブジェクトのみを回収します。解析済みの証明書オブジェクトは cert_cache という LRU テーブルから参照され続けているため到達可能であり、GC は回収できません。これは GC のバグではなく、キャッシュによる無制限な保持です。

どの Lua オブジェクトがメモリをリークしているかをどうやって見つけますか?

OpenResty XRay の Lua GC オブジェクトフレームグラフは、Lua レベルでのオブジェクト参照関係を表示するため、どのテーブルがメモリを保持しているかを直接確認できます。本事例では _LOADED.dynamic_cert.cert_cache を直接指し示し、その内部フィールド(.free_queu.hashht.key2node.node2key)から lua-resty-lrucache オブジェクトであると判明しました——すべてコードの変更もプロセスの再起動もなしにです。

OpenResty XRay について

OpenResty XRay動的トレーシング製品であり、実行中のアプリケーションを自動的に分析して、パフォーマンスの問題、動作の問題、セキュリティの脆弱性を解決し、実行可能な提案を提供いたします。基盤となる実装において、OpenResty XRay は弊社の Y 言語によって駆動され、Stap+、eBPF+、GDB、ODB など、様々な環境下で複数の異なるランタイムをサポートしております。

著者について

章亦春(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 以上のオープンソースソフトウェアライブラリを執筆しております。

翻訳

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