ビジネスの成否を分ける重要な局面では、コアサービスの安定性が直接的に影響します。最近、当社のお客様の一社が、運用チームを悩ませる問題に直面しました。Nginx サービスのメモリが「ブラックホール」のように際限なく膨張し続けていたのです。一時的な再起動は一時的な対処療法に過ぎず、問題は常に再発していました。

本記事では、OpenResty XRay を使用して、この潜在的なメモリリーク問題を迅速に発見し特定する方法、そして複雑で時間のかかるメモリリークの特定プロセスを「診断 → 計画 → 検証」という再利用可能なクローズドループとして確立する方法をご紹介します。

技術的な課題と初期診断

あるお客様の基幹 Nginx サービスにおいて、メモリ使用量が継続的に増加し、「メモリブラックホール」と化していました。一時的な再起動でその場をしのぐことはできましたが、問題はすぐに再発してしまいます。

Screenshot

課題とリスク:

  • 技術面: 本番環境における C/C++ モジュールのメモリリークは、最も解決が困難な問題の一つです。従来のデバッグツール(GDB など)は稼働中のシステムで直接使用できず、コード監査は大海で針を探すようなもので、時間と手間がかかり非効率的です。
  • ビジネス面: もし放置すれば、メモリ消費は悪化の一途をたどり、応答速度の低下や頻繁なクラッシュを引き起こし、最終的には基幹ビジネスの継続性を脅かします。これは具体的に、ユーザーエクスペリエンスの低下、ユーザーの段階的な離脱、ブランドイメージの毀損、そして直接的な経済的損失を招きます。

定期的な再起動で「延命」を図ることは、まさに「毒を飲んで渇きを癒す」であり、ビジネス全体を長期的なリスクに晒すことになります。ご相談を受けた後、弊社は直ちに動的トレーシング製品 OpenResty XRay を利用し、本番環境で直接メモリ割り当ての分析を実施しました。わずか数分で、ツールは自動的にメモリリークのフレームグラフを生成し、問題特定への明確な手がかりを提供しました。

Screenshot

フレームグラフを見ると、メモリ割り当てが主に以下の 3 つの領域に集中していることが一目で明らかになりました。

  1. Nginx メモリプール
  2. TLS 関連メモリ
  3. ngx_dubbo_module 関数

Nginx メモリプールと TLS は、いずれも長年の実績を持つ成熟したコンポーネントであり、メモリリークが発生する可能性は極めて低いと考えられます。そのため、私たちは分析の焦点を、最も疑わしい ngx_dubbo_module 関数に迅速に絞り込みました。

フレームグラフで特定!メモリリークの「ホットスポット」

ステップ 1:疑わしい関数を特定する。

OpenResty XRay の強力な動的追跡能力を活用することで、大量のログを精査したり、徹底的なコード監査を行ったりすることなく、まるで「庖丁解牛」のように効率的に問題を分析できます。今回は、最も疑わしい ngx_dubbo_module 関数に焦点を当てます。

Screenshot

メモリリークのフレームグラフは、メモリ割り当てのピークが ngx_dubbo_hessian2_encode_payload_map 関数を指していることを明確に示しており、ここがメモリリークの「ホットスポット」であることがわかります。

ステップ 2:コードを深く掘り下げ、疑わしい点を発見する。

当該関数のソースコードを確認したところ、すぐに疑わしい点を発見しました。

ngx_int_t ngx_dubbo_hessian2_encode_payload_map(ngx_pool_t *pool, ngx_array_t *in, ngx_str_t *out)
{
    try {
        // ...
        for (size_t i=0; i<in->nelts; i++) {
            string key((const char*)kv[i].key.data, kv[i].key.len);
            if (0 == (key.compare("body"))) {
                // ここでは new 演算子を用いてオブジェクトが作成されます
                ByteArray *tmp = new ByteArray((const char*)kv[i].value.data, kv[i].value.len);
                ObjectValue key_obj(key);
                ObjectValue value_obj((Object*)tmp);
                // オブジェクトは Map に格納されますが、その後 delete 処理は確認できませんでした
                strMap.put(key_obj, value_obj);
            } else {
                // ...
            }
        }
        // ...
    } catch (...) {
        // ...
    }
}

コード内では new ByteArray を使用して tmp オブジェクトが作成されていますが、コード全体を確認しても、それに対応する delete 処理が見当たりませんでした。直感的に、問題はここにあると推測されます。tmp オブジェクトは ObjectValue にラップされた後、strMap.put メソッドに渡されます。そのライフサイクルは一体誰が管理しているのでしょうか?

ステップ 3:手がかりを辿り、オブジェクトのライフサイクルを追跡

真相を明らかにするため、hessian2 ライブラリの実装詳細を深く掘り下げる必要があります。

まず、tmp ポインタは ObjectValue を構築するために使用されます:ObjectValue value_obj((Object*)tmp);。そのコンストラクタは非常にシンプルで、ポインタを格納するだけで、スマートポインタや所有権の移転操作は一切関与していません。

// ObjectValue コンストラクタ
ObjectValue(Object* obj) : _type(OBJ) { _value.obj = obj; }

続いて、value_objstrMap.put メソッドに渡されます。このメソッドは内部で value.get_object() を呼び出し、元のオブジェクトポインタを取得します。

// Map::put メソッドの実装
void Map::put(const ObjectValue&amp; key, const ObjectValue&amp; value,
        bool chain_delete_key, bool chain_delete_value) {
    pair<Object*, bool> ret_key = key.get_object();
    pair<Object*, bool> ret_value = value.get_object();
    // ...
    // get_object の戻り値である bool 値に基づいて、オブジェクトを「削除チェーン」に追加するかどうかが決定されます
    if (ret_value.second || chain_delete_value) {
        _delete_chain.push_back(ret_value.first);
    }
    _map[ret_key.first] = ret_value.first;
}

Map の内部には _delete_chain が存在し、解放すべきメモリを管理しています。オブジェクトがこの「削除チェーン」に追加されるか否かは、get_object() が返す bool 値、または put メソッド呼び出し時に渡される chain_delete_value パラメータによって決まります。

では、最も重要な ObjectValue::get_object メソッドを見ていきましょう。

// ObjectValue::get_object メソッドの実装
pair<Object*, bool> ObjectValue::get_object() const {
    switch (_type) {
        // tmp オブジェクトの型は OBJ です
        case OBJ:        return pair<Object*, bool>(_value.obj, false);
        case C_OBJ:      return pair<Object*, bool>(_value.obj, false);
        // 他の型の場合、新しいオブジェクトを new で生成し、true を返します
        case IVAL:       return pair<Object*, bool>(new Integer(_value.ival), true);
        // ...
        default:         return pair<Object*, bool>(NULL, false);
    }
}

真相が判明しました!_typeOBJ の場合、get_object メソッドが返す bool 値は false となります。これは呼び出し元に対し、「このポインタは管理不要であり、新しいメモリは new していません」というメッセージを意味します。しかし、tmp オブジェクトはまさに外部で new されていました。

get_object()false を返し、かつ put メソッド呼び出し時に chain_delete_value が指定されていない(デフォルトは false)ため、if (ret_value.second || chain_delete_value) の条件が満たされませんでした。その結果、tmp オブジェクトは _delete_chain に追加されませんでした。リクエストが継続的に処理されるにつれて、無数の ByteArray オブジェクトが作成されますが、これらが解放されることはなく、メモリリークが発生する原因となっていました。

受動的な対応から能動的な価値創出へ:XRay が実現するトラブルシューティングの新たなサイクル

システムの安定性を追求する中で、多くのチームは悪循環に陥っています。問題を解決するために、より多くの手がかりを得ようとデータを収集し、そのために監視指標を増やし、複雑なダッシュボードを構築し、さらに多額の予算を投じて監視ツールを導入します。しかし、これらのツールは大量の断片的な情報をもたらすだけで、データは増え続ける一方で、信号対雑音比は低下する一方です。エンジニアたちは依然として「データの墓場」の中で、砂金採りのように価値ある情報を探し出すことに時間を費やしています。

このモデルの根本的な欠陥は、メモリリークのような技術的な問題がビジネスコストに与える影響を以下のように捉えている点にあります。

  • 一回のメモリリークアラートは、一見すると技術指標の変動に過ぎませんが、その裏ではエンジニアの貴重な時間が失われています。
  • 頻繁なオンライン異常は、システムの安定性を脅かすだけでなく、ビジネス中断という潜在的なリスクを伴います。
  • 継続的な高額な監視投資は、それに見合う洞察や成果をもたらしていません。

結局のところ、問題は「データが少なすぎる」ことではなく、「有用な答えが少なすぎる」ことにあります。本当に効率的な可観測性ツールは、「メス」のように問題の根源に直接切り込むべきであり、チームに「シャベル」をもう一本与えて、データ山の中で昼夜を問わず掘り続けさせるべきではありません。これこそが、次世代の可観測性プラットフォームが解決すべき核心的な矛盾です。

  • 表面的な指標から因果関係の連鎖へ
  • 受動的なアラートから能動的な分析と洞察へ
  • 人的リソースの消費から真の研究開発生産性の解放へ

まとめ

このケースでは、OpenResty XRay が、複雑で時間のかかるプロセスを、再利用可能なクローズドループへと変革する別のアプローチを提示しました。私たちはこれを 「診断 → 計画 → 検証」 と呼んでいます。

  • 的確なアプローチ: 従来の方法が「より多くのデータを見て、手がかりを見つける」というものであったのに対し、XRay のアプローチは異なります。フレームグラフなどの可視化ツールを通じて、XRay は問題の所在を直接特定し、チームが数分で根本原因を把握できるようにします。XRay が提供するのは、単なる監視指標の増加ではなく、行動を促す重要な洞察です。
  • 確実な証拠に基づく解決策の策定: 問題が特定のコード行や C/C++ モジュールに正確に特定された場合、修正案は直接的かつ的確なものになります。チームは試行錯誤を繰り返す必要がなく、明確な証拠に基づいて、迅速に明確で効果的な行動ステップを策定できます。
  • 即座の検証と完全なクローズドループの確立: 修正が本番環境に適用された後、XRay は再度分析を行い、問題が完全に解消されたかを確認できます。この迅速な検証メカニズムにより、プロセス全体が真に完結したクローズドループとなります。診断、行動、検証が密接に連携し、切れ目のないサイクルを構成します。

このクローズドループな機能がもたらす価値は、単なるバグ修正をはるかに超えています。

  • 技術的価値: チームが非侵入的な方法で本番環境の C/C++ モジュールに直接アクセスし、複雑な問題を単純化・可視化することを可能にします。
  • 商業的価値:
    • ビジネスの安定性確保: 潜在的な障害を迅速に排除し、サービスの高可用性を保証します。
    • 開発効率の向上: エンジニアを煩雑で非効率なデバッグ作業から解放し、より価値のあるビジネスイノベーションに注力できるようにします。
    • リソースコストの最適化: 未知のパフォーマンス問題によるリソースの過剰なプロビジョニングを避け、真のコストコントロールを実現します。

データ過多の時代において、技術チームが必要としているのは、もはや単なる監視ダッシュボードの増加ではありません。彼らが最短時間で問題を発見し、解決できるよう支援する、よりスマートで信頼性の高い方法です。これこそが、オブザーバビリティの真の価値なのです。

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 XRay(マイクロサービスおよび分散トラフィックに最適化された多機能ゲートウェイソフトウェア)は、世界中の多くの上場企業および大企業から高い評価を得ております。OpenResty 以外にも、章亦春は Linux カーネル、Nginx、LuaJITGDBSystemTapLLVM、Perl など、複数のオープンソースプロジェクトに累計 100 万行以上のコードを寄与し、60 以上のオープンソースソフトウェアライブラリを執筆しております。

翻訳

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