PostgreSQLでDDL対応論理レプリケーションを自作してみた話

本記事は[PostgreSQL Advent Calender 2024](https://qiita.com/advent-calendar/2024/postgresql) 25日目の記事です。
昨日は@taiki-kさんのパラレルクエリ検証記事でした。

https://qiita.com/taiki-k/items/4207caf70afc0b26e722

PostgreSQLでユーザデータに関する変更を抽出し、その内容を別ノードへと複製する拡張`pg_follower`を作成しました。本記事ではその使用方法と、内部アーキテクチャについて簡単に紹介します。

https://github.com/HUUTFJ/pg_follower

この拡張は以下の4種類のSQLコマンドを複製することができます。

1. INSERT
1. TRUNCATE
1. **CREATE TABLE**
1. **DROP TABLE**

本家論理レプリケーションと比較すると、UPDATE文やDELETE文には対応しておらず、実運用での利用は難しいでしょう。その代わりに一部データ定義言語 _(Data Definition Language, DDL)_ の複製をサポートしているのが特徴です。
論理レプリケーションの典型的なユースケースとしてはオンラインでのメジャーバージョンアップグレードがありますが、DDLレプリケーションがあればデータ移行中でもテーブルの追加・変更が可能になります。

## 使用方法・例

まず初めに、レプリケーションの上流側、下流側の両方で`CREATE EXTENSION`を行います。

```
postgres=# CREATE EXTENSION pg_follower ;
CREATE EXTENSION
```

次に、下流側で`start_follow`という専用のSQL関数を実行し、レプリケーションを開始します。この時、引数として上流への接続文字列を指定する必要があります。

```
downstream=# SELECT * FROM follower.start_follow('user=postgres port=5431');
 start_follow
--------------

(1 row)
```

以上で準備は完了です。一例として、テーブルの作成と10行の挿入を行ってみましょう。

```
upstream=# CREATE TABLE foo (id int);
CREATE TABLE
upstream=# INSERT INTO foo VALUES (generate_series(1, 10));
INSERT 0 10
```

その後下流側で確認してみると、確かに複製が行われています。

```
downstream=# SELECT * FROM foo ;
 id
----
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
(10 rows)
```

## アーキテクチャ

この拡張は主に3つの構成要素から成り立っています。いずれもPostgreSQLが提供する拡張基盤です。

* **バックグラウンドワーカ** - レプリケーションを開始し、受信した内容を適用する
* **ロジカルデコーディング出力プラグイン** - デコードされたWALの内容を、バックグラウンドワーカが理解可能な形で出力する
* **イベントトリガ** - DDLが実行された際にそれを検出し、その内容をWALに出力する

![outline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413266/b2c55c26-3c55-a1fd-57bd-949437245348.png)

### バックグラウンドワーカ

本拡張では、ユーザが実行した`start_follow()`を契機として、上流との通信と変更の適用を担うバックグラウンドワーカを起動します。このプロセスは引数として渡された接続文字列を元にレプリケーションの上流側ノードへと接続し、受信した変更を適用します。

参考：
バックグラウンドワーカプロセスに関する詳しい説明と利用方法は、以下のドキュメントに記載されています。

https://www.postgresql.jp/document/current/html/bgworker.html

### 出力プラグイン

ロジカルデコーディングの出力形式は、プラグインにより自由に設定することができます。論理レプリケーションでは`pgoutput`という拡張が使われており、このプラグインの出力内容がパブリッシャからサブスクライバへと転送されています。

本拡張では、`pg_follower`という独自のプラグインを実装しています。WALからINSERT文やTRUNCATE文を検出すると、その内容を元に**SQL文を再構築**し、下流ノードでサーバプログラミングインターフェース _(Server Programming Interface, SPI)_ の機能を用いて実行します。

![decoding.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413266/317cc1e0-8901-5677-1fc9-1776cf6d6ba5.png)


参考：
ロジカルデコーディングの出力プラグインで実装するべきAPIは、以下のドキュメントに記載されています。

https://www.postgresql.jp/document/current/html/logicaldecoding-output-plugin.html

### イベントトリガ

DDLレプリケーションは、PostgreSQLのイベントトリガ機能を活用して実装されています。では、なぜこの仕組みが必要なのでしょうか？この理由を理解するためには、なぜ今現在の論理レプリケーションがDDLをレプリケーションできないかを知る必要があります。

#### 背景：なぜDDLの複製は難しい？

INSERTやUPDATE、DELETEといったDMLを実行した場合、それらに対応するWALレコードが生成されます。ロジカルデコーディングはこれらのWALからユーザデータへの変更を再構築し、任意の形式で出力するという仕組みです。それに対してCREATE TABLEのようなDDLは、実行してもそれらに直接対応するWALレコードが生成されることはありません。これらのデータ定義は、pg_classやpg_attributeといったシステムカタログへのデータの追記という形式で表現されます。

しかしながらロジカルデコーディングでは、**システムカタログへの変更をデコードしません**。この機能は他種DBMSへの変更の複製ができるように設計されており、PostgreSQL固有のテーブルであるシステムカタログへの変更は無視されるようになっています。これらの要因により、本家論理レプリケーションではCREATE TABLEやDROP TABLEのようなDDLを複製することができませんでした。

<img width="600" alt="catalogs.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413266/ab823358-9549-291e-2cb6-06bc52aa014b.png">

#### 解決方法：イベントトリガの使用

そこで本拡張が採用したのは、イベントトリガを用いる手法です。レプリケーションをサポートしているDDLに対してイベントトリガを作成し、DDLの実行後にWALへとメッセージを埋め込んでいます。出力内容としてはトリガの元となったSQLを再構築したものです。その後walsenderプロセスがWALに埋め込まれたメッセージを読み込み、その内容を下流側へと出力します。それ以降はその他DMLと同様に、バックグラウンドワーカによって随時再実行されていきます。

<img width="300" alt="event_trigger.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413266/807eb7aa-6f36-0e49-e3c0-7a83c133453a.png">

参考：

イベントトリガに関しては、以下の公式マニュアルを参照してください。

https://www.postgresql.jp/document/16/html/event-triggers.html

また本家論理レプリケーションで議論されているDDLレプリケーションに関しては、PGcon 2023で行われた講演資料に詳しく記載されています。出力形式やサポートしているDDLには差がありますが、基本的なアイデアは`pg_follower`と同じ、イベントトリガを使用してDDLを検知するというものです。

https://wiki.postgresql.org/images/d/dd/20230602-DDL_Replication.pdf

## TODO

ざっと思いつくだけでも、以下のような改善項目が考えられます。

1. ノードの再起動後もレプリケーションが継続できるようにする
1. UPDATE, DELETE文のサポート
1. パブリケーションを使用した選択的レプリケーションのサポート
1. ALTER TABLE, CREATE INDEXといったDDLのサポート

この拡張は不定期で更新していくつもりですが、皆様からのプルリクエストも絶賛大募集中です。
レプリケーションに関して勉強したい方がいらっしゃれば、ぜひとも一緒に取り組んでいきましょう。

## 実装してみての感想

### DDLレプリケーションの実装がつらい

pg_followerは2つのDDL、そのうちごく限られた構文にのみ対応していますが、それでもイベントトリガ関連のコードは200行程度あります。PostgreSQLには200個前後のDDLが存在するので、これらすべてに対応するためには万単位の開発が必要かもしれません。また仮にすべてのDDLに対応するイベントトリガを作成したとしても、構文のアップデートに追従していく必要があります。大変そう。

## おわりに - 自作のススメ

PostgreSQLの内部を理解するには、実際に拡張機能を作ることが最も効果的です。重要なのは、初めからすべてを作り込むのではなく、ユースケースを絞り小さな目標から始めることです。
`pg_follower`に関して言えば、初めに以下のような方針を立てて取り組みました。

* PostgreSQLが持つ拡張性を最大限活用する。本体に手を加えない
* 実装する機能をできるだけ絞る。一度にすべてを解決しようとしない
* 処理性能の最適化は優先しない。まずは動けばよい
* 1つでもいいので、本家PostgreSQLにできないことをする

来年のアウトプットとして、拡張づくりをぜひとも検討してみてください。
よいPostgreSQLライフを！