DDIA 第10章:バッチ処理(やさしい解説)

Tony Duong

Tony Duong

5月 23, 20262

他の言語:🇫🇷🇬🇧
#distributed-systems#ddia#batch-processing#spark#mapreduce
DDIA 第10章:バッチ処理(やさしい解説)

『Designing Data-Intensive Applications』の第10章は、その時々のデータベースから視点を引いて、別の問いを投げかけます。リアルタイムでの応答が不要なときに、ギガバイト、テラバイト級の膨大なデータをどう処理するか? これがバッチ処理の世界です。この記事では、章の大きなアイデアをシンプルな比喩で追っていきます。

3種類のシステム

本題に入る前に、章では便利な思考モデルを提示しています。

  • サービス(オンライン): 到着するリクエストに応答する。レイテンシが重要。例:Webサーバー。
  • バッチ処理(オフライン): 固定された大きな入力を処理して出力を生成する。スループットが重要で、レイテンシは分や時間単位。例:夜間レポート。
  • ストリーム処理(ニアリアルタイム): その中間 — イベントを到着次第処理するが、ユーザーへの即時応答は不要。(これは次章の話題です。)

この章の残りは2番目について扱います。

小さく始める:Unixパイプ

DDIAは楽しい観察から始まります。数行のUnixシェルでかなりのことができる、という点です。

cat access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head

このパイプラインはログファイルの中で上位のURLを見つけ出します。各ツールは1つのことだけを行い、stdinから読み、stdoutに書き、シェルがそれらを繋ぎ合わせます。ファイルこそが普遍的なインタフェースです。

これがバッチ処理が受け継いだ哲学です。小さく組み合わせ可能なツール、不変な入力、決定的な出力。何か問題が起きたら再実行すればよい。入力ファイルは変わっていないのですから。

落とし穴は、「ログファイル」が100 TBになると1台のマシンには収まらないことです。

MapReduce:1000台のコンピュータのためのUnixパイプ

MapReduce(Googleが広め、その後オープンソースのHadoopで普及)は、本質的には同じアイデアをクラスタ全体にスケールアウトしたものです。

書くのは2つの関数だけです。

  • マッパー (mapper): 1レコードずつ受け取り、0個以上の(key, value)ペアを出力する。
  • リデューサー (reducer): ある特定のキーに対する全ての値を受け取り、出力を生成する。

その間で、フレームワークが魔法をかけます。すべての(key, value)ペアをネットワーク越しにシャッフル (shuffle) し、同じキーを持つものが同じリデューサーに集まるようにします。このシャッフルが重い処理の中心です。

具体例:単語カウント

  • マッパーは行を読み、各単語について(word, 1)を出力する。
  • シャッフルが"the"に対する1を全てまとめ、"cat"に対する1を全てまとめる、というように。
  • リデューサーは各キーの値を合計する。

同じ形は、ログ解析、検索インデックス構築、レコメンデーション計算など、「何かでグループ化して集約する」と定式化できる問題なら何にでも当てはまります。

MapReduceでのジョイン

ランダムなデータベースルックアップなしに2つのデータセット(例:ユーザーとクリック)を結合する話題は繰り返し登場します。

  • ソートマージジョイン (sort-merge join): 両側が同じキー(例:user_id)を出力し、リデューサーは全クリックとユーザーレコードを一緒に受け取る。
  • ブロードキャストハッシュジョイン (broadcast hash join): 小さい側を全マッパーのメモリにロードし、大きい側をストリーム処理する。
  • パーティションドハッシュジョイン (partitioned hash join): 両側があらかじめジョインキーでパーティション分割されている。

名前は大層ですが、考え方はラップトップでやるのと同じ — ただ分散しているだけです。

なぜMapReduceは愛されなくなったのか

MapReduceは革命的でしたが、書くのは苦痛です。

  • どのジョブもただのmap + reduce。それ以上に複雑なものはジョブの連鎖になる。
  • ジョブ間の中間結果はディスク(HDFS)に書かれる — 遅い。
  • 10ステージのワークフローは、データセットを10回読み書きすることになる。

ここでApache Sparkが登場します。

Apache Spark:同じアイデア、よりスマートな実行

Sparkは同じ思考モデル — データをパーティション分割し、関数を適用し、必要に応じてシャッフルする — を保ちつつ、苦痛だった部分を解消しました。

  • インメモリ実行: 中間データは可能な限りステージ間でRAMに保持される。大きな高速化。
  • より豊かなオペレータ: map/reduceだけでなく、filterjoingroupByreduceByKeyaggregateなどが流暢なAPIとして用意されている。
  • DAGプランナー: Sparkは記述された変換すべてのグラフを構築し、実行前に計画全体を最適化する。ステージを融合し、ジョイン戦略を決定し、不要なシャッフルを回避できる。
  • Resilient Distributed Datasets (RDD): 中心的な抽象 — 由来を知っているパーティション分割されたコレクション。パーティションが失われたら、Sparkはリネージ (lineage) から再計算する。レプリケーションは不要。

ユーザー視点の体験は、MapReduceジョブを書くというより、PythonやSQLを書くのに近いです。「CSVを読み、フィルタし、ジョインし、グループ化し、Parquetに書き出す」というパイプラインは、1つのプログラムとして読めます — 分散させ方はSparkが考えます。

これらのアイデアを手を動かしながら追えるよう、小さなリポジトリを作りました:learn-apache-spark

データフローエンジンと高レベルAPI

Sparkは、章でデータフローエンジン (dataflow engines) と呼ばれる広いカテゴリの一例です(他にはFlink、Tez)。これらはMapReduceのアイデアを、任意のオペレータDAGに一般化したものです。

これらのエンジンの上には、データフロー計画にコンパイルされる高レベルAPIが乗っています。

  • ビッグデータ上のSQL(Hive、Spark SQL、Presto)
  • DataFrame API(Spark DataFrames、pandas-on-Spark)
  • グラフ処理(Pregel流:GraphX、Giraph)
  • 機械学習(MLlib)

教訓は、もはやほとんどの人は手書きでmap/reduceを書かない、ということです。SQLやDataFrameのコードを書き、分散の配管はエンジンが面倒を見ます。

バッチのための設計:繰り返し現れる原則

章の中で何度も登場するアイデアがいくつかあります。

  • 不変な入力。 ソースデータを変更することは絶対にない。新しい出力を生成する。ジョブが間違っていたら、コードを直して再実行 — 入力はそのまま残っている。
  • 決定的な関数。 マッパーとリデューサーは、同じ入力に対して同じ出力を生成すべき。これがリトライを安全にする。
  • 再実行による耐障害性。 ノードが死んでも、フレームワークは作業の一部を別の場所で再実行するだけ。これは上の2点があってこそ可能。
  • ストレージとコンピュートの分離。 データはHDFS / S3 / オブジェクトストレージに置かれ、コンピュートクラスタはその上で起動・停止する。

バッチ vs リアルタイムデータベース

立派なデータベースがあるのに、なぜバッチを使うのでしょうか?

  • コスト: バッチジョブは安価で遅いストレージとバーストコンピュートを使う。トランザクショナルDBから配信するよりバイトあたりがずっと安い。
  • スループット: 100 TBをシーケンシャルにスキャンするのは速い。同じ量をDBへの個別クエリ100 TB分でやるのはそうではない。
  • 疎結合: バッチの出力(例:検索インデックス、レコメンデーションファイル)はその後、配信システムにロードされる。オフラインで構築し、オンラインで配信する — バッチパイプラインの障害がWebサイトを落とすことはない。

要点まとめ

  • Unixパイプは哲学的な祖先:小さなツール、不変なファイル、組み合わせ可能性。
  • MapReduceは、そのアイデアをクラスタ全体に分散させ、中間にシャッフルを置いたもの。
  • MapReduceは苦痛である。ステージ間で全てがディスクを経由し、map+reduceしか提供されないため。
  • Apache Sparkはそのモデルを保ちつつ、メモリ上で実行し、豊富なオペレータをサポートし、実行前にDAG全体を計画する。
  • RDDはレプリケーションではなくリネージによってSparkを耐障害性のあるものにする。
  • データフローエンジン + SQL/DataFrame API が、今日のバッチ作業のほとんどが行われる場所 — 生のmap/reduceを書くことはめったにない。
  • 不変性と決定性こそが、分散バッチ処理のリトライを安全にする。
  • バッチとオンラインシステムはパートナー: バッチが成果物(インデックス、モデル、レポート)を作り、オンラインシステムがそれを配信する。

🌐 Claudeによる翻訳

Tony Duong

著者: Tony Duong

デジタル日記。思考、経験、そして人生についての考え。