📝
概念 #データ設計 #WAL #レプリケーション #PostgreSQL #CDC #DDIA 📚 データ志向アプリケーション設計(DDIA)

WALと論理レプリケーション

PostgreSQLのWAL(Write-Ahead Log)の仕組みと、物理レプリケーション・論理レプリケーションの違い。CDCとの関係、レプリケーションスロットの設計を理解する

定義

WAL(Write-Ahead Log):データを実際に書き込む前に、「何をするか」をログに記録するメカニズム。「先にログ」という原則がクラッシュ安全性の基盤。

WALの役割

クラッシュリカバリ

WALなし(ダングラスDB)の問題:
  1. ページをディスクに書き込み中にクラッシュ
  2. ページが半分だけ書かれた状態で残る(Partial Write)
  3. 再起動後、データが壊れている

WALあり:
  1. まずWALに「このページをこう変更する」を追記(シーケンシャルI/O)
  2. WAL書き込み完了後にコミット応答
  3. 実際のデータページは後で書く(バックグラウンド)
  4. クラッシュ時: WALを再生して正しい状態に復元できる

シーケンシャルI/OはランダムI/Oより速い:WALはディスクの末尾に追記するだけなので、ページのランダム書き込みより大幅に高速。

PostgreSQLのWAL実装

$PGDATA/pg_wal/以下に格納
ファイル名例: 000000010000000000000001(24桁の16進数)

構造:
  LSN(Log Sequence Number): WALのバイトオフセット
  各WALレコード: LSN + 変更の内容(前のデータ, 後のデータ)
  
LSNで「どこまで処理したか」を管理する

物理レプリケーション vs 論理レプリケーション

物理レプリケーション(Streaming Replication)

WALのバイト列をそのままスタンバイに送る。

プライマリ
  │ WALストリーム(バイナリ)

スタンバイ1 → WALを適用してデータを復元
スタンバイ2

特徴:
  - バイナリ形式なのでバージョンが同じ必要
  - テーブル単位ではなくDB全体をレプリケーション
  - スタンバイは読み取り専用(Hot Standby)
  - フェイルオーバー用途に使われる

論理レプリケーション(Logical Replication)

WALを「行レベルの変更」にデコードして送る。

プライマリ
  │ WALのデコード(行の INSERT/UPDATE/DELETE に変換)

スタンバイ / 別のDB / Kafka / Debezium

特徴:
  - テーブル単位で選択可能
  - バージョンが違うDBへも送れる(アップグレード移行に使える)
  - 異なるDBエンジンへの送信も可能
  - サブスクライバー側での書き込みも可能(双方向)
-- 論理レプリケーションの設定
-- プライマリ側
ALTER SYSTEM SET wal_level = 'logical';
CREATE PUBLICATION my_pub FOR TABLE users, orders;

-- スタンバイ側
CREATE SUBSCRIPTION my_sub
  CONNECTION 'host=primary dbname=mydb'
  PUBLICATION my_pub;

レプリケーションスロット

スタンバイが遅れても、プライマリがWALを削除しないようにする仕組み。

問題:
  スタンバイが長時間落ちている間、プライマリはWALを削除する
  → スタンバイが復帰しても必要なWALがない → 全量コピーが必要

解決:
  レプリケーションスロットを作成
  → スタンバイの最後のLSNまでWALを保持し続ける
-- スロット確認
SELECT * FROM pg_replication_slots;

-- 危険: スタンバイが長期停止中にスロットが残っていると
-- pg_walディレクトリが無限に肥大化してディスク満杯になる

本番運用の注意点:モニタリングに pg_replication_slotslagpg_wal のディスク使用量を含める。

CDCとWAL

データシステムの統合設計で触れたCDCはWALを使って実現する。

Debeziumの動作:
  1. PostgreSQLにレプリケーションスロットを作成
     (論理レプリケーション用のpgoutputプラグイン使用)
  2. WALを論理デコード → 行レベルの変更イベント
  3. KafkaにJSON/Avroで発行

Kafkaトピック例:
  mydb.public.users → {"op":"u","before":{...},"after":{...}}
  op: c=insert, u=update, d=delete, r=read(スナップショット時)
メリット:
  - アプリコードを変更せずに変更ストリームを取得
  - DBの負荷がほぼゼロ(WAL読み取りのみ)
  - 削除も捕捉できる(アプリレベルのフックでは難しい)

デメリット:
  - スキーマ変更(ALTER TABLE)の扱いが複雑
  - 初期スナップショット取得時の整合性管理
  - レプリケーションスロットのディスク管理

WALの設定パラメータ

wal_level:
  minimal    → 最小限(クラッシュリカバリのみ)
  replica    → 物理レプリケーション(デフォルト)
  logical    → 論理レプリケーション・CDC用

synchronous_commit:
  on         → WALがディスクに書かれるまでコミット待ち(デフォルト)
  off        → WALのディスク書き込みを待たない(高速、クラッシュで最大数秒のデータロス)
  remote_apply → スタンバイへの適用まで待つ(強い整合性、低速)

wal_compression:
  on         → WALを圧縮(CPU使用増えるがI/O削減)

checkpoint_completion_target:
  0.9        → チェックポイント間隔内でゆっくりダーティページを書く
               (I/Oスパイクを抑制)

チェックポイント(Checkpoint)

WALが永遠に増え続けないよう、定期的に「ここまでデータページに反映した」という印をつける。

チェックポイントの動作:
  1. バッファプール内のダーティページをすべてディスクに書く
  2. WALに CHECKPOINT レコードを記録
  3. このLSNより前のWALは削除可能になる
  
設定:
  checkpoint_timeout = 5min   → 最大5分ごとにチェックポイント
  max_wal_size = 1GB          → WALがこのサイズを超えたらチェックポイント

チェックポイント直後はI/Oが集中する。checkpoint_completion_target=0.9 で次のチェックポイントまでの90%の時間をかけてゆっくり書くことでスパイクを抑制できる。

関連概念

出典・参考文献

  • Martin Kleppmann, Designing Data-Intensive Applications (2017) Chapter 5
  • PostgreSQL Documentation, “Write Ahead Logging” — postgresql.org/docs/current/wal.html
  • Debezium Documentation — debezium.io/documentation
  • Hironobu Suzuki, “The Internals of PostgreSQL” — interdb.jp/pg
  1. 1. 🗄️データ志向アプリケーション設計:概要
  2. 2. 🧩データモデルとクエリ言語
  3. 3. 💾ストレージエンジンとインデックス
  4. 4. 🔁レプリケーション
  5. 5. 🍕パーティショニング(シャーディング)
  6. 6. 🔒トランザクションとACID
  7. 7. 分散システムの本質的な問題
  8. 8. 🤝一貫性と分散合意
  9. 9. 📦バッチ処理
  10. 10. 🌊ストリーム処理
  11. 11. 📋エンコーディングとスキーマ進化
  12. 12. 🔗Sagaパターンと分散トランザクション
  13. 13. 🏗️データシステムの統合設計
  14. 14. 📸MVCC(多版型同時実行制御)
  15. 15. 📊列指向ストレージとOLAP設計
  16. 16. 🕰️ベクタークロックと因果順序
  17. 17. 🔀CRDT(競合なし複製データ型)
  18. 18. 🔍クエリオプティマイザーと実行計画
  19. 19. キャッシュ戦略とRedis設計
  20. 20. 🔎全文検索と転置インデックス
  21. 21. 🌐NewSQL(分散ACIDデータベース)
  22. 22. 📝WALと論理レプリケーション
  23. 23. 🔌コネクションプーリング
  24. 24. 🚧ゼロダウンタイムマイグレーション
  25. 25. 🆔分散ID生成
  26. 26. 🔄N+1問題とDataLoaderパターン
  27. 27. 📈タイムシリーズDB
  28. 28. 🛡️Row Level Security(行レベルセキュリティ)
  29. 29. 📤Outboxパターン(トランザクショナルアウトボックス)
  30. 30. 💾DBバックアップとPITR
  31. 31. ⚠️データベース設計アンチパターン
  32. 32. 🕸️グラフDB深掘り
  33. 33. 🔋バックプレッシャーとサーキットブレーカー

出典: Martin Kleppmann, 'Designing Data-Intensive Applications' (2017) Chapter 5