データ接続と統合パイプラインの最適化とデバッグSparkネイティブアクセラレーション

注: 以下の翻訳の正確性は検証されていません。AIPを利用して英語版の原文から機械的に翻訳されたものです。

ネイティブアクセラレーション

Velox ↗ によるネイティブアクセラレーションを有効にすると、Spark のパフォーマンスを向上させることができます。

ネイティブアクセラレーションは、低レベルのハードウェア最適化を活用してバッチジョブのパフォーマンスを向上させる手法です。このパフォーマンス向上は、計算処理を Java 仮想マシン(JVM)言語から、C++ などのネイティブ言語に移すことで実現します。ネイティブ言語はマシンコードにコンパイルされ、マシンのハードウェア上で直接実行されます。ネイティブアクセラレーションは、プラットフォーム固有の機能を利用することで、大規模なデータワークロードの処理に必要な時間を大幅に短縮し、ジョブの実行を高速化してリソース使用効率を向上させることを目指しています。

ネイティブアクセラレーションは、Python トランスフォームおよび Pipeline Builder で利用可能です。

ビルドの分析

Spark の詳細ページで、ネイティブアクセラレーションを使用したビルドの基本的な分析を行うことができます。クエリプランタブで物理プランを選択すると、次のような内容が表示されます。

== Physical Plan ==
AdaptiveSparkPlan
+- == Final Plan ==
   Execute InsertIntoHadoopFsRelationCommand
   +- WriteFiles
      +- CollectMetrics
         +- VeloxColumnarToRowExec
            +- ^ ProjectExecTransformer
               +- ^ InputIteratorTransformer
                  +- ^ InputAdapter
                     +- ^ RowToVeloxColumnar
                        +- ^ HashAggregate
                           +- ^ VeloxColumnarToRowExec
                              +- ^ AQEShuffleRead
                                 +- ^ ShuffleQueryStage
                                    +- ColumnarExchange
                                       +- ^ ProjectExecTransformer
                                          +- ^ RowToVeloxColumnar
                                             +- * ColumnarToRow
                                                +- BatchScan parquet

従来の Spark クエリプランとおおむね似ていますが、いくつかの重要な違いがあります。ProjectExec ノードの代わりに、ProjectExecTransformer があります。これは、その演算が Velox クエリエンジンでネイティブ実行されることを意味します。クエリプランでオフロードされるすべてのノードには、ツリー内で ^ 記号が付けられます。ネイティブ実行のブロックは、RowToVeloxColumnar と VeloxColumnarToRowExec に挟まれています。これらのノードは、Spark データセットから Arrow DataFrame への変換と、その逆方向の変換を担います。このシリアライズとデシリアライズには、大きなコストがかかります。

一般に、ネイティブアクセラレーションのパフォーマンスが低いことを示すパターンは2つあります。

  • ^ 記号で示される、ネイティブ実行されるノードの割合が小さい。
  • RowToVeloxColumnar および VeloxColumnarToRowExec ノードが多数あり、シリアライズのオーバーヘッドが大きい。

この分析は、パフォーマンスが期待どおりでない場合に役立ちます。パイプラインへの小さな変更が、オフロードされる計算処理の量に大きく影響することがあります。Checkpoint などの機能を使用すると、すべてをネイティブ実行できるビルドの部分を手動でまとめることができます。

ネイティブアクセラレーションの実装とアーキテクチャ

Foundry のネイティブアクセラレーションの実装は、Apache Gluten ↗ プロジェクトを基盤としています。Foundry のネイティブアクセラレーションは、Velox ↗ クエリエンジンを活用して、実行時に Spark ジョブを高速化します。Velox は C++ で記述されており、データベースの高速化を明確に念頭に置いて設計されています ↗。また、Arrow DataFrame ↗ に対して演算子レベルの演算を実行するための開発者向け API を提供します。Gluten は、Spark ランタイムと Velox を結び付けるために必要な「接着剤」の役割を果たします。

この構成では、パイプラインはまず、通常のビルド(ネイティブアクセラレーションを使用しないビルド)と同様に Spark クエリプランを生成します。次に、追加の最適化ルールがプランに適用され、クエリの一部を Velox で実行できるかどうかが判定されます。この判定は、Velox に同等の実装があるかどうか、およびその実装へのマッピングが Gluten に存在するかどうかに基づきます。クエリは演算子レベルでオフロードできます。これは、おおよそ SELECT、FILTER、JOIN などの SQL 文に相当します。この段階で、クエリプランのうちオフロードできるすべての部分に印が付けられます。

プランニングのステップが完了すると、クエリは通常の Spark エンジンを通じて実行されます。つまり、すべてのタスクのスケジューリング、エグゼキューターのオーケストレーション、ライフサイクル管理は通常どおり進みます。違いが生じるのは、エグゼキューターが、ネイティブ実行の対象として印が付けられたクエリプランの部分に到達したときです。この場合、Spark のデフォルトの実装を呼び出す代わりに、Velox の実装が呼び出されます。

このアーキテクチャは、計算処理の一部を Velox で実行できないクエリにも対応できるため、特に有利です。これは、オフロードの判定がプラン全体ではなく演算子レベルで行われるためです。サポートされる演算子の数は継続的に増えていますが、UDF のようなユーザーが作成したコードは、ネイティブ実装が存在しないため、オフロードできません。

サポートされる演算子と式の全一覧を表示する ↗

ネイティブアクセラレーションが高速なのはなぜですか?

Spark は JVM 言語である Scala で記述されており、パフォーマンスを向上させるためにコード生成 ↗など、多くの最適化を備えています。さらに、JVM 自体も、プラットフォーム固有の機能をできるだけ多く活用することを目指す C2 コンパイラー ↗などの最適化を備えています。しかし、C++ などのネイティブ言語は、次の3つの基本的な理由から、依然としてより高いパフォーマンスを発揮します。

  • コンパイル時の最適化: Java と Scala はバイトコードにコンパイルされ、その後 JVM によって実行されますが、C++ などのネイティブ言語は直接マシンコードにコンパイルされます。これにより、C++ コンパイラーはコンパイル時に広範な最適化を行い、実行時のオーバーヘッドを大幅に削減できます。一方、JVM 言語は実行中に行われる Just-In-Time(JIT)コンパイルに依存しています。JIT コンパイルでは、コンパイルに費やす時間と、実行をすばやく開始する必要性とのバランスを取らなければならないため、同じ水準の最適化を達成できない場合があります。
  • ガベージコレクション(GC)が不要: C++ ではメモリー管理を手動で行うため、ガベージコレクション(GC)に伴うオーバーヘッドがなくなります。JVM 言語では、GC 処理によって予測できない一時停止やオーバーヘッドが発生し、特にメモリーを大量に使用するアプリケーションでパフォーマンスに影響することがあります。
  • ハードウェアへの直接アクセスとベクトル化 API の利用: C++ は、ハードウェア機能や低レベルのシステムリソースへの直接アクセスを提供します。これにより、開発者はプラットフォーム固有の最適化や、SSE、AVX、その他の SIMD(Single Instruction Multiple Data)命令などのベクトル化 API を活用できます。この直接アクセスにより、細かく調整したパフォーマンス最適化が可能になります。JVM 言語では、抽象化レイヤーが同じ水準のハードウェアとのインタラクションを妨げる場合があるため、こうした最適化は容易ではありません。

ネイティブアクセラレーションにおけるメモリー設定の考慮事項

Foundry でネイティブアクセラレーションを使用して Spark を実行するには、通常のバッチパイプラインとは少し異なる設定が必要です。Spark は、オフヒープメモリー ↗を使用した一部の演算の実行をサポートしています。オフヒープメモリーは JVM が管理しないメモリーであり、GC のオーバーヘッドをなくすことでパフォーマンスが向上します。オフヒープメモリーを有効にすると、パイプラインの保守コストが増える可能性があるため、Foundry ではデフォルトで有効にしていません。Velox によって変更される DataFrame は、ネイティブプロセスからアクセスできるようにオフヒープに配置する必要があるため、ネイティブアクセラレーションにはオフヒープメモリーの有効化が必要です。Foundry では、Velox によるデータ変換以外のすべての処理に、引き続き十分なオンヒープメモリーが必要です(たとえば、オーケストレーション、スケジューリング、ビルド管理のコードは引き続き JVM で実行されます)。ただし、理想的には、処理の大部分がオフヒープで実行されるようになります。ネイティブアクセラレーションを使用するようにパイプラインを設定すると、オンヒープメモリーとオフヒープメモリーのバランスを取るための保守コストが追加で発生します。