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

詳細はこちら LIVE DEMO

OpenResty と Nginx サーバーは通常、すべてのワーカープロセス間で共有されるデータを格納するために共有メモリ領域を設定します。 例えば、Nginx の標準モジュールである ngx_http_limit_reqngx_http_limit_conn は、共有メモリ領域を使用して状態データを格納し、すべてのワーカープロセスにおけるユーザーリクエストのレートと同時接続数を制限します。 OpenResty の ngx_lua モジュールは、lua_shared_dict を通じて、ユーザーの Lua コードに共有メモリベースのデータディクショナリストレージを提供します。

本記事では、いくつかの簡単で独立した例を通じて、これらの共有メモリ領域が物理メモリリソース(または RAM)をどのように使用するかを探ります。 また、共有メモリの使用率が、ps などのシステムツールの結果に表示される VSZRSS などのシステムレベルのプロセスメモリ指標にどのような影響を与えるかについても検討します。

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

ほとんどの例で ngx_lua モジュールの lua_shared_dict を使用します。このモジュールではカスタム Lua コードでプログラミングができるためです。 これらの例で示す動作や問題は、すべての標準 Nginx モジュールや他のサードパーティモジュールの共有メモリ領域にも同様に適用されます。

Slab とメモリページ

Nginx とそのモジュールは通常、Nginx コアの slab アロケータ を使用して共有メモリ領域内のスペースを管理します。この slab アロケータは、固定サイズのメモリ領域内で小さなメモリブロックを割り当てたり解放したりするために特別に設計されています。 slab の基礎の上に、共有メモリ領域はより高レベルのデータ構造(例えば、赤黒木やリンクリストなど)を導入します。

slab は数バイトの小さなものから、複数のメモリページにまたがる大きなものまでさまざまです。

オペレーティングシステムはメモリページ単位でプロセスの共有メモリ(または他の種類のメモリ)を管理します。 x86_64 Linux システムでは、デフォルトのメモリページサイズは通常 4 KB ですが、具体的なサイズはアーキテクチャと Linux カーネルの設定に依存します。例えば、一部の Aarch64 Linux システムではメモリページサイズが 64 KB にもなります。

OpenResty と Nginx プロセスの共有メモリ領域について、メモリページレベルと slab レベルの両方で詳細情報を見ていきます。

割り当てられたメモリが必ずしも消費されるわけではない

ハードディスクのようなリソースとは異なり、物理メモリ(または RAM)は常に非常に貴重なリソースです。 ほとんどの現代的なオペレーティングシステムは、ユーザーアプリケーションの RAM リソースへの圧力を軽減するために、デマンドページングと呼ばれる最適化技術を実装しています。 具体的には、大きなメモリブロックを割り当てる際、オペレーティングシステムカーネルは RAM リソース(または物理メモリページ)の実際の割り当てを、メモリページ内のデータが実際に使用されるまで延期します。 例えば、ユーザープロセスが 10 個のメモリページを割り当てたが、実際には 3 個のメモリページしか使用していない場合、オペレーティングシステムはこの 3 個のメモリページだけを RAM デバイスにマッピングする可能性があります。この動作は、Nginx や OpenResty アプリケーションで割り当てられた共有メモリ領域にも同様に適用されます。ユーザーは nginx.conf ファイルで巨大な共有メモリ領域を設定できますが、サーバー起動後にほとんど追加のメモリを占有していないことに気づくかもしれません。なぜなら、通常、起動直後にはほとんど共有メモリページが実際に使用されていないからです。

空の共有メモリ領域

以下の nginx.conf ファイルを例として見てみましょう。このファイルは空の共有メモリ領域を割り当てていますが、一度も使用していません:

master_process on;
worker_processes 2;

events {
    worker_connections 1024;
}

http {
    lua_shared_dict dogs 100m;

    server {
        listen 8080;

        location = /t {
            return 200 "hello world\n";
        }
    }
}

私たちは lua_shared_dict ディレクティブを使用して、dogs という名前の 100 MB の共有メモリ領域を設定しました。 また、このサーバーには 2 つのワーカープロセスを設定しています。注目すべき点は、設定内でこの dogs 領域に触れていないため、この領域は空の状態であるということです。

以下のコマンドでこのサーバーを起動できます:

mkdir ~/work/
cd ~/work/
mkdir logs/ conf/
vim conf/nginx.conf  # paste the nginx.conf sample above here
/usr/local/openresty/nginx/sbin/nginx -p $PWD/

次に、以下のコマンドで nginx プロセスが実行中かどうかを確認できます:

$ ps aux|head -n1; ps aux|grep nginx
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
agentzh   9359  0.0  0.0 137508  1576 ?        Ss   09:10   0:00 nginx: master process /usr/local/openresty/nginx/sbin/nginx -p /home/agentzh/work/
agentzh   9360  0.0  0.0 137968  1924 ?        S    09:10   0:00 nginx: worker process
agentzh   9361  0.0  0.0 137968  1920 ?        S    09:10   0:00 nginx: worker process

これら 2 つのワーカープロセスのメモリ使用量はほぼ同じです。ここでは、PID が 9360 のワーカープロセスに焦点を当てて詳しく見ていきます。 OpenResty XRay コンソールの Web グラフィカルインターフェースでは、このプロセスが合計 134.73 MB の仮想メモリ(virtual memory)と 1.88 MB の常駐メモリ(resident memory)を使用していることがわかります。これは上記の ps コマンドの出力結果と完全に一致しています:

私たちの別の記事「OpenResty と Nginx がメモリをどのように割り当て、管理するか」で説明したように、最も注目すべきは常駐メモリの使用量です。常駐メモリは、ハードウェアリソースを実際のメモリページ(RAM 1 など)にマッピングします。 したがって、図から分かるように、ハードウェアリソースに実際にマッピングされているメモリ量は非常に少なく、合計でわずか 1.88MB です。 上記で設定した 100 MB の共有メモリ領域は、この常駐メモリのごく一部を占めているに過ぎません(詳細は後述の議論をご参照ください)。

もちろん、共有メモリ領域の 100 MB は、このプロセスの仮想メモリ総量に全て寄与しています。 オペレーティングシステムは、この共有メモリ領域のために仮想メモリのアドレス空間を予約しますが、これは単なる簿記上の記録であり、この時点では RAM リソースやその他のハードウェアリソースを一切使用していません。

空は完全に空ではない

プロセスの「アプリケーションレベルのメモリ使用量の分類詳細」図を通じて、空の共有メモリ領域が常駐(または物理)メモリを使用しているかどうかを確認できます。

興味深いことに、この図では Nginx Shm Loaded(ロードされた Nginx 共有メモリ)コンポーネントがゼロではないことがわかります。 この部分は非常に小さく、わずか 612 KB ですが、存在しています。つまり、空の共有メモリ領域も完全に空ではないのです。 これは、Nginx が新しく初期化された共有メモリ領域に、簿記目的のためのメタデータをすでに配置しているためです。 このメタデータは Nginx の slab アロケータによって使用されます。

ロードされたページと未ロードのページ

OpenResty XRay が自動生成した以下のチャートを通じて、共有メモリ領域内で実際に使用(またはロード)されているメモリページの数を確認できます。

dogs 領域内ですでにロード(または実際に使用)されているメモリサイズが 608 KB であることがわかります。同時に、特別な ngx_accept_mutex_ptr が Nginx コアによって自動的に割り当てられ、accept_mutex 機能のために使用されています。

これら 2 つのメモリ部分の合計サイズは 612 KB となり、上記の円グラフに表示されている Nginx Shm Loaded のサイズと一致します。

前述のように、dogs 領域で使用されている 608 KB のメモリは、実際には slab アロケータ が使用しているメタデータです。

未ロードのメモリページは、単に予約された仮想メモリアドレス空間であり、まだ使用されていません。

プロセスのページテーブルについて

言及していない複雑さの一つは、各 nginx ワーカープロセスが独自のページテーブルを持っているということです。CPU ハードウェアまたはオペレーティングシステムカーネルは、これらのページテーブルを参照して、仮想メモリページに対応するストレージを検索します。したがって、各プロセスは異なる共有メモリ領域内で異なるロード済みページのセットを持つ可能性があります。これは、各プロセスが実行中に異なるメモリページのセットにアクセスした可能性があるためです。この分析を簡略化するために、OpenResty XRay は、現在のターゲットワーカープロセスがそれらのメモリページに触れたことがない場合でも、いずれかのワーカープロセスにロードされたすべてのメモリページを表示します。このため、ロード済みメモリページの総サイズは、ターゲットプロセスの常駐メモリのサイズよりも(わずかに)大きくなる可能性があります。

空き slab と使用済み slab

前述のように、Nginx は通常、共有メモリ領域内のスペースを管理するためにメモリページではなく slab を使用します。 OpenResty XRay を使用して、特定の共有メモリ領域内の使用済みスラブと空き(または未使用)スラブの統計情報を直接確認できます:

予想通り、この例では大部分の slab が空きまたは未使用です。ここでのメモリサイズの数値が、前のセクションで示したメモリページレベルの統計よりもはるかに小さいことに注意してください。これは、slab レベルの抽象化がより高レベルであり、slab アロケータによるメモリページのサイズ調整とアドレスアラインメントのメモリ消費を含まないためです。

OpenResty XRay を使用して、この dogs 領域内の各 slab のサイズ分布をさらに観察できます:

この空の共有メモリ領域には、3 つの使用済みスラブと 157 の空き slab があることがわかります。 これらの slab の総数は:3 + 157 = 160 個です。この数字を覚えておいてください。後で、ユーザーデータが書き込まれた dogs 領域の状況と比較します。

ユーザーデータが書き込まれた共有メモリ領域

次に、以前の設定例を修正して、Nginx サーバーの起動時に積極的にデータを書き込みます。 具体的には、nginx.conf ファイルの http {} 設定ブロック内に以下の init_by_lua_block 設定ディレクティブを追加します:

init_by_lua_block {
    for i = 1, 300000 do
        ngx.shared.dogs:set("key" .. i, i)
    end
}

ここでは、サーバー起動時に dogs 共有メモリ領域を積極的に初期化し、300,000 のキーと値のペアを書き込んでいます。

次に、以下の shell コマンドを実行してサーバープロセスを再起動します:

kill -QUIT `cat logs/nginx.pid`
/usr/local/openresty/nginx/sbin/nginx -p $PWD/

新しく起動した Nginx プロセスは以下のようになります:

$ ps aux|head -n1; ps aux|grep nginx
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
agentzh  29733  0.0  0.0 137508  1420 ?        Ss   13:50   0:00 nginx: master process /usr/local/openresty/nginx/sbin/nginx -p /home/agentzh/work/
agentzh  29734 32.0  0.5 138544 41168 ?        S    13:50   0:00 nginx: worker process
agentzh  29735 32.0  0.5 138544 41044 ?        S    13:50   0:00 nginx: worker process

仮想メモリと常駐メモリ

Nginx ワーカープロセス 29735 に対して、OpenResty XRay は以下の円グラフを生成しました:

明らかに、常駐メモリのサイズは以前の空の共有領域の例よりもはるかに大きく、総仮想メモリサイズに占める割合も大きくなっています(29.6%)。

仮想メモリの使用量もわずかに増加しています(134.73 MB から 135.30 MB に)。 共有メモリ領域自体のサイズは変わっていないため、共有メモリ領域は仮想メモリ使用量の増加に実際には影響を与えていません。ここでわずかに増加した理由は、init_by_lua_block ディレクティブを通じて新しい Lua コードを導入したためです(この小さなメモリも同時に常駐メモリに寄与しています)。

アプリケーションレベルのメモリ使用量の内訳を見ると、Nginx 共有メモリ領域の読み込み済みメモリが最も多くの常駐メモリを占めていることがわかります:

読み込み済みおよび未読み込みメモリページ

現在、この dogs 共有メモリ領域では、読み込み済みのメモリページが大幅に増加し、未読み込みのメモリページも相応に大きく減少しています:

空の slab と使用済みの slab

現在、dogs 共有メモリ領域には、300,000 個の使用済み slab が追加されています(空の共有メモリ領域で常に事前割り当てされる 3 個の slab 以外):

明らかに、lua_shared_dict 領域内の各キーバリューペアは、実際には 1 つの slab に直接対応しています。

空き slab の数は、以前の空の共有メモリ領域と全く同じで、157 個の slab です:

偽のメモリリーク

上記で示したように、共有メモリ領域は、アプリケーションが実際にその内部のメモリページにアクセスするまで、物理メモリリソースを実際には消費しません。この理由により、ユーザーは Nginx ワーカープロセスの常駐メモリサイズが継続的に増加しているように見える場合があります。特にプロセスが起動した直後に顕著です。これにより、ユーザーはメモリリークが存在すると誤解する可能性があります。以下の図は、そのような例を示しています:

OpenResty XRay が生成したアプリケーションレベルのメモリ使用量の内訳図を見ると、Nginx の共有メモリ領域が実際には常駐メモリ空間の大部分を占めていることが明確にわかります:

このメモリ増加は一時的なもので、共有メモリ領域が満杯になると停止します。しかし、ユーザーが共有メモリ領域を非常に大きく設定し、現在のシステムで利用可能な物理メモリを超える場合、依然として潜在的なリスクがあります。そのため、以下に示すようなメモリページレベルのメモリ使用量の棒グラフに注意を払う必要があります:

グラフの青い部分は、最終的にプロセスによって使い果たされる可能性があり(つまり赤色に変わる)、現在のシステムに影響を与える可能性があります。

HUP リロード

Nginx は、HUP シグナルを通じてサーバーの設定を再読み込みすることをサポートしており、マスタープロセスを終了させる必要はありません(ワーカープロセスは依然として優雅に終了し、再起動します)。通常、Nginx の共有メモリ領域は HUP リロード後に自動的に元のデータを継承します。そのため、以前にアクセスされた共有メモリページに割り当てられた物理メモリページも保持されます。したがって、HUP リロードを通じて共有メモリ領域内の常駐メモリ空間を解放しようとする試みは失敗します。ユーザーは代わりに Nginx の再起動またはバイナリアップグレード操作を使用すべきです。

注意すべき点として、特定の Nginx モジュールは HUP リロード後に元のデータを保持するかどうかを決定する権限を持っています。そのため、例外が存在する可能性があります。

結論

上記の説明で、Nginx の共有メモリ領域が占有する物理メモリリソースが、nginx.conf ファイルで設定されたサイズよりも遥かに少ない可能性があることを説明しました。これは現代のオペレーティングシステムのデマンドページング機能のおかげです。空の共有メモリ領域内でも、slab アロケータ自体が必要とするメタデータを格納するために、いくつかのメモリページと slab が使用されることを示しました。OpenResty XRay の高度なアナライザを使用することで、実行中の nginx ワーカープロセスをリアルタイムで検査し、共有メモリ領域内で実際に使用または読み込まれているメモリを、メモリページと slab の両方のレベルで確認することができます。

一方で、デマンドページングの最適化により、ある期間メモリが継続的に増加する現象が発生する可能性があります。これは実際にはメモリリークではありませんが、依然としてある程度のリスクがあります。また、Nginx の HUP リロード操作は通常、共有メモリ領域内の既存のデータをクリアしないことも説明しました。

本ブログサイトの今後の記事では、共有メモリ領域で使用される高度なデータ構造(赤黒木やキューなど)や、共有メモリ領域内のメモリフラグメンテーションの問題を分析し緩和する方法について、引き続き探求していきます。

関連記事

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. スワッピング(swapping)が発生すると、一部の常駐メモリがディスクデバイスに保存およびマッピングされることがあります。 ↩︎