【実務・中級編】 論理デコーディング – PostgreSQL

お疲れ様!今日も元気にデータベースと格闘してるか?

今日はちょっとディープな話をするんだけど、PostgreSQLを実務で使っているなら、絶対に知っておくべき、そして使いこなせるとめちゃくちゃ役立つ技術、「論理デコーディング(Logical Decoding)」について深掘りしていくぞ。

「WAL (Write-Ahead Log) の内容を、人間様にもわかりやすい形に変換する魔法」って言ったら、ちょっとは興味湧くかな?

論理デコーディングって、結局何?

まず、「論理デコーディングって何が嬉しいの?」ってところから話そうか。

PostgreSQLは、データの一貫性と耐久性を保証するために、全ての変更をWALというログに書き出すんだ。これ、よく「データベースの心臓部」とか言われるけど、まさにその通り。物理レプリケーションでは、このWALをそのままスタンバイサーバーに送って、同じ状態を再現するわけだ。これはこれでシンプルで強力なんだけど、WALの中身は基本的に「物理的なバイト列」。人間が見ても、どのテーブルのどの行がどう変わったかなんて、そのままではさっぱり分からない。

ここで登場するのが、論理デコーディングだ。

論理デコーディングは、この物理的なWALの変更を、「論理的な変更ストリーム」として抽出する仕組みなんだ。つまり、「usersテーブルに新しいユーザーがINSERTされたよ」とか、「productsテーブルのIDが123の商品の価格がUPDATEされたよ」みたいに、人間や他のアプリケーションが理解しやすい形式で変更イベントを出力してくれる。

従来の物理レプリケーションと何が違うの?

物理レプリケーションは、OSのファイルシステムレベルでのコピーに近い。データベース全体をそっくりそのまま複製するイメージだ。だから、PostgreSQL同士の連携には最適だけど、異種データベースへのデータ同期とか、特定のテーブルの変更だけをリアルタイムで取り出したい、みたいな用途には不向きなんだ。

一方、論理デコーディングは、WALから「何が起こったか」というイベントを抽出する。だから、出力されたイベントを好きなように加工したり、他のシステムに流したり、柔軟な連携が可能になる。

イメージとしては、

  • 物理レプリケーション: 「データベース全体をコピーして、同じデータベースをもう一台用意する」
  • 論理デコーディング: 「データベースで行われた個々の変更イベントを、テキストやJSON形式でリアルタイムに吐き出す」

って感じかな。

なぜ論理デコーディングが必要なのか?(ユースケース)

「なるほど、じゃあ具体的にどんな時に使うんだ?」って疑問に思うよな。いくつか代表的なユースケースを挙げてみよう。

  • CDC (Change Data Capture) 実現:
  • 特定のテーブルの変更だけをリアルタイムでキャプチャして、データウェアハウスやデータレイクに流し込みたい。
  • マイクロサービス間で、データベースの変更イベントをPub/Subモデルで共有したい。
  • 異種データベースへのデータ同期:
  • PostgreSQLのデータをMySQLやOracle、NoSQLデータベースなんかにリアルタイムで同期させたい。
  • カスタムな監査ログや分析:
  • 特定のテーブルの変更履歴を、アプリケーション独自の形式で保持したり、分析したりしたい。
  • イベントソーシング:
  • ドメインイベントをデータベースの変更から生成して、イベントストアに永続化する。

どうだ?結構夢が広がるだろ?

仕組みのキーコンポーネントを理解しよう

論理デコーディングを動かすには、いくつかの重要な部品が連携して動いている。これらを理解しておけば、いざという時のトラブルシューティングにも役立つはずだ。

1. WAL (Write-Ahead Log)

これはもう説明した通り、PostgreSQLの全ての変更が記録されるログファイルだ。論理デコーディングは、このWALの中身を解析して、論理的な変更イベントを抽出する。

2. レプリケーションスロット (Replication Slot)

これ、めちゃくちゃ大事だから、しっかり覚えてくれ。

論理デコーディングを使う上で、「WALの読み飛ばしを防ぐ仕組み」が必須になる。変更イベントを消費するアプリケーションが何らかの理由で一時停止したり、処理が遅れたりしたらどうなる? WALはどんどん新しい情報で上書きされ、古いWALは削除されてしまう。そうなると、イベントを見逃してしまう可能性があるよな。

それを防ぐのが「レプリケーションスロット」だ。

レプリケーションスロットは、特定のクライアント(論理デコーディングを行うプロセス)が消費するまで、WALを削除せずに保持しておくための仕組みなんだ。これがあるおかげで、クライアントがどれだけ遅れても、必要なWALがちゃんと残されていることを保証できる。いわば、WALの「消費期限を延長してくれる予約券」みたいなもんだな。

3. 出力プラグイン (Output Plugin)

WALの中身を論理的な変更イベントとして抽出できたとして、それをどんな形式で出力するか?それが「出力プラグイン」の役割だ。

PostgreSQLにはいくつか標準の出力プラグインがあるけど、実務で一番よく使うのは間違いなく`pgoutput`だろう。

  • `pgoutput`: PostgreSQL 10から導入された標準の論理レプリケーションプロトコルを実装したプラグイン。PostgreSQLの論理レプリケーションや、`pg_recvlogical`などのツールで利用される。出力形式はPostgreSQLの内部表現に近いが、テキストとしてデコードすることも可能だ。後述する具体的な例では、これを使う。
  • `test_decoding`: これは主にテストやデバッグ用のプラグインだけど、単純なテキスト形式でWALの内容を出力してくれるから、仕組みを理解するのには便利かもしれない。
  • その他: サードパーティ製のプラグインを使えば、JSONやAvro、Protobufなどの形式で直接出力することも可能だ。Kafka Connect用のプラグインなんかも有名だな。

実践!手を動かしてみよう

よし、ここから具体的な手順を見ていこう。PostgreSQLの論理デコーディングを試すためのステップだ。

Step 1: PostgreSQLの設定変更

まず、PostgreSQLが論理デコーディングに必要な情報をWALに書き出すように設定を変更する必要がある。

`postgresql.conf` を開いて、以下の設定を探して変更するか、追記してくれ。

wal_level を ‘logical’ に設定する
wal_level = logical

必要に応じて、max_replication_slots や max_wal_senders も調整する
論理デコーディングを利用するクライアントの数に合わせて増やす
max_replication_slots = 10 # 例: 10個までスロットを作成できるようにする
max_wal_senders = 10 # 例: WALを送信できるプロセスの数を増やす

`wal_level`を`logical`にすると、WALに詳細な情報(例えば、UPDATE前の行のイメージなど)が書き込まれるようになる。これには少しオーバーヘッドがあるから、本番環境に適用する際は注意が必要だ。

設定を変更したら、PostgreSQLを再起動するのを忘れるな!単なるリロード(`pg_ctl reload`)では`wal_level`は反映されないぞ。

Step 2: レプリケーションスロットの作成

次に、論理デコーディング用のレプリケーションスロットを作成する。これは、WALを読み飛ばされないようにするための「予約券」だったな。

psqlでデータベースに接続して、以下のSQLを実行する。

SELECT pg_create_logical_replication_slot(‘my_logical_slot’, ‘pgoutput’);

  • `my_logical_slot`: これはスロットの名前だ。好きな名前をつけてくれ。
  • `pgoutput`: 使用する出力プラグインの名前だ。今回は標準の`pgoutput`を使う。

成功すると、スロット名と現在のWALの位置が返ってくるはずだ。

pg_create_logical_replication_slot
————————————
(my_logical_slot,0/16A03E8)
(1 row)

このスロットが作成されると、PostgreSQLはこのスロットが消費するまでWALを保持し続ける。

重要: 作成したスロットは、使わなくなったら必ず削除しろよ!削除しないと、WALがどんどん溜まってディスクを圧迫することになるからな。

SELECT pg_drop_replication_slot(‘my_logical_slot’);

Step 3: パブリケーションの作成 (PostgreSQL 10以降の論理レプリケーションの場合)

`pgoutput`プラグインを使う場合、PostgreSQL 10以降の「論理レプリケーション」の概念と密接に連携する。つまり、どのテーブルの変更を抽出するかを「パブリケーション(Publication)」として定義するのが一般的だ。

例えば、`products`テーブルと`orders`テーブルの変更を抽出したい場合、以下のようにパブリケーションを作成する。

CREATE PUBLICATION my_publication FOR TABLE products, orders;

これで、`my_publication`という名前のパブリケーションを通して、`products`と`orders`テーブルの変更イベントが発行されるようになる。

Step 4: データ変更を発生させる

さて、これで準備は整った。実際にデータを変更して、WALにイベントを書き込ませてみよう。

まず、適当なテーブルを作成する。

CREATE TABLE my_table (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
);

そして、いくつかデータを操作してみる。

INSERT INTO my_table (name, value) VALUES (‘Apple’, 100);
UPDATE my_table SET value = 120 WHERE name = ‘Apple’;
INSERT INTO my_table (name, value) VALUES (‘Banana’, 50);
DELETE FROM my_table WHERE name = ‘Banana’;

Step 5: 変更ストリームの確認

さあ、いよいよ本番だ。WALに書き込まれた変更を、論理デコーディングで抽出してみよう。

方法1: `pg_recvlogical` コマンドを使う

PostgreSQLには、論理デコーディングクライアントとして機能する`pg_recvlogical`というコマンドラインツールが用意されている。これは、データベースの外部からWALの変更ストリームを取得するのに便利だ。

pg_recvlogical -d postgres \
–slot my_logical_slot \
–plugin pgoutput \
–option ‘publication_names=my_publication’ \
–verbose

  • `-d postgres`: 接続するデータベース名
  • `–slot my_logical_slot`: 作成したレプリケーションスロットの名前
  • `–plugin pgoutput`: 使用する出力プラグイン
  • `–option ‘publication_names=my_publication’`: `pgoutput`プラグインに、どのパブリケーションの変更を抽出するかを指示する。
  • `–verbose`: 詳細なログを出力する (デバッグに便利)

このコマンドを実行すると、ターミナルに変更イベントがリアルタイムで流れ始めるはずだ!

出力はこんな感じになるだろう(これはテキスト形式でデコードした場合のイメージだ。実際にはバイナリプロトコルでやり取りされることが多い)。

BEGIN 12345
TABLE public.my_table INSERT: id[integer]:1 name[text]:’Apple’ value[integer]:100
COMMIT 12345
BEGIN 12346
TABLE public.my_table UPDATE: id[integer]:1 name[text]:’Apple’ value[integer]:120 old_value[integer]:100
COMMIT 12346
BEGIN 12347
TABLE public.my_table INSERT: id[integer]:2 name[text]:’Banana’ value[integer]:50
COMMIT 12347
BEGIN 12348
TABLE public.my_table DELETE: id[integer]:2
COMMIT 12348

どうだ? INSERT, UPDATE, DELETE のイベントが、どのテーブルのどのカラムがどう変更されたか、一目瞭然だろ?

方法2: SQL関数 `pg_logical_slot_get_changes` を使う

もう一つの方法は、psqlなどのクライアントからSQL関数を使って、現在のスロットの変更を取得することだ。これは、スクリプトから一時的に変更を確認したい場合などに便利だ。

SELECT FROM pg_logical_slot_get_changes(
‘my_logical_slot’, — スロット名
NULL, — WALの開始位置。NULLで現在の位置から
NULL, — 取得するWALのバイト数。NULLで全て
‘publication_names’, ‘my_publication’ — オプション
);

この関数を実行すると、それまでにスロットが消費していない変更イベントがまとめて返ってくる。実行するたびに、スロットの消費位置が進むことを覚えておいてくれ。

現場での注意点と落とし穴

便利な論理デコーディングだけど、いくつか注意すべき点がある。

1. `wal_level = logical` のオーバーヘッド

`wal_level = logical` に設定すると、WALに書き込まれる情報量が増えるため、わずかだが書き込み性能に影響が出る可能性がある。特に、UPDATE操作では、変更前の行イメージもWALに書き込む必要があるため、ディスクI/Oが増えることがある。

2. レプリケーションスロットの管理

これ、本当に大事!

レプリケーションスロットを作成すると、そのスロットがWALを消費するまで、PostgreSQLはWALセグメントファイルを削除しない。もし、論理デコーディングクライアントが長時間停止したり、そもそもクライアントが存在しなくなったりすると、WALファイルがディスクにどんどん溜まっていき、ディスク容量を使い果たしてデータベースが停止するという最悪の事態になりかねない。

  • 監視を怠るな!: `pg_replication_slots`ビューを定期的に監視し、`active`が`false`になっているスロットや、`restart_lsn`が長時間進んでいないスロットがないか確認すること。
  • 不要なスロットは削除しろ!: 使わないスロットは、必ず`pg_drop_replication_slot()`で削除すること。
  • WALディスク容量の余裕: `wal_level = logical`にする場合は、WALが一時的に多くなることも想定して、ディスク容量に余裕を持たせておこう。

3. 出力プラグインの選択

`pgoutput`は標準で強力だけど、用途によってはサードパーティ製のプラグイン(例えば、直接JSONやAvroを吐き出すもの)の方が開発が楽な場合もある。プロジェクトの要件に合わせて、適切なプラグインを選択することが重要だ。

4. トランザクション境界

論理デコーディングによって抽出される変更イベントは、トランザクション単位で出力される。つまり、複数のINSERTやUPDATEが1つのトランザクションで行われた場合、それらは`BEGIN`と`COMMIT`の間にまとめて出力される。これは、変更の原子性を保つ上で非常に重要だ。

5. スキーマ変更への対応

テーブルのスキーマが変更された場合(カラムの追加、削除、型変更など)、論理デコーディングの出力形式も変わる可能性がある。クライアント側では、このようなスキーマ変更に追随できるような設計にしておく必要がある。

どんな時に使うと便利?

もう一度、どんな時にこの強力な機能を使うと「お、こいつデキるな」って思われるか、まとめておこう。

  • リアルタイムデータパイプラインの構築: KafkaやKinesisといったメッセージキューと組み合わせて、データベースの変更をリアルタイムに他のシステムに連携する。
  • 異なるデータベース間のデータ同期: PostgreSQLからMySQL、Oracle、あるいはデータウェアハウスへのCDC。
  • マイクロサービス間でのデータ連携: データベースの変更をイベントとしてPublishし、他のサービスがSubscribeすることで、疎結合な連携を実現する。
  • データ監査や履歴管理の強化: 変更イベントを詳細に記録し、特定のビジネス要件に合わせた監査ログや履歴を構築する。

これらの課題に直面したら、「論理デコーディング、使えるかも?」と頭の片隅に置いておいてくれ。

まとめ

どうだった? PostgreSQLの論理デコーディング、なかなか奥が深くて面白いだろ?

最初は「WALを解読するなんて、黒魔術かよ!」って思うかもしれないけど、その仕組みを理解して使いこなせば、データベースを核としたシステム連携の幅が格段に広がる。これは、まさに「データベースエンジニアの腕の見せ所」だよ。

特に、レプリケーションスロットの管理は、本番運用で大きな問題を引き起こす可能性があるから、今日の話をしっかり頭に入れて、監視体制も含めて設計・運用してほしい。

これからも、PostgreSQLの深い世界を一緒に探求していこうな!何か困ったことがあったら、いつでも相談に来いよ!

コメント

タイトルとURLをコピーしました