データベース●●●●○

ストリーム処理

Stream Processing

流れ続けるイベントを溜めてから処理するのではなく、到着するそばから継続的に処理する方式。

概要

ストリーム処理は、絶え間なく発生し続けるデータ — クリック、決済、センサー値、ログの1行1行 — を、いったん溜めてからまとめて処理するのではなく、到着するそばから継続的に処理していく方式です。「1日分を貯めて夜間に集計する」のがバッチ処理だとすれば、ストリーム処理は「流れてくる1件1件をその場でさばき続ける」処理と言えます。

典型的な適用場面は、リアルタイムのダッシュボード(直近5分の売上・アクセス数)、クレジットカードの不正検知、在庫や価格の即時反映、メトリクスの集計とアラートなどです。共通するのは「答えが数時間後では価値が薄れる」こと。データの発生から反応までの遅延(レイテンシ)を、時間単位から秒〜ミリ秒単位へ縮めることがストリーム処理の存在意義です。メッセージキューで運ばれてくるイベントの流れを入力とし、変換・集計した結果を次の系へ流し込む、データ基盤の「動脈」にあたる語彙です。

なぜ生まれたか

長らくデータ処理の標準は夜間バッチでした。業務システムからETLでデータを吸い上げ、夜中に集計し、翌朝レポートができあがる。この方式は堅牢ですが、どうしても「昨日までのデータ」しか見えません。EC の不正注文を翌朝に検知しても商品は発送済みですし、障害の兆候を示すログを翌日に集計しても手遅れです。ビジネスがオンライン化するほど、「発生した瞬間に反応したい」要求とバッチの周期のギャップが広がっていきました。

素朴な解決策は「バッチの間隔をどんどん短くする」ことですが、1時間ごと、5分ごとと縮めていくと、ジョブの起動・突き合わせのオーバーヘッドが支配的になり破綻します。そこで発想を反転させ、「データが有限に区切られている」という前提を捨てて、終わりのないデータの流れをそのまま扱うエンジンが作られました。2010年代に Apache Kafka がイベントの流れを貯めて配る土台として普及し、Storm、Spark Streaming を経て、Apache Flink が「イベント時刻に基づく正確なウィンドウ集計」と「障害時も結果が壊れない状態管理」を実用にしたことで、ストリーム処理は特殊技術から標準的な選択肢になりました。かつては速報値をストリームで、正確な値を夜間バッチで別々に計算して重ねる「ラムダアーキテクチャ」が定石でしたが、同じロジックを2系統に実装する負担が大きく、現在はストリーム処理系に一本化する(バッチを「有限のストリーム」として同じエンジンで扱う)方向へ収束しつつあります。

詳細

バッチとの対比 — 有限か、終わりがないか

バッチ処理は「入力が有限」であることを前提にできます。全件読み終えてからソートも集計もできるし、失敗したら最初からやり直せばよい。一方ストリーム処理の入力には終わりがなく、「全部読み終えてから」が永遠に来ません。そのため、結果は一度きりの確定値ではなく「今この瞬間までの集計値」として継続的に更新され続けます。これは結果整合性的な世界観で、途中経過(状態)を持ち続けること、そして障害で処理が止まっても状態を復元して流れの途中から再開できることが、エンジンの中核機能になります。

イベント時刻と処理時刻 — 「いつ起きたか」と「いつ届いたか」

ストリーム処理を難しくしている最大の要因が、時刻が2種類あることです。イベント時刻はその出来事が実際に起きた時刻、処理時刻はそのイベントがシステムに到着して処理された時刻。ネットワークの遅延、モバイル端末のオフライン、リトライなどにより、イベントは発生順どおりには届かず、遅刻も順序の入れ替わりも日常的に起こります。「10時00分〜10時01分のクリック数」を正しく数えたいなら、到着順ではなくイベント時刻で数えなければなりません。

そこで登場するのがウィンドウ集計です。終わりのない流れをそのまま集計することはできないので、「1分ごと」のような時間の窓で区切って窓ごとに集計します。窓の区切り方には、重ならず並べるタンブリングウィンドウ、一定間隔でずらしながら重ねるスライディングウィンドウ、無活動期間で区切るセッションウィンドウなどがあります。そして遅刻イベントに対しては、ウォーターマーク — 「イベント時刻がこの時刻より前のものは、もうおおむね届き切ったはず」という見積もりの印 — を流れに混ぜて進め、ウォーターマークが窓の終端を越えたときに窓を閉じて結果を確定します。どこまで遅刻を待つかは、正確さと速報性のトレードオフそのものです。

イベント時刻で区切ったウィンドウ10:00 〜 10:01集計: 3件10:01 〜 10:02集計: 遅刻分も含めて3件10:02 〜 10:03集計: 1件処理時刻 — システムへの到着順10:01:50 に発生したが遅れて到着到着順ではなくイベント時刻の窓へ算入する
イベント時刻によるウィンドウ集計 — 到着が遅れたイベントも「起きた時刻」の窓に数える

現代の典型的な構成では、まず Apache Kafka がイベントの受け口になります。Kafka はメッセージキューの一種ですが、読んだら消える伝統的なキューと違い、イベントを追記専用のログとして一定期間保持し、複数の消費者がそれぞれ自分の読み位置(オフセット)で読み進められるのが特徴です。過去に巻き戻して再処理できるこの性質は、イベントソーシングとも相性がよく、ストリーム処理基盤の土台として事実上の標準になっています。その下流で Apache Flink や Kafka Streams、Spark Structured Streaming といった処理エンジンが、フィルタ・変換・結合・ウィンドウ集計を実行します。エンジンは集計途中の状態を内部に保持し、定期的なチェックポイント(状態のスナップショット)とオフセットの巻き戻しを組み合わせることで、障害が起きても「結果への反映がちょうど1回」に見える実効的 exactly-once を実現します。処理結果はデータレイクデータウェアハウス、検索インデックス、あるいは常に最新へ更新され続けるマテリアライズドビューのような形で下流へ届けられます。

実務の勘所と落とし穴

ストリーム処理は本質的に分散システムであり、バッチにはなかった運用課題を持ち込みます。第一に流量の変動です。セールやバズで入力が急増したとき、処理が追いつかなければ遅延(ラグ)が積み上がります。下流の処理能力に合わせて流れを堰き止めるバックプレッシャーと、ラグの監視は必須です。第二に重複です。exactly-once とうたわれていても、外部システムへの出力まで含めると「少なくとも1回」に落ちる境界がどこかに残るため、出力側を冪等に設計しておくのが安全です。第三に状態と再処理です。集計ロジックを直したら過去分を流し直したくなりますが、状態のスキーマ変更や巻き戻し再処理は手順を誤ると結果を汚します。「Kafka に生イベントが残っているうちは、いつでも計算し直せる」という設計 — 導出結果を使い捨てにできる構え — を保つことが、ストリーム基盤を長く運用する鍵です。逆に言えば、日次で十分な集計まで無理にストリーム化する必要はありません。レイテンシ要件が本当に厳しい処理だけを流れに乗せ、残りはバッチに任せるのが健全な使い分けです。