pg_stat_replication_slotsの基本的な内部機構 – PostgreSQL 14でコミットされた機能の紹介:技術者Blog
PostgreSQLインサイド

大墨 昂道

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

はじめに

今回は、PostgreSQL 14で新たに導入されたシステムビュー「pg_stat_replication_slots」と、それに関連する内部機構について説明します。読者の皆様には、pg_stat_replication_slotsと論理レプリケーションについての理解を深めていただければ幸いです。

背景

論理レプリケーションは、PostgreSQL 10でリリースされて以来、PostgreSQLコミュニティーの多くの開発者が多面的な改良に最も力を尽くした分野の1つです。この論理レプリケーションの開発のなかで、pg_stat_replication_slotsは、ロジカルデコーディングの内部リソース使用量に関連する統計情報を監視する目的で実装されたのものです。

pg_stat_replication_slotsビューのすべての列定義はPostgreSQL文書で確認することができます。
利用者によっては、各列をどのように識別したら良いか、特にトランザクションの列と、ストリーミングまたはディスクへの書き込みによって増加する列との関係性で混乱するかもしれません。
そのため先に、理解の足がかりとして、ロジカルデコーディングやこのトピックと切り離せないGUCパラメーターの1つであるlogical_decoding_work_memを俯瞰して見ることをお勧めします。この記事は、ロジカルデコーディングや論理レプリケーション機能の利用者、それらのレビュー活動に興味がある人、初めて利用する人が、いくつかの内部処理の基本的な流れを把握するのに役立つと思います。

目次

pg_stat_replication_slotsはシステム全体でどのように扱われるか

ご存知のように、論理レプリケーションの構成には、パブリッシャーとサブスクライバーという2つの役割があります。パブリッシャー側では、wal senderプロセスがWALを1つ1つ抽出し、pgoutputプラグインでWALを論理レプリケーションのプロトコルに変換する役割を担います。そして、パブリケーションの指定に合致したデータを、サブスクライバー側のapply workerプロセスに連続的に転送します。
サブスクリプション(複数)は、遠隔のパブリッシャー側の論理レプリケーションスロットからデータを受信します。論理レプリケーションスロットは、元のサーバーで行われた順序でクライアントに再生できるWALのストリームを司るメカニズムで、デコードのために必要なWALの削除を防ぎます。pg_stat_replication_slotsの各統計情報のレコード(行)は、上記の論理レプリケーションスロットごとに作成されます。あるスロットからWALをデコードした内容を受信して消費することができる受信者は常に1人だけですが、異なる時間に同じスロットを使用する異なる受信者がいることもありえます。このスロットの統計情報は、他のデコード出力プラグインまたはサブスクライバーのいずれかに使用できます。なお、本記事では例として論理レプリケーションでのサブスクライバー用として使用します。

ロジカルデコーディングとそのメモリーサイズ

wal senderプロセスのWAL読み込みフェーズでは、WALの内容をメモリー上にReorderBufferChangeという構造体で展開し、ロジカルデコーディングの各タイプのWALを表現します。(ただ、これはWALをデータ構造として単純に展開しただけでなく、カタログ変更に伴う新しいスナップショットの追加のように、ロジカルデコーディングの利用者が必ずしも見る必要のない内部情報も保持します。)
デコードごとのトランザクションデータ使用量は、この構造体自体のサイズと、WAL内に記録されている変更のために追加で展開された重要情報の合計として計算されます。例えば、INSERTをデコードする場合、基本データサイズとしてReorderBufferChange構造体、およびHeapTupleData構造体とそのタプルの長さが、トランザクションのデータ使用量として計算されます。同様に、TRUNCATEのデコードでは、OID(オブジェクト識別子)のサイズに、切り捨てられるリレーションの数を乗算した値が基本データサイズに加算されます。
このようにして、どのような変更の操作が行われたかに応じて、pg_stat_replication_slotsビューに記録される、トランザクションのデコードに必要なメモリー使用量を計算します。wal senderプロセスは各トランザクションの変更をソートするために、キーがxid(トランザクションID)で値がトランザクションの変更とするハッシュテーブルを保持します。当然ながら、トランザクションで実施した変更が多ければ多いほど、そのトランザクションのメモリー消費量は大きくなります。

logical_decoding_work_memの仕組み

次に、logical_decoding_work_memの仕組みについて説明します。大規模なトランザクションや多数のセッションによってサーバーがメモリー不足になることを防ぐために、コミュニティーはデコード処理に必要なメモリーを制限できます。
このパラメーターは、デコードされた変更がローカルディスクに書き込まれるか、あるいはサブスクライバーにトランザクションの進行中にストリーミングされるかを決定する前に、ロジカルデコーディングによって使用されるメモリーの最大量を利用者が指定できます。このチェックは、前述のようにデコードごとに実行され、トランザクションに必要なメモリーが閾値を超えると、streamingオプションに応じて、ディスクへの書き込み処理か、あるいはトランザクション進行中にストリームする処理が実行されます。

pg_stat_replication_slotsでディスクへの書き込みを観察する

以下に、簡単な書き込みの事例を説明します。PostgreSQL 14の論理レプリケーションのセットアップで、整数(integer)列のテーブル"tab"を1つ用意します。サブスクリプションは、CREATE SUBSCRIPTION文でstreaming = offとします。

  1. logical_decoding_work_mem を64KBに設定し、3,000件のデータを挿入することで、必要なメモリーサイズを超えるようにします。
INSERT INTO tab VALUES (generate_series(1, 3000));
  1. spill関連列(ディスクへの書き込みに関する情報)の統計情報の更新を確認します。
postgres=# SELECT slot_name, spill_txns, spill_count, spill_bytes FROM pg_stat_replication_slots;
-[ RECORD 1 ]-------
slot_name   | mysub
spill_txns  | 1
spill_count | 7
spill_bytes | 396000

デコードされたデータは、トランザクションメモリーがlogical_decoding_work_memに達するたびに、ディスクへの書き込まれます。さらに、コミットレコードのデコード処理の一環として、サイズが閾値以下であっても、最後に残ったトランザクションデータもディスクに書き込まれる仕組みがあります。上記2.の場合、spill_txnsの列が示すように、3000行を挿入するトランザクションの回数は1回だけです。デコードされたデータの全体サイズは396,000バイトです。wal senderプロセスの書き込み回数は、logical_decoding_work_memの閾値に達した6回と、最後の残りの1回の合計で7回になります。
ここで重要なのは、 トランザクションのデータがディスクに書き込まれる動作は、1回でもメモリーの閾値を超過すると対象のトランザクションの全サイズがディスクに書き込まれる、ということです。書き込み回数は、logical_decoding_work_memに依存します。

pg_stat_replication_slotsで進行中のトランザクションのストリーミング処理を観察する

この項では、より興味深いシナリオでstreamingをオンにしたときのストリーミングの流れを説明します。デコード処理ごとにメモリーサイズを確認する基本的な仕組みは、ディスクへの書き込みの説明と基本的に同じです。
ここでは、トランザクションの選択という考えを含めた流れを把握するために、3つのサイズの挿入レコード(比較的大-300レコード、中-200レコード、小-10レコード)を使い、wal senderプロセスの統計値の更新を見ながら、論理レプリケーションを構築し、2セッションで1つずつトランザクションを実行します。新規のクラスタを作成してから、こちらのシナリオを実行します。テーブル名などは同じです。
大・中の挿入を1トランザクションずつ実行しても、64KBに設定されたlogical_decoding_work_memに達しません。しかし、それらを並列に実行することで、ストリーミングシナリオをエミュレートするための仕掛けが動作します。今回のサブスクリプションは、streaming = onとします。

図1. wal senderプロセスによるストリーミング処理のエミュレート
図1. wal senderプロセスによるストリーミング処理のエミュレート

図1に示すように、まずTxn1がトランザクションを開始し、比較的大きなサイズを挿入します。次に、別のセッションTxn2がトランザクションを開始し、中程度の量の新規レコードを並行して挿入します。WALレコードは、これらのバックエンドによって書き込まれます。一方、WALのデコードはwal senderプロセスが担当します。wal senderプロセスはWALの内容を抽出し、メモリーの閾値に達するタイミングでデータをストリームするトランザクションをピックアップします。
この場合、wal senderプロセスはいくつかの処理が非同期に独立して動作するため、 厳密には④の終了前後に⑤がトリガーされる可能性があります。⑤の終了タイミングは若干早まったり遅くなったりしますが、この例では統計情報値の更新を確認してから進めます。
ここで重要なのは、Txn2で④を並列に実行するとメモリーの閾値を超えてしまうが、この場合wal senderプロセスはTxn2ではなく、Txn1を最大のトランザクションとして選択する点です。こうして、300レコードの挿入が最初にサブスクライバーにストリーミングされます。結果として、Txn1のトランザクション内の他のデコード情報⑥も、同じ方式でストリームされます。これは、「pg_stat_replication_slotsでディスクへの書き込みを観察する」の事例と同様に、一度ストリーミングが起動されると、トランザクション全体がストリーミングされるためです。図1には書かれていませんが、もちろんコミット後にTxn2データはまとめてサブスクライバーに送られます。
以下に、今回のエミュレーション中に5つのタイミングで取得したpg_stat_replication_slotsのストリーミング関連列の統計情報の更新を見てみます。

  1. 最初、②と③の間ではストリーミングのトランザクションとカウントはありません。
postgres=# SELECT stream_txns, stream_count, stream_bytes FROM pg_stat_replication_slots;
-[ RECORD 1 ]+--
stream_txns  | 0
stream_count | 0
stream_bytes | 0
  1. ⑤と⑥の間では、Txn1の最初の挿入(300件)のストリーミングが完了した状態です。
postgres=# SELECT stream_txns, stream_count, stream_bytes FROM pg_stat_replication_slots;
-[ RECORD 1 ]+------
stream_txns  | 1
stream_count | 1
stream_bytes | 39600
  1. ⑥と⑦の間では、追加の挿入(10件)が開始された後ですがコミットされる前で、値に変化はありません。
postgres=# SELECT stream_txns, stream_count, stream_bytes FROM pg_stat_replication_slots;
-[ RECORD 1 ]+------
stream_txns  | 1
stream_count | 1
stream_bytes | 39600
  1. ⑧と⑨の間では、Txn1の残りのストリーミングが完了した状態です。
postgres=# SELECT stream_txns, stream_count, stream_bytes FROM pg_stat_replication_slots;
-[ RECORD 1 ]+------
stream_txns  | 1
stream_count | 2
stream_bytes | 40920
  1. ⑨でTxn2がコミットされた後では、ストリーミングの値に変化はありません。
postgres=# SELECT stream_txns, stream_count, stream_bytes FROM pg_stat_replication_slots;
-[ RECORD 1 ]+------
stream_txns  | 1
stream_count | 2
stream_bytes | 40920

最大の(トップ)トランザクションを選択するための処理には例外があることに注意してください。例えば、投機的挿入をデコードしたトランザクションのストリーミングは、wal senderプロセスが投機的確認または中止のデコードを取得するまで延期されます。詳しくはPostgreSQL文書のストリーミングの例外を参照してください。
もう少し理解を深めるために、先ほどのシナリオがどのように適用されるのか、サブスクライバーでのメッセージフローを図2で簡単に見てみましょう。
ここで注意したいのは、サブスクライバーは進行中のトランザクションをストリームする方式で送られてきたストリーム・データをスプールし、後続のメッセージに従って内容をコミットまたはアボートする仕組みになっていることです。図2では、INSERTメッセージに対して、実際の挿入レコード値を書くのではなく、説明を簡単にするために連続している同系のメッセージを折りたたんでいます。INSERTの横の数字は、折りたたまれたレコードの数です。

図2. サブスクライバー側でメッセージを適用する単純化されたフロー
図2. サブスクライバー側でメッセージを適用する単純化されたフロー

説明を簡単にするために、複数の論理レプリケーションのメッセージを、処理の段階に応じて四角で囲みました。ご覧のように、Txn1のメッセージはSTREAM STARTとSTREAM STOPのメッセージで囲まれています。これは、図1に描かれた2つのストリームデータの流れに対応します。Txn1のコミット時に10レコードの小サイズの挿入が送信されるストリームデータを思い出してください。それによって、STREAM COMMITは2番目のストリーミングブロックの直後に送信されます。STREAM COMMITはスプールされた内容を処理し、トランザクションをコミットする契機となります。続くTxn2においても、サブスクライバー側に送信されますが、進行中のトランザクションを途中でストリーミングする方式とは異なった、基本的なデコード形式で送られます。
pg_stat_replication_slotsには、すべての種類のトランザクションに対応する列が存在します。これらの列は、書き込まれたトランザクションとストリームされたトランザクションの両方を含む、すべてのトランザクションリソースの活動を表し、記録します。Txn2の統計情報も、これらの列に格納されます。

結論

pg_stat_replication_slotsビューを参照することで、内部のデコードの動きを解釈できることを説明しました。主な要点は以下のとおりです。

  • wal senderプロセスは、デコードされたデータ量の大きさを管理する。
  • logical_decoding_work_memを超えないトランザクションは、ディスクへの書き込みでも、コミット前に進行中のトランザクションのデータを分割してストリームする方式でも処理されない。
  • ディスクへの書き込みと、進行中のトランザクションを分割してストリーミングする方式の両方について、一部の例外を除き、メソッドはトリガーされるとトランザクション全体に適用される。

pg_stat_replication_slotsビューを利用することで、利用者はメモリーの閾値を超えたトランザクションを処理するために、どれだけのリソースが使われ、どれだけのイベントが発生したかを把握できます。
現実の世界では、本番稼動しているビジネスシステムで発行されるトランザクションは、今回紹介した例よりもはるかに複雑です。しかし、ここで説明したことは、より高度なケースを理解するための基礎になります。

今後の展望

最後になりましたが、この機能のためにコミュニティーが尽力してくれたことに感謝します。このビュー自体は PostgreSQL 14で導入されましたが、ビューを構築するための舞台裏での多くのコミットの数は、ビューの実装に対するコミュニティーの献身的な作業を物語っています。
現在、コミュニティーでは、サブスクライバー側の統計情報を充実させるための議論が行われています。それらの改善案がコミットされ、利用者がその恩恵を享受できることを楽しみにしています。

2022年2月18日公開

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

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

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

お電話でのお問い合わせ

Webでのお問い合わせ

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

ページの先頭へ