Kafka

RDBとKafkaの整合性を守る Outbox パターン実践

2026-04-19
NicheeLab編集部

アプリからRDBに書き込み、同じ操作をKafkaにも発行する“二重書き”は、どちらか一方の失敗で整合性が崩れます。Kafkaは外部DBとの分散トランザクション(XA)をサポートしないため、設計段階で回避が必要です。

Outboxパターンは、RDB内の単一トランザクションで業務データとイベントを一緒に永続化し、その後にKafkaへ確実に届けるための堅実な手法です。CCDAK対策としても頻出の基本設計を実務目線で解説します。

なぜOutboxパターンが必要か:二重書き問題と整合性

アプリがRDB更新とKafka送信を別々に行うと、途中失敗やリトライにより不整合(DB成功・Kafka未送信、または逆)が生じます。Kafkaは外部システムと2相コミットしないため(公式仕様)、アプリ側での原子的な「同時成功・同時失敗」は実現できません。

Outboxパターンでは、RDB内で業務テーブル更新とOutboxテーブルへのイベント行挿入を同一トランザクションでコミットします。これにより“イベントの存在”はDB側で原子的に保証されます。Kafkaへの配信は後続のCDC(Change Data Capture)やポーラーが担い、少なくとも一度の配信と冪等設計で重複を吸収します。

  • Kafkaは外部DBとの分散トランザクションを提供しないため、DB内で原子化するのが基軸
  • Outboxテーブルにイベントを行として記録し、CDC/ポーラーがKafkaへ非同期送信
  • 配信は少なくとも一度が基本。重複はキー設計やコンパクション、コンシューマの冪等処理で吸収
Applicationbegin tx / write business rows / insert into outbox / commitKafka Connect / CDC(Outbox Event Router等)CDC / PollerKafka Topic(s)key = aggregate_idConsumers / StreamsEOSv2/idempotencyOutboxパターンの全体像(RDB原子化 + CDC/ポーラー → Kafka)

Outboxテーブル設計:最小カラムとメタデータ

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やポーラー側で消費位置を管理します。

  • キー設計:Kafkaのkeyにaggregate_idを用い、同一集約の順序性をパーティションで担保
  • 順序指標:commit順序に依存しつつ、occurred_atやシーケンス番号で補助
  • シリアライズ:Schema Registryを使うならpayloadにスキーマID参照を格納するか、CDC側でエンコード
  • クリーンアップ:テーブル肥大化対策はパーティショニングや期限削除(CDC後)。CDC対象外の物理削除は注意
  • トリガや更新系ロジックは最小化。基本はINSERTのみで副作用を減らす

実装オプション: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ルーティング(シンプルな責務分離、監視・再処理が容易)
  • 代替: アプリ内ポーラー(制御しやすいが運用・再送・ロック制御の自前実装が増える)
  • 非推奨: アプリからDBとKafkaへ直接二重書き(不整合の温床)
手法配信保証冪等・重複対策順序性
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の原則に沿う

Kafkaのidempotent producerはプロデューサ再試行による重複を抑制しますが、DBとKafka間の“ちょうど一度”を保証するものではありません。OutboxはDB内の原子性を担保し、Kafka側は少なくとも一度で届く前提で冪等化します。

順序性はパーティション単位でのみ保証されます。同一aggregate_idでキーを固定し、同じパーティションに入るようにします。異なる集約間のグローバル順序は設計しないのが原則です。

Kafka Streamsではprocessing.guarantee=exactly_once_v2により、Kafka内の読み書きとオフセットコミットをトランザクションでまとめられます。これはKafka内部の処理区間の話で、外部DBとの原子性とは別問題である点に注意してください。

  • プロデューサ: enable.idempotence=true、必要ならacks=all、適切なretries
  • キー: aggregate_idで固定し、同一集約の順序性を確保
  • 重複排除: messageIdをキー化、compacted topic、消費側のlast-seenテーブル等で吸収
  • Streams/Sink: EOSv2やトランザクショナルシンクで“Kafka内の原子性”を確保

スキーマ設計と進化:Envelope戦略とSchema Registry

Outboxのpayloadは、スキーマ管理のしやすさを重視します。Envelope(type, version, data)の形にすると、コンシューマがevent_typeとversionでパースを切り替えやすくなります。Schema Registry(Avro/Protobuf/JSON Schema)を使えば互換性チェックや進化管理が安定します。

スキーマ進化は後方互換(backward)を基本に、フィールド追加はデフォルト値付きで行います。breaking changeが必要な場合はevent_typeやバージョンを上げ、トピックを分ける(例: order.v2)か、コンシューマで分岐します。

  • Envelope: {type, version, data, metadata} で拡張に強い形に
  • 互換性: backward互換を基本。デフォルト値とnullableで移行を滑らかに
  • ルーティング: event_typeやaggregate_type単位でトピック分割を検討
  • コンパクション: upsert系ならcompacted topicが有効(キー重複の最新化)

運用・監視:クリーンアップ、再処理、遅延の扱い

クリーンアップはCDCで取り込み済みを確認したうえで期限削除するか、パーティションテーブルでローテーションします。アプリ側で“処理済み”フラグを更新するより、読み取り側(CDC/ポーラー)がオフセットを持ち、再処理可能性を確保する方が安全です。

再処理・バックフィルは、Outboxを全走査して再送するだけでよい設計にしておくと運用事故に強くなります。重複を前提にしておけば、再送の心理的コストが下がります。

監視は、コネクタのタスク状態、ソースレイテンシ(DBコミット時刻−Kafka投入時刻)、コンシューマラグ、DLT(Dead Letter Topic)の件数を基本指標とします。

  • クリーンアップ: 期限削除 or パーティションローテーション。CDC対象外化の順序に注意
  • 再処理: Outbox全件の再送を想定し、重複安全にする
  • 監視: CDCタスク健全性、レイテンシ、ラグ、DLT件数、スループット
  • 容量見積: Outboxはピーク時のバックログを吸収できるサイズに。インデックス最適化も忘れずに

問題で確認

CCDAK

問題 1

RDBのトランザクションとKafkaへのイベント配信の整合性を担保したい。Kafkaは外部DBとの2相コミットを提供しない前提で、最も堅実なアーキテクチャはどれか。

  1. RDB内の単一トランザクションで業務データとOutbox行をコミットし、CDC(Outboxルーティング)でKafkaへ送る
  2. アプリのDBトランザクション中にKafkaトランザクションも開始して同時にコミットする
  3. DBには書かず、Kafkaだけに書いて後でバッチでDBへ反映する
  4. アプリからDBとKafkaに直接二重書きし、失敗時はリトライに任せる

正解: 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のスループットを監視し、レイテンシが閾値を超えたらスケールアウト。取り込み済み範囲の期限削除でサイズを抑制します。

この記事で学んだ内容を問題で確認しましょう

16,000問以上の問題で実力チェック

無料で問題を解いてみる
この記事の著者

NicheeLab編集部

データエンジニアリング・クラウド資格の専門家。Databricks・Snowflake等の認定資格を保有し、実務経験に基づいた問題作成・解説を行っています。NicheeLab運営。


関連記事
Kafka

Kafka Topic と Partition の基礎: 分散とスケーラビリティの要

CCDAK 対策と実務の両立を意識し、Topic/Partition/Replica/Consumer Group の役...

Kafka

CCDAK 試験ガイド:出題範囲・配点・申込み・対策

Confluent Certified Developer for Apache Kafka (CCDAK) の出題範囲...

Kafka

Confluent Certified Administrator (CCAAK) 対策: 出題範囲・配点の考え方・運用観点の要点

CCAAKに向けて、試験領域の押さえどころを運用目線で整理。プロダクションで通用する設定・監視・セキュリティの実践知を、...

Kafka

Kafka の Replica と In-Sync Replicas を正しく設計する: 耐障害性と一貫性

レプリカとISRの仕組みを起点に、acks と min.insync.replicas、クリーン/アンクリーンリーダー選...

Kafka

Kafka の Offset とコミット: ポジション管理と at-least-once の基礎

CCDAK 対策と実務の両立を意識して、Kafka コンシューマのオフセット管理とコミット戦略を整理。at-least-...

Kafkaの記事一覧 (100件)
© 2026 NicheeLab All rights reserved.