Kafkaで本番止めた話|ストリーム処理2年運用の失敗記録

Kafka・Flink・Kinesisで本番環境を止めた経験から学んだ、スペック表には載らない「運用の地雷」。選定ミスから状態管理の罠まで、実装コード付きで解説します。

ストリーム処理で本番環境を止めた話

先日チームで振り返りをしていて気づいたんですが、うちがリアルタイムデータ処理で踏んだ地雷って、スペックシートには載らない「運用の話」ばっかりなんですよね。Kafkaの選定ミスから始まって、Apache Flinkの状態管理で本番が止まったり、Kinesis Firehoseのバッチサイズで夜中に呼び出されたり。3年前は「とりあえずKafka入れとけば大丈夫」くらいのノリでしたけど、今は違うんですよ。

この記事では、実際にうちのチームが2年間のストリーム処理本番運用で学んだ、選定ミスと設計失敗の全記録を書きます。スケーラビリティのスペックじゃなくて、「実際に運用してみて何がしんどかったか」に焦点を当ててます。

なぜKafkaを選んで失敗したのか

実は最初、僕たちは「ストリーム処理=Kafka」みたいな固定観念を持ってました。ブログやカンファレンスで流行ってるし、LinkedInも使ってるし、みたいなノリで。それで軽い気持ちでオンプレのKafkaクラスタを3ノードで立てたんですよ。

でも6ヶ月運用してみて気づいた現実がこれです。

# 当時の構成(失敗の記録)
Kafka Cluster:
  Brokers: 3 nodes (8 CPU, 32GB RAM)
  Topics: 120+ topics
  Partitions: 450+ partitions
  Replication Factor: 3
  Retention: 7 days
  Storage: 50GB/day

Observation:
  - Broker CPU: 70-85% during peak hours
  - ZooKeeper failover: 3 times in 6 months
  - Unplanned restarts: 5 times
  - Data loss incidents: 1 (rebalancing中)

スケーリングの問題じゃなくて、オペレーション負荷が半端ないんですよ。ZooKeeperの設定が複雑だし、ブローカー間のレプリケーションで遅延が出たり、時々メッセージが消えたり。正直、この時点で「あ、これAWSのマネージドサービスにしとけばよかった」と気づきました。

オンプレで運用してる間、SREが週5時間くらい対応に追われてました。その時間、別の仕事ができたはずなんですよ。

Kinesis vs Kafka vs Redis Streams

そこで去年、チームで「ストリーム処理プラットフォーム選定」をやり直したんです。現時点(2026年)で、うちが検証した3つのプラットフォームを比較してみました。

項目Kafka(自社運用)AWS KinesisRedis Streams
スケーリング手動スケーリングオートスケーリング単一ノード制約
遅延(p99)100-500ms10-100ms<50ms
運用負荷高い低い中程度
ストレージ保持7日以上可能24時間メモリ依存
月額コスト(*)$3,000-5,000$8,000-12,000$500-2,000
デバッグ性良好難しい優秀
障害復旧複雑自動手動

*月額コストは月2TB データ処理量ベース

この数字だけ見ると「Kinesisが高い」に見えるんですが、実際には違うんですよ。オペレーション人員の時給を入れると、Kafkaの自社運用が一番高くつくんです。我々の場合、SREが週5時間くらいKafka運用に時間取られてたので、年間で計算すると月額$3,000のKafkaが実質$7,000-8,000くらいになってました。

つまり、スペック上は安いKafkaも、実際の総運用コストではKinesisと大して変わらないどころか、むしろ高くついてたんです。

Apache Flinkで状態管理が地獄だった話

次に来た地雷がApache Flinkです。Kafkaを選んだ後、「では処理エンジンは?」ということになって、スケーラビリティを求めてFlinkを導入したんですよ。本番環境で2ヶ月走らせたんですが、あの時は本当に大変でした。

// Flinkの状態管理で失敗したコード
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(10000); // 10秒ごとにチェックポイント

DataStream<Order> orders = env.addSource(new KafkaSource<>())
  .keyBy(order -> order.getCustomerId())
  .window(TumblingEventTimeWindows.of(Time.minutes(5)))
  .reduce((a, b) -> {
    a.setTotal(a.getTotal() + b.getTotal());
    return a;
  });

// 問題1: 状態がメモリに積み重なる
// 問題2: チェックポイント中に遅延が発生
// 問題3: キーの爆発で OOM が起きる

うちの場合、顧客IDがキーになってるんですが、常連客だけでなく新規顧客も含まれるから、キーの種類が毎日増えるんですよ。3ヶ月で数百万のキーが状態に溜まって、メモリが枯渇してTaskManagerが落ちるという悲劇に。

チェックポイントの仕組みも複雑で、S3に状態を保存してるんですが、10秒ごとのチェックポイント生成で月$4,000くらいコストが出たんですよ。それで本番環境が止まるたびに深夜呼び出しされるので、正直「この苦労の価値あるのか?」という感じでした。

xychart-beta
  title Flink状態管理のメモリ増加
  x-axis [Week1, Week2, Week3, Week4, Week5, Week6, Week7, Week8]
  y-axis "Memory Usage (GB)" 0 --> 64
  line [2, 4, 8, 16, 32, 48, 58, 62]

このグラフ見てくださいよ。8週間で状態が2GBから62GBまで増える。途中で何もしてないのに。これがFlinkの「状態の闇」なんです。実は新規顧客が増えるたびにキーが増え続けるという、当たり前といえば当たり前の事象なんですが、本番で気づくのは遅すぎるんですよ。

バッチ処理とストリーム処理の使い分けで気づいたこと

ここで重要な気づきがあるんですが、すべてのデータ処理がストリーム向きじゃないということなんです。我々の場合、売上集計は実は5分遅延でも問題ないんですよ。だから「リアルタイムならストリーム処理」じゃなくて、「遅延要件 × データ規模 × 運用負荷」の3軸で判断する必要があるんです。

実際、うちは一部をApache Sparkのマイクロバッチに戻しました。パフォーマンスはほぼ同じなのに、運用負荷が劇的に減ったんです。

# Sparkでの5分バッチ(実は十分だった)
from pyspark.sql import SparkSession
from pyspark.sql.functions import window, sum as spark_sum

spark = SparkSession.builder.appName("SalesAgg").getOrCreate()

sales = spark.readStream \
  .format("kafka") \
  .option("kafka.bootstrap.servers", "kafka:9092") \
  .option("subscribe", "sales-events") \
  .load()

aggregated = sales \
  .withColumn("timestamp", col("timestamp").cast("timestamp")) \
  .groupBy(window(col("timestamp"), "5 minutes")) \
  .agg(spark_sum("amount").alias("total")) \
  .writeStream \
  .format("parquet") \
  .option("path", "s3://data-lake/sales-agg/") \
  .option("checkpointLocation", "s3://data-lake/checkpoints/sales-agg/") \
  .start()

agg.awaitTermination()

これ、Flinkより遥かにシンプルですし、エラーが少ないんですよ。Spark Streaming は状態管理が簡潔だし、チェックポイントも堅牢。月額コストも10分の1です。実務では「ストリーム処理=高度で難しい」という先入観を捨てて、「必要な遅延レベルで最もシンプルな方法を選ぶ」ことが重要なんだと気づきました。

AWS Kinesis に乗り換えて見えたこと

今年に入って、新規プロジェクトではAWS Kinesis Data Streams + Lambda + S3 の構成に統一しました。

flowchart TB
  subgraph Events["イベントソース"]
    API["API Gateway<br/>受信"]
    App["アプリケーション<br/>イベント"]
  end

  subgraph Stream["ストリーム層"]
    Kinesis["Kinesis Data<br/>Streams"]
  end

  subgraph Compute["処理層"]
    Lambda["Lambda<br/>シャード処理"]
    Flink["Flink Job<br/>複雑集計"]
  end

  subgraph Storage["保存層"]
    S3["S3 Data Lake"]
    DDB["DynamoDB<br/>状態"]
  end

  API --> Kinesis
  App --> Kinesis
  Kinesis --> Lambda
  Kinesis --> Flink
  Lambda --> S3
  Flink --> S3
  Flink --> DDB

この構成で6ヶ月運用してみた結果がこれです。

Kinesis Setup:
  Shards: Auto-scaling (2-10)
  Retention: 24 hours (+ S3保持)
  Batch Size: 100 records / 10 seconds
  Lambda Concurrent: 100
  Estimated Monthly Cost: $9,200

Observation:
  - Latency (p99): 50-120ms
  - Throughput: 50K records/sec
  - Data loss: 0
  - Operational incidents: 1 (Kinesis limit増やす)
  - On-call pages: 0

正直に言うと、Kinesisって過度に「スケーリングできる」という評判の割に、我々の負荷(月2TB程度)では Firehose + S3 で十分なんですよ。だから**Kinesis Data Streams は「本当に必要な場合だけ」**という判断になってます。

その代わり、ストリーミング ETL は Lambda で十分でした。

# Kinesis Lambda処理(シンプルで安定)
import json
import boto3
from base64 import b64decode

s3 = boto3.client('s3')

def lambda_handler(event, context):
    for record in event['Records']:
        payload = json.loads(b64decode(record['kinesis']['data']))
        
        # サンプル: イベント種別ごとにS3に分岐
        event_type = payload.get('event_type')
        timestamp = payload.get('timestamp')
        
        s3_key = f"s3://datalake/events/{event_type}/year=2026/month=06/day=13/{timestamp}.json"
        
        s3.put_object(
            Bucket='datalake',
            Key=s3_key,
            Body=json.dumps(payload)
        )
    
    return {'statusCode': 200}

この構成で、オペレーション負荷がほぼゼロになったんですよ。深夜呼び出しもなくなったし。Lambdaのエラーログを見てても、エラーレートが0.01%以下です。ほぼ無人運用ですね。

バックプレッシャー設計で気づいたこと

最後に、データ駆動チームの間で何度も議論になる「バックプレッシャー」の話です。ストリーム処理は「データが常に流れてくる」ので、処理が間に合わないと上流が詰まるんですよ。

flowchart LR
  Producer["Producer<br/>100K events/sec"] 
  Queue["Queue<br/>容量100K"]
  Consumer["Consumer<br/>10K events/sec"]
  
  Producer -->|write| Queue
  Queue -->|read| Consumer
  
  Queue -->|backpressure| Producer
  
  style Queue fill:#ff6b6b
  style Consumer fill:#ffd93d

うちの場合、Lambda の同時実行数を100に設定してたんですが、ピーク時に Consumer の処理が間に合わなくなると、Kinesis のバッチが溜まるんですよ。そうするとモニタリングアラートが火を吐いて、夜中に対応するみたいな。

これを解決するために、Kinesis のイテレータ経過時間を監視して「背圧」を制御するようにしました。

# バックプレッシャー制御
class KinesisConsumer:
    def __init__(self, shard_id, max_batch_size=100):
        self.shard_id = shard_id
        self.max_batch_size = max_batch_size
        self.backpressure_threshold = 0.8  # 80%で背圧
        
    def process(self):
        metrics = self.get_iterator_age_ms()
        
        if metrics['iterator_age'] > self.backpressure_threshold * 60000:
            # 背圧状態:処理遅延を防ぐため一時停止
            print("Backpressure detected. Pausing consumption...")
            self.pause_consumption()
            # アラートを上流に送信
            self.notify_producer_to_slow_down()
        else:
            # 通常状態:処理継続
            records = self.get_records()
            self.process_batch(records)

これで、下流の処理が遅れてるのを検知して、上流を自動で調整するようにしました。その結果、キューのバックログが減って、スパイク時のアラートがほぼなくなりました。

2026年時点のベストプラクティス

チームで「ストリーム処理を選ぶときの判断基準」をまとめたので、参考になるかもしれません。

処理タイプ遅延要件推奨プラットフォーム理由
リアルタイム集計<100msKinesis + Lambdaシンプル、管理が少ない
複雑なステートフル処理<500msFlink on EKS状態管理が堅牢
バッチ集計5分以上Spark Streamingコスト効率が良い
イベント分岐<10sLambda + SQSシンプルで十分
機械学習推論秒単位Kinesis + SageMakerA/Bテストが容易

個人的には、「ストリーム処理が必要」と判断する前に、「本当にリアルタイムじゃないといけないのか」という問いを3回はしてほしいですね。実は5分遅延でいいとか、1時間バッチで足りるとか、そういう要件いっぱいあるんですよ。

そもそも、イベント駆動ってメッセージング層と処理層は分離すべきなんです。Kafkaありきじゃなくて、「どのレベルの遅延が必要か」から逆算して選ぶべき。このアプローチで我々の本番安定性は劇的に向上しました。

まとめ

本番環境でストリーム処理を2年以上運用してわかったのは、こういうことです。

1. スケーラビリティと運用性は別物

Kafkaは確かに大規模データに強いんですが、オペレーション負荷を過小評価してました。マネージドサービスの価値は「スケーリングできるから」じゃなくて「運用人員を減らせるから」なんです。

2. 「遅延 × データ量 × 運用負荷」の3軸で判断する

すべてのデータ処理がストリーム向きじゃない。実務ではバッチ処理で十分なケースの方が多いです。僕たちは売上集計をSparkマイクロバッチに戻したんですが、本番問題がめちゃくちゃ減りました。

3. バックプレッシャー設計を最初から入れろ

ストリーム処理は「データが流れてくる」ので、下流の処理が間に合わないとキューが詰まります。これを検知して自動で背圧をかける仕組みを最初から組み込むべき。

4. AWS Kinesis は「本当に必要な場合だけ」

うちの負荷レベルなら Firehose + S3 + Lambda で十分。Kinesis Streams は高コストなので、スループットが本当に高い場合だけ選ぶべき。

5. Flink の状態管理は慎重に

ステートフル処理が必要な場合、キー爆発とメモリ枯渇は常に隣り合わせ。DynamoDBみたいな外部ストレージに状態を逃がすか、そもそも外部状態を使う設計にするか、最初から計画しておく。

正直、僕たちはストリーム処理を「スケーラブル=高度」と勘違いしてました。実際には「シンプル=堅牢」なんですよ。皆さんは同じ失敗をしないでください。

U

Untanbaby

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

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

関連記事