TL

CDCとログベース連携の内部

バッチのポーリングをやめ、DBの変更を低遅延で正確に下流へ流したい人へ。トランザクションログ(WAL/binlog)を読んで変更イベント化するCDCの仕組みと、順序保証・初期スナップショットとの接続・スキーマ変更追従の原理がわかります。

応用CDCDebeziumWALbinlogデータ連携ストリーミング最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.ログベースCDCはアプリへの変更を一切要求せず、DBが既にコミット済みのトランザクションログ(PostgreSQLのWAL、MySQLのbinlog)を読み、INSERT/UPDATE/DELETEを構造化イベントに変換する。クエリポーリング方式と違い削除を取りこぼさず、低遅延でDBへの追加負荷も小さい。
  • 2.ログはコミット順に並ぶ単一系列なので、CDCは1パーティション内ではトランザクションのコミット順(全順序)を保てる。コネクタはログ内の位置(LSN/GTID)をオフセットとして永続化し、再起動時はそこから再開する。これにより少なくとも一度の配信になり、下流はべき等処理を要する。
  • 3.稼働中のテーブルを取り込む初回スナップショットは『一貫したスナップショット位置のログ』と接続するのが肝で、スナップショット中の更新を取りこぼさないよう、スナップショット開始時のログ位置を記録してから読み出し、その位置以降のログを流すことで継ぎ目を埋める。

なぜログを読むのか:ポーリング方式の限界

あるDBの変更を別のシステム(検索インデックス、データウェアハウス、キャッシュ、マイクロサービス)へ伝えたい。素朴な方法はクエリポーリングです。updated_at 列を持たせ、定期的に「前回以降に更新された行」を SELECT で拾う。これは動きますが、原理的な穴があります。

  • 削除を捕捉できないDELETE された行はクエリ結果に現れないため、下流は消えたことを知れない。
  • 中間状態を取りこぼす。ポーリング間隔の中で同じ行が複数回更新されると、最後の値しか見えず履歴が欠落する。
  • 遅延とDB負荷のトレードオフ。間隔を縮めれば遅延は減るが、全件スキャンに近いクエリがDBを叩き続ける。

これらの根は同じで、ポーリングはDBの「現在の状態」を見るだけで、状態に至った「変更の系列」を見ていないことにあります。ところがDBは内部に、まさにその変更系列を持っています。クラッシュ後に復旧するためのトランザクションログです。ログベースCDC(Change Data Capture) は、このログを読むことで変更系列そのものを取り出します。

トランザクションログは『真実の系列』

PostgreSQLのWAL(Write-Ahead Log)もMySQLのbinlogも、コミットされた変更を起きた順に追記する単一の系列です。DBはこれを使ってクラッシュリカバリやレプリケーションを行います。CDCはレプリカのふりをしてこのログを購読し、DBが既に正として確定した変更だけを受け取ります。アプリ側のコード変更もスキーマへのトリガー追加も不要で、捕捉漏れも原理的に起きません。

ログから変更イベントへ:デコードの中身

CDCコネクタ(Debeziumが代表例)は、自身をDBのレプリケーションクライアントとして登録します。PostgreSQLなら論理レプリケーションのスロットを作り、MySQLならレプリカサーバーとしてbinlogをストリーム購読します。受け取る生のログレコードは物理的・低レベルな表現なので、これを論理的な行変更イベントへデコードします。

# CDCイベント1件の論理構造(概念図)
{
  "op": "u",                  # c=作成, u=更新, d=削除, r=スナップショット読み出し
  "before": { id:7, qty:3 },  # 更新前の行(UPDATE/DELETEで意味を持つ)
  "after":  { id:7, qty:5 },  # 更新後の行(INSERT/UPDATEで意味を持つ)
  "source": { lsn:..., txid:..., table:"orders", ts_ms:... }
}

before を取れるかはDB設定に依存します。PostgreSQLはテーブルの REPLICA IDENTITYFULL でないと更新前イメージが主キーだけになり、MySQLはbinlogが ROW 形式(STATEMENT ではなく)でないと行単位の前後像が得られません。「実行されたSQL文」ではなく「変更された行イメージ」を流すのがログベースCDCの要点で、これにより下流は文を再解釈せずに状態を組み立てられます。

順序保証:なぜ単一系列が効くのか

下流の正しさは順序にかかっています。同じ行に対する「在庫を5にする→3にする」が逆順で届けば、最終状態が壊れます。ここでログの性質が効きます。トランザクションログはコミット順に並ぶ単一の全順序系列なので、CDCはその順序をそのまま保てます。

ただし保証の範囲には境界があります。

スコープ順序保証理由
単一パーティション内コミット順の全順序を維持できるログが単一系列で、その順に読み出し配信するため
同一行(同一キー)常に正しい順序になる同じキーは同じパーティションへ送られるため(キーでハッシュ)
異なるパーティション間全順序は保証されない下流(Kafka等)が並列化のためキーで分割するため

実務上の定石は、イベントを行の主キーでパーティショニングすることです。こうすれば「同じ行への変更は同じパーティションに入り、コミット順を維持する」。異なる行どうしの相対順序は緩むものの、ほとんどの下流処理は行ごとに正しければ整合します。グローバルな全順序が必要なら単一パーティションにせざるを得ず、並列度を失います。これは順序と並列性のトレードオフで、一貫性モデル(/devops/consistency-models/)の選択そのものです。

オフセットと配信保証:少なくとも一度になる理由

コネクタはログ内の現在位置をオフセットとして永続化します。PostgreSQLなら LSN(Log Sequence Number)、MySQLなら GTID(Global Transaction ID) やファイル名+位置です。再起動時はこの保存済み位置から読み直すことで、欠落なく再開できます。

問題は「いつオフセットを保存するか」です。変更を下流へ書く処理と、オフセットを保存する処理は別操作なので、両者の間でクラッシュすると齟齬が出ます。

ケースA: イベント送信 → (クラッシュ) → オフセット未保存
         → 再起動後、同じ位置から再送 → 重複(at-least-once)
ケースB: オフセット先に保存 → (クラッシュ) → イベント未送信
         → 再起動後、保存位置から進む → 欠落(at-most-once)

業務データで欠落は致命的なので、CDCは先にイベントを送ってからオフセットを保存する設計を採り、少なくとも一度(at-least-once)の配信になります。すなわち重複は定常的に起こりうる前提で、下流は同じイベントが二度来ても壊れないべき等処理が必要です。source 内のLSNやtxidは安定したべき等キーとして使えます。配信保証の原理は二将軍問題に根ざしており、詳細はべき等性とexactly-once(/devops/idempotency-exactly-once/)に通じます。

レプリケーションスロットは溜まると危険

PostgreSQLの論理レプリケーションスロットは、CDCがまだ読んでいないWALをDB側に保持させ続けます。コネクタが長時間停止すると未読WALが溜まり続け、最悪ディスクを食い潰してDB本体を止めます。CDCは便利な反面、下流の停止がDBの可用性に跳ね返る結合を生む点に注意が必要です。スロットの遅延(保持WAL量)は必ず監視対象にします。

初期スナップショットとログの接続:継ぎ目を埋める

新しくCDCを始めるとき、テーブルには既に大量の既存データがあります。ログには「これから起きる変更」しか流れてこないので、最初に既存全行を読む初期スナップショットが要ります。難所は、スナップショット中もアプリは書き込みを続けるため、スナップショットとログの境目で欠落も重複も出さないことです。

鍵は順序です。スナップショットを開始する時点のログ位置を先に記録し、その一貫した読み取りでテーブルを読み切り、記録した位置からログのストリーミングを始める

1) 現在のログ位置 P を記録する
2) 一貫した読み取り(スナップショット)で全行を読み、op="r" として流す
3) 位置 P からログを購読し直し、以降の変更を op=c/u/d で流す

スナップショット中に起きた更新は、(2) で読んだ行イメージか (3) のログのどちらか(または両方)に現れます。重複しても、配信が元々 at-least-once でべき等処理を前提とするため、同じキーの後勝ちで収束します。逆に位置 Pスナップショット開始時点に取るのが肝で、これより後ろに取ると P 以前の更新を取りこぼします。Debeziumの増分スナップショット(DDD-3/ウォーターマーク方式)は、テーブルをチャンクに区切り、各チャンク読み出しの前後にログへウォーターマークを書き込むことで、巨大テーブルを止めずに、スナップショットとログの重なりを正確に裁定します。

スキーマ変更への追従

長期運用で必ず来るのが**スキーマ変更(DDL)**です。列の追加・削除・型変更が起きると、ログ中の行イメージの構造も変わります。CDCはこれをどう追うか。

MySQLのbinlogはDDLを文として記録するため、コネクタはDDLを読んでスキーマの現在像を更新します。重要なのは、過去のイベントは当時のスキーマで解釈しなければならない点です。コネクタは時点ごとのスキーマ履歴を保持し、各ログ位置のイベントをその位置で有効だったスキーマで復元します。PostgreSQLの論理デコードは出力プラグインが各変更にスキーマ情報を添えるため、構造はイベント自体から取れますが、いずれにせよスキーマのバージョン管理がCDCの一部です。

後方互換なスキーマ進化を選ぶ

下流のスキーマレジストリ(Avro/Protobuf等)と組み合わせるとき、列の追加のような後方互換な変更なら、古い消費者を壊さずに進化できます。一方で列の削除や非互換な型変更は下流を一斉に壊しかねません。CDCを敷くとDBのスキーマ変更が下流全体への公開API変更になるため、追加中心の進化に寄せ、削除は猶予期間を置くのが安全です。マイクロサービス間の結合(/devops/microservices/)と同じ規律が要ります。

まとめ

  • ログベースCDCはDBのトランザクションログ(WAL/binlog)を読み、INSERT/UPDATE/DELETEを構造化イベント化する。ポーリングと違い削除や中間状態を取りこぼさず、低遅延でDBへの追加クエリ負荷も小さい。
  • 流すのは「SQL文」ではなく変更後(と可能なら変更前)の行イメージ。前イメージには REPLICA IDENTITY FULL やbinlogのROW形式が要る。
  • ログはコミット順の単一全順序系列なので、主キーでパーティショニングすれば同一行の順序を保てる。全順序と並列度はトレードオフ。
  • コネクタはLSN/GTIDをオフセットとして永続化し、先にイベント送信→後でオフセット保存とするためat-least-once。下流はべき等処理が必須。
  • 初期スナップショットは開始時点のログ位置を先に記録してから全行を読み、その位置以降のログへ接続することで継ぎ目を埋める。
  • スキーマ変更は時点ごとのスキーマ履歴で追従し、各イベントを当時のスキーマで解釈する。DBの変更が下流の公開API変更になるため、後方互換な進化が安全。

DevOps/インフラ Article

CDCとログベース連携の内部を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

CDC

比較で見る軸

難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6

導入後に効く点

ログはコミット順に並ぶ単一系列なので、CDCは1パーティション内ではトランザクションのコミット順(全順序)を保てる。コネクタはログ内の位置(LSN/GTID)をオフセットとして永続化し、再起動時はそこから再開する。これにより少なくとも一度の配信になり、下流はべき等処理を要する。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
DevOps/インフラ
タグ数
6

判断チェックリスト

  • 自社の用途が「CDC / Debezium」に近いか確認する。
  • 強みである「ログベースCDCはアプリへの変更を一切要求せず、DBが既にコミット済みのトランザクションログ(PostgreSQLのWAL、MySQLのbinlog)を読み、INSERT/UPDATE/DELETEを構造化イベントに変換する。クエリポーリング方式と違い削除を取りこぼさず、低遅延でDBへの追加負荷も小さい。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

CDCDebeziumWALbinlogデータ連携CDCDebeziumWAL