アプリからRDBに書き込み、同じ操作をKafkaにも発行する“二重書き”は、どちらか一方の失敗で整合性が崩れます。Kafkaは外部DBとの分散トランザクション(XA)をサポートしないため、設計段階で回避が必要です。
Outboxパターンは、RDB内の単一トランザクションで業務データとイベントを一緒に永続化し、その後にKafkaへ確実に届けるための堅実な手法です。CCDAK対策としても頻出の基本設計を実務目線で解説します。
アプリがRDB更新とKafka送信を別々に行うと、途中失敗やリトライにより不整合(DB成功・Kafka未送信、または逆)が生じます。Kafkaは外部システムと2相コミットしないため(公式仕様)、アプリ側での原子的な「同時成功・同時失敗」は実現できません。
Outboxパターンでは、RDB内で業務テーブル更新とOutboxテーブルへのイベント行挿入を同一トランザクションでコミットします。これにより“イベントの存在”はDB側で原子的に保証されます。Kafkaへの配信は後続のCDC(Change Data Capture)やポーラーが担い、少なくとも一度の配信と冪等設計で重複を吸収します。
Outboxは“更新事実”を時系列で並べるログです。基本は挿入専用(append-only)とし、アプリ側トランザクション内で業務テーブルと同時に書き込みます。代表的なカラムは以下です。
id(イベントID、UUID推奨)、aggregate_type(例: Order)、aggregate_id(例: order_id)、event_type(例: OrderCreated)、payload(JSON/Avroバイナリなど)、headers(任意。schemaVersionやmessageId等)、occurred_at/created_at(順序付け用の単調増加タイムスタンプ)、partition_hint(Kafkaキーに使う値)。処理済みフラグは基本不要で、CDCやポーラー側で消費位置を管理します。
配信経路はおおむね3択です。現場ではCDC + Outboxルーティング(Kafka Connect)が扱いやすく、落ち着いた運用になりやすいです。アプリ内ポーラーは制御しやすい反面、重複と並行制御の自前実装が増えます。DBとKafkaに直接二重書きする方式は整合性リスクが高く、推奨されません。
Kafka ConnectのCDCコネクタにOutbox Event Router(例:DebeziumのEventRouter SMT)を組み合わせると、outboxテーブルの行を集約タイプやevent_typeに応じてトピックへ振り分けられます。Kafka Connectのソースは一般に少なくとも一度の配信で、重複はメッセージIDやコンパクションで吸収します(利用可否はコネクタとプラットフォームの機能に依存)。
| 手法 | 配信保証 | 冪等・重複対策 | 順序性 |
|---|---|---|---|
| CDC + Outboxルーティング(Kafka Connect) | 少なくとも一度(コネクタ実装に依存してEOSの選択肢がある場合も) | messageIdキー化 + compacted topic、コンシューマ側で重複排除 | key=aggregate_idでパーティション内順序を確保 |
| アプリ内ポーラー + トランザクショナルプロデューサ | 少なくとも一度(idempotent producerで重複抑制) | Outbox行に固有ID、プロデューサの幂等性、消費側の重複排除 | アプリでキー制御。並行ポーリング/ロックに注意 |
| DBとKafkaへ直接二重書き(アンチパターン) | 非原子的。途中失敗で不整合 | 重複/欠落の両リスク | 順序破壊の可能性あり |
Kafka Connect(Debezium Postgres + Outbox Event Router)の例
{
"name": "pg-outbox-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "debezium",
"database.password": "*****",
"database.dbname": "appdb",
"topic.prefix": "pg",
"table.include.list": "public.outbox",
"tombstones.on.delete": "false",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
"transforms.outbox.route.by.field": "aggregate_type",
"transforms.outbox.route.topic.replacement": "${routedBy}",
"transforms.outbox.table.fields.additional.placement": "event_type:header:eventType,aggregate_id:header:aggregateId",
"transforms.outbox.table.expand.json.payload": "true",
"key.converter": "org.apache.kafka.connect.storage.StringConverter",
"value.converter": "io.confluent.connect.avro.AvroConverter",
"value.converter.schema.registry.url": "http://schema-registry:8081"
}
}Kafkaのidempotent producerはプロデューサ再試行による重複を抑制しますが、DBとKafka間の“ちょうど一度”を保証するものではありません。OutboxはDB内の原子性を担保し、Kafka側は少なくとも一度で届く前提で冪等化します。
順序性はパーティション単位でのみ保証されます。同一aggregate_idでキーを固定し、同じパーティションに入るようにします。異なる集約間のグローバル順序は設計しないのが原則です。
Kafka Streamsではprocessing.guarantee=exactly_once_v2により、Kafka内の読み書きとオフセットコミットをトランザクションでまとめられます。これはKafka内部の処理区間の話で、外部DBとの原子性とは別問題である点に注意してください。
Outboxのpayloadは、スキーマ管理のしやすさを重視します。Envelope(type, version, data)の形にすると、コンシューマがevent_typeとversionでパースを切り替えやすくなります。Schema Registry(Avro/Protobuf/JSON Schema)を使えば互換性チェックや進化管理が安定します。
スキーマ進化は後方互換(backward)を基本に、フィールド追加はデフォルト値付きで行います。breaking changeが必要な場合はevent_typeやバージョンを上げ、トピックを分ける(例: order.v2)か、コンシューマで分岐します。
クリーンアップはCDCで取り込み済みを確認したうえで期限削除するか、パーティションテーブルでローテーションします。アプリ側で“処理済み”フラグを更新するより、読み取り側(CDC/ポーラー)がオフセットを持ち、再処理可能性を確保する方が安全です。
再処理・バックフィルは、Outboxを全走査して再送するだけでよい設計にしておくと運用事故に強くなります。重複を前提にしておけば、再送の心理的コストが下がります。
監視は、コネクタのタスク状態、ソースレイテンシ(DBコミット時刻−Kafka投入時刻)、コンシューマラグ、DLT(Dead Letter Topic)の件数を基本指標とします。
CCDAK
問題 1
RDBのトランザクションとKafkaへのイベント配信の整合性を担保したい。Kafkaは外部DBとの2相コミットを提供しない前提で、最も堅実なアーキテクチャはどれか。
正解: A
Kafkaは外部DBとの分散トランザクション(2PC/XA)を提供しません。OutboxパターンならRDB内で業務データとイベントを原子的に確定し、その後CDC等でKafkaへ確実に届けられます。Bはサポート外、Cはソースオブトゥルースがずれやすく、Dは二重書き問題で不整合を招きます。
Kafkaだけで“DBとのちょうど一度”は実現できますか?
できません。Kafkaのトランザクション/幂等性はKafka内の書き込みとオフセットコミットに作用します。外部DBとの原子性は提供しないため、DB側で原子的に確定させるOutboxが必要です。
Outboxのスキーマ変更はどう進めるべきですか?
Envelopeにversionを持たせ、Schema Registryで後方互換ポリシーを設定します。互換を壊す変更は新しいevent_typeや新トピック(例: order.v2)で並行運用し、コンシューマを順次移行します。
テーブル肥大化や遅延が心配です。対策は?
Outboxを日付パーティション化し、インデックス(aggregate_id, created_at)を最適化。CDCのスループットを監視し、レイテンシが閾値を超えたらスケールアウト。取り込み済み範囲の期限削除でサイズを抑制します。
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-...