Skip to content
Published on

Apache Spark 実運用ガイド: 性能チューニング、シャッフル、スキュー、AQE、ストリーミング運用

Authors
Apache Spark 実運用ガイド

はじめに

Apache Sparkは高速な処理エンジンですが、本番環境で遅くなる理由もかなり予測可能です。多くのチームは最初に「executor memoryを増やせば解決する」と考えますが、実際のボトルネックは シャッフル, パーティション設計, 結合戦略, ファイルレイアウト, ストリーミング状態管理 にあることが多いです。

特にDatabricks、EMR、self-managed YARN、Kubernetes上でSparkを運用すると、よく似た失敗パターンが繰り返されます。

  • 小さなファイルが多すぎてスキャンのオーバーヘッドが大きい
  • skewed keyにより一部タスクだけが異常に長く実行される
  • broadcastで済むはずの結合が大きなshuffle joinになっている
  • cacheを無差別に使ってメモリ圧迫とspillが増える
  • Structured Streamingでwatermarkやstate store方針が曖昧で状態が無限に膨らむ

この文章はSpark入門ではなく、本番環境でSparkジョブを速く、安定して回すための運用基準を整理したものです。

実際に重要なSpark実行モデル

性能を理解するうえで大事なのはAPIより実行モデルです。

  1. DriverはDAGを作り、スケジューリングを担当する
  2. Executorは実際のtaskを実行する
  3. Stage境界は多くの場合shuffleが入る地点で分かれる
  4. task数はpartition数とほぼ直結する

つまりSparkチューニングは「ノードを何台増やすか」ではなく、どこでshuffleが発生し、partitionがどう作られているかを見ることから始まります。

実運用で最初に確認すべき質問はシンプルです。

  • 最も遅いstageはどこか
  • そのstageでshuffle readまたはshuffle writeが大きいか
  • skewed taskがあるか
  • input file数とpartition数が過剰か不足か
  • join、aggregation、sortのどれが本当のボトルネックか

そのためSpark UIのSQLタブ、stageタブ、task分布を確認する習慣が重要になります。

シャッフル、パーティショニング、スキューが大半を決める

シャッフルはいつ高くつくのか

shuffleではネットワーク、ディスクI/O、ソートコストが重なります。特に次の条件で高コストになります。

  • groupByKey、大規模aggregation、large sort
  • join keyの分布が悪くデータ量も大きい場合
  • 小さすぎるpartitionが大量に作られる場合
  • 一部キーだけにデータが集中している場合

partition数でよくある誤り

partitionが少なすぎると並列性が不足し、多すぎるとschedulerオーバーヘッドとsmall file問題が増えます。正解は固定値ではなく、データサイズとstageの性質に依存します。

実運用では次の原則がより有効です。

  • 入力データ量と平均task durationを一緒に見る
  • 出力ファイル数をストレージレイアウトの観点で見る
  • spark.sql.shuffle.partitionsをデフォルトのまま放置しない
  • AQEを有効にしていても、自動最適化を過信しない
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.shuffle.partitions", "400")

スキューは平均値では見えない

データスキューは平均実行時間だけ見ても見落としやすいです。stage平均が良く見えても、一部taskだけが数倍長く走ればジョブ全体の壁時計時間を支配します。

典型的な対応は次の通りです。

  • hot keyを特定する
  • saltingやpre-aggregationを検討する
  • broadcast可能な小さなdimension tableを分離する
  • AQEのskew join handlingを使う
  • 上流データモデルを見直してhot keyを減らす

スキューはSpark設定だけの問題ではなく、上流データモデルの問題として現れることが多いです。

AQE、結合戦略、キャッシュはどう使うべきか

AQEは魔法ではなく運用のガードレール

Adaptive Query ExecutionはSpark 3以降の本番運用で非常に重要です。実行中の統計を使ってpartition coalescing、skew handling、join strategy変更を行うため、日ごとにデータ分布が変わるパイプラインで特に有効です。

ただしAQEを有効にしただけで全てが解決するわけではありません。

  • 統計収集が遅いと高価なshuffleがすでに始まっている
  • filter pushdownが弱いと処理量自体が大きすぎる
  • source側のpartition設計が悪いとAQEだけでは回復できない

結合戦略は明示的に確認する

実務では次の順序で見るのが安全です。

  1. 小さいテーブルをbroadcastできるか
  2. join keyの分布は均一か
  3. sort-merge joinが本当に必要か
  4. partition pruningが効いているか
  5. 実行計画で意図したjoin strategyが選ばれているか
import org.apache.spark.sql.functions.broadcast

val result = factDf.join(broadcast(dimDf), Seq("customer_id"), "left")

キャッシュは「多く」ではなく「再利用価値があるときだけ」

cacheは、高価な中間結果を何度も使う場合にだけ有効です。一度しか使わないデータや、もともと列指向で効率よく保存されたデータに広く適用すると逆効果になりがちです。

確認すべき観点は以下です。

  • 再利用回数
  • 元データのスキャンコスト
  • executor memory pressure
  • spillの発生量
  • cache解除のタイミング

cache()より重要なのは、いつ unpersist() するかです。

Structured Streaming運用で壊れやすいポイント

Structured Streamingはbatchに似たAPIで扱いやすく見えますが、運用面ではより厳密な設計が必要です。

watermarkなしでstateful処理を増やさない

stateful aggregationやstream-stream joinでwatermarkがないと、state storeが終わりなく大きくなる可能性があります。

val aggregated =
  events
    .withWatermark("event_time", "10 minutes")
    .groupBy(window($"event_time", "5 minutes"), $"user_id")
    .count()

運用チェックリストは次の通りです。

  • event timeとprocessing timeを区別する
  • watermark遅延を実際のSLAに合わせる
  • checkpoint保存先の耐久性と権限を確認する
  • output modeとsinkのidempotencyを確認する
  • late data比率を監視する

ストリーミングではコードより運用契約が重要

障害復旧、再処理、checkpoint保持期間、schema evolution、sink重複許容が決まっていないと、障害のたびに同じパイプラインの意味が変わります。

必ず文書化したい問いは次の通りです。

  • checkpointはどこに保存するか
  • 再デプロイ時に変えてはいけない設定は何か
  • exactly-onceでない場合、どの重複を許容するか
  • backfill戦略は何か

本番チューニングの実践的ワークフロー

最も実用的なのは次の流れです。

  1. Spark UIで最も遅いstageを一つ選ぶ
  2. shuffle read/write、spill、skewed taskを先に見る
  3. 実行計画でjoin strategyとpartition数を確認する
  4. source file layoutとpredicate pushdownを確認する
  5. AQE、broadcast、repartition、pre-aggregationのどれか一つだけ変えて再測定する
  6. throughputとcostを一緒に見る

重要なのは、一度に多くを変えないことです。Sparkチューニングが難しいのは設定が多いからではなく、実験設計なしに変更すると何が効いたのか分からなくなるからです。

まとめ

Spark運用の本質は「メモリを増やすこと」ではなく、不要なデータ移動を減らし、スキューを制御し、実行計画を意図通りにし、ストリーミング状態を制限することにあります。

特に次の四つをチームの基本ルールにすると効果が大きいです。

  • 遅いjobは必ずSpark UIと実行計画から逆算する
  • partition数と出力ファイル数を意図的に設計する
  • AQEとbroadcast戦略を基本ガードレールにする
  • Structured Streamingはstate、watermark、checkpoint契約を文書化する

Sparkは強力ですが、本番性能はデフォルトではなく運用習慣から生まれます。

References