二相コミットのロジカルデコーディング – PostgreSQL 14でコミットされた機能の先行紹介:技術者Blog
PostgreSQLインサイド

Ajin Cherian

Fujitsu Australia Software Technology
Senior Software Development Engineer

はじめに

この記事では、論理レプリケーション(ロジカルレプリケーション)における二相コミットのデコードを可能にするために、富士通OSSチームがPostgreSQLオープンソースコミュニティーと協力してPostgreSQL 14で機能追加したことについて、説明します。

背景

Ajin
名前が示すように、二相コミットは、トランザクションが2つの相(フェーズ)でコミットされるメカニズムです。これは通常、分散データベースにおいて一貫性を保つために行われます。トランザクションの2つの相は、「PREPARE」相と「COMMIT / ROLLBACK」相です。
PostgreSQLで二相コミットを行うために使用されるコマンドは、以下のとおりです。

  • PREPARE TRANSACTION
  • COMMIT PREPARED
  • ROLLBACK PREPARED

PostgreSQL 8.0で二相コミットを、PostgreSQL 10.0で論理レプリケーションをサポートしましたが、論理レプリケーションにおける二相コミットはサポートされていませんでした。コマンドのPREPARE TRANSACTION、COMMIT PREPARED、およびROLLBACK PREPAREDはインスタンス内ではサポートされていましたが、これらのコマンドをスタンバイに論理的にレプリケーションする必要がある場合には、元の意味を保たなくなりました。

PREPARE TRANSACTIONコマンドはNOPとして扱われ、まったくデコードされませんでした。COMMIT PREPAREDコマンドはCOMMITとして扱われ、ROLLBACK PREPAREDコマンドはABORTとして扱われました。

二相コミットとは?

Ajin
二相コミットはアトミックコミットプロトコルの一種で、分散データベース間における一貫性の維持に役立ちます。データベース内で原子性を提供するプレーンなコミットでは、複数のデータベースにまたがるトランザクションの一貫性を維持できない場合があります。

この問題を説明するために、例として以下のような前提条件を持った2つの異なる銀行における口座間での送金を見てみましょう。

  • Johnは銀行Aに残高300ドルの口座を持っています。
  • Markは銀行Bに残高100ドルの口座を持っています。
  • JohnはMarkに100ドル送金したいと思っています。

取り引きを実行すると以下のようなトランザクションが実行され、取り引きの終了時に両方の口座残高が200ドルになる必要があります。もし、いずれかのトランザクションが失敗した場合、口座の状態は取り引き開始前の状態に戻るはずです。

  1. 銀行AにあるJohnの口座残高から100ドルを差し引く
  2. 銀行BにあるMarkの口座残高に100ドルを加算する

トランザクションは、複数の理由で失敗する可能性があります。データベースは、トランザクションがコミットされる前に障害が発生した場合、そのトランザクションがロールバックされるように構築されています。
前出の例では、Johnの口座から差し引かれている間に障害が発生した場合、障害発生後のJohnの口座には、トランザクションが失敗したために差し引かれた金額は反映されません。これが、単一のコミットがデータベース内の一貫性を維持する方法です。

しかし、銀行AにあるJohnの口座から100ドルを差し引く取り引きは1回のコミットで成功したが、銀行BにあるMarkの口座に100ドルを加える取り引きは失敗し、ロールバックされたというシナリオを考えてみましょう。
この場合、Johnの口座からは100ドルが差し引かれますが、Markの口座には100ドルが加算されず、100ドルが消えてしまいます。
単一のコミットでは、このような分散トランザクションを扱うときに失敗する可能性があります。二相コミットは、この問題を解決するためのメカニズムとして進化しました。

二相コミットの場合、データベースの1つ(または外部の裁定者)が分散トランザクションのコーディネーターとして機能します。

第1相

1つのデータベースがトランザクションの適用を開始し、次に「プリペア」を行います。プリペアドトランザクションをプリペアメッセージで他のデータベースに送信します。第2のデータベースはプリペアメッセージを取得し、トランザクションの準備も行います。準備では、トランザクションに含まれる変更を行いますが、コミットは行いません。変更はディスクに書き込まれるため、障害に耐えることができます。両方のデータベースがトランザクションを「プリペア」し、トランザクションに関するすべての必要な情報がディスクに格納されると、プリペア相(第1相)が完了します。

第2相

次に、裁定者はコミット・フェーズを開始します。第2のデータベースが何らかの理由でトランザクションの「プリペア」に失敗した場合、裁定者はロールバック・フェーズを開始します。したがって、2番目のデータベースで「プリペア」が完了したどうかによって、変更は両方ともコミットされるか、両方ともロールバックされます。最終コミット・フェーズで発生した障害は、必要なプリペアドトランザクションがディスクに書き込まれ、再適用できるため、リカバリー可能です。

二相コミットは、実際には単一インスタンスのデータベース・インストレーションには関係ありませんが、複数のデータベース・インスタンス間でデータがレプリケーションされる大規模なインストレーションには関係があります。

このことからPostgreSQLが、論理レプリケーションにおいて二相コミットをサポートすることが重要なのです。

機能概要

Ajin
PostgreSQL 13までは、論理レプリケーションのトランザクションは、トランザクションがコミットされた後にのみデコードおよびレプリケーションされました。これは、最終的に中断される可能性のあるトランザクションのレプリケーションを避けるためでした。

図1:COMMITによるトランザクションのデコード
図1:COMMITによるトランザクションのデコード

富士通OSSチームがPostgreSQLオープンソースコミュニティーのメンバーと協力してPostgreSQL 14に実装した新機能により、PREPARE TRANSACTION、COMMIT PREPARED、ROLLBACK PREPAREDの各コマンドが論理レプリケーションでサポートされるようになりました。PREPARE TRANSACTIONコマンドのデコード時にトランザクションのデコードとレプリケーションが行われます。PREPARE TRANSACTIONは、WAL senderのCOMMITコマンドと同様に、トランザクションの再実行とデコードを開始します。

図2:PREPAREによるトランザクションのデコード
図2:PREPAREによるトランザクションのデコード

また、ロジカルデコーディングのプラグインが二相コミットをサポートできるようにするための、新しいプラグインコールバックも定義しました。

新しい出力プラグインコールバックの一覧表

コールバック名 説明
filter_prepare_cb PREPARE TRANSACTIONコマンドで使用されるGIDに基づいて、プラグインが準備時にデコードする必要のないトランザクションをフィルタリングできるようにします。
begin_prepare_cb プリペアドトランザクションの開始がデコードされる際に呼び出されます。
prepare_cb PREPARE TRANSACTIONコマンドがデコードされる際に呼び出されます。
commit_prepared_cb COMMIT PREPAREDコマンドがデコードされる際に呼び出されます。
rollback_prepared_cb ROLLBACK PREPAREDコマンドがデコードされる際に呼び出されます。

実装の詳細は以下を参照してください。

プラグインの変更:test_decoding

test_decodingプラグインは、ユーザーが独自のロジカルデコーディング用プラグインを開発するための例となるロジカルデコーディング出力プラグインです。test_decodingは、ロジカルデコーディング機構を通してWALを受け取り、実行された操作のテキスト表現にデコードします。

test_decodingプラグインは、準備時に新しい二相コールバックとデコードトランザクションを使用できるように変更されました。

関数の変更:pg_create_logical_replication_slot()

この関数では、スロットが二相コミットをサポートするかどうかを指定する新しいオプションが追加されました。two_phaseオプションを指定して作成されたレプリケーションスロットは、二相コミットをサポートするために出力プラグインで使用できます。

pg_create_logical_replication_slot(slot_name name, plugin name [, temporary boolean, two_phase boolean ] )

詳細は以下を参照してください。

実行例

Ajin
二相コミットにおいてトランザクションの出力をデコードしてみます。

  1. レプリケーションスロットの作成

    「regression_slot」という名前のレプリケーションスロットを作成します。出力プラグインとしてtest_decodingを使用し、trueを渡して、スロットが二相コミットのデコードをサポートするようにします。

postgres=# SELECT * FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding', false, true);
    slot_name    |    lsn
-----------------+-----------
 regression_slot | 0/16B1970
(1 row)
  1. テーブルの作成

postgres=# CREATE TABLE data(id serial primary key, data text);
CREATE TABLE
  1. 二相コミットの実行とデコードされた結果の出力

    プリペアドトランザクションとコミットされたトランザクションのデコードされた変更情報を出力します。

postgres=# BEGIN;
postgres=*# INSERT INTO data(data) VALUES('5');
postgres=*# PREPARE TRANSACTION 'test_prepared1';

postgres=# SELECT * FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL);
    lsn    | xid |                          data
-----------+-----+---------------------------------------------------------
 0/1689DC0 | 529 | BEGIN 529
 0/1689DC0 | 529 | table public.data: INSERT: id[integer]:3 data[text]:'5'
 0/1689FC0 | 529 | PREPARE TRANSACTION 'test_prepared1', txid 529
(3 rows)

postgres=# COMMIT PREPARED 'test_prepared1';
postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NULL);
    lsn    | xid |                    data
-----------+-----+--------------------------------------------
 0/168A060 | 529 | COMMIT PREPARED 'test_prepared1', txid 529
(4 row)

postgres=# select * from data;
id | data
----+------
  1 | 5
(1 row)
  1. プリペアドトランザクションのロールバック実行とデコードされた結果の出力

    プリペアドトランザクションとロールバックされたトランザクションのデコードされた変更情報を出力します。

postgres=#-- you can also rollback a prepared transaction
postgres=# BEGIN;
postgres=*# INSERT INTO data(data) VALUES('6');
postgres=*# PREPARE TRANSACTION 'test_prepared2';
postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NULL);
    lsn    | xid |                          data
-----------+-----+---------------------------------------------------------
 0/168A180 | 530 | BEGIN 530
 0/168A1E8 | 530 | table public.data: INSERT: id[integer]:4 data[text]:'6'
 0/168A430 | 530 | PREPARE TRANSACTION 'test_prepared2', txid 530
(3 rows)

postgres=# ROLLBACK PREPARED 'test_prepared2';
postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NULL);
    lsn    | xid |                     data
-----------+-----+----------------------------------------------
 0/168A4B8 | 530 | ROLLBACK PREPARED 'test_prepared2', txid 530
(1 row)

postgres=# select * from data;
id | data
----+------
  1 | 5
(1 row)

今後に向けて

Ajin
PostgreSQL 14で加えられた機能変更により、準備時に二相コミットをデコードできるデコーダー側の基盤ができました。この基盤を利用するためにtest_decodingプラグインも変更しました。

次のステップは、PostgreSQL内で最大のロジカルデコーディングプラグインであるpgoutputプラグインに、二相コミットのサポートを実装することです。pgoutputプラグインは、PostgreSQLの論理レプリケーションのPUBLISHER / SUBSCRIBERモデルをサポートします。また、最も広く使用されている論理レプリケーション用のプラグインでもあります。富士通OSSチームは、PostgreSQL 15でこのサポートを追加するために、オープンソースコミュニティーと協力しています。

二相トランザクションが分散データベースで動作するためには、PostgreSQLは、失敗した「プリペア」をマスターに通知し、「ロールバック」を開始するようスタンバイ側をサポートする必要もあります。このタイプのスタンバイフィードバック機構はPostgreSQLには存在せず、将来の改善候補です。

2021年8月27日公開

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

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

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

お電話でのお問い合わせ

Webでのお問い合わせ

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

ページの先頭へ