論理レプリケーションにおける二相コミット - PostgreSQL 15でコミットされた機能の先行紹介:技術者Blog
PostgreSQLインサイド

Ajin Cherian

Fujitsu Australia Software Technology
Senior Software Development Engineer

はじめに

このブログでは、パブリケーション / サブスクリプションを使用した論理レプリケーション(ロジカルレプリケーション)において二相コミットを可能にするために、富士通OSSチームがPostgreSQLオープンソースコミュニティーと協力してPostgreSQL 15に追加した新機能について説明します。
この機能は、二相トランザクションのためにレプリケーションのデコードができる、パブリケーション / サブスクリプション構成の作成をサポートします。また、必要な二相コールバックをすべてサポートするように、ロジカルデコーディングのプラグインpgoutputを変更しました。

背景

Ajin
PostgreSQL 14では、PREPARE TRANSACTION時に二相コミットのデコードを可能にするフレームワークと、デコーダー側のインフラストラクチャーを既に追加しています。また、このフレームワークを使用するためにtest_decodingプラグインも変更しました。
ただしPostgreSQL 14では、論理レプリケーションにパブリケーション / サブスクリプションを使用したクライアントから、この機能に直接アクセスすることはできませんでした。つまり、プリペアドトランザクションはPREPARE TRANSACTIONのデコード中にサブスクライバーに送信されるのではなく、対応するCOMMIT PREPAREDのデコード中にのみサブスクライバーに送信されます。

例えば、PostgreSQL 14での動作は以下のようになります。

  1. パブリッシャー側:

postgres=# CREATE TABLE test (a int, b text, primary key(a));
CREATE TABLE
postgres=# CREATE PUBLICATION pub FOR TABLE test;
  1. サブスクライバー側:

postgres=# CREATE TABLE test (a int, b text, primary key(a));
CREATE TABLE
postgres=# CREATE SUBSCRIPTION sub CONNECTION 'dbname=postgres host=localhost' PUBLICATION pub;
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION
  1. パブリッシャー側:

postgres=# BEGIN;
BEGIN
postgres=*# INSERT INTO test VALUES (7,'aa');
INSERT 0 1
postgres=*# PREPARE TRANSACTION 't1';
PREPARE TRANSACTION
postgres=# SELECT * FROM pg_prepared_xacts;
 transaction | gid |           prepared            | owner | database
-------------+-----+-------------------------------+-------+----------
         790 | t1  | 2022-03-14 06:59:49.341013-04 | ajin  | postgres
(1 row)
  1. サブスクライバー側:

    プリペアドトランザクションがサブスクライバーにレプリケートされていないことに注目してください。

postgres=# SELECT * FROM pg_prepared_xacts;
 transaction | gid | prepared | owner | database
-------------+-----+----------+-------+----------
(0 rows)

機能の紹介

概要

Ajin
CREATE SUBSCRIPTIONに新しく追加したオプション「two_phase」は、このSUBSCRIPTIONに対して二相コミットを有効にするかどうかを指定します。デフォルトはfalseです。

CREATE SUBSCRIPTION sub
CONNECTION 'conninfo'
PUBLICATION pub
WITH (two_phase = on);

上記の構文のようにtwo_phase = onを指定して二相コミットが有効になっている場合、プリペアドトランザクションはPREPARE TRANSACTIONの時点でサブスクライバーにレプリケートされ、サブスクライバー側も二相トランザクションとして処理されます。two_phase = offの場合は、プリペアドトランザクションはコミット時にのみサブスクライバーにレプリケートされ、直ちに処理されます。

プリペア複雑化の回避

Ajin
二相トランザクションは、PREPARE TRANSACTIONで再生され、COMMIT PREPAREDおよびROLLBACK PREPAREDでそれぞれコミットまたはロールバックされます。
テーブル同期ワーカがまだ初期コピーの処理中である間に、プリペアドトランザクションが適用ワーカに到着する可能性があります。この場合、適用ワーカは新しいトランザクションを開始しますが、実行中のテーブル同期ワーカがそれらを処理していると仮定して、後続のすべての変更(例えば挿入)をスキップします。一方、テーブル同期ワーカはプリペアドトランザクションをまったく見ていないかもしれません(これは、テーブル同期ワーカが変更の適用を開始するconsistent pointより前に行われたためです)。
これで、テーブル同期ワーカはプリペアドトランザクションに関する処理を行わずに終了します。その後、適用ワーカがCOMMIT PREPAREDを実行すると、「empty prepare」エラーが発生します(適用ワーカが以前に挿入をスキップしたため、トランザクションは空です)。
この複雑さを回避するために、二相コミットの実装では、レプリケーションが初期テーブル同期フェーズを正常に完了している必要があります。つまり、1つのサブスクリプションに対してtwo_phaseが有効になっていても、すべてのテーブル初期化が完了するまで、内部的に二相状態は一時的に 「保留中」 になります。

二相モード有効化の3つの状態

Ajin
システムカタログpg_subscriptionに、二相モードの状態を示す「subtwophasestate」という新しい列が追加されました。 二相モードは以下の3つの状態が遷移することによって有効化されます。

コード 状態
d 無効(DISABLED)
p 有効化保留中(PENDING)
e 有効(ENABLED)

ユーザーがtwo_phase=onのサブスクリプションを指定した場合でも、内部的には状態がPENDINGで始まり、すべてのテーブル同期の初期化が完了した後(すべてのテーブル同期ワーカがREADY状態に達したとき)にのみ、ENABLEDになります。つまり、PENDINGはサブスクリプション起動時の一時的な状態にすぎません。
二相が適切に利用可能(状態がENABLED)になるまで、サブスクリプションはtwo_phase=offのように動作します。適用ワーカは、すべてのテーブル同期がREADYになったことを検出すると(状態がPENDINGのときに)、適用ワーカプロセスを再開します。
再開された適用ワーカは、二相の状態PENDINGに対してすべてのテーブル同期ワーカがREADYであることを検出すると、wal_startstreaming関数を呼び出して、パブリッシャーの二相コミットを有効にし、状態をPENDINGからENABLEDに更新します。

図1:テーブル同期フェーズ
図1:テーブル同期フェーズ

  1. サブスクリプションは、two_phaseが有効な状態で作成されます。
  2. 初めに、サブスクリプションはテーブル同期フェーズにあり、テーブル同期ワーカがテーブルごとに起動されます。
  3. 各テーブル同期ワーカは、パブリッシャー上の各テーブルに対してテーブル同期レプリケーションスロットを作成します。
  4. subtwophasestate列にPENDINGが設定されます。

この後、適用ワーカのフェーズが始まります。

図2:適用ワーカのフェーズ
図2:適用ワーカのフェーズ

  1. テーブル同期フェーズが完了すると、テーブル同期ワーカはパブリッシャーのテーブル同期レプリケーションスロットを削除し、終了します。
  2. 次に、適用ワーカが引き継ぎます。
  3. その後、適用ワーカはパブリッシャーのサブスクリプションレプリケーションスロットを作成します。
  4. subtwophasestate列がENABLEDに変更されます。

ユーザーがsubtwophasestateの値を確認したい場合は、pg_subscriptionカタログから取得することができます。

subtwophasestateの値を確認する実行例

postgres=# SELECT subtwophasestate FROM pg_subscription;
 subtwophasestate
------------------
 e

ALTER SUBSCRIPTIONの制限

Ajin
ALTER SUBSCRIPTIONでtwo_phaseオプションを変更することはできません。
この制限は、プリペアドトランザクションの開始からCOMMIT PREPAREDの実行までにおいて、two_phaseオプションの状態(有効化や無効化)が変わるようなケースを回避するためです。このシナリオでは、デコーダーは、トランザクションを完全にデコードする必要があるのか、COMMIT PREPAREDを単に送信する必要があるのかを判断できません。

サブスクライバー上のグローバルID(GID)

Ajin
サブスクライバー上で複製されるプリペアドトランザクションは、パブリッシャーで指定されたものと同じGIDを持ちません。パブリッシャーに特定のプリペアドトランザクションを適用するサブスクライバーが複数存在し、それらのサブスクライバーのすべてがパブリッシャーと同じGIDを使用している場合、2番目のトランザクションが同じGIDを使用して準備しようとすると失敗します。
このような競合を回避するために、サブスクライバーの適用ワーカは、サブスクライバーIDおよびパブリッシャーのトランザクションIDに基づいて「pg_gid_<subscriber-id>_<transaction-id>」のフォーマットで生成される一意のGIDで置き換えます。

置き換えられたGIDの記述例

pg_gid_24576_790

コールバックAPI

Ajin
この機能のために、以下のプラグインpgoutputの関数が実装され、二相コミットに必要なコールバックが割り当てられるようになりました。

cb->begin_prepare_cb = pgoutput_begin_prepare_txn;
cb->prepare_cb = pgoutput_prepare_txn;
cb->commit_prepared_cb = pgoutput_commit_prepared_txn;
cb->rollback_prepared_cb = pgoutput_rollback_prepared_txn;
cb->stream_prepare_cb = pgoutput_stream_prepare_txn;

これらのコールバックの詳細については、私の前回のブログ「二相コミットのロジカルデコーディング – PostgreSQL 14でコミットされた機能の先行紹介」を参照してください。

実行例

two_phase = onを指定して二相コミットを有効としたサブスクリプションを作成し、プリペアドトランザクションをレプリケートする実行例を示します。

  1. パブリッシャー側:

    • テーブルとパブリケーションの作成
postgres=# CREATE TABLE test (a int, b text, primary key(a));
CREATE TABLE
postgres=# CREATE PUBLICATION pub FOR TABLE test;
  1. サブスクライバー側:

    • 同じテーブルを作成し、two_phaseモードを有効にしてサブスクリプションを作成
    • システムカタログpg_subscriptionでsubtwophasestateを調べて、two_phaseモードが有効(‘e’)であることを確認
postgres=# CREATE TABLE test (a int, b text, primary key(a));
CREATE TABLE
postgres=# CREATE SUBSCRIPTION sub CONNECTION 'dbname=postgres host=localhost' PUBLICATION pub WITH (two_phase = on);
NOTICE:  created replication slot "sub" on publisher
CREATE SUBSCRIPTION
postgres=# SELECT subtwophasestate FROM pg_subscription;
 subtwophasestate
------------------
 e
(1 row)
  1. パブリッシャー側:

    • トランザクションを開始
    • データを挿入
    • トランザクションの準備とGIDの確認
postgres=# BEGIN;
BEGIN
postgres=*# INSERT INTO test VALUES (7,'aa');
INSERT 0 1
postgres=*# PREPARE TRANSACTION 't1';
PREPARE TRANSACTION
postgres=# SELECT * FROM pg_prepared_xacts;
 transaction | gid |           prepared            | owner | database
-------------+-----+-------------------------------+-------+----------
         790 | t1  | 2022-03-14 06:59:49.341013-04 | ajin  | postgres
(1 row)
  1. サブスクライバー側:

    • サブスクライバー側を検査して、生成されたプリペアドトランザクションGIDがそこにもレプリケートされていることを確認
postgres=# SELECT * FROM pg_prepared_xacts;
 transaction |       gid        |           prepared            | owner | database
-------------+------------------+-------------------------------+-------+----------
         877 | pg_gid_24576_790 | 2022-03-14 06:59:49.350815-04 | ajin  | postgres
(1 row)
  1. パブリッシャー側:

    • プリペアドトランザクションをコミット
    • コミットされたことにより、プリペアドトランザクションGIDがなくなったことを確認
    • 挿入されたデータを確認
postgres=# COMMIT PREPARED 't1';
COMMIT PREPARED
postgres=# SELECT * FROM pg_prepared_xacts;
 transaction | gid | prepared | owner | database
-------------+-----+----------+-------+----------
(0 rows)
postgres=# SELECT * FROM test;
 a | b
---+----
 7 | aa
(1 row)
  1. サブスクライバー側:

    • コミットされたことにより、サブスクライバー側で生成されたGIDもなくなったことを確認
    • パブリッシュされたデータがレプリケートされたことを確認
postgres=# SELECT * FROM pg_prepared_xacts;
 transaction | gid | prepared | owner | database
-------------+-----+----------+-------+----------
(0 rows)
postgres=# SELECT * FROM test;
 a | b
---+----
 7 | aa
(1 row)

今後に向けて

Ajin
PostgreSQL 15は、二相コミットをサポートする分散データベースを持つための基礎となるフレームワークを提供するようになりました。分散データベースで二相トランザクションが動作するには、スタンバイがマスターに対して失敗したプリペアを通知してロールバックを開始する必要があります。しかし、この種のスタンバイフィードバック機構は現在のPostgreSQLには存在せず、将来の改良候補です。

参考

Ajin
本ブログで解説した「論理レプリケーションにおける二相コミット」の改良についての詳細はGitHubに投稿したコミット情報をご覧ください。

2022年5月27日公開

オンデマンド(動画)セミナー

    • PostgreSQLに関連するセミナー動画を公開中。いつでもセミナーをご覧いただけます。
      • 【事例解説】運送業務改革をもたらす次世代の運送業界向けDXプラットフォームの構築
      • ハイブリッドクラウドに最適なOSSベースのデータベースご紹介

本コンテンツに関するお問い合わせ

お電話でのお問い合わせ

Webでのお問い合わせ

当社はセキュリティ保護の観点からSSL技術を使用しております。

ページの先頭へ