paint-brush
OOM クラッシュに別れを告げましょう@wydfy111
719 測定値
719 測定値

OOM クラッシュに別れを告げましょう

Jiafeng Zhang11m2023/06/12
Read on Terminal Reader

長すぎる; 読むには

メモリ割り当て、メモリ追跡、メモリ制限の最適化を備えた、より堅牢で柔軟なメモリ管理ソリューション。
featured image - OOM クラッシュに別れを告げましょう
Jiafeng Zhang HackerNoon profile picture

大規模データ クエリ タスクにおけるシステムの安定性を保証するものは何ですか?これは、効果的なメモリ割り当ておよび監視メカニズムです。これにより、計算を高速化し、メモリのホットスポットを回避し、メモリ不足に即座に対応し、OOM エラーを最小限に抑えることができます。




データベース ユーザーの観点から見ると、不適切なメモリ管理によってどのような問題が発生するのでしょうか?これは、ユーザーを悩ませていたもののリストです。


  • OOM エラーによりバックエンド プロセスがクラッシュします。私たちのコミュニティ メンバーの言葉を引用すると、「やあ、Apache Doris さん。メモリが不足しているときに処理が遅くなったり、いくつかのタスクが失敗したりするのは問題ありませんが、ダウンタイムを発生させるのはクールではありません。


  • バックエンド プロセスは大量のメモリ領域を消費しますが、原因となっている正確なタスクを見つけたり、単一のクエリのメモリ使用量を制限したりする方法はありません。


  • 各クエリに適切なメモリ サイズを設定するのは難しいため、十分なメモリ領域がある場合でもクエリがキャンセルされる可能性があります。


  • 同時実行性の高いクエリは異常に遅くなり、メモリのホットスポットを見つけるのが困難になります。


  • HashTable の作成中の中間データはディスクにフラッシュできないため、2 つの大きなテーブル間の結合クエリは OOM が原因で失敗することがよくあります。


幸いなことに、私たちはメモリ管理メカニズムを根本から改善したため、それらの暗い日々は過去のものになりました。さあ、準備をしましょう。物事は集中的に行われます。

メモリの割り当て

Apache Doris には、メモリ割り当て用の唯一のインターフェイスAllocatorがあります。メモリ使用量を効率的かつ制御下に保つために、適切と思われる調整が行われます。


また、割り当てまたは解放されたメモリ サイズを追跡するために MemTracker が配置されており、3 つの異なるデータ構造がオペレーター実行時の大量のメモリ割り当てを担当します (これらについてはすぐに説明します)。




メモリ内のデータ構造

クエリごとに実行時のメモリ ホットスポット パターンが異なるため、Apache Doris は 3 つの異なるメモリ内データ構造 ( ArenaHashTable 、およびPODArray )を提供します。彼らはすべてアロケーターの統治下にあります。



  1. アリーナ

アリーナは、アロケーターからの要求に応じて割り当てられるチャンクのリストを保持するメモリ プールです。チャンクはメモリのアライメントをサポートします。これらはアリーナの存続期間全体にわたって存在し、破壊されると (通常はクエリが完了したとき) 解放されます。


チャンクは主に、シャッフル中にシリアル化または逆シリアル化されたデータ、またはハッシュテーブル内のシリアル化されたキーを保存するために使用されます。


チャンクの初期サイズは 4096 バイトです。現在のチャンクが要求されたメモリより小さい場合、新しいチャンクがリストに追加されます。


現在のチャンクが 128M より小さい場合、新しいチャンクのサイズは 2 倍になります。 128M より大きい場合、新しいチャンクは最大でも必要なチャンクより 128M 大きくなります。


古い小さなチャンクは新しいリクエストには割り当てられません。割り当てられたチャンクと割り当てられていないチャンクの間の境界線をマークするカーソルがあります。


  1. ハッシュ表

HashTable は、ハッシュ結合、集計、集合演算、およびウィンドウ関数に適用できます。 PartitionedHashTable 構造は 16 個以下のサブ HashTable をサポートします。また、HashTable の並列マージもサポートしており、各サブハッシュ結合を個別にスケーリングできます。


これらにより、全体的なメモリ使用量とスケーリングによって生じる遅延を削減できます。


現在の HashTable が 8M より小さい場合、係数 4 でスケーリングされます。

8M より大きい場合は、2 倍に拡大縮小されます。

2G より小さい場合は、50% がいっぱいになったときにスケーリングされます。

2G より大きい場合は、75% がいっぱいになったときにスケーリングされます。


新しく作成された HashTable は、含まれるデータの量に基づいて事前にスケーリングされます。また、さまざまなシナリオに応じてさまざまなタイプの HashTable も提供しています。たとえば、集計の場合、PHmap を適用できます。


  1. PODアレイ

PODArray は、その名前が示すように、POD の動的配列です。 std::vectorとの違いは、PODArray は要素を初期化しないことです。メモリ アライメントとstd::vectorの一部のインターフェイスをサポートします。


これは 2 の係数でスケールされます。破棄では、各要素のデストラクター関数を呼び出す代わりに、PODArray 全体のメモリが解放されます。 PODArray は主に列に文字列を保存するために使用され、多くの関数計算や式フィルタリングに適用できます。

メモリインターフェース

Arena、PODArray、HashTable を調整する唯一のインターフェイスとして、Allocator は 64M を超えるリクエストに対してメモリ マッピング (MMAP) 割り当てを実行します。


4K より小さいものは、malloc/free を介してシステムから直接割り当てられます。その間の処理は汎用キャッシュ ChunkAllocator によって高速化され、ベンチマーク結果によると 10% のパフォーマンス向上がもたらされます。


ChunkAllocator は、ロックフリーの方法で現在のコアの FreeList から指定されたサイズのチャンクを取得しようとします。そのようなチャンクが存在しない場合は、ロックベースの方法で他のコアから試行します。それでも失敗する場合は、指定されたメモリ サイズをシステムに要求し、それをチャンクにカプセル化します。


両方を経験した後、私たちは TCMalloc ではなく Jemalloc を選択しました。高同時実行テストで TCMalloc を試したところ、CentralFreeList の Spin Lock が総クエリ時間の 40% を占めていることがわかりました。


「積極的なメモリ デコミット」を無効にすると状況は改善されましたが、メモリ使用量が大幅に増加したため、定期的にキャッシュをリサイクルするために個別のスレッドを使用する必要がありました。一方、Jemalloc は、同時実行性の高いクエリにおいてパフォーマンスが高く、安定していました。


他のシナリオに合わせて微調整した後、TCMalloc と同じパフォーマンスを実現しましたが、メモリ消費量は少なくなりました。

メモリの再利用

メモリの再利用は、Apache Doris の実行層で広く実行されます。たとえば、データ ブロックはクエリの実行中に再利用されます。シャッフル中、送信側には 2 つのブロックがあり、それらは交互に動作し、1 つはデータを受信し、もう 1 つは RPC トランスポートで動作します。


タブレットを読み取るとき、Doris は述語列を再利用し、循環読み取りを実装し、フィルタリングし、フィルタリングされたデータを上のブロックにコピーしてからクリアします。


集計キー テーブルにデータを取り込む場合、データをキャッシュする MemTable が一定のサイズに達すると、事前に集計され、さらに多くのデータが書き込まれます。


データスキャン時にもメモリの再利用が行われます。スキャンが開始される前に、いくつかの空きブロック (スキャナーとスレッドの数に応じて) がスキャン タスクに割り当てられます。


各スキャナーのスケジューリング中に、空きブロックの 1 つがデータ読み取りのためにストレージ層に渡されます。


データの読み取り後、ブロックは後続の計算で上位の演算子を使用するためにプロデューサー キューに入れられます。上位オペレータがブロックから計算データをコピーすると、ブロックは次のスキャナ スケジューリングのために空きブロックに戻ります。


空きブロックを事前に割り当てるスレッドは、データ スキャン後に空きブロックを解放する役割も担うため、余分なオーバーヘッドは発生しません。フリー ブロックの数によって、データ スキャンの同時実行性が何らかの形で決まります。

記憶の追跡

Apache Doris は、MemTrackers を使用して、メモリのホットスポットを分析しながらメモリの割り当てと解放を追跡します。 MemTrackers は、各データ クエリ、データ インジェスト、データ圧縮タスク、およびキャッシュや TabletMeta などの各グローバル オブジェクトのメモリ サイズの記録を保持します。


手動カウントと MemHook 自動追跡の両方をサポートします。ユーザーは、Web ページ上の Doris バックエンドでのリアルタイムのメモリ使用量を表示できます。

MemTrackers の構造

Apache Doris 1.2.0 より前の MemTracker システムは、process_mem_tracker、query_pool_mem_tracker、query_mem_tracker、instance_mem_tracker、ExecNode_mem_tracker などで構成される階層ツリー構造になっていました。


隣接する 2 つの層の MemTracker は親子関係にあります。したがって、子 MemTracker での計算ミスはすべて累積され、より大きな規模の信じられない結果をもたらします。



Apache Doris 1.2.0 以降では、MemTrackers の構造がより単純になりました。 MemTracker は、その役割に基づいて、 MemTracker Limiterとその他の 2 つのタイプにのみ分類されます。


メモリ使用量を監視する MemTracker Limiter は、すべてのクエリ/取り込み/圧縮タスクおよびグローバル オブジェクトで固有です。一方、他の MemTracker は、結合/集計/並べ替え/ウィンドウ関数のハッシュテーブルやシリアル化の中間データなど、クエリ実行時のメモリ ホットスポットをトレースして、さまざまな演算子でメモリがどのように使用されているかを把握したり、メモリ制御のリファレンスを提供します。データのフラッシュ。


MemTracker Limiter と他の MemTracker の間の親子関係は、スナップショットの印刷でのみ明示されます。このような関係は、シンボリック リンクと考えることができます。これらは同時に消費されることはなく、一方のライフサイクルが他方のライフサイクルに影響を与えることはありません。


これにより、開発者はそれらを理解し、使用することがはるかに簡単になります。


MemTracker (MemTracker Limiter などを含む) はマップのグループに入れられます。これにより、ユーザーは全体的な MemTracker タイプのスナップショット、クエリ/ロード/圧縮タスクのスナップショットを印刷し、メモリ使用量が最も多いクエリ/ロード、またはメモリ超過使用量が最も多いクエリ/ロードを見つけることができます。



MemTracker の仕組み

特定の実行のメモリ使用量を計算するには、MemTracker が現在のスレッドのスレッド ローカルのスタックに追加されます。 Jemalloc または TCMalloc の malloc/free/realloc をリロードすることにより、MemHook は割り当てまたは解放されたメモリの実際のサイズを取得し、現在のスレッドの Thread Local に記録します。


実行が完了すると、関連する MemTracker がスタックから削除されます。スタックの一番下には、クエリ/ロード実行プロセス全体中のメモリ使用量を記録する MemTracker があります。


ここで、クエリの実行プロセスを簡略化して説明します。


  • Doris バックエンド ノードが起動すると、すべてのスレッドのメモリ使用量が Process MemTracker に記録されます。


  • クエリが送信されると、クエリ MemTracker がフラグメント実行スレッドのスレッド ローカル ストレージ (TLS) スタックに追加されます。


  • ScanNode がスケジュールされると、 ScanNode MemTracker がフラグメント実行スレッドのスレッド ローカル ストレージ (TLS) スタックに追加されます。その後、このスレッドで割り当てまたは解放されたメモリはすべて、Query MemTracker と ScanNode MemTracker の両方に記録されます。


  • スキャナーがスケジュールされた後、Query MemTracker とスキャナー MemTracker がスキャナー スレッドの TLS スタックに追加されます。


  • スキャンが完了すると、スキャナ スレッド TLS スタック内のすべての MemTracker が削除されます。 ScanNode のスケジューリングが完了すると、ScanNode MemTracker はフラグメント実行スレッドから削除されます。次に、同様に、集約ノードがスケジュールされると、 AggregationNode MemTrackerがフラグメント実行スレッド TLS スタックに追加され、スケジュールが完了すると削除されます。


  • クエリが完了すると、Query MemTracker はフラグメント実行スレッド TLS スタックから削除されます。この時点で、このスタックは空になっているはずです。次に、QueryProfile から、クエリ実行全体および各フェーズ (スキャン、集計など) 中のピーク メモリ使用量を表示できます。



MemTrackerの使用方法

Doris バックエンド Web ページは、クエリ/ロード/コンパクション/グローバルのタイプに分けられたリアルタイムのメモリ使用量を示します。現在のメモリ消費量とピーク消費量が表示されます。



グローバル タイプには、Cache と TabletMeta の MemTrackers が含まれます。



クエリ タイプから、現在のクエリの現在のメモリ消費量とピーク消費量、およびクエリに含まれる演算子を確認できます (ラベルからそれらの関係がわかります)。履歴クエリのメモリ統計については、Doris FE 監査ログまたは BE INFO ログを確認できます。



メモリ制限

Doris バックエンドにメモリ追跡が広く実装されているため、バックエンドのダウンタイムや大規模なクエリの失敗の原因である OOM の排除にまた一歩近づいています。次のステップは、クエリとプロセスのメモリ制限を最適化し、メモリ使用量を制御することです。

クエリのメモリ制限

ユーザーはすべてのクエリにメモリ制限を設定できます。実行中にその制限を超えると、クエリはキャンセルされます。しかし、バージョン 1.2 以降、より柔軟なメモリ制限制御であるメモリ オーバーコミットが可能になりました。


十分なメモリ リソースがある場合、クエリはキャンセルされずに制限を超えるメモリを消費できるため、ユーザーはメモリ使用量に特別な注意を払う必要はありません。存在しない場合、クエリは新しいメモリ領域が割り当てられるまで待機します。新しく解放されたメモリがクエリに十分でない場合にのみ、クエリはキャンセルされます。


Apache Doris 2.0 では、クエリの例外安全性を実現しました。つまり、メモリ割り当てが不十分な場合はクエリが直ちにキャンセルされるため、後続の手順で「キャンセル」ステータスを確認する手間が省けます。

プロセスのメモリ制限

Doris バックエンドは、プロセスの物理メモリと現在利用可能なメモリ サイズをシステムから定期的に取得します。同時に、すべてのクエリ/ロード/圧縮タスクの MemTracker スナップショットを収集します。


バックエンド プロセスがメモリ制限を超えた場合、またはメモリが不足している場合、Doris はキャッシュをクリアし、多数のクエリまたはデータ インジェスト タスクをキャンセルすることでメモリ領域を解放します。これらは個々の GC スレッドによって定期的に実行されます。



消費されるプロセス メモリが SoftMemLimit (デフォルトでは総システム メモリの 81%) を超える場合、または利用可能なシステム メモリが警告ウォーター マーク (3.2GB 未満) を下回る場合、マイナー GCがトリガーされます。


この時点で、クエリの実行はメモリ割り当てステップで一時停止され、データ取り込みタスクのキャッシュされたデータは強制的にフラッシュされ、データ ページ キャッシュと古いセグメント キャッシュの一部が解放されます。


新しく解放されたメモリがプロセス メモリの 10% をカバーしていない場合、メモリ オーバーコミットを有効にすると、Doris は 10% の目標が達成されるか、すべてのクエリがキャンセルされるまで、最大の「オーバーコミッター」であるクエリのキャンセルを開始します。


次に、Doris はシステム メモリのチェック間隔と GC 間隔を短縮します。より多くのメモリが使用可能になった後、クエリは続行されます。


消費されたプロセス メモリが MemLimit (デフォルトでは総システム メモリの 90%) を超えた場合、または利用可能なシステム メモリが Low Water Mark (1.6 GB 未満) を下回った場合、フル GCがトリガーされます。


この時点で、データ取り込みタスクが停止され、すべてのデータ ページ キャッシュとその他のほとんどのキャッシュが解放されます。


これらすべての手順を行った後、新しく解放されたメモリがプロセス メモリの 20% をカバーしていない場合、Doris はすべての MemTracker を調べて、最もメモリを消費するクエリと取り込みタスクを見つけて、それらを 1 つずつキャンセルします。


20% の目標が達成された場合にのみ、システム メモリのチェック間隔と GC 間隔が延長され、クエリと取り込みタスクが継続されます。 (1 回のガベージ コレクション操作には通常、数百μs ~ 数十ミリ秒かかります。)

影響と結果

メモリ割り当て、メモリ追跡、およびメモリ制限の最適化により、リアルタイム分析データ ウェアハウス プラットフォームとしての Apache Doris の安定性と高同時実行パフォーマンスが大幅に向上しました。バックエンドでの OOM クラッシュは、現在では珍しい光景です。


OOM が存在する場合でも、ユーザーはログに基づいて問題の根本を特定し、修正できます。さらに、クエリとデータの取り込みに対するメモリ制限がより柔軟になったため、メモリ領域が十分であれば、ユーザーはメモリの管理に余分な労力を費やす必要がなくなります。


次のフェーズでは、メモリ オーバーコミットメントでクエリが確実に完了するようにする予定です。つまり、メモリ不足によりキャンセルする必要があるクエリが少なくなります。


私たちはこの目標を、例外の安全性、リソース グループ間のメモリの分離、中間データのフラッシュ メカニズムという特定の作業方向に分割しました。


当社の開発者に会いたい場合は、ここで当社を見つけてください