Skip to content
Published on

Kafka と Event-Driven Architecture 完全解剖 — Partition/ISR、Exactly-Once、Outbox、Schema Registry、Flink

Authors

はじめに — 「Kafka を使っています」の空虚さ

カンファレンス定番: 「弊社は Event-Driven で Kafka を使っています」。深掘りすると:

  • 「Partition 数の根拠は?」 → 「デフォルトで...」
  • 「Exactly-Once 保証されてる?」 → 「たぶん...」
  • 「Consumer がメッセージを失ったら?」 → 「...?」

Kafka は書きやすく運用は難しい典型。本記事では内部構造 (Partition、Leader、ISR、Log Segment)、Producer/Consumer 内部、Exactly-Once Semantics の真実、Transactional Outbox、Event Sourcing vs CDC vs Event Notification、Schema Registry、Kafka Streams vs Flink vs ksqlDB、DLQ、現代代替 (Pulsar、Redpanda)、100ms SLA 技法を解説。


1. Kafka 誕生 — 2010 LinkedIn

2010 年の LinkedIn、500 以上の ETL パイプラインが point-to-point で絡み合っていた。複雑度が爆発。Jay Kreps の提案: 「全イベントを一つの統合 Log に入れ、Consumer がそれぞれ読む」。この単純なアイデアが Kafka。Franz Kafka (作家) のファンで「よく書くシステム」と命名。2011 OSS 化、2014 Confluent 創業 (Kreps、Narkhede、Rao)。現在 Fortune 100 の 80% が利用。


2. Kafka 構成要素 — 3 分まとめ

Topic: イベント種別 (例 user-clicksorder-created)。

Partition: Topic を複数に分割。順序ある Log。並列性と順序保証の単位。

Topic: orders
├── Partition 0: [evt1] [evt2] [evt3] ...
├── Partition 1: [evt4] [evt5] ...
├── Partition 2: [evt6] [evt7] [evt8] ...

Broker: Kafka サーバー。複数で Cluster。Producer/Consumer: 書き/読みクライアント。Consumer Group: 同じ Group ID の Consumer で各 Partition は Group 内 1 Consumer に割当。

Topic: orders (3 partitions)
Group: order-processor
├── Consumer A → Partition 0
├── Consumer B → Partition 1
└── Consumer C → Partition 2

Consumer 数 > Partition 数は余剰、逆は多重担当。


3. Partition 内部 — Log Segment の物理構造

Append-only Log: 各 Partition は末尾追記のみ。

/var/lib/kafka/orders-0/
├── 00000000000000000000.log
├── 00000000000000000000.index
├── 00000000000000000000.timeindex
├── 00000000000054321099.log
├── 00000000000054321099.index
├── 00000000000054321099.timeindex
└── leader-epoch-checkpoint

Segment 分割: 1GB または 1 週間で新 Segment。削除・圧縮がファイル単位で容易、特定時点のデータのみロード可能。

Index ファイル: .index は Offset → ファイル位置 (sparse)、.timeindex は timestamp → Offset。Offset 検索は O(log N)

Zero-Copy: Linux sendfile() でディスクから NIC へカーネル内直送。

従来方式:
  ディスク → カーネル Page Cache → ユーザーバッファ → カーネル Socket Buffer → NIC
  (4 回コピー)

Zero-copy:
  ディスク → カーネル Page Cache → NIC
  (2 回、CPU 介入最小)

単一 Broker で数 GB/s 処理。Page Cache: Kafka は自前キャッシュ最小、Linux Page Cache に依存。RAM が多いほど有利。


4. Replication — Leader と ISR の政治学

各 Partition に N 個の複製。1 つが Leader、残りは Follower。Producer/Consumer は Leader のみアクセス、Follower は pull。

ISR (In-Sync Replicas): 最近 replica.lag.time.max.ms (既定 30 秒) 以内に追いついている Follower。Producer の acks:

  • acks=0 — 応答待たず、最高性能、ロスあり
  • acks=1 — Leader のみ書き込み
  • acks=all — 全 ISR 書き込み待ち

acks=all + min.insync.replicas=2 で Leader 死亡でもロスなし。

Leader Election: Controller Broker が ISR から新 Leader を昇格。Unclean Leader Election: ISR が空の場合、unclean.leader.election.enable=false (既定) でデータ保護優先。金融は必ず false。

KRaft: 2022 以降 ZooKeeper 不要。Raft ベースのメタデータ管理。Kafka 4.0 (2025) で ZooKeeper 完全除去。


5. Producer 内部 — Batching と圧縮の芸術

[App]
   │ send(record)
[Serializer] → [Partitioner] → [Accumulator (batching)]
                                   │ batch.size 到達 or linger.ms 満了
                            [Sender Thread]
                                   │ Broker ごとに batch 送信
                            [Broker Leader]

チューニング:

  • batch.size — 最大 batch サイズ (既定 16KB)
  • linger.ms — batch 待機 (既定 0、5-20ms 推奨)
  • compression.typelz4 または zstd 推奨
  • max.in.flight.requests.per.connection — Idempotence 時は <=5

Partitioner: Partition 明示 → key の hash(key) % num_partitions → sticky。

Idempotent Producer: enable.idempotence=true。Producer retry による重複書き込みを PID + sequence で防止。


6. Consumer 内部 — Offset の政治学

Offset: Partition 内の連番。Consumer がどこまで読んだかを記録。

Offset 保存場所の変遷: 2010-2014 ZooKeeper (負荷過多) → 2014+ __consumer_offsets 内部 Topic。

Auto-commit (既定): enable.auto.commit=trueauto.commit.interval.ms=5000。処理中死亡で再処理リスク。

Manual commit:

for message in consumer:
    process(message)
    consumer.commit()

配信セマンティクス: At-least-once (既定、重複あり)、At-most-once (ロスあり)、Exactly-Once Semantics (複雑)。多くは At-least-once + Consumer 冪等性で事実上 Exactly-Once。

Rebalance: Consumer 増減で Partition 再割当、全 Consumer 停止 (stop-the-world)。緩和: Static Membership (2.3+)、Cooperative Rebalancing (2.4+)。


7. Exactly-Once Semantics — 伝説の真実

2017 Confluent が Kafka 0.11 で EOS 発表。二つの要素:

1. Idempotent Producer — retry による重複書き込みを防止 (PID + sequence)。

2. Transactions — 複数 Partition にまたがる原子的書き込み + Consumer offset commit も含む。

producer.initTransactions();
try {
    producer.beginTransaction();
    producer.send(new ProducerRecord<>("topic-a", key, value));
    producer.send(new ProducerRecord<>("topic-b", key, value));
    producer.sendOffsetsToTransaction(offsets, groupId);
    producer.commitTransaction();
} catch (Exception e) {
    producer.abortTransaction();
}

Read Committed: isolation.level=read_committed で commit 済みのみ読み、abort はスキップ。

限界: EOS は Kafka 内部のみ。Kafka → DB には Transactional Outbox が必要。コスト: Throughput 10-30% 減。重要経路に限定。


8. Transactional Outbox — DB とイベントの一貫性

問題:

def create_order(order):
    db.insert(order)              # 1
    kafka.send("orders", order)   # 2

1 成功 2 失敗なら DB に order、Kafka には event なし。順序入れ替えは更に悪化。

Outbox パターン: 同一 DB Transaction で本体と event を書く。

BEGIN;
INSERT INTO orders (...) VALUES (...);
INSERT INTO outbox (event_type, payload, created_at)
  VALUES ('order.created', ..., NOW());
COMMIT;

別 Relay プロセスが outbox を Kafka に publish。

[App] ──► [DB: orders + outbox] ──► [Relay/Debezium] ──► [Kafka]

Debezium: outbox の binlog/WAL を読み自動で Kafka に publish。DB Transaction で一貫性、Kafka 障害でも DB に残る、再処理可能。罠: outbox は肥大化 (cleanup 必須)、Relay は at-least-once (Consumer は冪等に)。


9. Event Sourcing vs CDC vs Event Notification

Event Notification: 最小 payload。Consumer は詳細を API で取得。

{ "type": "order.created", "id": 42 }

結合低いが追加呼び出し必要。

Event-Carried State Transfer: 全状態を event に載せる。

{
  "type": "order.created",
  "id": 42,
  "items": [],
  "total": 99.99,
  "customer": {}
}

追加呼び出し不要、payload 大、スキーマ変更の影響大。

Event Sourcing: 状態ではなく状態を生んだ全 event を保存。replay で現在状態再構成。

OrderCreated → ItemAdded → ItemAdded → DiscountApplied → OrderPaid

完全な監査ログ、過去再現。複雑、クエリ困難 (CQRS 必須)、スキーマ進化困難。

CDC (Change Data Capture): DB 変更そのものを event 化。

{
  "op": "u",
  "before": { "status": "pending" },
  "after": { "status": "paid" }
}

アプリ改修不要、Single Source of Truth。DB スキーマが契約化。

現実は三者混合: Core は Outbox、インデックス/分析は CDC、通知は Event Notification。


10. Schema Registry — 契約の守護者

問題: Producer がスキーマ変更、Consumer が追従できずパース失敗。Schema Registry は中央保存 + 互換性検証 (Confluent 発案)。

フロー: Producer が書く前に登録 → Registry が既存と互換性チェック → Breaking change は拒否 → メッセージは Schema ID のみ保持、Consumer が ID で取得。

互換性モード:

  • BACKWARD — 新スキーマで旧データ読める (追加 OK、削除 NG)
  • FORWARD — 旧スキーマで新データ読める
  • FULL — 双方向
  • NONE — 検証なし

Producer 先行デプロイは BACKWARD。

項目AvroProtobufJSON Schema
スキーマ進化優秀良好限定的
Payload サイズ最小
可読性
エコシステムHadoop/KafkagRPCWeb
Kafka 標準Avro 優勢急上昇レガシー

2025 トレンド: Protobuf (gRPC 連携、サイズ/速度)。


Kafka Streams (ライブラリ): Java/Scala アプリに embedded、別クラスタ不要。

stream.filter(...)
      .map(...)
      .groupByKey()
      .count()
      .to("output-topic");

シンプル、JVM 専用、大規模状態に弱い。

Apache Flink (別クラスタ): Stream 処理の覇王。True streaming、Exactly-Once checkpointing、TB 規模の状態、Event time 処理優秀。学習曲線急。

ksqlDB: Kafka 上の SQL エンジン。

CREATE STREAM orders_large AS
  SELECT * FROM orders WHERE amount > 1000;

高速プロトタイピング、複雑ロジックは Flink に劣る。

選択: JVM + 単純 + Kafka 中心 = Streams、複雑/大規模/多言語 = Flink、SQL 速攻 = ksqlDB。Flink 採用が近年急増。AWS Managed Flink、Confluent Flink 提供。


12. Dead Letter Queue と再処理

処理例外時の選択: 永遠 retry (poison pill で全停止)、skip + log (ロス)、DLQ に退避。

[topic: orders] ──► [Consumer] ──► 失敗 ──► [topic: orders-dlq]
                    [DLQ Processor] — 手動/自動再処理

戦略: Alert のみ、TTL 付き自動 retry、コード修正後 replay、条件で Kill Switch。Spring Cloud Stream、Kafka Streams DSL に DLQ 組込み (DeadLetterPublishingRecoverer)。


13. Pulsar、Redpanda — Kafka の代替

Apache Pulsar (Yahoo 2016): Broker (無状態) と Storage (BookKeeper) 分離。Multi-tenancy ネイティブ、Geo-replication 標準、Kafka プロトコル互換。スケール独立、運用複雑 (3 層)。

Redpanda (2020): C++ 再実装、JVM なし。Kafka プロトコル互換でドロップイン。ZooKeeper なしの Raft、単一バイナリ。同一ハードで 10 倍性能、p99 ミリ秒、運用単純。エコシステムは Kafka が大。

WarpStream (2023): S3 ベース、ローカルディスクなし。保存コスト激減、遅延は 200ms 程度 (S3 特性)、Streaming 分析向け。

選択: 標準/エコシステム = Kafka、単純運用 + 高性能 = Redpanda、Multi-tenant 大規模 = Pulsar、保存コスト最小 = WarpStream。


14. 100ms SLA 達成技法

目標: Producer → Broker <5ms、Broker ディスク反映 <10ms、Broker → Consumer <5ms、処理 <50ms、合計 <100ms

技法:

  1. linger.ms=0 — 即時送信 (Throughput 犠牲)
  2. SSD + 大 Page Cache
  3. min.insync.replicas=2 — 過剰耐久を避ける
  4. compression.type=lz4 — 最速 codec
  5. fetch.min.bytes=1 — 即時 fetch
  6. Hot Partition 監視
  7. Direct Memory I/O JVM チューニング
  8. Poll loop 最適化 (処理中 poll ブロック回避)

主要 metric: Producer record-send-ratebatch-size-avg、Broker RequestHandlerAvgIdlePercent、Consumer records-lag-max (最重要)。


15. 罠とアンチパターン

  1. Partition 数を後から増やす — key の hash mapping 変化で順序崩壊。最初に 3 倍確保。
  2. key なしで順序期待 — Round Robin で順序保証なし。
  3. Large Messages (>1MB) — 既定 1MB 制限。S3 に置いて Kafka は参照のみ。
  4. 遅い Consumer で Rebalance 多発max.poll.interval.ms (既定 5 分) 超過で kick。
  5. Transaction が万能の誤解 — 10-30% コスト。通知には不要。
  6. Topic 数爆発 — 1 万超で metadata 圧迫。header/type で統合。
  7. retention 未設定 — 既定 7 日。分析短く、Event Source 長く。compacted topic で最新のみ保持可能。
  8. Schema Registry なし運用 — Breaking change 検知不能。半年で後悔。

16. 実戦チェックリスト 12

  1. Partition は余裕をもって
  2. key 戦略を明確に
  3. acks=all + min.insync.replicas=2
  4. Idempotent Producer ON
  5. Schema Registry 導入
  6. Transactional Outbox
  7. DLQ Topic 準備
  8. Consumer lag 監視
  9. 圧縮 lz4 または zstd
  10. KRaft モード
  11. Rebalance 最小化 (Static、Cooperative)
  12. 要件に合う Stream 処理ツール選択

次回予告 — Redis 内部と分散キャッシュ戦略

Kafka がイベントのバスなら、Redis はリアルタイムデータの一時保存かつ演算エンジン。次回: シングルスレッド設計の速さ、データ構造 (String、List、Set、Hash、Sorted Set、Stream、HyperLogLog、Bitmap)、Redis Cluster の Hash Slot と Redirection、Sentinel HA、RDB vs AOF、Cache-Aside/Write-Through/Write-Behind、Thundering Herd 対策、Redlock 論争、Valkey (2024)、Dragonfly/KeyDB マルチスレッド代替、2024 ライセンス変更の衝撃。

「Redis はデータ構造サーバーだ。キャッシュは使い道の一つに過ぎない」 — Salvatore Sanfilippo (antirez)