はじめに
Sparkを使っているとよく聞く言葉に「オーバーヘッド」があります。
しかし実際には、
- どこで発生しているのか?
- 何が支配的なのか?
が曖昧なまま「なんとなく遅い」で済まされがちです。
本記事では、Sparkの実行構造
Application ⇒ Job ⇒ Stage ⇒ Task
に沿って、各レイヤーごとのオーバーヘッドを分解します。
結論(TL;DR)
-
オーバーヘッドは1箇所ではなく全レイヤーに存在する
-
特に効くのは以下の3つ:
- Application起動(クラスタ起動)
- Shuffle(Stage境界)
- Taskスケジューリング
-
小規模データでは起動コストが支配的になる
Sparkの実行構造
Sparkは以下の階層で実行されます:
- Application:Driver + Executorの集合
- Job:Action実行単位
- Stage:Shuffleで区切られる単位
- Task:Partition単位の処理
① Applicationレベルのオーバーヘッド(最重要)
発生する処理
- Driverの起動
- Executorの確保
- JVMの立ち上げ
- クラスタリソースの割り当て
特徴
- 固定コスト(データ量に関係なく発生)
- 数秒〜数分(環境によってはそれ以上)
影響
小規模データではこれだけで処理時間の大半を占める。
② Jobレベルのオーバーヘッド(遅延評価の代償)
発生する処理
- DAG(実行計画)の構築
- 依存関係の解析
- 最適化(Catalystなど)
特徴
- Action実行時に初めて発生
- 通常は軽いが、複雑な処理では無視できない
ポイント
Sparkは「遅延評価」なので、
書いた瞬間ではなく、実行時にまとめて計画される
③ Stageレベルのオーバーヘッド(Shuffle)
発生する処理
- データの再分配(Shuffle)
- ノード間通信
- ディスクへの書き出し
特徴
- 最もパフォーマンスに影響する要素の1つ
- ネットワーク + I/O が絡む
典型例
- join
- groupBy
- distinct
なぜ遅いのか
- データが別ノードへ移動する
- シリアライズ/デシリアライズが発生
- ディスクI/Oが入る
④ Taskレベルのオーバーヘッド
発生する処理
- タスク生成
- Executorへの割り当て
- 実行管理
特徴
- タスク数が多いと影響が大きくなる
- スケジューリング遅延が発生
さらに内部でもオーバーヘッド
Taskの中でも以下が発生:
- シリアライズ / デシリアライズ
- GC(ガーベジコレクション)
- データ読み書き
まとめ:オーバーヘッドの分布
| レイヤー | 主なオーバーヘッド |
|---|---|
| Application | クラスタ起動・JVM |
| Job | DAG構築・最適化 |
| Stage | Shuffle(通信・I/O) |
| Task | スケジューリング・内部処理 |
実務での優先順位
体感として重要なのは:
- Application起動
- Shuffle(Stage)
- Taskスケジューリング
ケース別に何が支配的か
小規模データ
- Application起動が支配的
- → Sparkは不利
大規模データ
- Shuffleが支配的
- → 設計次第で性能が大きく変わる
データ偏り(skew)
- 特定Taskがボトルネック
- → 全体が待たされる
アンチパターン
- 小規模データをSparkで処理する
- Shuffleを意識せずjoinを書く
- partition設計を無視する
まとめ
- オーバーヘッドは「1箇所」ではない
- 階層ごとに発生する
- ケースごとに支配的な要素が変わる
そして最も重要なのは:
どのオーバーヘッドが支配的かを見極めること
これができると、
「なんとなく遅い」から脱却できます。