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

大墨 昂道

富士通株式会社
ソフトウェアプロダクト事業本部 データマネジメント事業部

はじめに

この記事では、論理レプリケーションのコンフリクトとは何か、またPostgreSQL 15がコンフリクト解決のためにどのような優れたユーティリティーを提供するかについてお話しします。

背景

大墨
論理レプリケーションは、対象を選択してデータを複製する方法です。物理レプリケーションではクラスタ全体をコピーし、必要に応じてスタンバイ側で読み取り専用のクエリを受け付けますが、論理レプリケーションでは、データレプリケーションをより細かく、柔軟に制御することができます。

論理レプリケーションで利用できる機能には、次のようなものがあります。

  • 対象テーブルの選択と操作
  • サブスクライバーへの直接的書き込み
  • パブリケーションとサブスクリプション間の複雑なトポロジー

論理レプリケーションでは、データはサブスクリプションのワーカープロセスによってサブクスクライバーに適用されます。サブスクリプションのワーカープロセスは、ノード上でDML操作を実行するのと同様に動作します。そのため、新たに受信したデータがサブスクライバーのいずれかの制約に違反した場合、レプリケーションはエラーで停止します。
これは"コンフリクト"と呼ばれ、処理を進めるためにはユーザーが手動で操作する必要があります。

目次

PostgreSQL 15で何が変わったのか?

大墨
PostgreSQL 15で、PostgreSQLコミュニティーは論理レプリケーションのコンフリクトに対処するための改良と新機能を導入しています。ここでは、これらの改良点と、それらをどのように適用してコンフリクトを処理するかを説明します。この記事では、既存データとコンフリクトするトランザクションをスキップすることで、問題を解決します。この記事で紹介する自動無効化機能(disable_on_error オプション)について、私もコミュニティーで開発者の1人として働いていました。
次の図1では、コンフリクトが発生する仕組みを説明します。

  • 免責事項
    この記事では、実機検証に開発版のPostgreSQLを使用しましたが、PostgreSQL 15の公式リリース前のため、コミュニティーはこのブログに関連する設計を変更したり、完全に元に戻したりする可能性があることをご承知おきください。

図1:論理レプリケーション中のコンフリクト

図1:論理レプリケーション中のコンフリクト

論理レプリケーションの改良と新機能

大墨
コミュニティーは、PostgreSQLが論理レプリケーションに関して、信頼性が高く、効率的で、簡単な手段を提供できるように、懸命に努力してきました。この取り組みの一環として、以下の機能と改良がコミットされています。

サブスクリプションの統計情報のための新しいシステムビュー pg_stat_subscription_stats

このビューの各レコードは、サブスクリプションを参照します。このビューでは、2種類のエラーカウンターが実装されています。1つは初期テーブル同期のエラー用で、もう1つは変更適用のエラー用です。

新しいサブスクリプションのオプション disable_on_error

何らかのコンフリクトが発生すると、論理レプリケーションワーカーはデフォルトでエラーループに陥ります。理由は、ワーカーが変更の適用に失敗したとき、エラーで終了し、再起動し、バックグラウンドで同じ変更を繰り返し適用しようとするためです。しかし、この新しいオプションを使用すると、サブスクリプションのワーカーは自動的にサブスクリプションを無効にし、エラー時のループから抜けることができます。その後、ユーザーは次に何をするかを選択できます。初期テーブル同期に失敗した場合も、サブスクリプションを無効にします。デフォルト値はfalseで、falseであることは、コンフリクト時に同じエラーを繰り返すことを意味します。

サブスクリプションのワーカー・エラーについての拡張されたエラーコンテキスト情報

エラーコンテキストメッセージに、条件付きで2つの新しい情報が含まれるようになりました。

  • finish LSN(Log Sequence Number)

    一般的に、LSNはWAL上の位置へのポインターです。finish LSN は、コミットされたトランザクションではcommit_lsn、プリペアドトランザクションではprepare_lsnを意味します。

  • レプリケーション起点名

    レプリケーションの進捗状況を把握するレプリケーション起点の名前です。論理レプリケーションでは、サブスクリプションの定義とともに、対応する各レプリケーション起点が自動的に作成されます。

上記2つの情報は、ユーザーがpg_replication_origin_advance関数を使用する場合、引数に使えて便利です。

失敗したトランザクションをスキップすることによるコンフリクトの解決

大墨
ここまで本テーマの各拡張機能を確認しました。このセクションでは、1つのコンフリクトシナリオをエミュレートしてみます。
これを読む前に注意していただきたい点は、pg_replication_origin_advance関数を呼び出してトランザクションをスキップすることは、ユーザーが選択できる解決策の1つに過ぎない、ということです。ユーザーは、サブスクライバーのデータまたは権限を変更してコンフリクトを解決することもできることを覚えておいてください。

  1. パブリッシャー側:1つのテーブル(tab)とパブリケーション(mypub)を作成

postgres=# CREATE TABLE tab (id integer);
CREATE TABLE
postgres=# INSERT INTO tab VALUES (5);
INSERT 0 1
postgres=# CREATE PUBLICATION mypub FOR TABLE tab;
CREATE PUBLICATION

テーブルの初期同期を行うため、1レコードを挿入しました。

  1. サブスクライバー側:一意性制約を持つテーブル(tab)とサブスクリプション(mysub)を作成

postgres=# CREATE TABLE tab (id integer UNIQUE);
CREATE TABLE
postgres=# CREATE SUBSCRIPTION mysub CONNECTION '…' PUBLICATION mypub WITH (disable_on_error = true);
NOTICE: created replication slot "mysub" on publisher
CREATE SUBSCRIPTION

disable_on_errorオプションを有効にしたサブスクリプションが作成されました。同時に、この定義により、バックグラウンドで初期テーブル同期が行われますが、これは問題なく成功します。

  1. パブリッシャー側:テーブル同期後に3つのトランザクションを順次実行

postgres=# BEGIN; -- Txn1
BEGIN
postgres=*# INSERT INTO tab VALUES (1);
INSERT 0 1
postgres=*# COMMIT;
COMMIT
postgres=# BEGIN; -- Txn2
BEGIN
postgres=*# INSERT INTO tab VALUES (generate_series(2, 4));
INSERT 0 3
postgres=*# INSERT INTO tab VALUES (5);
INSERT 0 1
postgres=*# INSERT INTO tab VALUES (generate_series(6, 8));
INSERT 0 3
postgres=*# COMMIT;
COMMIT
postgres=# BEGIN; -- Txn3
BEGIN
postgres=*# INSERT INTO tab VALUES (9);
INSERT 0 1
postgres=*# COMMIT;
COMMIT

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

パブリッシャー側で実行されたTxn(トランザクション、以降Txn)1は、サブスクライバー側で正常に複製されます。しかし、上の青色で示したTxn2の2回目の挿入では、初期テーブル同期のデータと重複するデータ(手順1でも青で示した箇所)が含まれています。サブスクライバー側で、これはテーブルの一意性制約に違反します。したがって、コンフリクトが発生し、サブスクリプションが無効となって、このサブスクリプションに対する複製処理が行われなくなります。次の手順でコンフリクトが解決されるまで、Txn3は複製されません。

  1. サブスクライバー側:現在の状況を確認

postgres=# SELECT * FROM pg_stat_subscription_stats;
 subid | subname | apply_error_count | sync_error_count | stats_reset
-------+---------+-------------------+------------------+-------------
 16389 | mysub   |                 1 |                0 |
(1 row)

postgres=# SELECT oid, subname, subenabled, subdisableonerr FROM pg_subscription;
  oid  | subname | subenabled | subdisableonerr
-------+---------+------------+-----------------
 16389 | mysub   | f          | t
(1 row)

postgres=# SELECT * FROM tab;
 id
----
  5
  1
(2 rows)

トランザクションをスキップする前に、現在のサブスクライバー側の状態を確認します。
システムビューpg_stat_subscription_statsはこれまでの統計情報として、初期テーブル同期の失敗(sync_error_count)は無いが、変更適用フェーズで1つ失敗(apply_error_count)したことを表示しています。さらに、disable_on_errorオプションにtrueを設定してサブスクリプションを作成したので、サブスクリプション"mysub"は失敗によりに無効(f)が設定されたことを表示しています。テーブル"tab"には、正常に複製されたTxn1の結果までのデータのみが格納されています。

  1. サブスクライバー側のログ:コンフリクトのエラーメッセージと disable_on_error オプションのログを確認

ERROR:  duplicate key value violates unique constraint "tab_id_key"
DETAIL:  Key (id)=(5) already exists.
CONTEXT:  processing remote data for replication origin "pg_16389" during "INSERT" for replication target relation "public.tab" in transaction 730 finished at 0/1566D10
LOG:  logical replication subscription "mysub" has been disabled due to an error

上記では、レプリケーション起点名(pg_16389)とcommit_lsnを示すLSN(0/1566D10)を確認できます。それらを活用して、次のようにTxn2をスキップしてみます。

  1. サブスクライバー側:pg_replication_origin_advance関数を実行してTxn2をスキップし、サブスクリプション(mysub)を有効化

postgres=# SELECT pg_replication_origin_advance('pg_16389', '0/1566D11'::pg_lsn);
 pg_replication_origin_advance
-------------------------------

(1 row)

postgres=# ALTER SUBSCRIPTION mysub ENABLE;
ALTER SUBSCRIPTION
postgres=# SELECT * FROM tab;
 id
----
  5
  1
  9
(3 rows)

レプリケーション起点(pg_16389)とfinish_LSNの次のLSN(0/1566D11)を指定してLSNを進めた後、サブスクリプションを有効にして再度活性化しました。すぐに、Txn3の複製されたデータを確認できます。ここで、Txn2のコンフリクトの直接の原因とは無関係な他のいくつかのデータ(手順3.のTxn2で、赤色に示した別の挿入を行ったことを思い出してください)が、同じトランザクション内のタイミングにかかわらず、複製されていないことに注目してください。Txn2トランザクション全体がスキップされました。
ここでの一連の流れは次のとおりです。

  • pg_replication_origin_advance関数を使うことで、サブスクリプションを有効にしました。
  • サブスクリプションを有効にすると適用ワーカーが起動し、pg_replication_origin_advance関数を介して渡されたLSNをパブリッシャー上のwal senderプロセスに送信しました。
  • このwal senderプロセスは、デコードコミット時に関連LSNを比較してTxn2を送信すべきかスキップすべきかを評価しました。
  • wal senderプロセスは、Txn2のトランザクションがスキップされるべきと判断しました。

最後に、適切なLSNをpg_replication_origin_advance関数に渡すよう注意する必要があることを強調します。このブログで紹介しているコミュニティーの新しい改良により、誤用の可能性はかなり低くなっていますが、使い方を誤るとコンフリクトとは無関係の他のトランザクションを簡単にスキップできてしまいます。

間違ったパラメーターを指定するとどうなるのか?

大墨
参考までに、pg_replication_origin_advance関数の誤使用例を示します。次の例では、上記のシナリオを再実行し、Txn3の後にTxn4を追加して10を挿入しました。そして、pg_replication_origin_advance関数の引数に、Txn3のコミットレコードよりも大きく、Txn4のコミットレコード(pg_waldumpコマンドで取得)よりも小さなLSNを設定しました。サブスクリプションを有効にした後、Txn3の値が無いレプリケーションデータを取得してしまいました。

pg_replication_origin_advance関数を間違って使用した場合のサブスクライバー側の結果

postgres=# SELECT * FROM tab;
 id
----
  5
  1
 10
(3 rows)

上に示したように、コンフリクトを解決するために手動でレプリケーションを操作する場合、絶対に間違えないように注意する必要があります。
この点において、コミュニティーはすでに別の機能(ALTER SUBSCRIPTION ... SKIPコマンド)を導入しています。この機能は、論理レプリケーションのコンフリクトの処理において、pg_replication_origin_advance関数より1歩進んでいます。詳しくは私の次のブログ記事「論理レプリケーションにおけるコンフリクトの対処方法(ALTER SUBSCRIPTION ... SKIPコマンド)」を参照ください。

まとめ

大墨
企業における論理レプリケーションの導入が進むにつれて、論理レプリケーションのコンフリクトなどの実務的な問題への対処がますます重要になります。このため、PostgreSQLに追加された改良は必要不可欠です。
PostgreSQLコミュニティーでは、データベースの強化を進めており、この記事では、論理レプリケーションのコンフリクトを簡単に処理する方法について説明しました。ただし、使用するツール(この場合はpg_replication_origin_advance関数)に正しい情報を提供するように注意して利用していく必要があります。

もっと詳しく知りたい方は

大墨
PostgreSQLの論理レプリケーションとその仕組みについてもっと知りたい場合は、「pg_stat_replication_slotsの基本的な内部構造」という私のブログ記事を参照ください。また、私の同僚であるAjin Cherianが、PostgreSQL 14における「二相コミットのロジカルデコーディング」というブログ記事を書いているので、こちらも参考にしてください。

2022年7月8日公開

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

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

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

お電話でのお問い合わせ

Webでのお問い合わせ

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

ページの先頭へ