OpenResty XRay がどのようにしてアプリケーションの問題特定と効率化を支援するかをご覧ください。

詳細はこちら LIVE DEMO

メモリフラグメンテーションは、多くの巧妙なアルゴリズムが存在するにもかかわらず、コンピューターシステムにおける一般的な問題です。 メモリフラグメンテーションは、メモリ領域内の空きメモリブロックを無駄にします。これらの空きメモリブロックは、アプリケーションが将来より大きなメモリブロックを要求した際に満たすための大きなメモリ領域に結合することができず、また他の用途のためにオペレーティングシステムに解放することもできません1。 これにより、大きなメモリブロックの要求が増えるにつれて、これらの要求を満たすために必要な総メモリサイズが無限に増加するため、メモリリークのような現象が発生します。 このメモリ使用量の無限の増加は通常、メモリリークとは見なされません。なぜなら、未使用のメモリブロックは実際に解放され、空きメモリとしてマークされているからです。ただし、これらのメモリブロックは、より大きなメモリブロック要求を満たすために再利用することができず、同時に他のプロセスが使用するためにオペレーティングシステムに返還することもできません。

OpenResty または Nginx の共有メモリ領域内では、要求されるメモリ slab やメモリブロックのサイズに大きな差がある場合、メモリフラグメンテーションの問題が発生しやすくなります。 例えば、ngx_lua モジュールの lua_shared_dict 領域は、任意の長さの任意のユーザーデータ項目の書き込みをサポートしています。これらのデータ項目は、単一の数値のように小さいものから、非常に長い文字列まで様々です。 注意を払わないと、ユーザーの Lua プログラムは共有メモリ領域内で深刻なメモリフラグメンテーションの問題を引き起こし、大量の空きメモリを無駄にする可能性があります。 本記事では、この問題と関連する動作の詳細を示すために、いくつかの独立した例を挙げて詳しく説明します。 本記事では、OpenResty XRay 動的トレースプラットフォーム製品を使用して、視覚的な方法でメモリフラグメンテーションを直接観察します。 記事の最後に、共有メモリ領域内のメモリフラグメンテーション問題を軽減するためのベストプラクティスを紹介します。

このブログサイトのほぼすべての技術記事と同様に、我々は OpenResty XRay という動的トレース製品を使用して、変更を加えていない OpenResty または Nginx サーバーとアプリケーションの内部を深く分析し、視覚化して表示します。 OpenResty XRay は非侵襲的な分析プラットフォームであるため、OpenResty または Nginx の対象プロセスに対して何の変更も加える必要がありません - コード注入も必要なく、対象プロセス内に特別なプラグインやモジュールをロードする必要もありません。 これにより、OpenResty XRay 分析ツールを通じて見る対象プロセスの内部状態が、観察者がいない場合の状態と完全に一致することが保証されます。

OpenResty または Nginx の共有メモリ領域内のメモリ割り当てと使用についてまだ詳しくない場合は、前回のブログ記事「OpenResty と Nginx の共有メモリ領域が物理メモリをどのように消費するか」をご参照ください。

空の共有メモリ領域

まず、ユーザーデータが書き込まれていない空の共有メモリ領域を例に取り、この領域内部の slab やメモリブロックを見ることで、「ベースラインデータ」を理解しましょう:

master_process on;
worker_processes 1;

events {
    worker_connections 1024;
}

http {
    lua_shared_dict cats 40m;

    server {
        listen 8080;
    }
}

ここでは、cats という名前の 40MB の共有メモリ領域を設定しました。 この設定では cats 領域に何も触れていないため、この領域は空の状態です。 しかし、前回のブログ記事 「OpenResty と Nginx の共有メモリ領域が物理メモリをどのように消費するか」 で学んだように、空の共有メモリ領域でも、スラブ アロケータが使用するメタデータを格納するために 160 個の事前割り当てされたスラブがあります。 以下の仮想メモリ空間内のスラブ分布図もこれを裏付けています:

この図は OpenResty XRay によって自動生成され、実行中のプロセスを分析するために使用されます。 図から、3 つの使用済みスラブと 100 以上の空き slab があることがわかります。

同様のサイズのエントリで埋める

上記の nginx.conf の例に、以下の location = /t ディレクティブを追加します:

location = /t {
    content_by_lua_block {
        local cats = ngx.shared.cats
        local i = 0
        while true do
            local ok, err = cats:safe_set(i, i)
            if not ok then
                if err == "no memory" then
                    break
                end
                ngx.say("failed to set: ", err)
                return
            end
            i = i + 1
        end
        ngx.say("inserted ", i, " keys.")
    }
}

ここでは、location = /t 内に Lua コンテンツハンドラを定義し、空きメモリがなくなるまで cats 領域に非常に小さなキーと値のペアを書き込みます。 キーと値の両方が数値(number)であり、cats 共有メモリ領域が小さいため、この領域に書き込まれる各キーと値のペアのサイズはほぼ同じになるはずです。Nginx サーバープロセスを起動した後、以下のコマンドを使用してこの /t ロケーションにクエリを送信します:

$ curl 'localhost:8080/t'
inserted 255 keys.

レスポンスボディの結果から、この共有メモリ領域に 255 個のキーを書き込むことができたことがわかります。

この共有メモリ領域内の slab レイアウトを再度確認できます:

この図を上記の空の共有メモリ領域の分布図と比較すると、新しく追加されたより大きな slab が赤色(使用済みを示す)になっていることがわかります。 興味深いことに、中央の空き slab(緑色)は、より大きな slab によって再利用されることはありません。これらが近接しているにもかかわらずです。

OpenResty XRay を使用して、これらの slab のサイズ分布を確認してみましょう:

使用されている slab のほぼすべてが 128 バイトのサイズであることがわかります。

奇数キーの削除

次に、既存の Lua ハンドラの後に以下の Lua コードスニペットを追加して、共有メモリ領域内の奇数キーを削除してみましょう:

for j = 1, i, 2 do
    cats.delete(j)
end

サーバーを再起動した後、再度 /t をクエリすると、新しい cats 共有メモリ領域の slab レイアウト図が得られました:

現在、隣接していない多くの空きメモリブロックがありますが、これらのメモリブロックを大きなブロックに結合することができず、将来の大きなメモリ要求を満たすことができません。 Lua ハンドラの後に以下の Lua コードを追加して、より大きなキーと値のペアエントリを挿入してみましょう:

local ok, err = cats:safe_add("Jimmy", string.rep("a", 200))
if not ok then
    ngx.say("failed to add a big entry: ", err)
end

その後、サーバープロセスを再起動し、/t をクエリします:

$ curl 'localhost:8080/t'
inserted 255 keys.
failed to add a big entry: no memory

予想通り、新しく追加された大きなエントリには 200 バイトの文字列値があるため、対応する slab は共有メモリ領域で利用可能な最大の空き slab(前述の 128 バイト)よりも大きくなければなりません。 したがって、使用中の特定のキーと値のペアエントリを強制的に削除しない限り、このメモリブロック要求を満たすことはできません(例えば、メモリ領域のメモリが不足している場合に、set() メソッドが自動的に最も使用頻度の低い使用中のエントリデータを削除する動作など)。

前半部分のキーの削除

次に、異なる実験を行います。上記の奇数キーを削除する代わりに、以下の Lua コードを追加して共有メモリ領域の前半部分にあるキーを削除します:

for j = 0, i / 2 do
    assert(cats:delete(j))
end

サーバープロセスを再起動し、/t をクエリした後、以下の仮想メモリ空間内の slab レイアウトが得られました:

この共有メモリ領域の中央付近で、隣接する空き slab が自動的に 3 つのより大きな空き slab に結合されていることがわかります。 実際、これらは 3 つの空きメモリページであり、各メモリページのサイズは 4096 バイトです:

これらの空きメモリページは、さらに複数のメモリページにまたがるより大きな slab を形成することができます。

次に、上記で挿入に失敗した大きなエントリを再度書き込もうとします:

local ok, err = cats:safe_add("Jimmy", string.rep("a", 200))
if not ok then
    ngx.say("failed to add a big entry: ", err)
else
    ngx.say("succeeded in inserting the big entry.")
end

今回、私たちはついに大きな連続した空き領域があったため、このキーと値のペアを挿入することに成功しました:

$ curl 'localhost:8080/t'
inserted 255 keys.
succeeded in inserting the big entry.

新しい slab の分布図では、この新しいエントリーが明確に見えるようになりました:

図の前半部分にある最も細長い赤い四角形に注目してください。これが私たちの「大きなエントリー」です。 使用済み slab のサイズ分布図でより明確に見ることができます:

図から分かるように、「大きなエントリー」は実際には 512 バイトの slab です(キーサイズ、値サイズ、メモリーパディング、アドレスアラインメントのオーバーヘッドを含む)。

メモリーフラグメンテーションの緩和

上記で見たように、共有メモリー領域に散在する小さな空き slab は、メモリーフラグメンテーションの問題を引き起こしやすく、将来の大きなメモリー割り当て要求を満たせなくなる可能性があります。これは、すべての空き slab を合わせた総サイズがはるかに大きい場合でも発生します。 これらの空き slab スペースをより効果的に再利用するために、以下の 2 つの方法を推奨します:

  1. 常に近いサイズのデータエントリーを使用することで、はるかに大きなメモリーブロックの割り当てを満たす必要がなくなります。
  2. 削除されたデータエントリーを隣接させることで、これらのエントリーがより大きな空き slab ブロックに自動的にマージされやすくなります。

方法 1)については、単一の共有メモリー領域を、異なるデータエントリーサイズグループ用に人為的に分割することができます2。例えば、0 〜 128 バイトのサイズのエントリーのみを格納する共有メモリー領域と、128 〜 256 バイトのサイズのエントリーのみを格納する別の共有メモリー領域を設けることができます。

方法 2)については、エントリーの有効期限に基づいてグループ化することができます。例えば、有効期限の短いデータエントリーを専用の共有メモリー領域に集中させ、有効期限の長いエントリーを別の共有メモリー領域に格納することができます。これにより、同じ共有メモリー領域内のエントリーが同様のペースで期限切れになり、同時に期限切れになる可能性や、同時に削除されてマージされる可能性が高くなります。

結論

OpenResty や Nginx の共有メモリー領域内のメモリーフラグメンテーションの問題は、適切なツールがない場合、観察やデバッグが困難です。 幸いなことに、OpenResty XRay は強力な可観測性と視覚化を提供し、問題を迅速に発見し診断することができます。 本記事では、一連の小さな例を通じて、OpenResty XRay が自動生成したグラフとデータを使用し、背後で何が起こっているかを明らかにし、メモリーフラグメンテーションの問題とその緩和方法を示しました。最後に、OpenResty や Nginx の一般的な設定とプログラミングにおける共有メモリー領域の使用に関するベストプラクティスを紹介しました。

関連記事

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

翻訳

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


  1. OpenResty と Nginx の共有メモリ領域に関しては、割り当てられアクセスされたメモリページは、プロセスが終了するまで決してオペレーティングシステムに返還されません。もちろん、解放されたメモリページとメモリ slab は共有メモリ領域内で再利用することができます。 ↩︎

  2. 興味深いことに、Linux カーネルの Buddy メモリーアロケーターや Memcached のアロケーターも同様の戦略を使用しています。 ↩︎