プロンプトと適用シナリオ
glibc malloc を使用する64スレッドのLinuxサービスが、800 MiBのRSSで開始します。定期的なバッチ処理によってRSSが6 GiBまで上昇します。バッチ終了から20分後、アプリケーションのヒーププロファイラは1.1 GiBの使用中割り当てを報告しますが、RSSは5.2 GiBの高止まりのままです。プロセスは8 GiBのcgroup制限下で実行されており、120 msのp99レイテンシSLOが設定されています。free() がRSSの低下を保証しない理由を説明し、その差分がリーク、アロケータによる保持、断片化、または別のマッピングのどれであるかを証明し、安全な修正策を選択してください。
スレッド数、メモリ値、アイドル間隔、制限、およびSLOは演習用の前提条件です。ネイティブプロセス、glibc malloc、cgroup v2、子プロセスなし、および再現可能なバッチを想定します。マネージドランタイムの場合は、独自のヒープ、ガベージコレクタ、ネイティブ割り当てレイヤーが加わります。この質問が general に属するのは、中心となるスキルがアプリケーション言語の構文ではなく、Linuxのプロセスアカウンティングとアロケータの動作であるためです。
現在の面接資料では、フリーリスト、サイズクラス、スレッドローカル割り当て、断片化がシステム面接のディスカッションポイントとして明示的に扱われています。Redisのプロダクションドキュメントでも同じ観察可能な事象が説明されています。論理データを削除しても、空きチャンクがアロケータで再利用可能な状態で残るか、ページにまだ使用中のオブジェクトが含まれているため、RSSが前回のピーク付近にとどまることがあります。Linuxおよびglibcのドキュメントは、証拠に基づいた回答に必要な測定および制御のインターフェースを提供しています。これらの情報源は関連性と仕組みを説明するものであり、特定の企業がこの正確なプロンプトを使用していることや、特定の頻度で出題されることを証明するものではありません。
面接官が評価しているポイント
第1のシグナルは、所有権(ownership)と常駐性(residency)を区別できているかです。free(p) の呼び出し後、呼び出し元はその割り当ての所有権を失い、アロケータはそのブロックを再利用できるようになります。Cの割り当て規約は、munmap、RSSの低下、または即時の物理ページ回収を約束していません。「freeは常にメモリを返す」あるいは「freeは決してメモリを返さない」という回答は、どちらもアロケータ固有の処理パスを見落としています。
第2のシグナルは、4つの測定レイヤーを区別しているかです:
- アプリケーションの使用中割り当て(live allocations)
- アロケータの使用中、空き、マップ済み、および解放可能なバイト数
- プロセスマッピングと常駐ページ
- cgroup全体の課金とメモリ圧迫(pressure)
RSSは使用中ヒープのカウンタではありません。Linuxは VmRSS を RssAnon + RssFile + RssShmem と定義しています。ファイルマッピング、共有メモリ、スタック、アロケータのメタデータ、ネイティブライブラリはすべてこの差分を広げる可能性があります。したがって、ヒーププロファイルと1回の top の読み取りだけでは、リークの有無を証明できません。
第3のシグナルは診断の識別力です。リークでは割り当てが到達可能であるか、あるいは何らかの形で使用中のまま残ります。保持(retention)は、効率的な再利用のために空きメモリがマップされたままになっていることを意味します。断片化(fragmentation)は、アロケータに空きバイトがあるものの、少数の使用中オブジェクトがページを固定(ピン留め)しているか、空き領域がアリーナやサイズクラス間で分散しているために、ページサイズの解放可能領域を形成できないことを意味します。これらの状態は共存する可能性があるため、候補者には単一の比率から推測されたラベルではなく、制御された観察が必要です。
最後に、面接官はプロダクション環境での意思決定を求めています。アリーナ数を減らす、積極的にトリムを行う、またはアロケータを置き換えることで常駐メモリを削減できますが、ロック競合、ページフォールト、システムコール、またはCPU負荷が増加する可能性があります。優れた回答は、カナリアリリース、ワークロードのリプレイ、合否判定基準、ロールバックを備え、8 GiBの制限と120 msのp99 SLOの両方を保護します。
回答前に確認すべき質問
- どのアロケータとバージョンが有効か? glibcのチューニング項目や
malloc_trimは、jemalloc、tcmalloc、mimalloc、言語ランタイムのアロケータ、または静的リンクされた代替アロケータには当てはまりません。カウンタを使用する前に、ロードされているアロケータとデプロイイメージを確認してください。 - 1.1 GiBと報告しているのは具体的に何か? サンプリングされたヒーププロファイル、正確なアロケータカウンタ、マネージドランタイムのヒープ、ビジネスキャッシュのメトリクスでは、対象となるバイト数が異なります。ネイティブライブラリ、スタック、直接マッピング、アロケータのメタデータが含まれているか確認してください。
- RSSのどのコンポーネントが高止まりしているか?
RssAnonはヒープ、スタック、無名マッピングを示し、RssFileはファイルバックマッピングを、RssShmemは共有メモリを示します。増加が無名メモリでない場合、アロケータのチューニングは最初の手段として不適切です。 - 同一バッチの実行ごとに高止まり状態が維持されるか、それとも上昇し続けるか? 次のバッチを処理してもさらに5 GiB増加することなく対応できる安定した高止まり(plateau)は、再利用を示唆します。使用中割り当てやRSSが階段状に増加する場合は、リーク、ワークロード、またはマッピングの説明が必要です。
- スレッド数、割り当てサイズ、またはオブジェクトの生存期間に変更はあったか? 多数のスレッドやスレッド間での解放(cross-thread frees)は、アリーナやキャッシュ間でブロックを分散させる可能性があります。生存期間の短いオブジェクトと長いオブジェクトが混在すると、本来空きになるはずの多くのページに1つのオブジェクトだけが生き残る原因になります。
- cgroupは実際に圧迫されているか?
memory.current、memory.events、PSI、スワップ、および隣接プロセスを比較してください。十分なヘッドルームがある高いRSSは効率の問題かもしれませんが、頻繁なmemory.highイベントや8 GiBへの接近は、メモリ解放動作の対応を運用上急務にします。 - 次のバッチはいつ実行されるか? 4 GiBを再利用可能な状態で5分間保持することは合理的かもしれません。厳しい制限下で12時間のアイドル状態に保持する場合は、バッチ後のパージまたは異なる割り当てパターンの適用が正当化される可能性があります。
- 許容される性能劣化(リグレッション)の予算はどの程度か? サービスがCPUを2%多く消費してもp99を5 ms増加させられない場合や、コールドアロケーションのレイテンシよりもメモリコストが重視される場合など、状況によって解決策は変わります。
30秒の回答フレームワーク
「free() はブロックをアロケータに返却するだけであり、そのページのアンマップを保証するわけではないため、リークがなくてもRSSが高止まりすることがあります。私なら、単一バッチのタイムラインを整理し、使用中割り当てプロファイル、アロケータの使用中/空きバイト数、RssAnon、マッピングごとの smaps、およびcgroupの使用量を比較します。次にバッチを3回繰り返します。使用中バイト数が増加すればリークを示し、使用中バイト数が安定し次のバッチで再利用される平坦なRSSであれば保持を示し、アロケータの空きバイト数が豊富であるにもかかわらず解放可能メモリが少なく残存オブジェクトによってページがピン留めされている場合は断片化を示します。malloc_trim(0) は包括的な修正策としてではなく、glibcのカナリア実験としてのみ使用します。最終的な修復策としては、所有権の修正、生存期間の分離、バッチ後の制限付きトリム、テスト済みのアリーナ調整、またはアロケータの変更が考えられますが、本番相当のリプレイで120 msのp99を損なわずにメモリ目標を下回る場合にのみ採用します。」
ステップバイステップの詳細解説
ステップ1: 単一のメモリタイムラインを構築する
デプロイバージョン、PID、cgroupパス、スレッド数、リクエストおよびバッチのボリューム、割り当てレート分布、使用中バイト数、RSSコンポーネント、memory.current、memory.peak、メモリ圧迫(pressure)、およびOOMイベントを記録します。バッチ前、6 GiBのピーク時、割り当て解除直後、および20分間のアイドルウィンドウ全体でサンプリングします。PID、cgroup、またはワークロードが異なると、比較が無効になります。
最初の質問は運用上の問題です。RSSが単に高いだけなのか、それともサービスが制限に近づいて圧迫下で回収を行っているのか? 8 GiB制限内で5.2 GiBの場合、他のcgroup課金を考慮する前の見かけ上のヘッドルームは2.8 GiBです。この引き算は単なる規模の確認にすぎません。cgroupはこのプロセスの無名RSS以外のメモリも計上するためです。実際のドメインには memory.current と memory.stat を使用してください。
ステップ2: アロケータとカーネルの境界を理解する
アロケータはOSからより大きな領域を要求し、それをチャンクに分割します。glibcは通常のアリーナを拡張でき、十分に大きな割り当てに対しては個別の無名マッピングを作成できます。アプリケーションがチャンクを解放すると、アロケータはまずそのチャンクを再利用可能にします。隣接する空きチャンクを結合したり、ビンやキャッシュに配置したり、将来のシステムコールを回避するために保持したり、適切なページを解放したりできます。
個別にマップされた大きなチャンクは、解放時にアンマップできることがよくあります。通常のヒープメモリはより困難です。領域の途中にある空きブロックによってヒープの終端を縮小することはできず、生ポインタの生きたオブジェクトが1つでも含まれるページは、そのオブジェクトを再配置しない限りアンマップできません。CおよびC++のアロケータは、アプリケーションのポインタが無効になるため、通常、任意の生きたオブジェクトをコンパクション(再配置)することはできません。
これにより、3種類のオーバーヘッドが発生します:
- 内部断片化(Internal fragmentation): 20バイトの要求が、アライメントやサイズクラスによってより大きなブロックを消費する。
- 外部またはページレベルの断片化(External/page-level fragmentation): 空き領域は存在するが、分散しているかピン留めされているため、ページ全体を解放できない。
- 意図的な保持(Intentional retention): アロケータが再利用を予期しており、解放と再取得にコストがかかるため、ページ全体または一部がマップされたまま残る。
これらのラベルはメカニズムを説明するものであり、単一の RSS / live_bytes 比率から導き出される結論ではありません。
ステップ3: 4つの測定レイヤーを照合する
まず、Linuxのプロセスアカウンティングから始めます:
VmRSS = RssAnon + RssFile + RssShmem大まかな内訳には /proc/PID/status を、RSS、PSS、無名(anon)、ファイル、共有、およびlazy-free情報の集計には /proc/PID/smaps_rollup を読み取ります。増大した特定の無名マッピングまたはファイルバックマッピングを特定する必要がある場合にのみ、/proc/PID/smaps を使用します。1回限りの pmap やRSSの合計値は、同期された差分(delta)よりも得られる情報が少なくなります。
次にアロケータの統計情報を追加します。glibcでは、mallinfo2 によって sbrk 経由で取得されたバイト数、マップされたチャンク内のバイト数、呼び出し元に渡されたバイト数、空きバイト数、および解放可能な最上位チャンクを取得できます。これらのフィールドはすべての割り当てソースをカバーしているわけではなく、一貫してサンプリングする必要がありますが、無名メモリのギャップの大部分をアロケータが保持しているかどうかを判断するのに役立ちます。マップ済み、アクティブ、常駐、保持、サイズクラスごとのより完全なデータが提供される場合は、アプリケーションのアロケータ固有のプロファイリングを優先してください。
最後に、cgroup v2と照合します。cgroupにはその階層内で課金されたすべてのメモリが含まれるため、1つのプロセスのRSSを超えたり、別の理由で変動したりすることがあります。memory.current と memory.stat をプロセスの証拠と無理に一致させようとせず、突き合わせて比較してください。
ステップ4: 3サイクルの再利用実験を行う
同じ64スレッドの並行性と入力サイズ分布を使用して、本番相当のカナリア環境で同じバッチを3回実行します。各バッチの後、同じ20分間待機し、同じカウンタをキャプチャします。
パターンの解釈:
| 観察結果 | より有力な仮説 | 次の確認事項 |
|---|---|---|
| アイドル期間を経る度に使用中割り当てが増加する | リークまたはアプリケーションによる保持 | 生存オブジェクトと割り当てスタックのプロファイルを比較 |
| 使用中バイト数は1.1 GiBに戻り、RSSは5.2 GiB付近にとどまるが、以降のバッチ割り当てで同様のRSS上昇が起きない | 再利用可能なアロケータの保持 | ページフォールト、割り当てレイテンシ、アロケータの空きバイト数を測定 |
| 使用中バイト数は横ばいで、アロケータの空きバイト数は多いが、解放可能バイト数は少なく、生存期間やサイズの構成変更により高止まりの水準が変化する | 断片化またはアリーナの分散 | サイズクラス、アリーナ、スレッド間解放、ピン留めされたマッピングを調査 |
RssFile または RssShmem が差分の大部分を占める | ファイルマッピングまたは共有メモリのライフサイクル | マッピングと所有者を特定。malloc のチューニングを中止 |
| 使用中バイト数とヒープ以外の無名マッピングの両方が増加する | 複数の原因が共存 | ヒープとネイティブマッピングを個別にプロファイル |
高止まりが自動的に無害であるとは限りません。古い保持ページで新しいサイズ分布に対応できないために次のバッチが8 GiBを超えてピークに達する場合、論理データが安定していてもサービスは障害を起こす可能性があります。逆に、同じワークロードを効率的に処理する高いプラトーは、強制的な解放と繰り返されるフォールトよりも望ましい場合があります。
ステップ5: 制限付きの診断としてトリムを使用する
glibcのカナリア環境で、バッチ処理完了の境界で malloc_trim(0) を1回呼び出し、その戻り値、RSSコンポーネント、アロケータの使用中バイト数、ページフォールト、CPU、および後続のリクエストレイテンシを記録します。GNUインターフェースは空きヒープメモリの解放を試み、sbrk または madvise を使用する可能性がありますが、特定のRSS削減を約束するものではありません。
使用中割り当てが1.1 GiBのままで RssAnon が大幅に減少した場合、アロケータが所有するページの一部が解放可能であったことを示します。これにより診断は絞り込まれますが、本番環境でtrimを呼び出すことが最善のポリシーであるとは証明されません。RSSがほとんど変化しない場合、ページ全体の空きが存在しないか、増加要因がglibcの外部にあるか、メトリクスに異なるマッピングが含まれている可能性があります。トリムの失敗はリークを証明するものではありません。
すべてのリクエストでtrimを実行しないでください。ページを解放すると、次のバッチが到着したときに、常駐メモリと引き換えにシステムコール、マイナーフォールト、ゼロクリア処理、キャッシュ消失、およびテールレイテンシが発生する可能性があります。長いアイドル時間を伴う自然なフェーズ境界であれば検討の余地がありますが、それでもカナリアテストが必要です。
ステップ6: 証明された原因に対応する解決策を適用する
- リーク: 参照の保持、キャッシュの上限、不足している解放処理、またはライブラリのライフサイクルを修正します。トリムを行っても使用中メモリは解放可能になりません。
- 短期的な再利用を伴う意図的な保持: そのまま維持し、測定されたピークに合わせてプロビジョニングを行い、RSSを使用中バイト数に無理に合わせるのではなく、階段状の連続増加に対してアラートを設定します。
- 生存期間に起因する断片化: 短命なバッチオブジェクトを長寿命のサービス状態から分離し、一括解放可能なバッチアリーナまたはリージョンを使用し、1つの長寿命オブジェクトが多くの揮発性ページに散らばらないようにします。
- アリーナまたはスレッドキャッシュの分散: アリーナ数の削減、割り当てホットスポットでの並行性の低減、または異なる所有権パターンをテストします。アリーナを減らすとメモリを節約できる可能性がありますが、競合が増加する可能性があります。
- 厳しい制限下での長いアイドルフェーズ: レート制限とロールバックフラグを備えた、バッチ後の明示的な1回のトリムまたはアロケータ固有のパージをテストします。
- アロケータの不一致: 正確なトレースのもとで、glibcと適切な代替アロケータを比較します。Microsoft Researchのmimallocの設計は基本的なトレードオフを示しています。スレッドローカルページはスケーラビリティと局所性を向上させますが、分離された所有権により、他のスレッドが即座に再利用できないメモリが保持される可能性があります。
glibcの arena_max、trim_threshold、および mmap_threshold は実験用のパラメータであり、魔法の定数ではありません。これらを設定すると動作がより静的になり、競合、マッピング数、解放頻度、システムコールコストが変化する可能性があります。一度に1つの要素のみを変更し、ロールバック用にもとのイメージを保持してください。
ステップ7: メモリとレイテンシを合わせて検証する
本番相当のハードウェア上で30回の同一サイクルをリプレイします。「30」という数値は演習用のテスト期間であり、普遍的な要件ではありません。使用中割り当て、アロケータのマップ済み/空き/解放可能バイト数、RssAnon、合計RSS、memory.current、メモリ圧迫、ページフォールト、割り当てレイテンシ、CPU、スループット、およびp50/p99リクエストレイテンシを追跡します。
このシナリオにおける合否基準の例としては、アイドル後の RssAnon が20分以内に2.2 GiB以下、30サイクルにわたる階段状の上昇がないこと、OOMや持続的なメモリ圧迫がないこと、p99が120 ms以下、CPU劣化が3%以下などが挙げられます。これらのしきい値は実践用の仮定であり、サービスの実際の予算に置き換えてください。2.2 GiBを達成してもp99を145 msに悪化させる解決策は不合格です。また、レイテンシは維持されるものの、次の有効なサイズ構成で8 GiB制限に達してしまう解決策も不合格です。
質の高い模範回答
「RSSの値だけでこれをリークと決めつけることはしません。free() はアプリケーションによるブロックの所有権を終了させますが、glibcは再利用のためにそのブロックをアリーナ内に保持することがあり、少数の使用中オブジェクトによって本来空きとなるはずのページが常駐し続けることがあります。まず1.1 GiBという数値がネイティブの使用中割り当てを網羅しているかを確認し、1回の完全なバッチを通じて RssAnon、RssFile、RssShmem、smaps のマッピング、アロケータの統計情報、およびcgroupの課金状況と照合します。
同じバッチを3回リプレイします。20分のアイドル期間を経る度に使用中割り当てが増加する場合、生存オブジェクトのプロファイルの差分を取り、所有権のパスを修正します。使用中バイト数が1.1 GiBのままで、RSSが5.2 GiB付近を維持し、次のバッチがさらなる増加なしにその領域を再利用する場合、保持がより有力な説明となります。アロケータの空きバイト数は多いものの解放可能なページが少なく、オブジェクトの生存期間や64スレッドの並行性によって高止まりの水準が変化する場合は、断片化とアリーナの分散を調査します。
glibc固有の診断として、バッチ終了後にカナリア環境で malloc_trim(0) を1回呼び出します。使用中バイト数が変わらずに RssAnon が低下した場合、アロケータの一部のページが解放可能であったことを示しますが、すべてのリクエストでトリムを行うことを正当化するものではありません。その後、原因に応じた最小限の変更を選択します。リークの修正、バッチの生存期間に応じた解放可能リージョンへの分離、あるいはバッチ後の制限付きトリムやアリーナ設定のカナリアテストなどです。30サイクルをリプレイし、アイドル後メモリが合意された目標を満たし、RSSが階段状に増加せず、cgroupが安全に保たれ、p99が120 ms以内に収まる場合にのみ変更を受け入れます。」
よくある間違い
- 4.1 GiBの差分をリークと決めつける → RSSにはアロケータの空きページやヒープ以外のマッピングが含まれます → 同期されたプロファイルを用いて、使用中割り当ての増加または保持された所有権を証明してください。
free()は常にRSSを低下させると主張する → 解放されたチャンクがアリーナ内に残るか、使用中チャンクと同じページを共有している可能性があります → 再利用、ページ単位の解放、および個別にマップされた割り当てについて説明してください。free()は決してメモリを返さないと主張する → アロケータは大きなマッピングをアンマップしたり、最上位チャンクをトリムしたり、ページ全体をmadviseで無効化したりできます → 解放はアロケータ、レイアウト、ポリシーに依存すると述べてください。- 単一の断片化比率を証拠として使用する → 直近のピーク、ファイルマッピング、キャッシュ、または意図的な保持によって比率が膨らむ可能性があります → 繰り返しのサイクルにわたって使用中、空き、マップ済み、常駐、解放可能なバイト数を比較してください。
- すべてのリクエストで
malloc_trim(0)を呼び出す → 強制的な解放はシステムコール、フォールト、テールレイテンシを増加させる可能性があります → 自然なアイドル境界で1回テストし、次の割り当てバーストを測定してください。 - 64スレッドあるからといって
arena_maxを1に設定する → メモリは減少するかもしれませんが、ロックの競合が増加します → 同じ並行性のもとで候補値をスイープテストし、p99を保護してください。 - ベンチマークの見出しだけでアロケータを切り替える → 割り当てサイズ、生存期間、スレッド間解放のパターンが結果を左右します → ロールバック手段を用意し、メモリとレイテンシの両方の基準を用いて、正確なトレースでA/Bテストを実施してください。
- cgroupのアカウンティングを無視する → 単一プロセスのRSSはメモリドメイン全体ではありません →
memory.current、memory.stat、子孫プロセス、およびメモリ圧迫をプロセスメトリクスと照合してください。
フォローアップの質問と回答
malloc_trim(0) によってRSSが5.2 GiBから1.8 GiBに減少した場合、何が証明されたことになりますか?
その時点でglibcがページ単位で解放可能な大量のメモリを保持していたこと、および高いRSSのすべてが使用中のアプリケーションデータではなかったことが示されました。より小さなリークの不在、保持の原因、または頻繁なトリムの安全性までは証明されていません。ポリシーを選択する前に、ワークロードを再実行し、ページフォールト、CPU、割り当てレイテンシ、およびp99を測定してください。
トリムが0を返し、RSSが変化しない場合、それはリークですか?
いいえ。空き領域がまだ使用中チャンクを含むページに分散しているか、メモリが別のアロケータやマッピングに保持されているか、glibcに解放可能なページが存在しない可能性があります。使用中プロファイル、アロケータの空きバイト数および解放可能バイト数、smaps の所有権を比較してください。リークの証明には、単にトリムが失敗したことではなく、使用中または保持された割り当てが増加している証拠が必要です。
RSSは横ばいなのに memory.current が増加し続ける場合はどうしますか?
ヒープではなくcgroupの差分を調査してください。memory.stat はファイルキャッシュ、shmem、ソケット、またはカーネルメモリの関与を明らかにする可能性があり、cgroupには他のプロセスや子孫プロセスが含まれている場合があります。階層とマッピングの所有権を確認してください。1つのプロセスのRSSが横ばいであるのにglibcをチューニングするのは、間違ったレイヤーを対象にすることになります。
glibcに1つのアリーナのみを使用させないのはなぜですか?
アリーナを1つにするとメモリの分散は抑えられますが、割り当て処理がより直列化されます。64スレッドの場合、常駐メモリの削減と引き換えにロック競合とp99の悪化を招く可能性があります。実際の割り当てトレース下で制限付きのアリーナ数をいくつかテストし、アロケータの競合とレイテンシを取得して、両方の予算を満たす最小のカウントを選択してください。
バッチアリーナやリージョンアロケータが適しているのはどのような場合ですか?
大部分のオブジェクトが単一の明確な生存期間を共有している場合に適しています。バッチ処理中にリージョンから割り当て、終了後にそのリージョンを一括解放します。長寿命のサービス状態へ参照が漏れる場合、デストラクタやオブジェクトごとのクリーンアップが必要な場合、または1つのバッチに無関係な生存期間が多数含まれる場合は安全ではありません。一括解放に依存する前に、所有権の境界を徹底してください。
代替アロケータはどのように評価しますか?
アロケータのリンク設定以外は同じビルド、同じ入力トレース、スレッド数、CPU配置、ウォームアップ、および30サイクルのテスト期間を使用します。ピーク時およびアイドル後のRSS、使用中と常駐のギャップ、割り当てスループット、CPU、ページフォールト、p50/p99、および8 GiB制限付近での障害動作を比較します。デプロイのロールバックを用意した上で優れた方をカナリアリリースします。平均RSSが低いというだけでは不十分です。