論理レプリケーションとCDC・論理デコーディング
WALを行レベルの変更ストリームへ変える仕組みが分かります。論理デコーディング・レプリケーションスロット・出力プラグインの原理から、本番無負荷でCDCを成立させる設計までを内部から解き明かします。
- 1.論理デコーディングはWALレコードを再アセンブルして「どの行がどう変わったか」という論理変更へ変換する。リオーダーバッファでトランザクション単位にまとめ、コミット順に出力する。
- 2.レプリケーションスロットは消費位置(confirmed_flush LSN)を永続化し、未消費のWALが回収されないよう保持する。これがat-least-once配送と再接続時の欠落防止を支える。
- 3.出力プラグインがデコード結果をJSON・protobuf・論理複製プロトコルなどへ整形し、ダウンストリームのCDCパイプライン(検索・キャッシュ・DWH)へ流す。
WAL から「行の変更」を取り出すという発想
WAL(先行書き込みログ)は本来、クラッシュリカバリのために「ページのどのバイトをどう書き換えたか」という物理操作を記録します。レプリカはこれを redo と同じ機構でそのまま再生すれば、プライマリと厳密に同一のバイト像になります。しかしこの物理表現は、版・OS・ページ形式の完全一致を要求し、テーブル単位の選択も異種DB連携もできません(物理 vs 論理の全体像)。
論理デコーディングは、この同じWALを起点に、まったく別の抽象へ翻訳する仕組みです。狙いは「ストレージの再現」ではなく「データの再現」、すなわち テーブル T の行が INSERT され、各列の値はこれ という行レベルの変更イベントをWALから取り出すことにあります。これができれば、レプリケーションだけでなく、変更を外部のあらゆるシステムへ波及させる**CDC(Change Data Capture)**の土台になります。
問題は、WALは論理的な意図をそのまま持っていない点です。記録されているのは物理操作であり、しかも複数トランザクションのレコードが時間順にインターリーブされています。論理デコーディングは、ここから元のトランザクション構造と列の意味を復元しなければなりません。
論理デコーディングの内部動作
論理デコーディングは、おおむね次の段階でWALを論理変更へ変換します。
| 段階 | やること | なぜ必要か |
|---|---|---|
| WAL 読み出し | スロットの開始位置からWALレコードを順に読む | 変更の発生源はWALだけ |
| 再アセンブル | ヒープの物理変更を tuple(行イメージ)へ復元する | 物理操作には行の論理像が直接無い |
| カタログ参照 | TupleをテーブルID・列定義に結び付け、列名と型を与える | WALには列名が無く OID しか無い |
| リオーダー | リオーダーバッファでXID単位に変更を蓄積する | WALは複数Txがインターリーブしている |
| コミット時出力 | コミットレコード到達時に、そのTxの変更をコミット順で出す | 未コミット変更を外部へ見せないため |
中核は**リオーダーバッファ(reorder buffer)**です。WALを前から読むと、まだコミットしていない複数トランザクションの変更が混ざって流れてきます。これをそのまま出すと、ロールバックされる変更まで配ってしまいます。そこでデコーダはトランザクションID(XID)ごとに変更を一時バッファへ溜め、コミットレコードに到達した瞬間に、そのトランザクションの変更群をまとめてコミット順に出力します。アボートしたトランザクションのバッファは破棄されます。これにより、出力ストリームは「コミット済みの変更だけが、コミットされた順に並ぶ」ことが保証されます。
WALレコードはテーブルを OID(オブジェクト識別子)で、列を番号で参照し、列名や型は持ちません。デコーダはシステムカタログを引いて意味を与えますが、ここに罠があります。古いWALをデコードしている最中に、その間にテーブルのスキーマが変わっていることがあるのです。正しいデコードには「その変更が起きた時点のカタログ」が必要で、これを**履歴スナップショット(historic snapshot)**で再現します。論理デコーディングがDDLの扱いに敏感なのはこのためです。
UPDATE と DELETE では、どの行が対象かを下流が特定できなければなりません。そのために**識別子(PostgreSQL では REPLICA IDENTITY)**の設定が効きます。既定では主キーを旧像として送りますが、FULL にすると全列の旧値を送れます。主キーが無く識別子も未設定のテーブルでは、UPDATE/DELETE をデコードできず失敗します。設計時に見落としやすい前提です。
レプリケーションスロット――位置と保持の管理
論理デコーディングを「止めても欠落しない」配送にしているのがレプリケーションスロットです。スロットは消費者ごとに作られる永続的な状態で、二つの役割を持ちます。
第一に、消費位置の永続化です。消費者が「ここまで受け取って永続化した」と通知すると、スロットはその位置(confirmed_flush LSN)を記録します。消費者が切断・再接続しても、スロットはこの位置を覚えているので、最後に確定した位置の直後からストリームを再開できます。これが再接続時の取りこぼし防止の根幹です。
第二に、**WALの保持(retention)**です。スロットは「まだ消費されていない最古の位置」を restart_lsn として保持し、データベースに対して「この位置より新しいWALは捨てるな」と要求します。論理デコードはコミット前から変更を読み始める必要があるため、restart_lsn はコミットされていないトランザクションの開始位置までさかのぼります。
スロットの保持機能は両刃の剣です。消費者が止まったまま放置されると、restart_lsn が進まず、WALが回収されずに無限に溜まり、最終的にディスクを満杯にしてプライマリを停止させます。これは論理レプリケーション運用で最も多い障害の一つです。対策として、保持上限(max_slot_wal_keep_size 等)を設けてスロットを犠牲にする設定や、不要スロットの監視・削除が必須になります。
物理レプリケーション用スロットも restart_lsn でWAL保持を行いますが、デコードはしません。論理スロットはこれに加えて出力プラグインと履歴スナップショットを束ね、1スロット=1論理ストリームとして、特定のデータベース・出力形式に固定されます。だから論理スロットは作成時にプラグインとDBが決まり、後から付け替えできません。
出力プラグイン――デコード結果をどの形で出すか
リオーダーバッファが組み立てた論理変更を、実際に流す形式へ整形するのが出力プラグインです。デコーダ本体(変更の取り出しと順序付け)と、出力形式(バイト列への変換)を分離した設計であり、ここがCDCの拡張点になります。
プラグインは、デコーダから渡されるコールバック(トランザクション開始・各行の変更・コミット)に応じて、任意のフォーマットを生成します。代表例は次の通りです。
| 出力プラグイン | 形式・用途 | 特徴 |
|---|---|---|
| pgoutput | 論理レプリケーションの標準プロトコル | PostgreSQL同士の論理複製で既定。バイナリ |
| wal2json | 変更を JSON で出力 | 外部CDCで扱いやすく可読。デバッグにも有用 |
| decoderbufs | protobuf で出力 | スキーマ付きで効率的。Debezium 等が利用 |
| test_decoding | 人間可読のテキスト | 学習・検証用。本番配送には非推奨 |
この分離があるおかげで、同じ論理デコード基盤の上に、PostgreSQL間レプリケーションも、JSONを食う外部パイプラインも、protobufで効率配送するCDC基盤も、プラグインを差し替えるだけで載せられます。MySQL には論理デコーダという同名の機構はありませんが、行ベースの binlog(ROW フォーマット)を読むことで等価な行レベル変更ストリームが得られ、CDC ツールはこれを抽象化して同じインターフェースで扱います。
CDC パイプラインへの応用
論理デコーディングと出力プラグインを組み合わせると、行レベルの変更ストリームを外部へ公開するCDCが成立します。狙いはレプリカ作成にとどまらず、検索インデックス・キャッシュ・データウェアハウス・ストリーム処理へ、データベースの全変更を順序付きで波及させることにあります。ログベースCDCは元からあるWALを読むだけなので、本番の書き込み経路にほぼ追加負荷をかけず、削除も含めて取りこぼしません。トリガベースやポーリング差分が抱える「アプリ負荷の増大」「削除の取りこぼし」を構造的に回避できます。
典型的なパイプラインは「初期スナップショット+以後の変更ストリーム」で構成されます。
1. スナップショット取得開始時の LSN を記録(snapshot_lsn)
2. 既存データを一括コピー(初期ロード)
3. snapshot_lsn 以降の論理変更ストリームを購読
4. 各変更を主キー UPSERT で冪等に適用
5. confirmed_flush LSN を永続化し、スロットへ ack
スナップショットを撮っている間も変更は流れ続けます。両者の境界を雑に繋ぐと、スナップショットに含まれた変更がストリームでも届いて重複したり、逆にどちらにも入らず欠落したりします。正しい実装は、スナップショット地点の LSN を起点にストリームを重なりを許して接合し、重複は後段の冪等適用(主キー UPSERT+適用済み LSN の比較)で吸収します。論理デコードがコミット順を保証することが、この接合の前提になります。
整合性の保証は、本質的に二本柱です。第一に位置管理――各変更は単調増加する LSN(MySQL なら GTID)を持ち、消費者は「どこまで適用したか」をこれで永続化します。第二に冪等適用――ストリーミングはほぼ at-least-once(重複しうる)なので、再接続で同じ変更を二度受け取りえます。主キー UPSERT と適用済み位置の比較で、重複しても結果が変わらないようにします。さらに、トランザクション境界をまたいで適用しないことで、部分適用による中間状態の露出を防ぎます。リオーダーバッファがTx単位でまとめて出すのは、この境界尊重を下流で容易にするためでもあります。
「論理デコーディングを一言で」と問われたら、WALレコードを再アセンブルし、カタログで意味を与え、リオーダーバッファでTx単位・コミット順に並べて、行レベルの変更へ変換する仕組み。配送の信頼性はレプリケーションスロット(消費位置の永続化+WAL保持)が支え、出力形式は出力プラグインが決める。CDCはこれを外部公開した応用で、整合性はLSN/GTIDの位置管理+at-least-once+冪等適用で守る、と層を分けて答えられると強いです。
まとめ
論理デコーディングは、クラッシュリカバリ用の物理ログであるWALを、行レベルの論理変更ストリームへ翻訳する仕組みです。WALレコードを再アセンブルし、システムカタログ(履歴スナップショット)で列の意味を与え、リオーダーバッファでトランザクション単位に集約してコミット順に出力します。これを「止めても欠落しない」配送にしているのがレプリケーションスロットで、消費位置の永続化とWAL保持という二役を担いますが、止まったスロットがWALを溜め込む運用リスクと裏表です。出力形式は出力プラグインで差し替えでき、同じ基盤の上に論理レプリケーションも外部CDCも載せられます。応用としてのCDCは、本番に無負荷で全変更を順序付きに公開し、整合性はLSN/GTIDの位置管理と冪等適用で担保します。トランザクションの境界を尊重したコミット順出力こそが、下流での正しい再構成を可能にする要です。
データベース Article
論理レプリケーションとCDC・論理デコーディングを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
論理デコーディング
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
レプリケーションスロットは消費位置(confirmed_flush LSN)を永続化し、未消費のWALが回収されないよう保持する。これがat-least-once配送と再接続時の欠落防止を支える。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「論理デコーディング / CDC」に近いか確認する。
- 強みである「論理デコーディングはWALレコードを再アセンブルして「どの行がどう変わったか」という論理変更へ変換する。リオーダーバッファでトランザクション単位にまとめ、コミット順に出力する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。