2PC に頼らずに業務一貫性を保つ手法として Saga パターンは定番ですが、実運用で破綻しない設計・実装には Kafka の公式機能理解が欠かせません。
本記事は CCDAK(Confluent Certified Developer for Apache Kafka)出題範囲に寄せて、Kafka のトランザクション API、Exactly-Once セマンティクス、トピック/キー設計、補償設計を実務目線でまとめます。
Saga は「一連のローカルトランザクション」と「失敗時の補償アクション」の組合せで最終的整合性を実現します。制御方式はオーケストレーション(中央の調停役が指揮)とコレオグラフィ(各サービスがイベントを購読して自律的に次を起動)の二つ。Kafka は耐久・順序・リプレイ・スケール特性に優れ、どちらの方式でも堅牢な実装を支えます。
CCDAK 観点では、トピック分割とキーでの順序保証、プロデューサの冪等性とトランザクション、コンシューマの sendOffsetsToTransaction、Kafka Streams の exactly_once_v2 が頻出です。設計段階からこれらを織り込むと、障害時の再処理や監査も単純化します。
| 手法 | 一貫性/可用性 | 実装/運用の要点 |
|---|---|---|
| Saga-オーケストレーション(Kafka) | 最終的整合性。可観測性と制御性が高い | Orchestrator のスケール/冗長化、タイムアウト・補償の集中管理、コマンド/イベント分離 |
| Saga-コレオグラフィ(Kafka) | 最終的整合性。疎結合で拡張しやすい | イベント・スキーマの進化管理、循環や重複起動の抑止、各サービスで補償を実装 |
| 2PC/分散TX | 強整合だが可用性・レイテンシ悪化しがち | コーディネータ障害・ブロッキングのリスク。マイクロサービスと相性が悪い |
| 補償なし(アンチパターン) | 失敗時に不整合が残る | ビジネス整合性の担保不可。採用しない |
Kafka での Saga(コレオグラフィ)例
Saga の基本は「事実のイベント」を記録し、それを基に次の行為を決めることです。Kafka ではイベントとコマンドを論理的に分け、キーで順序と局所性を確保します。キーは同一ビジネスエンティティ(例: orderId)で固定し、1 パーティション内での順序性を得ます。
スキーマは後方互換を基本にし、破壊的変更を避けます。Schema Registry を用いるとスキーマ進化を測定可能にでき、CCDAK の出題意図(スキーマ互換性モード)にも沿います。ログ圧縮(compact)を併用するトピックはスナップショット性に向き、Saga の状態投影にも応用できます。
サービス内 DB 更新と Kafka 送信の一貫性は Outbox パターンで担保します。アプリは同一ローカルトランザクションでビジネス行と Outbox 行をコミットし、Outbox をポーリングするコンポーネント(または CDC/Connect)が Kafka に配信します。これにより DB と Kafka の間の二相問題を解消できます。
Kafka プロデューサは idempotence を有効化し、必要に応じてトランザクションを使います。トランザクションを用いると複数トピック/パーティションへの書き込みとコンシューマのオフセットコミットを原子的にまとめられます。Kafka Streams は exactly_once_v2 を設定すると内部的にトランザクション制御を行い、入力処理と出力の重複・欠落を抑止します。
Saga では失敗が前提です。各ステップでタイムアウトを設定し、失敗や期限切れをイベントとして事実記録します。補償はビジネス的に可逆なアクションとして設計し、冪等化します(同じ補償を何度実行しても安全)。
リトライは指数バックオフ+デッドレター(DLQ)で運用可能にします。順序性が重要な場合は、同一キー内での逐次処理と、エラーでの停止が全体をブロックしないように設計します。
オーケストレータ方式では、Saga の状態機械をステートストアに保持し、各ステップの結果イベントを受けて次のコマンドを発行します。Kafka Streams を使うと、トピック間のルーティングと状態遷移を 1 トポロジ内で管理でき、exactly_once_v2 により重複や部分失敗を抑制できます。
下記は注文イベントを起点に支払い→在庫→出荷を順次進める最小例です。実運用ではタイムアウト管理、補償遷移、監査ログ、DLQ などを追加します。
Kafka Streams による簡易オーケストレータ(Java)
Properties p = new Properties();
p.put(StreamsConfig.APPLICATION_ID_CONFIG, "saga-orchestrator");
p.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "broker:9092");
p.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2);
StreamsBuilder b = new StreamsBuilder();
StoreBuilder<KeyValueStore<String, String>> store = Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("saga-store"),
Serdes.String(), Serdes.String());
b.addStateStore(store);
KStream<String, OrderEvent> orders = b.stream("order-events",
Consumed.with(Serdes.String(), orderEventSerde()));
KStream<String, Command> cmds = orders.transformValues(() -> new SagaOrchestrator("saga-store"), "saga-store")
.flatMapValues((SagaStepResult r) -> r.outgoingCommands());
cmds.split()
.branch((k, c) -> c.type() == CommandType.AUTH_PAYMENT,
Named.as("payment"))
.to("payment-commands", Produced.with(Serdes.String(), commandSerde()));
cmds.split()
.branch((k, c) -> c.type() == CommandType.RESERVE_STOCK,
Named.as("inventory"))
.to("inventory-commands", Produced.with(Serdes.String(), commandSerde()));
// 支払い結果などのイベントを別ストリームで受け、状態を進める
KStream<String, PaymentEvent> pay = b.stream("payment-events",
Consumed.with(Serdes.String(), paymentEventSerde()));
pay.process(() -> new Processor<String, PaymentEvent>() {
private KeyValueStore<String, String> kv;
@Override public void init(ProcessorContext ctx) {
kv = (KeyValueStore<String, String>) ctx.getStateStore("saga-store");
}
@Override public void process(String key, PaymentEvent ev) {
// 状態更新と次コマンド作成(省略)
}
}, "saga-store");
KafkaStreams s = new KafkaStreams(b.build(), p);
s.start();
// 注意: Streams は内部でトランザクションを管理し、EOSv2 を実現するSaga は「観測できるか」で成否が決まります。各ステップの開始・成功・失敗・補償をイベントとして残し、相関 ID でたどれるようにします。メトリクスはコンシューマレイテンシ、リトライ率、DLQ 件数、トランザクションの中断率を継続監視します。
CCDAK 対策では、トランザクション境界、オフセットコミットの一貫性、EOS の前提、キーとパーティションでの順序保証、コンパクションと保持ポリシー、スキーマ互換性あたりを確実に押さえます。
CCDAK
問題 1
Kafka ベースの Saga 実装で、入力イベントを処理して複数トピックへ出力しつつ、同一処理単位でオフセットも原子的にコミットしたい。正しい実装はどれか。
正解: A
Kafka のトランザクションは複数トピック/パーティションへの書き込みとコンシューマオフセットを一つの原子的な境界でコミットできます。正しい手順は beginTransaction → レコード送信 → sendOffsetsToTransaction(コンシューマグループ ID とともに)→ commitTransaction。auto.commit だけでは原子性は担保されず、順次呼び出しでも障害で不整合が起き得ます。
Kafka のトランザクションは分散トランザクション(2PC)の代替になるのか?
Kafka のトランザクションは Kafka 内の複数パーティション/トピックへの書き込みとコンシューマオフセットの原子性を提供します。外部 DB と Kafka の間を直接一つの分散トランザクションにまとめるものではありません。外部系とは Outbox/C DC などのパターンを併用し、業務補償で最終的整合性を確保します。
Exactly-Once セマンティクス(EOS)は本当に"1 回きり"を保証するのか?
Kafka Streams の exactly_once_v2 やプロデューサのトランザクションにより、処理と出力、オフセットコミットの一貫性を Kafka 境界内で実現します。ただしシンク(外部 DB/API)が冪等でない場合は外側で重複防止が必要です。EOS は Kafka 内の重複・欠落を抑制するもので、システム全体の"絶対 1 回"を無条件に保証するものではありません。
補償処理が複雑で設計が難しい。どこから着手すべきか?
先にビジネス不変条件を列挙し、各ステップで破れた場合の"可逆な最小アクション"を定義します。補償は通常フローと同じくイベント化・可観測化し、冪等キーで再実行安全にします。タイムアウトと最大リトライ回数をまず明確化し、DLQ で手動介入の経路を用意すると設計が前に進みます。
NicheeLab編集部
データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。
Kafka Topic と Partition の基礎: 分散とスケーラビリティの要
CCDAK 対策と実務の両立を意識し、Topic/Partition/Replica/Consumer Group の役...
CCDAK 試験ガイド:出題範囲・配点・申込み・対策
Confluent Certified Developer for Apache Kafka (CCDAK) の出題範囲...
Confluent Certified Administrator (CCAAK) 対策: 出題範囲・配点の考え方・運用観点の要点
CCAAKに向けて、試験領域の押さえどころを運用目線で整理。プロダクションで通用する設定・監視・セキュリティの実践知を、...
Kafka の Replica と In-Sync Replicas を正しく設計する: 耐障害性と一貫性
レプリカとISRの仕組みを起点に、acks と min.insync.replicas、クリーン/アンクリーンリーダー選...
Kafka の Offset とコミット: ポジション管理と at-least-once の基礎
CCDAK 対策と実務の両立を意識して、Kafka コンシューマのオフセット管理とコミット戦略を整理。at-least-...