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

- Name
- Youngju Kim
- @fjvbn20031
はじめに — 「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-clicks、order-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.type —
lz4または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=true、auto.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。
| 項目 | Avro | Protobuf | JSON Schema |
|---|---|---|---|
| スキーマ進化 | 優秀 | 良好 | 限定的 |
| Payload サイズ | 小 | 最小 | 大 |
| 可読性 | 低 | 低 | 高 |
| エコシステム | Hadoop/Kafka | gRPC | Web |
| Kafka 標準 | Avro 優勢 | 急上昇 | レガシー |
2025 トレンド: Protobuf (gRPC 連携、サイズ/速度)。
11. Stream Processing — Kafka Streams vs Flink vs ksqlDB
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。
技法:
linger.ms=0— 即時送信 (Throughput 犠牲)- SSD + 大 Page Cache
min.insync.replicas=2— 過剰耐久を避けるcompression.type=lz4— 最速 codecfetch.min.bytes=1— 即時 fetch- Hot Partition 監視
- Direct Memory I/O JVM チューニング
- Poll loop 最適化 (処理中 poll ブロック回避)
主要 metric: Producer record-send-rate、batch-size-avg、Broker RequestHandlerAvgIdlePercent、Consumer records-lag-max (最重要)。
15. 罠とアンチパターン
- Partition 数を後から増やす — key の hash mapping 変化で順序崩壊。最初に 3 倍確保。
- key なしで順序期待 — Round Robin で順序保証なし。
- Large Messages (
>1MB) — 既定 1MB 制限。S3 に置いて Kafka は参照のみ。 - 遅い Consumer で Rebalance 多発 —
max.poll.interval.ms(既定 5 分) 超過で kick。 - Transaction が万能の誤解 — 10-30% コスト。通知には不要。
- Topic 数爆発 — 1 万超で metadata 圧迫。header/type で統合。
- retention 未設定 — 既定 7 日。分析短く、Event Source 長く。compacted topic で最新のみ保持可能。
- Schema Registry なし運用 — Breaking change 検知不能。半年で後悔。
16. 実戦チェックリスト 12
- Partition は余裕をもって
- key 戦略を明確に
acks=all+min.insync.replicas=2- Idempotent Producer ON
- Schema Registry 導入
- Transactional Outbox
- DLQ Topic 準備
- Consumer lag 監視
- 圧縮
lz4またはzstd - KRaft モード
- Rebalance 最小化 (Static、Cooperative)
- 要件に合う 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)