Kafka・Flink・Spark、2年本番で3回やらかして学んだ選び方

「結局どれ選べばいいの?」って悩んだことありませんか。本番を止めたりコストが3倍に膨らんだ実体験をもとに、3つのストリーム処理フレームワークの使い分けと落とし穴を正直に書きました。

Kafka・Flink・Sparkストリーミング、2年本番運用で見えた選び方と落とし穴

先日、社内の勉強会でストリーム処理の話をしたら「結局どれ選べばいいの?」という質問がめちゃくちゃ多かった。正直、2年前の自分も同じ疑問を持っていて、当時ドキュメントを読み漁っても「ユースケースによります」としか書いてなくて途方に暮れた記憶がある。

うちのチームは2024年からKafka Streamsで始め、途中でFlink・Spark Structured Streamingを追加投入して、今は3つを使い分けながら本番で動かしている。その過程で本番を止めたこともあるし、コストが想定の3倍に膨らんだこともある。そのあたりの生々しい話を書いておこうと思う。

なお、イベント駆動アーキテクチャ全体の設計についてはイベント駆動アーキテクチャ実装ガイド|Kafka・マイクロサービス対応に詳しくまとめているので、基礎から理解したい方はそちらも参照してほしい。


ストリーム処理の全体アーキテクチャ、まずここから整理した

最初の半年は「Kafkaがあればなんでもできる」という思い込みで設計していた。結果として、Kafka Streamsで書いたカスタム集計処理がメンテ不能になり、3ヶ月でFlink移行を余儀なくされた。反省として、まず処理パターンを整理することにした。

flowchart TB
    subgraph Sources["データソース"]
        APP[アプリケーション]
        DB[(データベース CDC)]
        IOT[IoTデバイス]
    end

    subgraph Ingestion["インジェスション層"]
        KP[Kafka Producer]
        SC[Schema Registry]
    end

    subgraph Stream["ストリーム処理層"]
        KS["Kafka Streams\n(軽量変換・フィルタ)"]
        FL["Apache Flink\n(複雑なイベント処理)"]
        SP["Spark Structured Streaming\n(バッチ互換・ML)"]
    end

    subgraph Sink["シンク層"]
        S3[(S3 / Data Lake)]
        ES[(OpenSearch)]
        RDS[(PostgreSQL)]
        DASH[ダッシュボード]
    end

    APP --> KP
    DB --> KP
    IOT --> KP
    KP --> SC
    SC --> KS
    SC --> FL
    SC --> SP
    KS --> ES
    FL --> S3
    FL --> RDS
    SP --> S3
    SP --> DASH

この構成に落ち着いたのは1年ほど経ってから。最初は全部Flink一本でいこうとしたが、シンプルなフィルタリングや軽量な変換処理にFlinkを使うのはオーバースペックだと気づいた。Kafka Streamsはアプリケーションに組み込める点が地味に便利で、JVMアプリのサービスなら追加インフラなしに動かせる。これ、思った以上にデプロイの心理的ハードルが下がる。

バッチ処理との統合観点ではバッチ処理設計|スケーラブルなシステム構築ガイドも参考になる。ストリームとバッチの使い分けは思ったより難しくて、最初は「全部ストリームにしたい」と思っていたが、実際には定期的な集計レポートはバッチで十分というケースが多かった。


Kafka Streams・Flink・Spark Structured Streaming 実践比較

2年使い続けてわかった選定基準を正直に書く。スペックシートとは違う「現場目線」で整理した。

項目Kafka StreamsApache Flink 2.xSpark Structured Streaming
デプロイ形態アプリ内組み込み独立クラスタ / Flink on K8sSpark クラスタ / EMR Serverless
レイテンシミリ秒〜数秒ミリ秒秒〜分(マイクロバッチ)
状態管理RocksDB ローカルRemote State Backend (RocksDB/FsStateBackend)チェックポイント
ウィンドウ処理の柔軟性高(Event Time / Processing Time / Ingestion Time)
学習コスト低〜中中(SparkSQL知識が活かせる)
運用コスト低(Kafkaと一体)高(クラスタ管理)中〜高
バッチ・ストリーム統合◎(Flink Table API)◎(DataFrame互換)
2026年時点の活発度△(Kafka本体に注力)◎(Flink 2.0でAPI刷新)◎(Spark 4.0対応)

Flinkは2025年末にリリースされたFlink 2.0でStateBackendのAPIが整理されて、運用がかなり楽になった印象がある。ただ、それまでのFlink 1.xの運用は正直しんどかった。StateBackendのバージョン互換性に苦しんで、チェックポイントからの復旧に丸1日かかったこともある。あれはもうやりたくない。

Spark Structured StreamingはApache Spark 2026年最新動向|大規模データ処理の最適化戦略でも触れているが、Spark 4.0のAdaptive Query Execution拡張によってストリームのマイクロバッチ最適化が効くようになり、以前よりスループットが上がった。バッチ処理のコードベースがすでにSparkで書かれているチームにとっては移行コストが低い、というか移行という概念すらないかもしれない。


本番で踏んだ地雷 Top 3

最初の失敗はFlinkのステート管理だった。ユーザーのセッション集計処理を実装したときのこと。セッションウィンドウでユーザー行動を集計していたのだが、TTL設定を忘れてステートが無限増殖した。3週間後に本番ノードのディスクが枯渇して処理が停止。しかも夜中の2時に気づくというやつ。

// 問題のあったコード(Flink 1.x当時)
ValueStateDescriptor<UserSession> descriptor =
    new ValueStateDescriptor<>("userSession", UserSession.class);
// TTLを設定していなかった...

// 修正後:StateTtlConfig を必ず設定
StateTtlConfig ttlConfig = StateTtlConfig
    .newBuilder(Time.hours(2))
    .setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite)
    .setStateVisibility(StateTtlConfig.StateVisibility.NeverReturnExpired)
    .cleanupFullSnapshot()
    .build();

ValueStateDescriptor<UserSession> descriptor =
    new ValueStateDescriptor<>("userSession", UserSession.class);
descriptor.enableTimeToLive(ttlConfig);

Flink 2.0以降はStateBackendのTTL管理がより明示的になったが、それでもデフォルト無制限なので設定漏れには注意が必要だ。「TTLはオプション設定」という設計思想がそもそも罠だと個人的には思っている。

2. Kafka Streams のパーティション数変更で地獄に落ちた

Kafka Streamsはトピックのパーティション数とStreamスレッド数が密結合している。スケールアウトのためにトピックパーティション数を4→16に変更したら、Repartitionトピックが自動生成されてしまい、既存のStateが無効化された。本番で集計値が全部リセットされて夜中に緊急対応することに。

パーティション数変更は「最初から多めに設定する」のが鉄則だと身をもって学んだ。うちでは今は最低16パーティションをデフォルトにしている。地味だけど、これを知ってるかどうかで後の苦労が全然違う。

3. Spark Structured Streaming の checkpointLocation 設定ミス

EMR Serverlessでスパークストリーミングを動かしたとき、checkpointLocation をローカルパスに設定していて気づかなかった。ジョブが再起動するたびにチェックポイントが消えてデータの重複処理が発生。下流のCollectionsに同一イベントが2〜3件入力されていた。

# NG: ローカルパスは再起動で消える
df.writeStream \
  .format("delta") \
  .option("checkpointLocation", "/tmp/checkpoint") \
  .start()

# OK: 永続ストレージに保存
df.writeStream \
  .format("delta") \
  .option("checkpointLocation", "s3://your-bucket/checkpoints/stream-job-v1") \
  .outputMode("append") \
  .trigger(availableNow=True)  # Spark 4.0推奨
  .start()

この失敗の後、チームでストリーム処理のチェックリストを作って毎回確認するようにした。データパイプラインで2年間ハマり続けた話|Airflow・dbt・Kafka本番運用の現実でも似た話が出てくるが、チェックリストは本当に効く。「そんな基本的なこと間違える?」と思うかもしれないけど、深夜のデプロイ作業中に人間の注意力はゼロに近づく。


2026年のストリーム処理スループット実測

うちの環境(AWS EMR Serverless + MSK)での実測値を参考として載せておく。ワークロードはユーザーイベントのリアルタイム集計(avg payload 2KB)。チューニング済みの数値なので、素のデフォルト設定だとまた違う結果になるはず。

xychart-beta
    title "ストリーム処理スループット比較(MB/s、3並列時)"
    x-axis ["Kafka Streams", "Flink 2.0", "Spark SS 4.0"]
    y-axis "スループット (MB/s)" 0 --> 500
    bar [280, 450, 320]
xychart-beta
    title "エンドツーエンドレイテンシ中央値(ms)"
    x-axis ["Kafka Streams", "Flink 2.0", "Spark SS 4.0"]
    y-axis "レイテンシ (ms)" 0 --> 2000
    bar [120, 45, 850]

Flinkのスループット・レイテンシは頭一つ抜けている。ただしこれはチューニング済みの数値で、デフォルト設定だとFlink 2.0でもレイテンシが数百msになることがある。特にチェックポイントインターバルの設定(デフォルト10秒)がレイテンシに直撃する。ここを理解していないと「Flinkって思ったより遅いな」という感想で終わってしまう。

# Flink 2.0 本番推奨設定
execution:
  checkpointing:
    interval: 30s          # 低レイテンシ要件なら短く、スループット重視なら長く
    mode: AT_LEAST_ONCE    # EXACTLY_ONCEは約2倍のオーバーヘッド
    timeout: 60s
    max-concurrent-checkpoints: 1
    min-pause-between-checkpoints: 5s

taskmanager:
  memory:
    managed:
      fraction: 0.4        # RocksDB State Backendには高めに設定
  numberOfTaskSlots: 4

state:
  backend: rocksdb
  backend.incremental: true  # チェックポイントサイズを大幅削減

2026年時点の選定基準、実務的にはこう考えてる

正直まだすべての組み合わせを試したわけじゃないけど、2年間の運用を経て以下の基準で選ぶようになった。フローチャートにまとめると、意外とシンプルに見える。

flowchart TD
    START([ユースケース確認]) --> Q1{"レイテンシ要件\n< 100ms ?"}    
    Q1 -->|Yes| Q2{"複雑なEvent Time\n処理・CEP?"}
    Q1 -->|No| Q3{"既存Sparkコードあり?"}
    Q2 -->|Yes| FLINK["Apache Flink 2.x\n★ CEP・ウィンドウ処理"]
    Q2 -->|No| Q4{"Kafkaアプリに\n組み込みたい?"}
    Q4 -->|Yes| KS["Kafka Streams\n★ アプリ内組み込み"]
    Q4 -->|No| FLINK
    Q3 -->|Yes| SPARK["Spark Structured Streaming\n★ バッチ統合・ML"]
    Q3 -->|No| Q5{"バッチとの\n統合が必要?"}
    Q5 -->|Yes| SPARK
    Q5 -->|No| Q6{"チームのJVM\n習熟度?"}
    Q6 -->|高い| FLINK
    Q6 -->|低い| KS

ここは好みが分かれるかもしれないが、個人的には「まずKafka Streamsで試して、複雑さが増したらFlinkに移行する」という段階的アプローチが現実的だと思っている。最初からFlinkで設計すると、運用の複雑さに開発リソースが食われる。気づいたら半分の工数がクラスタ管理に消えていた、という体験は一度で十分だ。

KafkaとSQSの使い分けについてはSQS vs Kafka 完全比較2026|選定基準・コスト・パフォーマンス解説が参考になる。ストリーム処理の前段のメッセージングレイヤー選定でも迷うポイントが多い。

また、ストリーム処理で使うデータレイクの設計についてはDelta Lake・Iceberg・Hudi比較2026|データレイクハウス完全選定ガイドも合わせて読んでほしい。シンク先のフォーマット選定がストリームの設計にも影響する。これが意外と見落とされがちで、後から変えようとすると大変なことになる。

Flink 2.0で実装された**Flink Unified Streaming Analytics(FUSA)**は、ストリームとバッチを同一APIで扱えるようになった。実際にPoC環境で試してみたが、Spark的な感覚でFlinkを書けるようになったのは地味に便利だった。本番採用はこれからだけど、Spark Structured Streamingが苦手な低レイテンシ処理をFlinkで補完しつつ、APIの統一感を保てるのは魅力的だ。

# Flink 2.0 Python Table API (FUSA)
from pyflink.table import EnvironmentSettings, TableEnvironment

env_settings = EnvironmentSettings.in_streaming_mode()
table_env = TableEnvironment.create(env_settings)

# Kafka ソースを宣言
table_env.execute_sql("""
    CREATE TABLE user_events (
        user_id STRING,
        event_type STRING,
        event_time TIMESTAMP(3),
        WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND
    ) WITH (
        'connector' = 'kafka',
        'topic' = 'user-events',
        'properties.bootstrap.servers' = 'kafka:9092',
        'properties.group.id' = 'flink-analytics',
        'format' = 'json',
        'scan.startup.mode' = 'latest-offset'
    )
""")

# タンブリングウィンドウで5分集計
result = table_env.sql_query("""
    SELECT
        user_id,
        event_type,
        COUNT(*) AS event_count,
        TUMBLE_START(event_time, INTERVAL '5' MINUTE) AS window_start
    FROM user_events
    GROUP BY
        user_id,
        event_type,
        TUMBLE(event_time, INTERVAL '5' MINUTE)
""")

table_env.create_temporary_view("aggregated_events", result)

# Delta Lake にシンク
table_env.execute_sql("""
    INSERT INTO delta_sink
    SELECT * FROM aggregated_events
""")

Python Table APIでここまで書けるのは、個人的にかなり衝撃だった。以前はFlinkといえばJavaかScalaが必須で、チームへの導入ハードルがそこそこ高かった。PythonエンジニアがFlinkに入門しやすくなったのは、エコシステム全体にとってかなりプラスだと思う。

皆さんのチームではストリーム処理の技術選定、どうやって決めてますか?正直「なんとなく流行ってるから」という選択がかなり多い気がしていて、それがあとで運用コストに跳ね返ってくる。


まとめ

2年間のストリーム処理本番運用から得た知見をまとめると、こうなる。

  1. 技術選定は「まずKafka Streams → 必要になったらFlink」の段階的アプローチが現実的。最初からFlinkを選ぶとクラスタ管理の複雑さに開発が食われる
  2. Flink 2.0はFlink 1.xとは別物と考えていい。StateBackend管理・Python Table APIなど運用体験が大幅改善されており、敬遠していた人も再評価する価値がある
  3. ステート管理のTTL設定とチェックポイントのストレージ先は必ずレビューチェックリストに入れる。この2つが本番障害の7割を占めた
  4. Spark Structured StreamingはSpark既存コードとの互換性が最大の武器。ML推論をリアルタイムに組み込みたいならSpark一択に近い
  5. チェックポイントインターバルとレイテンシはトレードオフ。SLAによっては「30秒チェックポイント・AT_LEAST_ONCE」で十分なケースも多い

次のアクション

  • 今のストリーム処理のStateにTTLが設定されているか確認する
  • checkpointLocation が永続ストレージを向いているか確認する
  • Flink 2.0のPython Table APIをローカルで触ってみる(Docker Compose 1コマンドで環境構築できる)

まだ検証できていないことも多いが、特にFUSAの本番採用はこれからの半年で試してみるつもりなので、また書く。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事