この記事ディレクトリ:
-
SparkShuffleの進化の歴史
-
オンヒープおよびオフヒープのメモリ計画
-
メモリスペースの割り当て
-
ストレージメモリ管理
-
メモリ管理を実行します
必読のリスト(ビッグデータの宝物)を備えた何百もの高品質のビッグデータブック
序文
メモリベースの分散コンピューティングエンジンとして、Sparkのメモリ管理モジュールはシステム全体で非常に重要な役割を果たします。Sparkのメモリ管理の基本を理解すると、Sparkアプリケーションをより適切に開発し、パフォーマンスの調整を実行するのに役立ちます。この記事の目的は、Sparkメモリ管理のコンテキストを整理し、いくつかのアイデアを紹介し、このトピックに関する読者の詳細な議論を引き出すことです。この記事で説明する原則は、Spark 2.1バージョンに基づいています。この記事を読むには、読者は特定のSparkおよびJavaの基盤を持ち、RDD、シャッフル、JVM、およびその他の関連概念を理解している必要があります。
Sparkアプリケーションを実行すると、Sparkクラスターはドライバーとエグゼキューターの2つのJVMプロセスを開始します。前者はマスタープロセスであり、Sparkコンテキストの作成、Sparkジョブの送信(ジョブ)、およびジョブのコンピューティングタスクへの変換(タスク)を担当します。 。各Executorプロセスは、タスクのスケジュールを調整します。後者は、ワーカーノードで特定のコンピューティングタスクを実行し、結果をドライバーに返し、永続的である必要があるRDDにストレージ機能を提供します。ドライバのメモリ管理は比較的単純なので、この記事では主にエグゼキュータのメモリ管理を分析します。以下のSparkメモリはエグゼキュータのメモリを指します。
1.スパークシャッフルの進化の歴史
MapReduceフレームワークでは、シャッフルはMapとReduceの間のブリッジです。Mapの出力は、Reduceのシャッフルリンクを経由する必要があります。シャッフルのパフォーマンスは、プログラム全体のパフォーマンスとスループットに直接影響します。Sparkは、MapReduceフレームワークの実装として、当然、シャッフルのロジックも実装します。
シャッフルはMapReduceフレームワークの特定のフェーズであり、MapフェーズとReduceフェーズの間にあります。Mapの出力結果をReduceで使用する場合、出力結果をキーでハッシュし、各Reducerに配布する必要があります。このプロセスはシャッフルです。シャッフルにはディスクの読み取りと書き込み、およびネットワーク送信が含まれるため、シャッフルのパフォーマンスはプログラム全体の実行効率に直接影響します。
次の図は、MapReduceアルゴリズムのプロセス全体を明確に示しています。ここで、シャッフルフェーズはMapフェーズとReduceフェーズの間にあります。
概念的には、シャッフルはデータ接続を通信するためのブリッジですが、シャッフル(パーティション)は実際にどのように実装されますか?Sparkでのシャッフルの実装について説明するためにSparkを例として取り上げましょう。
Sparkでのシャッフルのプロセス全体を簡単に説明するために、例として写真を撮りましょう。
-
まず、各マッパーはレデューサーの数に応じて対応するバケットを作成します。バケットの数はMM×RRです。ここで、MMはマップの数、RRはレデューサーの数です。
-
次に、マッパーによって生成された結果は、設定されたパーティションアルゴリズムに従って各バケットに入力されます。ここでのパーティションアルゴリズムはカスタマイズできます。もちろん、デフォルトのアルゴリズムは、キーに応じて異なるバケットにハッシュすることです。
-
レデューサーが起動すると、リモートまたはローカルのブロックマネージャーから、自身のタスクのIDと依存するマッパーのIDに従って、レデューサーの入力として対応するバケットを取得します。
ここでのバケットは抽象的な概念です。実装では、各バケットはファイルに対応でき、ファイルの一部または他の部分に対応できます。
Apache Sparkのシャッフルプロセスは、Apache Hadoopのシャッフルプロセスに似ており、いくつかの概念を直接適用できます。たとえば、シャッフルプロセスでは、データを提供するエンドはマップエンドと呼ばれ、データを生成する各タスクはマップエンドはマッパーと呼ばれます。これに対応して、データを受信する側はリデュース側と呼ばれ、リデュース側でデータをプルする各タスクはリデューサーと呼ばれます。シャッフルプロセスは、基本的にマップ側から取得したデータをパーティショナーであり、データを対応するレデューサープロセスに送信します。
2.オンヒープおよびオフヒープのメモリ計画
JVMプロセスとして、Executorのメモリ管理はJVMのメモリ管理に基づいて構築され、Sparkはメモリを最大限に活用するためにJVMのオンヒープスペースをより詳細に割り当てます。同時に、Sparkはオフヒープメモリを導入するため、ワーカーノードのシステムメモリのスペースを直接開くことができ、メモリの使用がさらに最適化されます。
ヒープ内およびヒープ外のメモリの概略図:
2.1ヒープ内メモリ
Sparkアプリケーションの起動–executor-memory
時spark.executor.memory
orパラメーターで構成されたオンヒープメモリのサイズ。エグゼキュータで実行されている並行タスクはJVMヒープメモリを共有します。RDDデータのキャッシュとデータのブロードキャスト時にこれらのタスクが占有するメモリはストレージメモリとして計画され、シャッフルの実行時にこれらのタスクが占有するメモリは実行メモリとして計画されます。一部は特別に計画されていません。Spark内のオブジェクトインスタンス、またはユーザー定義のSparkアプリケーションのオブジェクトインスタンスはすべて残りのスペースを占有します。異なる管理モードでは、これら3つの部分が占めるスペースが異なります(以下のセクション2で紹介します)。
オブジェクトインスタンスによって占有されているメモリの適用と解放はJVMによって完了するため、Sparkのヒープ内メモリの管理は論理的な「計画」管理です。Sparkは適用後と解放前にのみメモリを記録できます。その特定のプロセスを見てみましょう。
-
メモリを要求する:
-
コード内の新しいオブジェクトインスタンスをスパークします
-
JVMは、ヒープ上のメモリからスペースを割り当て、オブジェクトを作成し、オブジェクト参照を返します
-
Sparkはオブジェクトへの参照を保存し、オブジェクトが占有するメモリを記録します
-
メモリを解放します:
-
Sparkは、オブジェクトによって解放されたメモリを記録し、オブジェクトへの参照を削除します
-
JVMのガベージコレクションメカニズムがオブジェクトによって占有されているヒープメモリを解放するのを待ちます
JVMオブジェクトはシリアル化された方法で格納できることがわかっています。シリアル化のプロセスは、オブジェクトをバイナリバイトストリームに変換することです。本質的には、非連続空間のチェーンストレージを連続空間またはブロックストレージに変換することと理解できます。アクセスするには、シリアル化の逆のプロセスが必要です。つまり、バイトストリームをオブジェクトに変換する逆シリアル化です。シリアル化方式では、ストレージスペースを節約できますが、保存および読み取りの際の計算オーバーヘッドが増加します。
Sparkのシリアル化されたオブジェクトの場合、バイトストリームの形式であるため、それが占めるメモリサイズを直接計算できますが、シリアル化されていないオブジェクトの場合、オブジェクトが占めるメモリは定期的なサンプリングによって概算されます。つまり、新しいデータ項目が追加されるたびに、占有メモリサイズが一度計算されるわけではありません。この方法では、時間のオーバーヘッドは削減されますが、大きなエラーが発生する可能性があり、特定の瞬間の実際のメモリがはるかに大きくなる可能性があります。期待。さらに、Sparkによってリリースのマークが付けられたオブジェクトインスタンスが実際にはJVMによって再利用されない可能性が非常に高く、その結果、実際に使用可能なメモリがSparkによって記録された使用可能なメモリより少なくなります。したがって、Sparkは実際に使用可能なオンヒープメモリを正確に記録できないため、メモリ不足(OOM、メモリ不足)の例外を完全に回避することはできません。
ヒープ内メモリの適用と解放を正確に制御することはできませんが、Sparkは、ストレージメモリと実行メモリの独立した計画と管理を通じて、新しいRDDをストレージメモリにキャッシュするかどうか、および実行メモリを新しいタスクに割り当てるかどうかを決定できます。ある程度、メモリの使用率を改善し、例外の発生を減らすことができます。
2.2オフヒープメモリ
メモリの使用をさらに最適化し、シャッフル中の並べ替えの効率を向上させるために、Sparkはオフヒープメモリを導入し、ワーカーノードのシステムメモリのスペースを直接開いてシリアル化されたバイナリデータを格納できるようにします。JDK Unsafe API(Spark 2.0以降、ヒープ外のストレージメモリを管理する場合はTachyonに基づいていませんが、ヒープ外の実行メモリのようにJDK Unsafe APIに基づいて実装されています)を使用して、Sparkは直接操作できます。オフヒープメモリ、削減不要なメモリオーバーヘッド、および頻繁なGCスキャンと収集を排除し、処理パフォーマンスを向上させます。オフヒープメモリを正確に適用および解放でき、シリアル化されたデータが占めるスペースを正確に計算できるため、オンヒープメモリと比較して、管理の難しさが軽減され、エラーも軽減されます。
デフォルトでは、オフヒープメモリは有効になっておらず、構成spark.memory.offHeap.enabled
パラメータspark.memory.offHeap.size
。オフヒープスペースのサイズは、パラメータによって設定されます。他にスペースがないことを除いて、オフヒープメモリはオンヒープメモリと同じ方法で分割され、実行中のすべての同時タスクはストレージメモリと実行メモリを共有します。
2.3メモリ管理インターフェイス
Sparkは、ストレージメモリと実行メモリを管理するための統合インターフェイス(MemoryManager)を提供します。同じエグゼキュータ内のタスクは、このインターフェイスのメソッドを呼び出して、メモリを適用または解放します。
リスト1:メモリ管理インターフェースの主なメソッド
名前 | 方法 |
---|---|
1.ストレージメモリを申請する | defacquireStorageMemory(blockId:BlockId、numBytes:Long、memoryMode:MemoryMode):ブール値 |
2.メモリを拡張するために適用します | defacquireUnrollMemory(blockId:BlockId、numBytes:Long、memoryMode:MemoryMode):ブール値 |
3.実行メモリを申請します | defacquireExecutionMemory(numBytes:Long、taskAttemptId:Long、memoryMode:MemoryMode):Long |
4.ストレージメモリを解放します | def releaseStorageMemory(numBytes:Long、memoryMode:MemoryMode):ユニット |
5.実行メモリを解放します | def releaseExecutionMemory(numBytes:Long、taskAttemptId:Long、memoryMode:MemoryMode):Unit |
6.アンワインドメモリを解放します | def releaseUnrollMemory(numBytes:Long、memoryMode:MemoryMode):ユニット |
これらのメソッドを呼び出すときは、そのメモリモード(MemoryMode)を指定する必要があることがわかります。このパラメータは、操作がヒープ内で実行されるか、ヒープ外で実行されるかを決定します。
MemoryManagerの特定の実装に関して、Spark1.6はデフォルトでUnifiedMemory Managerモードになり、1.6より前に使用されていたStatic Memory Managerモードは引き続き予約されています。これは、spark.memory.useLegacyModeパラメーターを構成することで有効にできます。2つの方法の違いは、スペースの割り当て方法にあります。次のセクション2では、これら2つの方法をそれぞれ紹介します。
3.メモリスペースの割り当て
3.1静的メモリ管理
Sparkが最初に採用した静的メモリ管理メカニズムでは、Sparkアプリケーションの実行中にストレージメモリ、実行メモリ、その他のメモリのサイズが固定されますが、ユーザーはアプリケーションの起動前に構成できます。ヒープ内のメモリの割り当てを以下に示します。下の図。表示:
静的メモリ管理図-ヒープ内:
ご覧のとおり、使用可能なヒープメモリのサイズは次のように計算する必要があります。
使用可能なストレージメモリ=systemMaxMemory * spark.storage.memoryFraction * spark.storage.safetyFraction
使用可能な実行メモリ=systemMaxMemory * spark.shuffle.memoryFraction * spark.shuffle.safetyFraction
systemMaxMemoryは、現在のJVMヒープメモリのサイズに依存し、最後に使用可能な実行メモリまたはストレージメモリに、これに基づいてそれぞれのmemoryFractionおよびsafetyFractionパラメータが乗算されます。上記の計算式における2つのsafetyFractionパラメータの重要性は、1-safetyFractionなどの保険領域を論理的に予約して、実際のメモリが現在のプリセット範囲を超えることによって生じるOOMのリスクを減らすことです(前述の非順次メモリサンプリングの場合)。オブジェクトの推定にはエラーが発生する可能性があります)。この予約済みの保険エリアは論理的な計画にすぎないことに注意してください。Sparkは、使用時にそれを異なる方法で処理しません。「他のメモリ」のように管理するためにJVMに渡されます。
次の図に示すように、ヒープ外のスペース割り当ては比較的単純で、ストレージメモリと実行メモリのみです。使用可能な実行メモリとストレージメモリが占めるスペースのサイズは、パラメータspark.memory.storageFractionによって直接決定されます。オフヒープメモリが占めるスペースは正確に計算できるため、保険エリアを設定する必要はありません。 。
静的メモリ管理図-オフヒープ:
静的メモリ管理メカニズムの実装は比較的簡単ですが、ユーザーがSparkのストレージメカニズムに精通していない場合、または特定のデータスケールやコンピューティングタスクに応じて対応する構成を行わない場合、「半海水、半炎」、つまりストレージメモリと実行メモリの一方の側には多くのスペースが残っていますが、もう一方の側は早くいっぱいになり、新しいコンテンツを保存するために古いコンテンツを削除または移動する必要があります。新しいメモリ管理メカニズムの出現により、この方法が開発者によって使用されることはめったにありません。古いバージョンのアプリケーションとの互換性のために、Sparkは引き続きその実装を保持しています。
3.2統合メモリ管理
次の2つの図に示すように、Spark 1.6以降に導入された統合メモリ管理メカニズムは、ストレージメモリと実行メモリが同じスペースを共有し、互いの空き領域を動的に占有できるという点で静的メモリ管理とは異なります。
統合メモリ管理図-ヒープ内:
統合メモリ管理図-オフヒープ:
最も重要な最適化の1つは動的占有メカニズムであり、そのルールは次のとおりです。
-
基本ストレージメモリと実行メモリ領域(
spark.storage.storageFraction 参数
)を設定します。これにより、それぞれの領域の範囲が決まります。 -
双方のスペースが不足している場合はハードディスクに保存します。一方のスペースが不足していて、もう一方のスペースが空いている場合は、もう一方のスペースを借りることができます。(ストレージスペースが不足しています。完全なブロックを置くだけでは不十分であることを意味します)
-
実行メモリスペースが相手によって占有された後、相手は占有された部分をハードディスクに転送し、借用したスペースを「返す」ことができます。
-
保管スペースが相手によって占有された後は、シャッフルプロセスの多くの要素を考慮する必要があり、実装がより複雑になるため、相手が「返却」することはできません。
動的占有メカニズム図:
統合メモリ管理メカニズムにより、Sparkはオンヒープおよびオフヒープメモリリソースの使用率をある程度改善し、開発者がSparkメモリを維持する難しさを軽減しますが、開発者がくつろいでリラックスできることを意味するわけではありません。たとえば、ストレージメモリスペースが大きすぎるか、キャッシュされたデータが大きすぎると、キャッシュされたRDDデータが通常長期間メモリに存在するため、完全なガベージコレクションが頻繁に発生し、タスク実行のパフォーマンスが低下します。 。したがって、Sparkのパフォーマンスを十分に活用するには、開発者はストレージメモリと実行メモリの管理方法と実装原理をさらに理解する必要があります。
4.ストレージメモリ管理
4.1RDDの永続性メカニズム
Sparkの最も基本的なデータ抽象化としてのResilientDistributedDataset(RDD)は、読み取り専用パーティションレコード(Partition)のコレクションであり、安定した物理ストレージ内のデータセット、または他の既存のRDDに基づいてのみ作成できます。変換を実行します。 (変換)新しいRDDを生成する操作。変換されたRDDと元のRDDの間に生じる依存関係は、系統を構成します。Sparkは系統を使用して、すべてのRDDを再回復できることを保証します。ただし、RDDのすべての変換は遅延します。つまり、結果をドライバーに返すアクション(Action)が発生した場合にのみ、SparkはRDDを読み取るタスクを作成し、実際に変換の実行をトリガーします。
タスクが起動時にパーティションを読み取ると、最初にパーティションが永続化されているかどうかを判断します。永続化されていない場合は、チェックポイントを確認するか、系統に従って再計算する必要があります。したがって、RDDで複数のアクションを実行する場合は、最初のアクションでpersistまたはcacheメソッドを使用して、RDDをメモリまたはディスクに永続化またはキャッシュし、後続のアクションでの計算速度を向上させることができます。実際、キャッシュ方式はデフォルトのMEMORY_ONLYストレージレベルを使用してRDDをメモリに永続化するため、キャッシュは特別な種類の永続化です。オンヒープおよびオフヒープストレージメモリの設計により、RDDのキャッシュ時に使用されるメモリの統合された計画と管理が可能になります(ブロードキャストデータのキャッシュなど、ストレージメモリの他のアプリケーションシナリオは一時的にこの記事の範囲外です)。
RDDの永続性は、RDDと物理ストレージの分離を実現するSparkのストレージモジュールを担当します。ストレージモジュールは、コンピューティングプロセスでSparkによって生成されたデータの管理を担当し、ローカルまたはリモートでメモリまたはディスクのデータにアクセスする機能をカプセル化します。特定の実装では、ドライバー側とエグゼキューター側のストレージモジュールがマスタースレーブアーキテクチャを構成します。つまり、ドライバー側のBlockManagerがマスターであり、エグゼキューター側のBlockManagerがスレーブです。ストレージモジュールは論理的にブロックを基本ストレージユニットとして使用し、RDDの各パーティションは処理後のブロックに一意に対応します(BlockIdの形式はrdd_RDD-ID_PARTITION-IDです)。マスターは、Sparkアプリケーション全体のブロックのメタデータ情報の管理と保守を担当します。スレーブは、ブロックの更新やその他のステータスをマスターに報告し、マスターから追加や削除などのコマンドを受け取る必要があります。 RDD。
ストレージモジュール図:
RDDを永続化する場合、Sparkはなどの7つの異なるストレージレベルを指定し、ストレージレベルは次の5つの変数の組み合わせですMEMORY_ONLY
。MEMORY_AND_DISK
class StorageLevel private(
private var _useDisk: Boolean, //磁盘
private var _useMemory: Boolean, //这里其实是指堆内内存
private var _useOffHeap: Boolean, //堆外内存
private var _deserialized: Boolean, //是否为非序列化
private var _replication: Int = 1 //副本个数
)
データ構造の分析を通じて、ストレージレベルがRDDのパーティション(ブロックとも呼ばれる)のストレージ方法を3つの次元から定義していることがわかります。
-
保存場所:ディスク/ヒープ内メモリ/ヒープ外メモリ。たとえば、MEMORY_AND_DISKはディスクとヒープ内メモリに同時に保存され、冗長バックアップを実現します。OFF_HEAPは、オフヒープメモリにのみ保存されます。現在、オフヒープメモリを選択した場合、同時に他の場所に保存することはできません。
-
ストレージ形式:ブロックがストレージメモリにキャッシュされた後、シリアル化されていない形式であるかどうか。たとえば、MEMORY_ONLYは非シリアル化モードで保存され、OFF_HEAPはシリアル化モードで保存されます。
-
レプリカの数:1より大きい場合、他のノードへのリモート冗長バックアップが必要です。たとえば、DISK_ONLY_2にはリモートバックアップコピーが必要です。
4.2RDDキャッシングのプロセス
RDDがストレージメモリにキャッシュされる前に、Partitionのデータは通常、Iterator(Iterator)データ構造としてアクセスされます。これは、Scala言語でデータコレクションをトラバースする方法です。イテレータを使用して、パーティション内の各シリアル化または非シリアル化されたデータ項目(レコード)を取得できます。これらのレコードのオブジェクトインスタンスは、JVMヒープ内のメモリの他の部分のスペースを論理的に占有します。同じパーティションは連続していません。
RDDがストレージメモリにキャッシュされた後、パーティションはブロックに変換され、レコードはヒープまたはオフヒープストレージメモリの連続スペースを占有します。パーティションを不連続なストレージスペースから連続的なストレージスペースに変換するプロセスであるSparkは、これを「展開」と呼びます。ブロックには、RDDのストレージレベルに応じて、シリアル化と非シリアル化の2つのストレージ形式があります。非シリアル化ブロックはDeserializedMemoryEntryデータ構造によって定義され、配列はすべてのオブジェクトインスタンスを格納するために使用され、シリアル化ブロックはバイトバッファー(ByteBuffer)を使用してバイナリデータを格納するSerializedMemoryEntryデータ構造によって定義されます。各エグゼキュータのストレージモジュールは、リンクされたマップ構造(LinkedHashMap)を使用して、ヒープおよびオフヒープストレージメモリ内のすべてのブロックオブジェクトのインスタンスを管理します。このLinkedHashMapを追加および削除すると、メモリのアプリケーションと解放が間接的に記録されます。
ストレージスペースがIterator内のすべてのデータを一度に収容できるという保証はないため、現在のコンピューティングタスクは、アンロール時にスペースを一時的に占有するのに十分なアンロールスペースをMemoryManagerに適用する必要があります。シリアル化されたパーティションの場合、必要な展開スペースを直接計算して一度に適用できます。シリアル化されていないパーティションは、レコードをトラバースするプロセス中に順番に適用する必要があります。つまり、レコードが読み取られるたびに、必要な展開スペースをサンプリングして見積もり、それを適用します。スペースが不足している場合は、中断して占有されていたアンロールスペースを解放できます。最後の展開が成功すると、次の図に示すように、現在のパーティションが占有している展開スペースが通常のキャッシュRDDストレージスペースに変換されます。
Spark Unrollの概略図:
上記の静的メモリ管理のセクションでわかるように、静的メモリ管理中に、Sparkはストレージメモリ内のUnrollスペースの一部を特別に分割し、そのサイズは固定されます。統合メモリ管理では、Unrollの間に特別な区別はありません。スペース。ストレージスペースが不足している場合は、動的占有メカニズムに従って処理されます。
4.3除去と配置
同じエグゼキュータのすべてのコンピューティングタスクは限られたストレージメモリスペースを共有するため、キャッシュする必要がある新しいブロックがあるが、残りのスペースが不十分で動的に占有できない場合、LinkedHashMapの古いブロックを削除して削除する必要があります。ブロックのストレージレベルには、ディスクに保存するための要件も含まれています。ドロップ(ドロップ)する必要があります。そうしないと、ブロックが直接削除されます。
ストレージメモリの削除ルールは次のとおりです。
-
削除された古いブロックは、新しいブロックと同じMemoryModeを持っている必要があります。つまり、両方ともオフヒープまたはオンヒープメモリに属している必要があります。
-
循環除去を回避するために、古いブロックと新しいブロックを同じRDDに含めることはできません
-
整合性の問題を回避するために、古いブロックが属するRDDを読み取ることができません
-
LinkedHashMapのブロックをトラバースし、新しいブロックに必要なスペースが満たされるまで、使用頻度の低い順に(LRU)ブロックを削除します。ここで、LRUはLinkedHashMapの特性です。
-
ディスクを配置するプロセスは比較的簡単です。ストレージレベルが_useDiskがtrueであるという条件を満たしている場合は、その_deserializedに従って、シリアル化されていない形式であるかどうかを判断し、そうである場合は、シリアル化して、最後にデータを保存します。ディスクに。モジュール内の情報を更新します。
5.メモリ管理を実行します
5.1マルチタスク間のメモリ割り当て
Executorで実行されているタスクも実行メモリを共有し、SparkはHashMap構造を使用して、タスクからメモリ消費へのマッピングを保存します。各タスクが占有できる実行メモリのサイズは、1/2Nから1/Nの範囲です。ここで、Nは現在のエグゼキュータで実行中のタスクの数です。各タスクを開始するとき、MemoryManagerに少なくとも1 / 2Nの実行メモリを要求する必要があります。要件を満たせない場合、タスクはブロックされ、別のタスクが十分な実行メモリを解放するまでタスクをウェイクアップできます。
5.2シャッフルメモリ使用量
実行メモリは主に、シャッフルの実行時にタスクが占有するメモリを格納するために使用されます。シャッフルは、特定のルールに従ってRDDデータを再パーティション化するプロセスです。シャッフルの書き込みフェーズと読み取りフェーズでの実行メモリの使用を見てみましょう。
シャッフル書き込み
-
マップ側で通常のソート方法を選択した場合、ExternalSorterが流出に使用され、データをメモリに格納するときにヒープ内の実行スペースが主に占有されます。
-
マップ側でタングステンのソート方法を選択した場合、ShuffleExternalSorterを使用して、シリアル化された形式で格納されたデータを直接ソートします。データをメモリに格納する場合、ヒープ外またはヒープ内の実行スペースを占有する可能性があります。ユーザーがオフヒープメモリを有効にしているかどうか、およびオフヒープ実行メモリが十分であるかどうか。
シャッフル読み取り
リデュース側のデータが集約される場合、データはアグリゲーターに渡されて処理される必要があり、データがメモリーに格納されるときにヒープ内の実行スペースが占有されます。
最終結果をソートする必要がある場合は、データをExternalSorterに渡して再度処理し、ヒープ内の実行スペースを占有する必要があります。
ExternalSorterおよびAggregatorでは、SparkはAppendOnlyMapというハッシュテーブルを使用してデータをヒープ実行メモリに格納しますが、ハッシュテーブルがメモリを占有している場合、シャッフルプロセス中にすべてのデータをハッシュテーブルに格納できるわけではありません。定期的に。MemoryManagerから新しい実行メモリを適用するには大きすぎる場合、Sparkはそのコンテンツ全体をディスクファイルに保存します。このプロセスはオーバーフロー(スピル)と呼ばれ、ディスクにスピルされたファイルは最終的にマージされます(マージ)。 。
シャッフル書き込みフェーズで使用されるタングステンは、SparkのメモリとCPU使用率を最適化するために、Databricksによって提案された計画であり、JVMのパフォーマンスの制限と欠点を解決します。Sparkは、シャッフルに従ってタングステンの並べ替えを使用するかどうかを自動的に選択します。Tungstenで採用されているページベースのメモリ管理メカニズムはMemoryManagerに基づいて構築されています。つまり、Tungstenは実行メモリの使用を1ステップ抽象化するため、シャッフルプロセスでは、データがに格納されているかどうかを気にする必要はありません。ヒープまたはヒープ外。各メモリページはMemoryBlockによって定義され、Object objとlongoffsetの2つの変数を使用して、システムメモリ内のメモリページのアドレスを一律に識別します。ヒープ内のMemoryBlockは、長い配列の形式で割り当てられたメモリです。objの値は、配列のオブジェクト参照です。offsetは、JVM内の長い配列の初期オフセットアドレスです。2つは一緒に使用できます。ヒープ内の絶対アドレス。ヒープ外のMemoryBlockは直接適用されるメモリブロックであり、そのobjはnullであり、オフセットはシステムメモリ内のこのメモリブロックの64ビット絶対アドレスです。Sparkは、MemoryBlockを使用してヒープ内およびヒープ外のメモリページを微妙に抽象的にカプセル化し、ページテーブル(pageTable)を使用して各タスクによって適用されるメモリページを管理します。
タングステンページ管理下のすべてのメモリは、ページ番号とページ内のオフセットで構成される64ビットの論理アドレスで表されます。
-
ページ番号:13ビットを占有し、メモリページを一意に識別します。Sparkは、メモリページを申請する前に、空きページ番号を申請する必要があります。
-
ページ内オフセット:51ビットを占有し、メモリページを使用してデータを格納するときのページ内のデータのオフセットアドレスです。
統一されたアドレス指定方法により、Sparkは64ビットの論理アドレスポインターを使用して、ヒープの内側または外側のメモリを見つけることができます。シャッフル書き込みの並べ替えプロセス全体は、ポインターを並べ替えるだけで済み、逆シリアル化は必要ありません。プロセス全体は非常に効率的です。 。、これにより、メモリアクセス効率とCPU使用効率が大幅に向上します。
Sparkのストレージメモリと実行メモリの管理方法は完全に異なります。ストレージメモリの場合、SparkはLinkedHashMapを使用して、キャッシュする必要のあるRDDのパーティションから変換されるすべてのブロックを一元管理します。実行メモリの場合、SparkはAppendOnlyMapを使用します。シャッフルプロセスでデータを保存するために使用され、Tungstenソートでページベースのメモリ管理に抽象化されて、新しいJVMメモリ管理メカニズムを開きます。