1. 改訂履歴

改訂日 変更内容
1.0 2017/06/20
2016年度版として新規作成
- PostgreSQL 9.6 を対象バージョンとする
2.0 2018/04/11
2017年度版として主に以下の章を加筆修正
- 5章  ストリーミングレプリケーション
     PostgreSQL 10 に対応
     複数スレーブ対応 (1.0版では2ノードに限定していた)
- 6章  ロジカルレプリケーションの追加
     PostgreSQL 10 の新機能として追加

2. ライセンス

本作品はCC-BYライセンスによって許諾されています。 ライセンスの内容を知りたい方は こちら でご確認ください。 文書の内容、表記に関する誤り、ご要望、感想等につきましては、PGEConsのサイト を通じてお寄せいただきますようお願いいたします。

  • Eclipseは、Eclipse Foundation Incの米国、およびその他の国における商標もしくは登録商標です。
  • IBMおよびDB2は、世界の多くの国で登録されたInternational Business Machines Corporationの商標です。
  • Intel、インテルおよびXeonは、米国およびその他の国における Intel Corporation の商標です。
  • Javaは、Oracle Corporation 及びその子会社、関連会社の米国及びその他の国における登録商標です。 文中の社名、商品名等は各社の商標または登録商標である場合があります。
  • Linux は、Linus Torvalds 氏の日本およびその他の国における登録商標または商標です。
  • Red HatおよびShadowman logoは、米国およびその他の国におけるRed Hat,Inc.の商標または登録商標です。
  • Microsoft、Windows Server、SQL Server、米国 Microsoft Corporationの米国及びその他の国における登録商標または商標です。
  • MySQLは、Oracle Corporation 及びその子会社、関連会社の米国及びその他の国における登録商標です。 文中の社名、商品名等は各社の商標または登録商標である場合があります。
  • Oracleは、Oracle Corporation 及びその子会社、関連会社の米国及びその他の国における登録商標です。 文中の社名、商品名等は各社の商標または登録商標である場合があります。
  • PostgreSQLは、PostgreSQL Community Association of Canadaのカナダにおける登録商標およびその他の国における商標です。
  • Windows は米国 Microsoft Corporation の米国およびその他の国における登録商標です。
  • TPC, TPC Benchmark, TPC-B, TPC-C, TPC-E, tpmC, TPC-H, TPC-DS, QphHは米国Transaction Processing Performance Councilの商標です。
  • その他、本資料に記載されている社名及び商品名はそれぞれ各社が 商標または登録商標として使用している場合があります 。

3. はじめに

3.1. PostgreSQLエンタープライズコンソーシアムとWG3について

PostgreSQLエンタープライズコンソーシアム(略称 PGECons)は、PostgreSQL本体および各種ツールの情報収集と提供、整備などの活動を通じて、ミッションクリティカル性の高いエンタープライズ領域へのPostgreSQLの普及を推進することを目的として設立された団体です。

PGECons 技術部会ではPostgreSQLの普及に資する課題を活動テーマとし、3つのワーキンググループで具体的な活動を行っています。

  • WG1(新技術検証ワーキンググループ)
  • WG2(移行ワーキンググループ)
  • WG3(課題検討ワーキンググループ)

これら3つのワーキンググループのうち、WG1、WG3については 2015 年度まではそれぞれ、「性能ワーキンググループ」、「設計運用ワーキンググループ」という名称で活動してきました。2016年度は、従来の活動領域を広げる意図のもとでそれらを再定義し、上記のような名称に改めました。

これに伴い、WG3ではPostgreSQLの設計運用を中心としたさまざまな課題の解決のための調査検証を行い、PostgreSQLが広く活用される事を推進していくこととしました。

3.2. 本資料の概要と目的

本資料はWG3の2017年度の活動としてPostgreSQLにおけるレプリケーション機能について、調査検討した結果をまとめたものです。

これまでにも、WG3では3度にわたってレプリケーションについて取り上げています。

2013年では、PostgreSQLをエンタープライズ領域で活用するにあたって、業務要件とコストとのバランスを考えて可能な限り業務を継続できるように、システムの運用や保守サービスをPostgreSQLで実現する手法についてまとめました [PGECons_WG3_2013] 。報告書では、可用性を向上させるための手段としてレプリケーションを取り上げています。今年度の報告書では取り上げなかった、ストレージレプリケーション(DRBD)、トリガベースレプリケーション(Slony-I)についても紹介しています。

2014年度では、可用性のうち「災害対策」に焦点を当て、ITサービス継続を可能とする PostgreSQL の構成について調査検討しています [PGECons_WG3_2014] 。ストリーミングレプリケーションを含む代表的な PostgreSQL のシステム構成を挙げて、データベースのデータ保全性、サービス継続性の観点から各構成の得失を示しました。

2016年度では、ストリーミングレプリケーションの2ノードにおける運用ノウハウの整理や PostgreSQL をベースとして双方向レプリケーションがリリースされています。

これらの開発動向と過去の検討結果とを踏まえて、2017年度は PostgreSQL 10にて機能拡充が進むストリーミングレプリケーションの3ノードにおける運用ノウハウの整理と、新機能のロジカルレプリケーションの調査を実施しました。

3.3. 本資料の構成

  • はじめに
  • PostgreSQLにおけるレプリケーション
    • レプリケーションの目的
    • 代表的なレプリケーションの手法
  • ストリーミングレプリケーション
    • はじめに
    • SR環境構築時の設定項目、推奨値
    • SR環境の監視
    • SR環境の障害時運用
    • スレーブのアーカイブ保存
    • まとめ
  • ロジカルレプリケーション(仮)
    • はじめに
    • ロジカルレプリケーション環境構築時の設定項目、推奨値
    • ロジカルレプリケーション動作検証
    • ロジカルレプリケーション性能検証
    • まとめ
  • Bi-Directional Replication (BDR)
    • はじめに
    • BDR環境構築時の設定項目、推奨値
    • BDR動作検証
    • BDR性能検証
    • まとめ
  • おわりに

3.4. 想定読者

本資料の読者は以下のような知識を有していることを想定しています。

  • DBMSを操作してデータベースの構築、保守、運用を行うDBAの知識
  • PostgreSQLを利用する上での基礎的な知識

3.5. 参考文献

[PGECons_WG3_2013]PostgreSQL Enterprise Consortium. 2013年度WG3活動成果報告書. 2014. https://www.pgecons.org/wp-content/uploads/PGECons/2013/WG3/pgecons-wg3-2013-report.pdf
[PGECons_WG3_2014]PostgreSQL Enterprise Consortium. 2014年度WG3活動成果報告書 (可用性編). 2014. https://www.pgecons.org/wp-content/uploads/PGECons/2014/WG3/PGECons_2014_WG3_Availability.pdf
[PostgresBDR]2ndQuadrant. Postgres-BDR. Version Postgres-BDR94 1.0.2, November 14, 2016. https://2ndquadrant.com/en-us/resources/bdr/

4. PostgreSQLにおけるレプリケーション

データベースにおいて、レプリケーションとは複数のデータベースサーバの間で、 何らかの一貫性を保ちながら、その内容を複製する手法を指します。PostgreSQLにおいては、 9.0 以降の各バージョンの基本機能として、レプリケーションが実現されています。 また、PostgreSQLに付加するツールによってもレプリケーションが実現されています。 この章では、各種のレプリケーション機能を目的に応じて使い分けるために、機能と特性について簡単に紹介します。

4.1. レプリケーションの目的

レプリケーションによって複数のデータベースサーバの複製を作ることによって、シングルサーバでは対応が難しい要件にも対応できるようになります。それらは、データの冗長性(複製があること)と複数サーバによる処理の分散の結果です。

4.1.1. 可用性の向上

可用性はITシステムの非機能要件の一つで、システムを継続的に利用可能とすることです [IPA] 。 可用性は「継続性」「耐障害性」「災害対策」「回復性」という4つの要素から構成されますが、レプリケーションによってデータベースサーバを冗長化することで、1つのサーバで故障や災害が生じたときにもレプリカが格納されている残りのサーバでサービスを継続することができるるようになります。また、運用上の停止が必要な場合であっても、各サーバを順次停止して作業することで、サービス全体としては停止させないようにできます。

先に挙げた可用性の4つの要素をどの程度満足するかはデータベースの構成によって変わってきます。詳しくは『2013年度WG3活動成果報告書』 [PGECons_WG3_2013] を参考にしてください。

4.1.2. 性能向上

レプリケーションによって、同じ情報を格納しているデータベースサーバが複数存在することになります。それらのサーバでアプリケーションからの要求にこたえることが出来れば、システム全体としての性能状況が期待できます(スケールアウト)。アプリケーションからの要求を複数のサーバに分散させる際には、更新(削除・挿入を含む)クエリを特定の1サーバに集約する「シングルマスタ」構成と、複数のサーバに分散する「マルチマスタ」構成があります。また、参照クエリを複数のサーバに分散させることを参照負荷分散と呼びます。

レプリケーションクラスタを性能向上に用いる場合、複数のデータをベースを同時に運用することから生じる特有の課題があります。レプリケーション方式を選択する際には、それらの課題をどの程度解決しているのかについても考慮する必要があります。

4.1.2.1. 参照の同期

ある瞬間に同一の参照クエリを異なるサーバに送った時に、まったく同じ結果が返ってくるものと、そうでないものとがあります。同じ結果が得られる場合、サーバは同期している、同期レプリケーションであると言います。

4.1.2.2. 更新による一貫性の維持

マルチマスタ構成の場合には同期の問題に加えて、更新の衝突と一貫性の維持が問題となります。

更新の衝突
複数のアプリケーションプログラムから同一の更新操作を実行しようとした場合、単一のDBサーバであればどちらかの操作が遅延され、場合によってはエラーとなります。マルチマスタ構成では、レプリケーションの送信側でなされた更新と、そのサーバに直接要求された更新とが衝突した場合に、どちらが優先するのかが問題となります。
一貫性の維持
更新が衝突した結果、レプリケーションの送信側と受信側でデータベースの内容が異なってしまうと、レプリケーションによって構成されるクラスタ全体でデータベースの一貫性が維持されなくなります。この問題を適切に対処する必要があります。

4.2. 代表的なレプリケーションの手法

ここでは、本報告書で取り上げるレプリケーション手法を中心に、PostgreSQL で利用できる代表的なレプリケーション手法を紹介します。レプリケーションを利用する立場からは、シングルマスタとマルチマスタに二分することができます。その上で、レプリカを生成する方法に着目して代表例を挙げ、そのメリット・デメリットを説明します。

なお、『2013年度WG3活動成果報告書』 [PGECons_WG3_2013] では可用性向上の観点から、レプリケーションを含めて様々な PostgreSQL の構成を取り上げていますので、併せてご覧ください。

コミュニティのWikiページには、PostgreSQL上で動作するクラスタソフトウェアについての解説があり、その中にレプリケーションも含まれています [PGWiki_replica] 。ここで紹介する紹介するレプリケーションソフトウェアについても紹介されています。

4.2.1. シングルマスタ

シングルマスタ構成の場合、レプリカを生成する手法には以下のようなものがあります。

  • ストレージレプリケーション
  • トリガベースレプリケーション
  • クエリベースレプリケーション
  • ストリーミングレプリケーション
  • ロジカルレプリケーション

4.2.1.1. ストレージレプリケーション

PostgreSQLやその上で動作するツールを介することなく、データを格納するストレージのレベルでデータを複製します。ストレージ装置自体がレプリカを生成するものや、DRBD [DRBD] のようにLinux上で動作するソフトウェアによる実現があります。

メリット
  • PostgreSQLからは単一のサーバに見えるので、単一サーバと同じように運用できます
デメリット
  • 受信側のサーバはデータベースとしては動作していないので、負荷分散に利用することができません

4.2.1.2. トリガベースレプリケーション

PostgreSQLのデータベース内に更新によって起動されるトリガを設定しておき、更新による変分を受信側のサーバに送り出すもの。代表的な製品に Slony-I があります。以下では Slony-I での主なメリット・デメリットを紹介します。

メリット
  • PostgreSQLのデータベースクラスタに含まれる表全体だけでなく、任意のテーブルについてだけ複製を作成することができる
  • 更新される表については参照負荷分散、それ以外の表については更新負荷分散が可能です
デメリット
  • 比較的オーバヘッドが大きいため、後述のストリーミングレプリケーション方式に比べて、性能が低い傾向があります [pglogical]

4.2.1.3. クエリベースレプリケーション

アプリケーションプログラムとDBサーバ(PostgreSQL)の間に入るミドルウェアによって、発行されたクエリを複製して複数のDBサーバに送信することで、データベースを複製します。代表的な製品に Pgpool-II [Pgpool-II] があります。

メリット
  • 複数のDBサーバを用いて負荷分散を実現する際に、参照クエリ・更新クエリともに適切なサーバにクエリが自動的に振り分けられるため、アプリケーションから見ると単一のDBサーバを利用しているように見える
デメリット
  • 一部のSQL文に対する挙動が単一のDBサーバとは異なる

4.2.1.4. ストリーミングレプリケーション

PostgreSQL データベースでは、更新をコミットした際にその結果をクラッシュ等で失わないように更新情報をファイルに書きこむログ先行書込み(Write Ahead Logging; WAL)を用いています。このWALファイルにはデータベースに対する更新を全て復元することができる情報が含まれていますから、これを他のDBサーバに転送することでデータベースを複製することができる --- これがストリーミングレプリケーションの基本的な考え方です。

ストリーミングレプリケーションは、WALファイルに書かれた内容をほぼそのまま受信側(スレーブサーバ)に送り出すことで、送信側(マスタサーバ)と物理的に一致するDBを複製します。

メリット
  • 受信側サーバに送信側サーバと物理的に一致したデータベースを複製することができる
  • 送信側サーバでコミット済みのデータを受信側で確実に書き込み済みにすることができるため、高信頼化に適している
  • 参照負荷分散ができる
デメリット
  • 特定のデータベース、表だけを複製することはできない
  • メジャーバージョンが異なるPostgreSQLの間では利用できない
  • 受信側サーバのデータベースは更新できない

4.2.1.5. ロジカルレプリケーション

ロジカルレプリケーションは、PostgreSQLバージョン10から標準採用された機能です。ストリーミングレプリケーションが送信側のWALをそのまま受信側に転送するのに対して、ロジカルレプリケーションは送信側でWALファイルをデコードし、必要な変更内容のみを受信側に送ります。

メリット
  • 送信側サーバの一部の表に対する更新だけを受信側に送ることができる
  • 複数の送信側サーバの出力を1つの受信側サーバで受け取ることができる
  • メジャーバージョンが異なるPostgreSQLの間でも利用できる
  • 受信側サーバのデータベースを更新することができる
デメリット
  • レプリケーションできないSQLやオブジェクトがあり、送信側と受信側で不整合が発生しないよう注意して運用する必要がある
  • 受信側サーバのデータベースを更新した場合、送信側サーバでの更新内容と競合する可能性がある

4.2.2. マルチマスタ

マルチマスタ構成の場合、レプリカを生成する手法には以下のようなものがあります。

  • Bi-Directional Replication
  • Bucardo

4.2.2.1. Bi-Directional Replication

2nd Quadrant社が公開している Bi-Directional Replication は、先に紹介した論理レプリケーションを用いてデータを複製しつつ、複数のサーバでデータの更新を可能としたものです。主な用途としては地理的に離れた場所にある複数のサーバ間で、データを共有する利用形態を想定しています [PostgresBDR]

メリット
  • 論理レプリケーションを利用しているため、他の方式によるマルチマスタに比べてオーバヘッドが小さい
デメリット
  • レプリケーション自体は非同期に行われるため、複数サーバに同時に発行した参照クエリの結果が異なることがあります。

4.2.2.2. Bucardo

Bucardoは、トリガベースレプリケーションを使ってデータを複製します [Bucardo] 。そのため、メリット、デメリットはシングルマスタのトリガベースレプリケーションと同じです。

4.3. 参考文献

[IPA]独立行政法人情報処理推進機構. 非機能要求グレード 利用ガイド[活用編]. 2010. http://www.ipa.go.jp/files/000026853.pdf
[PGWiki_replica]Smith, G.; Grittner, K.; Pino, Conrad T.; Ringer, C.; Simon, R. et al. Replication, Clustering, and Connection Pooling. 2017. https://wiki.postgresql.org/wiki/Replication,_Clustering,_and_Connection_Pooling
[DRBD]LINBIT, Inc. http://www.drbd.org/en/
[pglogical]2ndQuadrant. pglogical. https://2ndquadrant.com/en/resources/pglogical/
[Pgpool-II]PgPool Global Development Group. http://www.pgpool.net/mediawiki/index.php/Main_Page
[Bucardo]Jensen, G.; Sabino, G. M. et al. https://bucardo.org/wiki/Main_Page

5. ストリーミングレプリケーション

5.1. はじめに

5.1.1. PostgreSQLのストリーミングレプリケーションの特徴

PostgreSQLのストリーミングレプリケーション(以下、SR構成)は以下を目的とした構成です。

  • 可用性の向上
  • 性能向上

以下の特徴があります。

  • 変更履歴が格納されたWALを操作単位でマスタ側からスレーブ側へ転送することでデータを同期
SR構成概要図

SR基本構成図(スレーブ1台)

PostgreSQL 10 における主な機能を示します。

  • 完全同期方式 により、スレーブ活用度の向上が期待できる
    • 同期式はWALの転送までの保証であり、データ保護が目的。
    • 完全同期式はWALの適用までの保証であり、データ同期が保証されるため、リアルタイムのデータ参照に期待。
    • synchronous_commitには以下の設定が可能であり、要件に応じて選択。
      • remote_apply :完全同期。WAL適用まで保証。昇格時の時間短縮と参照負荷分散の拡充が目的。
      • on      :同期。WAL転送(ディスク書き込み)まで保証。データ保護が目的。
      • remote_write :準同期。WAL転送(メモリ書き込み)まで保証。データ保護とパフォーマンスのバランスが目的。
      • local    :非同期。ローカルのWAL書き込みまで保証。パフォーマンス優先が目的。
      • off     :完全非同期。ローカルのWAL書き込みすら保証しない。最も高パフォーマンスだが非現実的。
    • 以下の図はsynchronous_commitの設定による保証時点を示す。
synchronous_commitの設定値概要図

  • レプリケーションスロット により、スレーブに必要なWALをマスタが確保し続ける
    • スレーブの障害時(ネットワーク不調含む)等にマスタでWALを保持し切れず、WALファイルの再利用によりロストする懸念がある。
    • レプリケーションスロットの登場前はwal_keep_segmentsパラメータによる調整、またはアーカイブモード運用が必要であった - wal_keep_segmentsはWAL数を指定するものであるため、見積もりが困難である。 - アーカイブモードも複数スレーブ構成における不要WALファイルの判断はやはり困難。
    • レプリケーションスロットによりWALファイルの要・不要の判断をシステムに任せる事ができる。
    • SR構成のWALファイル管理のためにアーカイブモード運用は必要はなく、過去の時点に復旧する(PITR)要件がある場合に設定する。
    • 複数スレーブの場合は、スレーブ毎に専用のレプリケーションスロットを作成する。
レプリケーションスロット
  • 複数同期スレーブ構成 が可能
    • 従来も複数スレーブ構成は可能であったが、同期スレーブはその内の1台までという制限があったがそれが取り払われた。
    • 同期スレーブを任意(ANY)に選択するQuorum-based 同期レプリケーションが実装された。
複数同期スレーブ
  • 遅延レプリケーション機能 により、オペレーションミスを反映させない事が可能に
    • スレーブでの適用を一定時間待機する機能である。マスタでのオペミスが即座にスレーブに伝搬されるのを防ぐのが主な目的。
    • WALファイルの転送は遅延なく処理されるため、データ保全(RPO)の観点では問題ない。同期モードで設定する事もできる。
    • recovery.conf のrecovery_min_apply_delayパラメータに遅延時間を設定。
    • 従来からの手法として、PITRを使用してオペミス直前時点を指定してリカバリすることで障害回復を図る方法もある。
遅延レプリケーション
  • 巻き戻し機能 (pg_rewind)により、効率の良いSR再構成が可能に
    • 旧マスタを分岐時点へ巻き戻す事で、新マスタに追い付く事が可能な状態とする機能である。
    • 巻き戻し後、旧マスタに新マスタのWALを適用(追い付き)する事で旧マスタを新スレーブとして構成。
    • 従来はpg_basebackup等による再作成が必要であった。
    • 大規模データベースでは、大幅な時間短縮が期待できる。
    • 下図に巻き戻し機能のイメージを示す。
pg_rewindイメージ図
  • WAL圧縮機能 により、WAL転送の効率を改善
    • WAL圧縮でサイズを縮小することにより、WAL転送の効率改善が目的。
    • スレーブを遠隔地に配置する場合に特に有効。
    • 注意点としては、圧縮(マスタ)や解凍(スレーブ)の負荷が発生する。

SR構成の運用上の注意点

  • 死活監視と障害発生時のフェイルオーバはPostgreSQLの機能ではできないため、クラスタソフトを利用する必要があります。 商用クラスタソフトが使用される場合もありますが、オープンソースソフトウェアのPgpool-II と呼ばれるクラスタソフトを使用した例も多く報告されています。 特に参照負荷分散を行う場合は、Pgpool-IIを使用します。

5.1.2. 検証を実施したSR構成

以下のSR構成にて検証を実施しています。

  • 対象バージョンは PostgreSQL 10

    • 2017年11月 時点の最新版
  • 3ノード構成

      1. 複数スレーブ方式(親 - 子1 , 子2) スレーブ2はマスタと繋がっている。
    複数スレーブ基本図
      1. カスケード方式(親 - 子 - 孫) スレーブ2はスレーブ1と繋がっている
    カスケード基本図
  • PostgreSQLの機能に限定

    • Pgpool-II等のクラスタソフトに関するテーマは対象外

5.2. SR環境構築時の設定項目、推奨値

5.2.1. SRにおける目的別の設定

PostgreSQLのSR構成には様々な機能があり、 対処する障害に応じて適切に設定する必要があります。 また各機能は組み合わせることが可能です。

  • "ストリーミングレプリケーション"を"SR"と表記します。
  • "レプリケーションスロット"を"スロット"と表記します。
表 5.1 SRの機能
機能 内容・目的 注意点・補足
アーカイブWAL

必要なWALファイルの保持。以下の2種類。

  • 物理バックアップ(PITR)に必要な(フルバックアップ以降の)WAL。 障害発生直前まで復旧や操作ミス時に過去に戻す機能がある。
  • SR構成において未転送のWALを保持する。
  • アーカイブWALがディスク領域を圧迫しないように、不要となったアーカイブWALの明示的な削除が必要。
  • SR用途の場合、WALファイルの要不要の判断が困難であるため、現在では後述のレプリケーションスロットの使用が一般的。
  • SR構成には物理バックアップという側面もあるため、過去に戻す要件がなければ物理バックアップを取得する必要性は低い。
  • これらを考慮し、一般的には非アーカイブモード運用でも十分と考えられる。
レプリケーションスロット スレーブに必要なWALファイルを保持する事によるSR構成の維持。 対応する各スレーブの未転送のWALを保持する。
  • スレーブへWALファイルの転送ができない状態が続くと、WALファイルが溜まり領域を圧迫する。
  • 複数スレーブ構成では、スレーブ毎にスロットの使用/未使用を選択できる。
  • スレーブ毎にスロットを作成する必要がある。
遅延レプリケーション スレーブ側のWAL適用を意図して遅らせることで、スレーブを一定の過去の状態に維持する。 操作ミス対策に任意の時点まで巻き戻せるようにする。
  • スレーブは常に過去の状態であるため、参照活用には一定の制限がある。
  • フェイルオーバ時には遅延分の適用が必要になるため、RTOの点で難がある。そのため複数スレーブ構成の場合に1スレーブで構成するのが一般的。
WAL圧縮 Full Page Write時(チェックポイント後の最初の更新時)に、WALファイルに書き出すフルページイメージを圧縮する。 圧縮されたWALは適用時に解凍される。 WALサイズを大幅に圧縮できる可能性があり、転送負荷の低減が期待される。特にディザスタ・リカバリ構成で有効と考えられる。
  • マスタの圧縮時とスレーブの解凍時に通常より余分にCPU負荷と時間がかる。

5.2.2. SRの基本設定手順(2ノード)

基本的なSR環境の設定手順を紹介します。 尚、マスタとスレーブの両サーバにPostgreSQLはインストール済みであり、 マスタ側ではデータベースクラスタを構築していることを前提としています。

  1. 関連パラメータ

SR構成に最低限必要な設定は以下の通りです。 スレーブはマスタのベースバックアップから作成されるため、マスタに設定したパラメータは全てスレーブも同様の値に設定されます。

  • "ストリーミングレプリケーション"を"SR"と表記します。
  • "レプリケーションスロット"を"スロット"と表記します。
  説明の便宜上、以下の設定とします。
表 5.2 構成データ
項目 内容
マスタのIPアドレス およびサーバ名
192.168.100.101/24 server1
スレーブのIPアドレスおよびサーバ名
192.168.100.102/24 server2
ポート番号
5432
レプリケーション用ユーザ/パスワード
repuser/password

表 5.3 SR構成の設定(マスタのpg_hba.conf)
設定値 内容
host replication repuser 192.168.100.101/32 trust
host replication repuser 192.168.100.102/32 trust
またはCIDR指定にて
host replication repuser 192.168.102.0/24 trust
SR用ユーザの接続を許可する。
・pg_hba.confは両ノード共通の設定となる事から、
 どちらがマスタになっても使用できる設定とする。
・IPアドレスはWAL転送を行うネットワークLANを指定。
 パブリックLAN以外に、ハートビート系LANも有力。
・認証方式は任意だが、passwordやmd5ではパスワードファイルにより入力を求められないようにする。

表 5.4 SRの設定(マスタのpostgresql.conf)
パラメータ 設定値 内容
listen_addresses
'0.0.0.0'
接続を受け入れるIPを指定。
再起動で反映。
wal_level
replica
WALに書かれる情報量を指定。
SR構成の場合は replica に設定。
デフォルトがreplicaであるため変更不要。
再起動で反映。
wal_keep_segments
64
暫定値として、max_wal_size/16MBの値を設定。
スロットを使用する場合は設定不要。
リロードで反映。
synchronous_commit
on
目的に応じて設定する。

remote_apply : 完全同期。WAL適用までを保証。
on : 同期。WAL転送後のディスクへの書き込みまで保証。
remote_write : 準同期。WAL転送後のメモリ書き込みまで保証。
local : 非同期。ローカルへの書き込みまで保証(リモートは保証しない)。
off : 完全非同期。ローカルへの書き込みも保証しない。

同期式におけるスレーブ障害時および復旧時の同期/非同期の切り替えはsynchronous_standby_namesにて行う。本パラメータは固定。
リロードで反映。
synchronous_standby_names
''(非同期)
SRの非同期と同期を切り替えに使用。
非同期の場合 : ''
同期の場合 : '*' あるいは特定スレーブのapplication_name
リロードで反映。
max_wal_senders
10
起動するwal senders数を指定。
pg_basebackupでWALストリーミングを指定する場合はその分も考慮
通常はデフォルトの10で問題ない。
再起動で反映。
max_replication_slot
10
レプリケーションスロットを作成可能な最大数
通常はデフォルトの10で問題ない。
再起動で反映。
restart_after_crash
off
onの場合、インスタンス障害(postgresプロセスは残存)の場合に、自動的に再起動を行う。
シングル構成では便利な機能であるが、SR構成では管理が複雑化するため、offに設定するのが一般的。
リロードで反映。

表 5.5 SRの設定(スレーブのpostgresql.conf)
パラメータ 設定値 内容
hot_standby
on
ホットスタンバイとして参照可能な状態で起動する。
スレーブに対しても監視SQLを実行できるように通常は有効化する。
デフォルトでonであるため、変更は不要。
hot_standby_feedback
on
onの場合、スレーブが現在処理している問い合わせについて、マスタへフィードバックを送る。
通常は有効化する。特にスロットを作成する場合には有効化が必須。
デフォルトはoffであるため、変更が必要。

表 5.6 SRの設定(スレーブのrecovery.conf)
パラメータ 設定値 内容
standby_mode
on
スレーブとして起動か、PITRのリカバリ処理かを区別をする設定。
onの場合は、スレーブとして起動する。WALファイルの最後に達してもリカバリを終了せず、マスタへ接続して新WALセグメント取得を継続。
primary_conninfo
'host=server1 port=5432 user=repuser password=password application_name=server2'
マスタへの接続文字列を指定。

hostはWAL転送を行うLANを指定。2サーバ間の転送のみである事から、ハートビートLANでも良い。

portはPostgreSQLがリスニングしているポート番号(portパラメータ)。

userはreplication仮想データベースへ接続できるユーザを指定(ユーザのreplication属性およびpg_hba.confの設定)。

passwordはパスワードが必要な場合に設定。パスワードの記載を避けたい場合は、~/.pgpassを使用。

application_nameは視認性を高めるのが目的であり必須ではない。ここではスレーブ名を指定。
デフォルトで'wal_receiver'。
recovery_target_timeline
latest
リカバリが作成する個別のタイムラインを指定。
SR構成ではlatestを指定して最新タイムラインを追従。
primary_slot_name
<スロット名>
当該スレーブ用のスロットを作成している場合は、スロット名を設定。
最新バージョンではスロットを作成するのが一般的。
restore_command
アーカイブWALファイルのリストア(コピー)方法を指定
スロットを使用している場合は設定不要
  1. 構築手順

ここでは基本的な2ノード構成における手順を記載します。

表 5.7 SR構成条件
項目 内容
同期方式
非同期(synchronous_standby_names = '')
レプリケーションスロット
使用しない

(1)レプリケーションユーザ作成

マスタサーバでレプリケーション用のユーザを作成します。

postgres=# CREATE ROLE repuser LOGIN REPLICATION PASSWORD 'password';

(2)設定ファイルの変更

マスタで$PGDATA配下のpg_hba.confとpostgresql.confを変更します。

  • pg_hba.conf

    レプリケーションユーザがデータベース接続できるようpg_hba.confを次のように設定します。 ここでは便宜上、trust認証を指定しています。 md5認証を指定する場合はパスワードファイル(~/.pgpass)を使用して、パスワードの入力が不要となるように設定します。

    $ vi $PGDATA/pg_hba.conf
    
    [pg_hba.conf]
    # デフォルトで以下の設定が登録されています。
    local   replication     all                                     trust
    host    replication     all             127.0.0.1/32            trust
    host    replication     all             ::1/128                 trust
    
    # 以下の様に変更します。
    host    replication     repuser         192.168.100.100/32      trust
    host    replication     repuser         192.168.100.101/32      trust
    host    replication     repuser         192.168.100.102/32      trust
    
  • postgresql.conf

    マスタとして稼働させるため、postgresql.confを次のように設定します。 スレーブ側の設定も含めておきます。スレーブ側の設定は、マスタ側では無視されます。

    $ vi $PGDATA/postgresql.conf
    
    [postgresql.conf]
    port = 5432                     (デフォルト)
    listen_addresses = '*'
    wal_level = replica             (デフォルト)
    synchronous_commit = on         (デフォルト)
    synchronous_standby_names = ''  (デフォルト)
    max_wal_senders = 10            (デフォルト)
    max_replication_slots = 10      (デフォルト)
    restart_after_crash = off
    hot_standby = on                (デフォルト)
    hot_standby_feedback = on
    

パラメータの記述後、設定ファイルの変更を反映するためにPostgreSQLを再起動します。

$ pg_ctl restart

(3)pg_basebackupによる物理ファイルのコピー (スレーブ)

スレーブ側でpg_basebackupを実行し、スレーブデータベースを構築します。

$ pg_basebackup -h <マスタIP> -p 5432 -U repuser -D $PGDATA --progress --verbose

なおパスワードファイルはpg_basebackupではコピーされないため、必要に応じてコピーします。

$ scp <マスタIP>:~/.pgpass ~/

(4)recovery.confの設定(スレーブ)

pg_basebackupで取得したデータベースクラスタの設定を変更し、スレーブとして稼働するようにします。 取得先($PGDATA)配下でrecovery.confを作成します。 なおスレーブに必要なパラメータ設定は(postgresql.conf)は、マスタ側で設定済みのファイルがコピーされているため、変更不要です。

  • recovery.conf

    $PGDATA配下にrecovery.confを作成し、以下を記述します。

    $ vi $PGDATA/recovery.conf
    
    [recovery.conf]
    standby_mode = 'on'
    primary_conninfo = 'host=server1 port=5432 user=repuser password=repuser'
    recovery_target_timeline = latest
    

(5)PostgreSQLの起動(スレーブ)

スレーブのPostgreSQLを起動します。

$ pg_ctl start

(6)状況確認(マスタ)

SRが構築されていることを確認します。

[マスタ側で確認]

postgres=# \x
Expanded display is on.

postgres=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 40724
usesysid         | 16385
usename          | repuser
application_name | walreceiver
client_addr      | <スレーブのIP>
client_hostname  |
client_port      | 48077
backend_start    | 2018-01-04 01:30:54.01041+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/3000060
write_lsn        | 0/3000060
flush_lsn        | 0/3000060
replay_lsn       | 0/3000060
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

(7)状況確認(スレーブ)

[スレーブ側で確認]

postgres=# \x
Expanded display is on.

postgres=# SELECT * FROM pg_stat_wal_receiver;
-[ RECORD 1 ]---------+--------------------------------------------------
pid                   | 52484
status                | streaming
receive_start_lsn     | 0/3000000
receive_start_tli     | 1
received_lsn          | 0/3000140
received_tli          | 1
last_msg_send_time    | 2018-01-04 01:35:50.514256+09
last_msg_receipt_time | 2018-01-04 01:18:46.923761+09
latest_end_lsn        | 0/3000140
latest_end_time       | 2018-01-04 01:33:20.23195+09
slot_name             |
conninfo              | user=repuser password=******** dbname=replication
                        host=<マスタのIP> port=<マスタのポート番号>
                        fallback_application_name=walreceiver
                        sslmode=disable sslcompression=1 target_session_attrs=any

conninfo列は実際には1行で表示されますが、便宜上改行しています。

5.2.3. スレーブの追加

2ノード構成からスレーブ(server3)を追加する方法は、基本的には前述の手順と同様です。 複数スレーブ方式とカスケード方式の違いはserver3側でpg_basebackupを実行するのは同様で、マスタの指定の違いだけです。 カスケード方式では、マスタに負荷をかけないというメリットがあります。

  1. pg_basebackupの実行(server3)
(複数スレーブ構成:マスタはsever1)
$ pg_basebackup -h server1 -p 5432 -U repuser -D $PGDATA --progress --verbose
(カスケード構成 :マスタはserver2)
$ pg_basebackup -h server2 -p 5432 -U repuser -D $PGDATA --progress --verbose
  1. recovery.confの設定(server3)
  • recovery.conf

    $PGDATA配下にrecovery.confを作成します。 カスケード方式の場合にはserver2からコピーされたファイルが存在するので、それを修正します。 server2の設定とほぼ同様で、異なるのはマスタの指定です。

    $ vi $PGDATA/recovery.conf
    
    [recovery.conf]
    (複数スレーブ構成:マスタはsever1)
    primary_conninfo = 'host=server1 port=5432 user=repuser password=repuser'
    (カスケード構成 :マスタはserver2)
    primary_conninfo = 'host=server1 port=5432 user=repuser password=repuser'
    

5.2.4. アーカイブモード運用

SR構成においてもアーカイブモード運用は有力です。 スレーブが物理バックアップとも言えますのでアーカイブモード運用は必須ではありませんが、以下を目的として構成する事もあります。

・ PITR (Point In Time Recovery)にて一定の過去に戻す
 アーカイブモード運用の最大のメリットと言えます。
 類似機能として「遅延レプリケーション」もありますが、PITRの方がより汎用的です。
・アーカイブWALの保持
 フルバックアップを1日1回取得する場合、アーカイブWALも1日分(あるいはそれ以上)保持することになります。
 SRの伝搬の遅れが保持時間以内に収まっていれば、SRを継続できます。
 WALを保持する機能としては、レプリケーションスロットがあります。
 SRを意識してWALを確保するため、必要なWALが欠落するリスクはありません。
 ただし伝搬遅延が大きくなりすぎた場合、WAL領域がディスクフルになるリスクがあります。
 アーカイブモード運用の場合は定期的に削除するため、そのリスクは低いと言えます。
  1. 関連パラメータ

既存のレプリケーションスロットの設定に加え、次のパラメータを指定します。

サーバ 設定ファイル パラメータ 設定値 内容
マスタ postgresql.conf archive_mode on アーカイブを有効化する。
マスタ postgresql.conf archive_command cp %p <アーカイブの保存先>/%f アーカイブの実行コマンドを指定する。 スクリプトを指定する事も可能であるため、複雑な処理を組み込む事が可能。
スレーブ recovery.conf restore_command 'scp <マスタのユーザ名>@<マスタのホスト名>:<マスタのアーカイブ・ディレクトリ>%f %p' マスタのアーカイブを取得するコマンドを指定する。 WAL同期を保証する場合に必要となる。ただしレプリケーションスロットを使用する場合は不要である。

注釈

  • restore_commandにてscpコマンドを使用する場合、スレーブはマスタのPostgreSQLのOSユーザに sshにてパスワードなしで接続できるようになる必要があります。 またはアーカイブ・ディレクトリをNFSマウント等、マスタ/スレーブ間で共有可能なパスにする事で、 アーカイブの扱いが容易になります。
  • 障害時スレーブをマスタにする場合に備え、マスタに設定したパラメータは スレーブ側でも事前に有効化することを推奨します。
  1. 設定手順

(1)マスタ側のpostgresql.confのパラメータを以下のように設定する。

$ vi $PGDATA/postgresql.conf

[postgresql.conf]
archive_mode = on
archive_command = 'cp %p <アーカイブの保存先>/%f'

(2)必要ならばスレーブ側のrecovery.confのパラメータに以下を追加する。

$ vi $PGDATA/recovery.conf

[recovery.conf]
restore_command = 'scp <マスタのユーザ名>@<マスタのホスト名>:<マスタのアーカイブ・ディレクトリ>%f %p'

(3)マスタ側を再起動し、その後スレーブ側を再起動する。

[マスタ/スレーブの両方で実施]
$ pg_ctl restart

(4)マスタ側で強制的にWALファイルを切り替え、アーカイブWALファイルが出力されることを確認する。

postgres=# SELECT pg_switch_xlog();
 pg_switch_xlog
----------------
 0/7017008
(1 row)

$ <アーカイブ・ディレクトリ>
000000010000000000000007

5.2.5. レプリケーションスロット

スレーブに未転送のWALを保持することで、SRの維持を保証します。 アーカイブ運用でも同期の保証は可能ですが、アーカイブログの管理などが問題となり、 ノーアーカイブ運用をしている環境も多くあります。 そのような環境において、レプリケーションの維持を保証するためには、レプリケーションスロットの設定が必要です。 またアーカイブ運用においても、アーカイブWALファイルの削除にSRの考慮が不要になるため、有用な設定です。

  1. 関連パラメータ
既存のレプリケーションスロットの設定に加え、次のパラメータを指定します。
基本的なSRの構築手順にて設定済みのパラメータも、改めて記載しています。
表 5.8 レプリケーションスロットの設定
サーバ 設定ファイル パラメータ 設定値 内容
マスタ postgresql.conf max_replication_slots スレーブ数以上(設定済み) 作成可能なレプリケーションスロット数を指定する。
スレーブ postgresql.conf hot_standby_feedback on(設定済み) スレーブの状態をマスタにフィードバックする。
スレーブ recovery.conf primary_slot_name レプリケーションスロット名 使用するレプリケーションスロット名を指定する。

注釈

  • 障害時スレーブをマスタにする場合に備え、マスタに設定したパラメータは スレーブ側でも事前に有効化することを推奨します。
  1. レプリケーションスロットの作成方法

レプリケーションスロットに関連する関数は以下の通りです。

表 5.9 レプリケーションスロットの関数
関数名 説明
pg_create_physical_replication_slot(スロット名[, true/false])

レプリケーションスロットを作成する。

  • スロット名:作成するレプリケーションスロット名を指定する。
  • true/false:trueの場合、レプリケーションスロットは即座にWALを保持する。falseの場合従来通り、スレーブがレプリケーションスロットに繋いだ時点からWALを保持する。
pg_drop_replication_slot(スロット名)

レプリケーションスロットを削除する。

  • スロット名:削除するレプリケーションスロット名を指定する。
  1. 検証

(1)マスタにてpostgresql.confにmax_replication_slotsを加え、設定反映のため再起動。

$ vi $PGDATA/postgresql.conf

[postgresql.conf]
max_replication_slots = 10     <--- デフォルト

$ pg_ctl restart

(2)マスタにてレプリケーションスロットを作成。

postgres=# SELECT pg_create_physical_replication_slot('slot1', true);
 pg_create_physical_replication_slot
-------------------------------------
 (slot1,0/B000220)
(1 row)

(3)マスタにてレプリケーションスロットの作成を確認。

postgres=# SELECT slot_name, restart_lsn, active FROM pg_replication_slots;
 slot_name | restart_lsn | active
-----------+-------------+--------
 slot1     | 0/B000220   | f
(1 row)
・active列が'f'であることから、まだ使用されていない。
・restart_lsn列に値があることから、使用されていなくてもWALの保持は開始している。

(4)スレーブにてrecovery.confにprimary_slot_nameを加え、設定反映のため再起動。

$ vi $PGDATA/postgresql.conf

[recovery.conf]
primary_slot_name = 'slot1'

$ pg_ctl restart

(5)マスタ側でレプリケーションスロットが使用されていることを確認。

postgres=# SELECT slot_name, restart_lsn, active FROM pg_replication_slots;
 slot_name | restart_lsn | active
-----------+-------------+--------
 slot1     | 0/B0002C8   | t
(1 row)
・active列が't'であることから、使用されている。

5.2.6. 【参考情報】同期モードにおけるパフォーマンスへの影響

同期モードの懸念として、WAL転送の待機によるパフォーマンスへの影響があります。 以下の図はsynchronous_commitの各設定によるパフォーマンス比較です。(onの値を1.00とした相対値)。

  • サーバススペック
    • CPU  : Intel(R) Xeon(R) CPU E5-2640 v2 @ 2.00GHz 16Core
    • メモリ : 64GB
    • OS  : Red Hat Enterprise Linux Server release 6.5 (Santiago)
    • PostgreSQLのバージョン : PostgreSQL 9.6.1
  • パラメータ設定
    • キャッシュヒット率の影響を受けないようほぼ100%となるように調整
      • shared_buffers : 8GB
    • 処理中に自動VACUUMが発生しないように無効化
      • autovacuum :off
    • 処理中にチェックポイントが発生しないように調整
      • checkpoint_timeout :1h
      • max_wal_size :10GB
      • 処理前に毎回手動でチェックポイント実行
  • トランザクションツールはpgbench を使用。
    • 初期化スケール 100
    • 100セッション、25ワーカスレッドのトランザクションを実施(pgbench -c 100 -j 25)
  • 通常のOLTPアプリケーションではこれほど激しいトランザクションではないため、低下は限定的と考えられます。
  • 同期モードの実装にあたっては、実際のアプリケーションで検証してご確認ください。
synchronous_commitの設定毎パフォーマンス比較

5.2.7. 遅延レプリケーション

スレーブの適用を一時的に遅延させます。 マスタの操作ミスが即座に伝搬されるのを防ぐのが目的です。 WAL転送までは遅延なく処理されるため、データ保全には影響ありません。 同期転送(synchronous_commit=on)との組み合わせができます。

注意点
・マスタへの切り替わり時に遅延分の適用が発生するため切り替わりに時間がかかります。
 高可用性用途には向いていません。
 複数スレーブ構成にて、1台目位は同期、で2台目を遅延とするする構成が考えられます。
・以下の操作を行うと、遅延レプリケーションの設定(recovery_min_apply_delay)は無視され、最新の状態まで適用されます。ご注意下さい。
 - 再起動 (pg_ctl restart)
 - 昇格 (pg_ctl promote)
・完全同期(synchronous_commit=remote_apply)ではDML(自動コミットがオフの場合はCOMMIT)を待機するため、遅延レプリケーションは使用できません。

以下の検証を行います。

検証1: 遅延レプリケーションの設定および動作確認
     スレーブへの適用が指定した時間分、遅延する事。およびマスタでの遅延確認。
検証2: 問題発生前の状態まで適用
     オペレーションミス発生を想定し、発生前の状態まで適用し、昇格。
  1. 関連パラメータ

既存のレプリケーションスロットの設定に加え、次のパラメータを指定します。

表 5.10 遅延レプリケーションの設定
サーバ 設定ファイル パラメータ 設定値
スレーブ postgresql.conf hot_standby_feedback on(フィードバックを有効化)
スレーブ recovery.conf recovery_min_apply_delay 遅延させる時間を指定
  1. 検証1:遅延レプリケーションの設定および動作確認

(1)マスタにてテストに使用するテーブルを作成。

(マスタ)
postgres=# CREATE TABLE delay_test(id integer, time1 timestamp);
postgres=# \d delay_test
            Table "public.delay_test"
 Column |            Type             | Modifiers
--------+-----------------------------+-----------
 id     | integer                     |
 time1  | timestamp without time zone |

(2)スレーブで遅延レプリケーションの設定を行い、再起動。

(スレーブ)

$ vi $PGDATA/postgresql.conf

[postgresql.conf]
hot_standby_feedback = on

$ vi $PGDATA/recovery.conf

[recovery.conf]
recovery_min_apply_delay = '60min'

$ pg_ctl restart

(3)マスタにてテストテーブルにデータを追加。

(マスタ)
postgres=# INSERT INTO delay_test VALUES (1, localtimestamp);
postgres=# SELECT * FROM delay_test;
 id |           time1
----+----------------------------
  1 | 2018-03-10 18:05:46.29221
(1 rows)

(4)スレーブにて即座に反映されない事を確認。

(スレーブ)
postgres=# SELECT * FROM delay_test;
 id | time1
----+-------
(0 rows)                                <--- INSERTが反映されていない
(5)マスタにて適用の遅延を確認。
pg_stat_replicationビューのreplay_lag列で確認
(マスタ)
postgres=# SELECT write_lag,flush_lag,replay_lag FROM pg_stat_replication ;
-[ RECORD 1 ]---------------
write_lag  | 00:00:00.000323
flush_lag  | 00:00:00.00048
replay_lag | 00:00:38.354572   <--- recovery_min_apply_delayに達するまで適用を待機

(6)1時間経過後、スレーブにて適用された事を確認。

(スレーブ)
postgres=# SELECT *,localtimestamp FROM delay_test;
 id |           time1            |       localtimestamp
----+----------------------------+----------------------------
  1 | 2018-03-10 18:05:46.29221  | 2018-03-10 19:06:10.521195
(q rows)

(7)マスタにて適用の遅延がクリアされた事を確認。

(マスタ)
postgres=# SELECT write_lag,flush_lag,replay_lag FROM pg_stat_replication ;
-[ RECORD 1 ]---------------
write_lag  |
flush_lag  |
replay_lag |
  1. 検証2:問題発生前の状態まで適用

(1)マスタにてテストテーブルにデータを追加。

(マスタ)
postgres=# INSERT INTO delay_test VALUES (2, localtimestamp);
postgres=# SELECT * FROM delay_test;
 id |           time1
----+----------------------------
  1 | 2018-03-10 18:05:46.29221
  2 | 2018-03-10 19:30:11.23870
(1 rows)

id=2の更新(INSERT)をオペレーションミスとみなし、スレーブを更新前の時点まで適用

(2)マスタを停止。

$ pg_ctl stop

(3)スレーブにて1件も反映されない事を確認。

(スレーブ)
postgres=# SELECT * FROM delay_test;
 id | time1
----+-------
(0 rows)                                <--- INSERTが反映されていない

(4)スレーブを停止。

$ pg_ctl stop

(5)スレーブにてrecovery.confを修正。

$ vi $PGDATA/recovery.conf

[recovery.conf]
restore_command = 'cp <$PGDATAパス>/pg_wal/%f %p'  <--- WALのパスを指定
recovery_target_time = '2018-03-10 19:30:00'      <--- id=2のCOMMIT直前の時刻
recovery_target_timeline = latest                 <--- 最新のタイムライン
recovery_target_action = 'promote'                <--- 適用中断後、昇格

(6)スレーブにて指定時間までのリカバリおよび昇格(スレーブ→新マスタ)

$ pg_ctl start

$ pg_controldata | grep state                      <--- マスタ/スレーブの確認
Database cluster state:           in production    <--- マスタ
表 5.11 遅延レプリケーションのPITRログ
メッセージ 時間
LOG: starting point-in-time recovery to 2018-03-10 19:30:00+09 recovery_target_time
LOG: recovery stopping before commit of transaction 578, time 2018-03-10 19:30:11.423701 id=2のCOMMIT時間
LOG: last completed transaction was at log time 2018-03-10 18:05:46.314276 id=1のCOMMIT時間

(8)新マスタにてid=1のみ適用されていることを確認。

postgres=# SELECT * FROM delay_test ;
 id |           time1
----+---------------------------
  1 | 2018-03-10 18:05:46.29221
(1 row)

5.2.8. WAL圧縮

Full Page Write時(チェックポイント後の最初の更新時)に、WALに書き出すフルページイメージを圧縮します。 圧縮されたWALは適用時に解凍されます。 WALファイルのサイズが小さくなるため、書き込みや転送の時間短縮 が期待されます。 注意点として、圧縮処理および解凍処理が発生するため、通常より余分にCPUを使用します。 適用についてはそれらを総合的に判断します。 一般的な適用場面として、SR構成において効果的と考えられます。 特に以下の構成で有力です。

  • 同期モード(転送まで)または完全同期モード(適用まで)
    WAL転送の遅延を懸念して同期モード設定を躊躇する場面でWAL圧縮を検討します。
    非同期モードでもWAL圧縮は有力ですが、一定のWAL転送の遅延は許容されるため、
    マスタのCPU負荷を優先してWAL圧縮を適用しないという判断も考えられます。
  • スレーブの遠隔地配置(ディザスタ・リカバリ)
    スレーブを遠隔地に配置する構成において、非同期モードでもWAL転送の遅延が
    許容範囲を超える場合にWAL圧縮の適用を検討します。
  1. 関連パラメータ

既存のレプリケーションスロットの設定に加え、次のパラメータを指定します。

表 5.12 WAL圧縮の設定
サーバ 設定ファイル パラメータ 設定値
マスタ postgresql.conf wal_compression 'on'

5.3. SR環境の監視

5.3.1. レプリケーション操作ログの監視

5.3.1.1. 調査の目的

SR構成のサーバログ監視について有益な情報を提示する事を目的としています。
以下の挙動時にサーバログに出力されるメッセージを確認しました。
  • マスタの起動/停止
  • スレーブの起動/停止
  • カスケードスレーブの起動/停止
  • マスタ側のWAL再利用によるロスト(スロット不使用)
  • カスケードスレーブ構成時のスレーブ側のWAL再利用によるロスト(スロット不使用)
以下のパターンで検証しました。
  • レプリケーション操作ログ出力無効時(デフォルト)
    • log_replication_command = off
  • レプリケーション操作ログ出力有効時
    • log_replication_command = on

5.3.1.2. 調査結果

マスタ側またはスレーブ側のサーバログに出力されるレプリケーション情報についてまとめます。
ホスト名、IPアドレス、ポート番号、ユーザ名、データベース名は例です。
  • レプリケーション操作ログ出力無効時(デフォルト)
表 5.13 デフォルトで出力される情報
タイミング サイト メッセージ
スレーブ停止時 マスタ
スレーブが複数台存在し、Quorum-based 同期レプリケーションを設定していない場合に、
片系スレーブの起動または停止を行うことで同期優先順位が変化した場合のみされます。
START_REPLICATION 0/57000000 TIMELINE 1
マスタ停止時 スレーブ
エラーメッセージが繰り返し出力されます。
LOG: invalid record length at 132/EF8AD7F0: wanted 24, got 0
FATAL: could not connect to the primary server: could not connect to server: Connection refused
    Is the server running on host "master" (192.168.100.100) and accepting
    TCP/IP connections on port 5432?
WAL再利用によるロスト マスタ
エラーメッセージが繰り返し出力されます。
ERROR: requested WAL segment 0000001100000132000000EF has already been removed
WAL再利用によるロスト スレーブ
エラーメッセージが繰り返し出力されます。
LOG: started streaming WAL from primary at 132/EF000000 on timeline 17
FATAL: could not receive data from WAL stream: ERROR: requested WAL segment 0000001100000132000000EF has already been removed
  • レプリケーション操作ログ出力有効時

  デフォルトで出力される情報に加えて、以下が出力されます。

表 5.14 監視強化時に出力される情報
タイミング サイト メッセージ
スレーブ起動または停止時 マスタ
スレーブが複数台存在し、Quorum-based 同期レプリケーションを設定していない場合に、
片系スレーブの起動または停止を行うことで同期優先順位が変化した場合のみ出力されます。
LOG: received replication command: IDENTIFY_SYSTEM
LOG: received replication command:
START_REPLICATION 0/57000000 TIMELINE 1
WAL再利用によるロスト マスタ、スレーブ(スレーブ出力はカスケード・レプリケーション時のみ)
エラーメッセージが繰り返し出力されます。
LOG: received replication command: IDENTIFY_SYSTEM
LOG: received replication command: START_REPLICATION 134/4A000000 TIMELINE 17

5.3.1.3. 調査結果

  • SR関連のエラーが発生した場合は、デフォルトの設定(log_min_messages='warning')でサーバログに出力されます。
  • 表の太字の文字が監視キーワード候補です。
  • マスタが停止するとスレーブに出力がありますが、スレーブが単一の構成時にはスレーブが停止してもマスタに出力はありませんでした。
  • log_replication_command による出力はそれほど多くなく、監視に対する影響は限定的です。
    ただリロードで反映できることと合わせて、気軽に有効化できるとも言えます。
  • log_min_message = DEBUG[1-5] (例)DEBUG1、DEBUG2 とすることでもレプリケーション関連のメッセージが出力されます。
    ただし影響が大きいため、通常はlog_replication_commandを使用します。
  • log_replication_commandによる出力は、スレーブのwal receiverプロセスからフィードバックされた情報です。
    log_line_prefixにapplication_name(%a)を設定する事で確認できます。
  • SQLSTATEの値をキーワードに監視を行うために、log_line_prefixパラメータにSQLSTATE (%e)を設定します。

5.3.2. 同期状況の監視

非同期レプリケーションを構成した場合、ハードやNW構成、利用状況によって
マスタとスレーブの同期状況が変化します。
この状態を監視し、同期の遅延が許容範囲内であるかを確認します。

■前提

構成:

  • マスタ下に1台または2台のスレーブ構成
  • マスタ、スレーブに加えカスケードスレーブをスレーブ下に構成する計3台構成
    (カスケード・レプリケーション)

バージョン:PostgreSQL 10

同期モード:問わない

5.3.2.1. 同期遅延監視

(1)WALの書き込み位置による同期遅延量を用いて監視することは可能か
「2014年度WG3活動報告書- 可用性編 -」での検証結果をベースに実施します。

マスタのWALとスタンバイが適応したWALの2つの差分は、
マスタの最新WALの書き込み位置(LSN:Log Sequence Number)と
スタンバイが適用したWALの位置(LSN)との差分より算出します。
マスタのLSNは、pg_current_wal_insert_lsn()関数が返す現在のWALの挿入位置とみなします。
スタンバイのLSNは、統計情報pg_stat_replicationビューのreplay_lsnを使用します。
この2つのLSNの差分をバイト単位で表示するために、pg_wal_lsn_diff関数の引数にそれぞれを格納し
その結果が許容値を超過するかどうかで、監視を行います。

検証には、以下シェルスクリプトで実施しました。
監視通知部については、テストの為、標準出力のみとしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/bin/sh
HOST=localhost
LIMIT=1
SLEEPTIME=2
ALERT="echo"
SLAVENUM=2

function send_alert(){
    ${ALERT} $1
}

IFS_DEFAULT=$IFS
while :
do
    val1=$(psql -h $HOST -p 5432 -U postgres -q -t -c "select current_time;")
    case $(psql -p 5432 -U postgres -q -t -c "SELECT pg_is_in_recovery();"
          |awk '{print $NF}') in
        t)
            val2=$(psql --no-align -h $HOST -p 5432 -U postgres -q -t -c
            "select client_addr,pg_wal_lsn_diff(master,replay_lsn)
             as replaydiff from (select pg_last_wal_replay_lsn() master)
             as m,pg_stat_replication;")
        ;;
        f)
            val2=$(psql --no-align -h $HOST -p 5432 -U postgres -q -t -c
            "select client_addr,pg_wal_lsn_diff(master,replay_lsn)
             as replaydiff from (select pg_current_wal_insert_lsn() master)
             as m,pg_stat_replication;")
        ;;
    esac

    rownum=$(psql --no-align -h $HOST -p 5432 -U postgres -q -t -c
            "select client_addr,pg_wal_lsn_diff(master,replay_lsn)
             as replaydiff from (select pg_current_wal_insert_lsn() master)
             as m,pg_stat_replication;" |wc -l)

    for slave_lsn in ${val2};
    do
        if [ ${rownum} -ne ${SLAVENUM} ]; then
            send_alert "Slave Down |${val1}"
        fi
        IFS=$'|'
        byte=`echo ${slave_lsn} | awk  '{print $NF}'`
        if [ ${byte} -gt ${LIMIT} ]; then
            send_alert "Replication Delay ${val1},${slave_lsn}"
        fi
        IFS=${IFS_DEFAULT}
        sleep ${SLEEPTIME}
    done
done
今回の検証でスレーブが複数存在する場合も、それぞれのスレーブの遅延状況を検知することが可能であることがわかりました。
監視シェルを実行中にinsert文を実行した結果を記載します。
以下の通り、遅延状況を検知でき、メールサーバ等に連携することで監視することが可能です。
また間接的に更新待ち監視も行うことができます。詳細は、更新・読取監視の項に記載します。
$ ./delay_test.sh
Replication Delay   20:58:18.451839+09,192.168.100.101 11880
Replication Delay   20:58:18.451839+09,192.168.100.102 11880
Replication Delay   20:58:22.484532+09,192.168.100.101 11880
Replication Delay   20:58:22.484532+09,192.168.100.102 11880
Replication Delay   20:58:26.512656+09,192.168.100.101 12048
Replication Delay   20:58:26.512656+09,192.168.100.102 12048
Replication Delay   20:58:30.538009+09,192.168.100.102 12048
尚、カスケードスレーブ構成の場合にはスレーブ上で同シェルスクリプトを実行することで、
カスケードスレーブとの同期の遅延状況を検知することが可能です。

5.3.3. 更新、読み取りの監視

非同期レプリケーションを構成した場合、ハードやNW構成、利用状況によってマスタとスレーブの同期状況が変化します。
この状態を監視し、同期の遅延が許容範囲内であるかを確認します。

■前提構成

  • 3ノード構成

      1. 複数スレーブ方式(親 - 子1 , 子2) スレーブ2はマスタと繋がっている。
    複数スレーブ基本図
      1. カスケード方式(親 - 子 - 孫) スレーブ2はスレーブ1と繋がっている
    カスケード基本図

同期モード:同期または完全同期 (synchronous_commit = on / remote_apply)

5.3.3.1. 更新・読取監視

(1)PostgreSQLの機能により監視することは可能か

- 検証方法
 PostgreSQLの内部パラメータ(statement_timeout)により、マスタへの更新処理に対して
 スレーブ停止によって一定時間応答がない場合に接続を切断し、アラート通知することは可能かを検証を行いました。
 statement_timeoutは、postgresql.conf内での設定は推奨されていない為、SQL発行時に設定しました。
 また、pg_stat_replicationビューにより間接的に、検知できないかについて検証を行いました。

- 検証結果
  • statement_timeoutを設定しSQLを発行しましたが、有効に機能せず
    Ctrl+Cをキーインするまで停止しないという結果となりました。
  • 複数台のスレーブのうち1つでも稼働している場合には設定時間を待たずに
    一方のスレーブの応答をもって処理が完了してしまうため、他のスレーブの同期遅延の検知ができないという結果となりました。
全てのスレーブが応答不可能である場合
$ psql -U postgres -d testdb -q -t << EOF
> \timing on
> SET statement_timeout TO 5000;
> insert into test(id) values(1);
> delete from test where id=1;
> EOF
時間: 0.192 ms
^CCancel request sent
WARNING:  canceling wait for synchronous replication due to user request
DETAIL:  The transaction has already committed locally, but might not have been
replicated to the standby.
時間: 13626.496 ms
一方のスレーブのみ応答が可能である場合
$ psql -U postgres -d testdb -q -t << EOF
> \timing on
> SET statement_timeout TO 5000;
> insert into test(id) values(1);
> delete from test where id=1;
> EOF
Time: 0.147 ms
Time: 1.899 ms
Time: 1.277 ms
 pg_stat_replicationビューについては、スレーブサーバ停止時にレコードが取得できなくなる為
 この点を利用することで間接的に、同期待ちを検知することが可能であることがわかります。
testdb=# select * from pg_stat_replication;
  pid  | usesysid | usename | application_name |  client_addr   | client_hostnam
e | client_port |         backend_start         | backend_xmin |   state   | sen
t_lsn | write_lsn | flush_lsn | replay_lsn | write_lag | flush_lag | flush_lag |
 sync_priority | sync_state
-------+----------+---------+------------------+----------------+---------------
--+-------------+-------------------------------+--------------+-----------+----
-----------+----------------+----------------+-----------------+---------------+
------------
(0 )
(2)OSコマンドなどの外部機能により、更新処理に対して一定時間応答がない場合を監視する

 スレーブが単一であればtimeoutコマンドにより、指定秒数で強制切断に成功し、
 スタンバイと同期が取れていないメッセージが出力されます。
 また終了値として、124が返却されている為、全てのスレーブが応答できない場合に限り、
 メッセージ内容または、終了値を制御することでスレーブサーバ障害による同期待ちを検知することが可能です。

スレーブが単一である場合
$ timeout -sINT 5 psql -U postgres -d testdb -q -t << EOF
> \timing on
> insert into test(id) values(1);
> delete from test where id=1;
> EOF
Cancel request sent
WARNING:  canceling wait for synchronous replication due to user request
DETAIL:  The transaction has already committed locally, but might not have been replicated to the standby.
Time: 4994.166 ms (00:04.994)
[postgres@test_pg01 data]$ echo $?
124
 しかし statement_timeout設定による検証のケースと同様に、複数台のスレーブのうち1つでも稼働している場合には
 設定時間を待たずに一方のスレーブの応答をもって処理が完了してしまうため、
 他のスレーブの同期遅延の検知ができないという結果となりました。

一方のスレーブのみ応答が可能である場合
$ timeout -sINT 5 psql -U postgres -d testdb -q -t << EOF
> \timing on
> insert into test(id) values(1);
> delete from test where id=1;
> EOF
Time: 2.382 ms
Time: 1.246 ms
[postgres@test_pg01 data]$ echo $?
0

5.3.4. スプリットブレインの監視

5.3.4.1. スプリットブレインの定義と調査の目的

SR構成においては、通常はマスタの障害を検知した場合にスレーブを昇格させます。つまりマスタは常に1台のみです。 ただしオペレーションミスにより、マスタが正常な状態にもかかわらずスレーブを昇格させてしまうという事も有り得ます。 稼働中のマスタ(シングル)が2台という危険な状態になります。その状態をスプリットブレインと定義します。

その場合でも全てのアプリケーションが元のマスタにのみ接続していれば問題ありませんが、 2台目のマスタにも接続が発生するとデータの整合性が損なわれてしまいます。 それを避けるため、スプリットブレイン状態になっていないかの監視の方法を検討します。

5.3.4.2. 監視方法

以下の監視についてまとめます。

  • サーバログの監視
  • pg_controldataコマンドによる監視
  • pg_control_recovery関数による監視
  • pg_is_in_recovery関数による監視

5.3.4.3. サーバログの監視

pg_ctl promote コマンドにより昇格した場合は、昇格したノードのサーバログに以下のメッセージが出力されます。 timeline IDが変化 した事が分かります。
received promote request
selected new timeline ID: XX

マスタ稼働中にスレーブ側にこのようなメッセージが出力されていないかを監視します。

注意点
recovery.confを削除(または改名)した後に再起動する事でマスタ(シングル)として起動する方法もあります。
ただし timeline ID の変化がなく、特徴的なメッセージは出力されません。状態変更の捕捉が困難です。

5.3.4.4. pg_controldataコマンドによる監視

pg_controldataコマンドは制御ファイル($PGDATA/global/pg_control)の状態を表示します。
以下に実行例を示します。ここでは環境変数LANGの設定により英語で表示しています。
(実行例)
$ export LANG=C
$ pg_controldata
pg_control version number:            1002
Catalog version number:               201707211
Database system identifier:           6514575400714084610
Database cluster state:               in archive recovery
~以下略~

多数の項目がありますが、ここでは"Database cluster state"に着目します。 必要な項目のみ抽出する例です。

(スレーブでの実行例)
$ pg_controldata | grep "Database cluster state"
Database cluster state:        in archive recovery

4種類の状態があります。

  • マスタ/スレーブ
  • 稼働中/停止中
表 5.15 Database cluster stateで表示される値
表示される値(英語) 表示される値(日本語) 意味
in production 運用中 マスタとして稼働中
in archive recovery アーカイブリカバリ中 スレーブとして稼働中
shut down シャットダウン マスタとして停止中
shut down in recovery リカバリしながらシャットダウン中 スレーブとして停止中

"in archive recovery"はPITRとしてのリカバリ中の場合も含みますが、ここではスレーブ状態の意味です。
カスケード・レプリケーション構成の場合は、スレーブもリカバリ中との取り扱いになるため
カスケードスレーブと同様にスレーブ状態に準じた状態が表示されます。

両ノードとも"in production"状態でない事を確認します。

sshコマンドでリモートの状態を容易に取得できます。

注意点としては、リモート側の環境変数を認識しないため、明示的に指定する必要があります。
以下はローカルとリモートで同一設定の前提です。
(スプリットブレインの状態下での実行例)
$ ssh <remote> $PGHOME/bin/pg_controldata $PGDATA | \
>  grep "Database cluster state"
Database cluster state:               in production

5.3.4.5. pg_control_recoveryコマンドによる監視

pg_control_recoveryは制御ファイルの情報をpg_controldataコマンドに代わり取得する方法です。
取得できる項目は限られていますが、SELECT文で取得できるという特徴があります。
状態監視の選択肢の一つとしてご認識下さい。
(スレーブでの実行例)
=# SELECT pg_control_recovery();
pg_control_recovery
-----------------------------
 (0/5E0001B0,1,0/0,0/0,f)
 (1 )

カンマ区切りにより5項目から構成されています。何れもpg_controldataコマンドでも取得できます。

表 5.16 pg_control_recovery関数の表示内容例
項目 マスタ スレーブ
min_recovery_end_lsn 0/0 0/15000178
min_recovery_end_timeline 0 1
backup_start_lsn 0/0 0/0
backup_end_lsn 0/0 0/0
end_of_backup_record_required f f

min_recovery_end_locationおよび min_recovery_end_timelineにマスタとスレーブの違いが表れます。
ただしpg_controldataコマンドによる監視と同様、カスケード・レプリケーション構成の場合は
スレーブもリカバリ中との取り扱いになるため、例外的にカスケードスレーブとの違いが表れません。

5.3.4.6. pg_is_in_recovery関数による監視

リカバリ中かどうかを示します。 マスタであれば f (false) 、スレーブであれば t (true) を表示します。

(スレーブでの実行例)
=# SELECT pg_is_in_recovery();
pg_is_in_recovery
-------------------
 t
(1 )
両ノードとも f (false) 状態でない事を確認します。

5.4. SR環境の障害時運用

5.4.1. 本文書における用語の定義

5.4.1.1. フェイルオーバ

マスタへの障害発生時やアクセス不能時に、スレーブをマスタへ昇格させる処理を指します。
新マスタはシングル構成であり、レプリケーションされていない片系運用の状態です。

レプリケーション構成への復旧には後述の「フェイルバック」を実行しますが、
マスタの障害がインスタンス障害など、データファイルの障害でない場合は、「スイッチオーバ」が可能な場合もあります。

5.4.1.2. フェイルバック

フェイルオーバにより片系運用になった後、再度レプリケーション構成に復帰させる処理を指します。
シングル構成となった新マスタに、新スレーブを追加します。
新マスタと新スレーブを交代させてフェイルオーバ前に戻すことまでは含まない事とします。

複数スレーブ構成では、不要な処理となります。

_images/stream_failover_failback.png

5.4.1.3. スイッチオーバ

マスタとスレーブを入れ替える処理を指します。
マスタを停止し、スレーブを新マスタへ昇格させ旧マスタを新スレーブとして再追加します。
マスタのメンテナンスのための一時的な入れ替えなどが目的です。
フェイルオーバーとの違いは、マスタ停止が障害による停止か計画停止かであり、スレーブに対する処理は同様です。

5.4.1.4. スイッチバック

スイッチオーバ後に、マスタ/スレーブを入れ替えることを指します。
マスタ/スレーブとの関係性を除き処理内容は、「スイッチオーバ」と同様です。
_images/stream_switchover_switchback.png

5.4.2. 障害時運用手順(2ノード構成)

■前提 以降の手順では次の前提とします。

  • PostgreSQL 10.1
  • マスタ、スレーブの2台構成(ホスト名をそれぞれ server1,server2と表記する)
  • 同期モードは同期(synchronous_commit = on または remote_apply)
  • スレーブはホットスタンバイ機能により参照可能 (hot_standby = on )
  • レプリケーション用のユーザは rep_user
  • マスタ/スレーブとも、portは5432を使用
  • レプリケーションスロット使用
  • マスタ/スレーブとも、環境変数PGDATA,PGPORTは設定済み
  • 死活監視は実際の運用ではクラスタソフトを使用するのが一般的ですが、ここでは便宜上手動で実施
  • サーバのNICはパブリックのみ
  • 仮想IPについては考慮しない

■対処一覧 大別すると3種類の対処方法が考えられます。

表 5.17 障害別の対処
ID 障害箇所 障害状況 pg_basebackupとpg_rewindの使い分け
1
マスタ
マスタとスレーブの関係が崩れており再構成が必要
  • 物理障害
  • promoteを伴わないスレーブのマスタ化(recovery.conf削除)
pg_basebackupコマンドを使用してフェイルバック
2
マスタ
マスタとスレーブの関係は巻き戻しで復旧可能
  • 非同期レプリケーションのインスタンス障害(差異がある状態で昇格)
  • スプリットブレイン状態での旧マスタへの更新
pg_rewindコマンドを使用してスイッチバック
3
マスタ
マスタとスレーブの切り替え可能
  • 計画停止
  • 同期レプリケーションのインスタンス障害(物理障害なし)
pg_rewindコマンドを使用しないでスイッチバック
4
スレーブ
マスタとスレーブの連携再開可能
  • スレーブの障害
同期式の場合は非同期式に切り替え

5.4.2.1. フェイルオーバ

フェイルオーバについて記載します。

マスタにて、障害が発生した場合のスレーブを新マスタへ昇格したシングル構成図
_images/FailOver.png
(1) マスタの疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
マスタにて実行します。
$ pg_ctl -w -m immediate stop
$ kill -9 `head -1 $PGDATA/postmaster.pid`
(2) マスタの死活監視にて異常を検知
死活監視の方法には多数ありますが、ここではpg_isreadyコマンドを使用します。
pg_isreadyコマンドはクライアントツールですので、任意のノードから実行できます。
$ pg_isready -h server2 -U postgres -d postgres
server2:5432 - no response
(3) スレーブを新マスタへ昇格
旧スレーブで実行します。
$ pg_ctl promote
※サーバログに下記内容が記載されること
"database system is ready to accept connections"

ただしこの時点ではsynchronous_standby_namesパラメータに値が設定されているため、新マスタで更新処理ができない状態です。

(4) 新マスタを非同期に切り替え
synchronous_standby_namesパラメータの設定を''に設定し、リロードで反映します。
新マスタで実行します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_standby_names = '*'

[編集後]
synchronous_standby_names = ''

$ pg_ctl reload
これで更新処理ができる状態になりました。

(5) 新マスタの死活監視にて正常を確認
$ pg_isready -h server2 -U postgres -d postgres
server2:5432 - accepting connections

以上でファイルオーバーは完了です。

5.4.2.2. フェイルバック

pg_basebackupを使用したフェイルバックについて記載します。 初期構築手順とほぼ同じです。

マスタ障害発生によるフェイルオーバ後、旧マスタを新スレーブとしたレプリケーション構成図

_images/FailBack.png

■パラメータ

pg_basebackupに必要な設定を記載します。

表 5.18 pg_basebackupに必要な設定
サーバ 設定ファイル パラメータ 設定値 内容
マスタ postgresql.conf listen_address 0.0.0.0 全てのIPアドレス(v4)からの接続を受け付ける
マスタ postgresql.conf max_wal_senders 2 WALストリームオプションを付与する場合は、2以上を設定

■pg_basebackupコマンド

pg_basebackupコマンドの主なオプションは次の通りです。

表 5.19 pg_basebackupのオプション
オプション 内容
-D <directory>
出力を書き出すディレクトリを指定。
-X <method>
--wal-method=<method>
必要なWALファイルをバックアップに含める。method(収集方式)は以下から選択。
  • fetch :WALファイルは最後に収集
  • stream:バックアップ作成中に同時にWALをストリームで収集
運用中にpg_basebackupを実行する場合には stream を指定する。
fetch (最後に収集)では、必要なWALファイルが削除される可能性があるため。
-S <slot_name>
--slot=<slot_name>
WALストリーミングの収集に指定したレプリケーションスロットを使用。
-X stream とセットで指定。
必要なWALファイルが削除されるのを防ぐ事を目的とする。
運用中にpg_basebackupを実行する場合に使用を検討する。
以下に注意する。
  • 事前にレプリケーションスロットを作成する必要がある
  • マスタのWAL領域の空きが十分である事を確認する
-R
--write-recovery-conf
最低限のrecovery.confを作成。必要に応じてrecovery.confを加筆修正。
-r <rate>
--max-rate=<rate>
サーバから転送されるデータの最大転送速度を指定。
運用中にpg_basebackupを実行する場合に使用を検討する。
転送速度を抑える事で、マスタに対する影響を制限する事を目的とする。
-P
進行状況報告を有効化。
pg_basebackup処理中におおよその進行状況を報告する。
運用中に実行した場合、データベースクラスタのサイズが増加して進行状況が100%を超える場合がある。
-v
冗長モードを有効化。
進行状況報告も有効な場合、現在処理中のファイル名を出力。

pg_basebackupでレプリケーションスロットが使用できます。 WAL収集方式に stream を指定する事でWALをほぼ確保できますが、スロットを指定する事でより確実になります。 スロットを使用する運用であれば、この段階で作成するのが有力です。

■フェイルバック手順

pg_basebackupにスロットを指定する場合を記載します。


(1) 旧マスタのデータベースクラスタを削除
旧マスタにて実行します。
$ rm -rf $PGDATA/*
※$PGDATA以外に表領域を作成している場合、そのファイルも削除します。
※$PGDATAディレクトリを削除する場合は、postgresユーザで$PGDATAディレクトリを作成できるよう
 親ディレクトリのオーナーまたはパーミッションを設定します。

(2) レプリケーションスロットの作成
新マスタにて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server1',true);  -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,144/EEFC8940)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots ;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
plugin              |
slot_type           | physical
datoid              |
database            |
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 144/EEFC8940   -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(3) pg_basebackupコマンドにて、新マスタからデータベースクラスタをコピー
旧マスタにて実行します。
$ pg_basebackup -h server2 -U rep_user -D $PGDATA -X stream -S slot_server1 -P -v -R
transaction log start point: 144/F3000028 on timeline 17
pg_basebackup: starting background WAL receiver
10503022/10503022 kB (100%), 1/1 tablespace
transaction log end point: 144/F3000130
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: base backup completed
(4) recovery.confの修正
旧マスタにて実行します。
pg_basebackupにて-Rオプションを指定した事で、recovery.confが作成されます。
スロットを指定している事から、primary_slot_nameの指定があります。以下を追記します。
  • recovery_target_timelineパラメータ
  • primary_conninfパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=rep_user host=server2 port=5432 sslmode=prefer sslcompression=1'
primary_slot_name = 'slot_server1'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=rep_user host=server2 port=5432 application_name=slave_server1 asslmode=prefer sslcompression=1'
primary_slot_name = 'slot_server1'
recovery_target_timeline = latest
(5) 旧マスタのpostgresql.confの修正
旧マスタにて、パラメータの調整を行います。
  • synchronous_standby_namesパラメータの無効化
  • pg_statsinfoの無効化(有効化されている場合)
同期レプリケーションの設定を無効化します。
新スレーブのshared_preload_librariesパラメータに pg_statsinfoが設定されている場合は、書き込みができずにエラーが発生します。
pg_statsinfoの指定を削除します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_standby_names = '*'
shared_preload_libraries = 'pg_stat_statements,pg_statsinfo'

[編集後]
synchronous_standby_names = ''
shared_preload_libraries = ''
(6) 新スレーブの起動
新スレーブを起動します。
$ pg_ctl start
(7) 新マスタでのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
    -[ RECORD 1 ]----+------------------------------
    pid              | 11262
    usesysid         | 16384
    usename          | rep_user
    application_name | slave_server1
    client_addr      | <新スレーブIP>
    client_hostname  |
    client_port      | 44548
    backend_start    | 2017-03-22 06:36:06.362576+09
    backend_xmin     | 1781
    state            | streaming            --- ストリーミング中
    sent_location    | 0/10000060
    write_location   | 0/10000060
    flush_location   | 0/10000060
    replay_location  | 0/10000000
    sync_priority    | 2
    sync_state       | async                --- 非同期

 =# SELECT slot_name,active FROM pg_replication_slots;
 -[ RECORD 1 ]-------+-------------
 slot_name           | slot_server1
 active              | t                --- アクティブ
(8) レプリケーション方式を同期式に変更
新マスタにて、synchronous_standby_namesパラメータを設定し、リロードで反映します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_standby_names = ''

[編集後]
synchronous_standby_names = '*'

$ pg_ctl reload

$ psql -h server2 -U postgres postgres -c "SELECT * FROM pg_stat_replication" -x
-[ RECORD 1 ]----+------------------------------
pid              | 11262
usesysid         | 16384
usename          | rep_user
application_name | s1
client_addr      | <新スレーブIP>
client_hostname  |
client_port      | 44548
backend_start    | 2017-03-22 06:36:06.362576+09
backend_xmin     | 1781
state            | streaming            --- ストリーミング中
sent_location    | 0/10000060
write_location   | 0/10000060
flush_location   | 0/10000060
replay_location  | 0/10000000
sync_priority    | 2
sync_state       | sync                 --- 同期

これにて、以下の構成に復旧しました。

  • レプリケーションスロット使用
  • 同期式レプリケーション構成

5.4.2.3. スイッチオーバ

スイッチオーバについて記載します。

■スイッチオーバ手順

計画停止におけるマスタ/スレーブの切り替え手順です。 pg_basebackupやpg_rewindが不要であるためシンプルな手順です。

計画停止にてマスタとスレーブの役割を切り替えたレプリケーション構成図
_images/SwitchOver.png
(1) マスタの正常停止
マスタにて実行します。
$ pg_ctl stop -m fast
(2) スレーブの昇格
スレーブにて実行します。

以下の場合にはpg_rewind不要です。
  • 計画停止
  • 同期レプリケーションのインスタンス障害(物理障害なし)
$ pg_ctl promote

以降の手順はフェイルオーバの場合と同様であるため省略します。

    1. レプリケーションスロットの作成
    1. recovery.confの作成
    1. 旧マスタのpostgresql.confの修正
    1. 新スレーブの起動
    1. 新マスタでのレプリケーション確認
    1. レプリケーション方式を同期式に変更
(9) レプリケーションスロットの削除(スレーブ)
フェイルオーバとの違いとしては、新スレーブに旧マスタ時代のスロットが残る事があります。
restart_lsn列に値が残っている状態では、マスタのVACUUM処理を阻害するなどの
悪影響の可能性があるため、削除します。
スロットの削除は関数で行うため、スレーブでも実行可能です。
$ psql

=# SELECT pg_drop_replication_slot('slot_server2');
pg_create_physical_replication_slot
-------------------------------------

(1 row)

=# SELECT slot_name FROM pg_replication_slots ;
(0 rows)

以上でスイッチオーバが完了しました。

5.4.2.4. スイッチバック

pg_rewindを使用したスイッチバックについて記載します。

pg_rewindはタイムラインのずれたレプリケーションを再同期させる機能です。 実行後、ターゲットクラスタはソースクラスタと置き換えられた状態になります。 そのためpg_rewind後の操作は、通常のフェイルオーバ時と同じです。 タイムラインの分岐点からソースクラスタのWALを適用するため、更新量が少なければpg_basebackによる複製より高速です。 これによりフェイルオーバ時、旧マスタを容易に新スレーブとして起動させることができます。

スイッチオーバー後に役割を元に戻したレプリケーション構成図
_images/SwitchBack.png

■関連パラメータ

pg_rewindに必要な設定を記載します。

表 5.20 pg_rewindに必要な設定
サーバ 設定ファイル パラメータ 設定値 内容
マスタ postgresql.conf full_page_writes on チェックポイント後の更新時、ディスクページの全内容をWALに書き込む。
マスタ postgresql.conf wal_log_hints on ヒントビット更新時もfull_page_writesを実行する。

■pg_rewindコマンド

pg_rewindコマンドのの主なオプションは次の通りです。

表 5.21 pg_rewindのオプション
オプション 内容
D <ターゲットクラスタ> pg_rewindを実行し、ソースクラスタの内容に置き換えるクラスタを指定する。
source-server="<ソースクラスタ>"

同期対象であるソースクラスタを指定します。主に次の接続文字列を使用します。

  • host:ソースクラスタのホスト名またはIPアドレス
  • port:ソースクラスタのポート番号
  • dbname:ソースクラスタの接続先データベース名
  • user:ソースクラスタの接続先ユーザ
P 進行状況をレポートとして表示する。

■スイッチバック手順

※事前にマスタ/スレーブで(1) 関連パラメータの設定がされていることを前提とします。
(1) 旧マスタの正常停止
pg_rewindを使用するには正常停止する必要があります。
停止した旧マスタを一旦起動した後、正常停止させます。
障害により正常に起動や停止ができない状態ではpg_rewindは使用できません。
その場合は、pg_basebackupを使用します。
$ pg_ctl start -w
$ pg_ctl stop -m fast -w
(2) pg_rewindの実行

 旧マスタでpg_rewindを実行する。
$ pg_rewind -D $PGDATA --source-server="host=server2 port=5432"
servers diverged at WAL position 0/5015B70 on timeline 1
rewinding from last common checkpoint at 0/5015B00 on timeline 1
Done!
(3) スレーブのrecovery.confを編集
旧マスタの$PGDATA配下にrecovery.confを作成し、以下を設定します。
$ vi $PGDATA/recovery.conf

[編集後]
standby_mode = 'on'
primary_conninfo = 'host=server2 port=5432 user=rep_user'
recovery_target_timeline = 'latest'
(4) 新スレーブを起動し、新マスタとタイムラインIDが揃っていることを確認する。
タイムラインIDの取得には、pg_controldataコマンドを使用する。
厳密には最新チェックポイント実行時のタイムラインIDであるため、
タイムラインIDが揃っていないときは、マスタにてチェックポイント実行後、再確認します。
ssh経由で実行する事で任意のノードから全データベースクラスタの情報が取得できる。
[新マスタ]

$ export LANG=C
$ pg_controldata | grep " TimeLineID"
Latest checkpoint's TimeLineID:       2

$ ssh server1 $PGHOME/bin/pg_controldata $PGDATA | grep " TimeLineID"
Latest checkpoint's TimeLineID:       2

■pg_rewind使用時の注意点

  • 正常停止が必要

    pg_rewindを実行するデータベースクラスタは正常終了しなければいけません。
    物理障害等により正常停止できない場合、pg_rewindは使用できません。pg_basebackupを使用します。
  • 同一タイムラインの場合は実施不可

    pg_promoteを実行せずに旧スレーブを新マスタにした場合、新マスタのタイムラインIDは変わらないため、
    新マスタと旧マスタのタイムラインIDは同じ状態です。
    この場合は、pg_rewindは実行できません。
  • 実行時期と所要時間の関係

    pg_rewindによるフェイルバックの所要時間は2つの要素から構成されます。

    Step1. pg_rewindによる巻き戻し (旧マスタのWALを使用)
    Step2. WAL適用による追い付き (新マスタのWALを使用)

    新マスタで大量更新がある場合は、Step1は短時間で終了してもStep2で時間がかかります。
    結果として、pg_basebackupの方が効率が良い場合もあり得ます。
    また新マスタ昇格時のWALが削除されている場合は、後述するようにStep2でエラーとなる可能性もあります。その場合はpg_basebackupが必要となります。
    pg_rewindはフェイルオーバー後、あまり時間を置かずに実行する事がポイントです。
  • 旧マスタのWAL削除

    旧マスタの巻き戻しに必要な旧マスタのWALが削除されているいる場合、pg_rewindは失敗します。
    例えば旧マスタが障害により大量更新の途中で異常終了した場合などに発生します。
    pg_rewind実行時に次のようなエラーが発生します。
    could not open file "/home/pg96/pg96_data/pg_xlog/0000000D00000002000000CF": No such file or directory
    could not find previous WAL record at 2/CF000140
    Failure, exiting
    
    pg_rewindが成功するかどうかは検証(dry-runオプション)にて事前に確認する事ができます。
$ pg_rewind -D $PGDATA --source-server="host=server2 port=5432" --dry-run
servers diverged at WAL position 0/5015B70 on timeline 1
rewinding from last common checkpoint at 0/5015B00 on timeline 1
Done!

| メッセージはdry-runオプションが無い場合と同じです。
| pg_rewindでエラーが発生する場合(スレーブのWAL削除)は、この検証にて確認できますが、
| pg_rewindでエラーが発生しないで、後から発生する場合(マスタのWAL削除)は検知できません。
| 検証の仕様について認識下さい。
  • 新マスタのWAL削除

    pg_rewind後、新スレーブは新マスタのWALを適用することで、新マスタと同期します。
    新マスタに昇格時のWALファイルが残っていない場合、新スレーブは追い付きができず、次のエラーがサーバログに出力され続けます。
    対策としてレプリケーションスロットの有効化が有力ですが、新マスタのWAL領域の枯渇にご注意下さい。
    ERROR:  requested WAL segment 0000000D00000000000000F3 has already been removed
    
  • タイムラインの巻き戻し

    pg_rewindはPostgreSQL9.6からタイムラインの巻き戻しができるよになっています。
    これによりスプリットブレインが発生しても、新マスタをスレーブに戻すことが可能です。
  • pg_rewindが不要な場合

    pg_rewindはターゲットとソースクラスタのタイムラインIDが分岐した場合に実行が必要です。
    そのためタイムラインが枝分かれしなかった場合、pg_rewindを実行する必要はありません。
    例えばpg_rewind実行時に次のようなメッセージが出た場合、pg_rewindは実行せずに、以降の操作を継続します。
    servers diverged at WAL position 0/503A428 on timeline 2
    no rewind required
    

5.4.2.5. スレーブ障害による対処

スレーブ障害時の緊急対応の必要性は、非同期モードが同期モードかによって異なります。

非同期モードの場合は、スレーブ障害がマスタの更新処理を阻害しないため、緊急対応は必要ありません。
とはいえ、シングル状態であるため早期にレプリケーション構成に復旧します。

同期モードの場合は、スレーブ障害によりマスタの更新処理が阻害されハング状態となります
$ psql -h server1 -U postgres postgres -c "INSERT INTO test1_t VALUES ( 1 )"
Cancel request sent
WARNING:  canceling wait for synchronous replication due to user request
DETAIL:  ** The transaction has already committed locally, but might not have been replicated to the standby. **
INSERT 0 1
※Ctrl+Cをキーインする等、意図的にキャンセルしない限り、応答が返ってきません。
そのため、直ちに非同期に設定する必要があります。
非同期への切り替え処理は以降に示す様に再起動不要であるため、即時対応が可能です。

非同期への切り替えではなく、スレーブの再起動を試みる方法も考えられます。その方が効率的のようにも考えられます。
即座に起動できればその通りですが、起動に時間がかかる、あるいは物理的な障害で起動できない状態である事も考えられます。
そのような試行錯誤より、まずは確実にマスタのハング状態解消を優先します。

マスタの復旧後は、スレーブの復旧を試みます。
物理障害により起動できない場合は、フェイルバック処理と同様の作業を行います。
_images/slave_failure.png
(1) スレーブの疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
スレーブにて実行します。
$ pg_ctl -w -m immediate stop
(2) スレーブの死活監視にて異常を検知
死活監視の方法には多数ありますが、ここではpg_isreadyコマンドを使用します。
pg_isreadyコマンドはクライアントツールですので、任意のノードから実行できます。
$ pg_isready -h server2 -U postgres -d postgres
    server2:5432 - no response
(3) 非同期モードに切り替え
synchronous_standby_namesパラメータを''に設定する事で、非同期の設定となります。

$ vi $PGDATA/postgresql.conf

[変更前]
synchronous_standby_names = '*'

[変更後]
synchronous_standby_names = ''

$ pg_ctl reload
(4) 非同期モードの確認
レプリケーションモードが非同期(async)に変更された事を確認します。
$ psql -At -c "SELECT sync_state FROM pg_stat_replication;"
async

これでマスタが更新処理が可能な状態に復旧しました。 ただしシングル状態であるため、フェイルオーバと同様の作業を行います。

5.4.3. 障害時運用手順(複数スレーブ)

■同期モードについて

スレーブが複数ある場合、障害時にどのスレーブを新マスタに昇格するかが重要になります。 基本的には同期運用しているスレーブを昇格すべきですが、同期対象も複数選べるため、どの同期スレーブを優先するかが問題です。

これに対しPostgreSQLは、通常の同期運用では、同期優先度を明確にすることで、昇格すべきスレーブを絞っています。 しかし同期優先度が明確な場合、同期対象スレーブの性能に、マスタ側も大きく影響を受けます。 そのため複数同期においても、最も処理が進んでいるスレーブを同期対象とすることでスレーブの影響を抑えるクォーラムコミットという機能が用意されています。

ただしクォーラムコミットの場合、最も同期が進んでいるスレーブがその時によって異なるため、運用に工夫が必要です。 性能面と運用面のトレードオフを考慮した上で、同期モードを選択する必要があります。

  • 通常の同期
synchronous_standby_names = 'FIRST X (standby_name1, standby_name2, ...)'

通常の同期モードで運用する場合、上記のようにsynchronous_standby_namesを設定します。 FIRSTは通常の同期モードの選択であり、省略可能です。 数字Xは同期対象のスレーブ数であり、カッコ内の左から順に優先して同期対象になります。 カッコ内のスレーブ名は、同期候補のスレーブです。

以下のような設定の場合、slave1~3が同期運用、slave4~5は潜在的な同期運用がされます。 潜在的な同期運用は、普段は非同期として運用され、必要な場合は同期へ昇格されます。 例えばslave2の環境がマスタから切断された場合、残ったスレーブで優先度が高いslave1,slave3,slave4が同期運用されます。 (つまり非同期運用であったslave4が同期運用へ昇格されます)

synchronous_standby_names = 'FIRST 3 (slave1, slave2, slave3, slave4, slave5)'

障害発生時は、同期の優先度が高いスレーブを新マスタへ昇格させます。 通常の同期モードではカッコ内の左側が優先度が高く、右に行くほど優先度が低くなります。

  • クォーラムコミット
synchronous_standby_names = 'ANY X (standby_name1, standby_name2, ...)'

クォーラムコミットで運用する場合、上記のようにsynchronous_standby_namesを設定します。 ANYはクォーラムコミットの選択であり、こちらは省略できません。 数字Xは同期対象のスレーブ数であり、カッコ内のうちX台のスレーブの同期を待ちます。 カッコ内のスレーブ名は、同期候補のスレーブです。

以下のような設定の場合、slave1~5の全てがクォーラムコミットで運用されます。 マスタ側で更新があった場合、slave1~5のうちいずれか計3台でマスタからの更新反映が完了すれば、同期したとみなします。

synchronous_standby_names = 'ANY 3 (slave1, slave2, slave3, slave4, slave5)'

障害発生時、新マスタへ昇格させるスレーブを選択するのには注意が必要です。 確認方法は「SELECT pg_control_recovery();」コマンドにより現在のWAL位置を比較することです。 同期が進んでいる側はWALも進んでいます。これにより判断が可能です。

■前提
以降の手順では次の前提とします。
  • PostgreSQL 10
  • マスタ、2台スレーブの計3台構成(ホスト名をそれぞれ server1,server2,slave3と表記する)
  • 同期モードは同期(synchronous_commit = on または remote_apply)
  • 同期にはクォーラムコミットを使用(synchronous_standby_names = 'ANY 1 (slave_server2, slave_server3)')
  • スレーブはホットスタンバイ機能により参照可能 (hot_standby = on )
  • レプリケーション用のユーザは rep_user
  • マスタ/スレーブとも、portは5432を使用
  • レプリケーションスロット使用
  • マスタ/スレーブとも、環境変数PGDATA,PGPORTは設定済み
  • 死活監視は実際の運用ではクラスタソフトを使用するのが一般的だが、ここでは便宜上手動で実施
  • サーバのNICはパブリックのみ
  • 仮想IPについては考慮しない

■対処一覧 大別すると4種類の対処方法が考えられます。

表 5.22 状況別の対処
ID 障害箇所 障害状況 対処
1
マスタ
マスタとスレーブの関係が崩れており再構成が必要
  • 物理障害
  • promoteを伴わないスレーブのマスタ化(recovery.conf削除)
pg_basebackupコマンドを使用してフェイルバック
2
マスタ
マスタとスレーブの関係は巻き戻しで復旧可能
  • 非同期レプリケーションのインスタンス障害(差異がある状態で昇格)
  • スプリットブレイン状態での旧マスタへの更新
pg_rewindコマンドを使用してスイッチバック
3
マスタ
マスタとスレーブの切り替え可能
  • 計画停止
  • 同期レプリケーションのインスタンス障害(物理障害なし)
pg_rewindコマンドを使用しないでスイッチバック
4
スレーブ
マスタとスレーブの連携再開可能
  • スレーブの障害
同期式の場合、残ったスレーブを非同期式に切り替え

5.4.3.1. フェイルオーバ(複数スレーブ)

フェイルオーバについて記載します。

次のような状況を想定しています。

  • server1:マスタ  → 障害により停止
  • server2:スレーブ → 新マスタへ昇格
  • server3:スレーブ → マスタからのWAL転送が途絶えたたえめ、更新が停止
マスタにて、障害が発生した場合のスレーブを新マスタへ昇格したシングル構成図
_images/FailOver_direct.png
(1) マスタの疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
マスタ(server1)にて実行します。
$ pg_ctl -w -m immediate stop
$ kill -9 `head -1 $PGDATA/postmaster.pid`
(2) マスタの死活監視にて異常を検知
死活監視の方法には多数ありますが、ここではpg_isreadyコマンドを使用します。
pg_isreadyコマンドはクライアントツールですので、任意のノードから実行できます。
$ pg_isready -h server1 -U postgres -d postgres
server1:5432 - no response
(3) 昇格対象のスレーブ決定
クォーラムコミットの場合、現在のWAL位置を確認し、新マスタへ昇格すべきスレーブを特定します。
両方のスレーブ(server1,slave2)にて実行し、結果を比較します。
※通常の同期モードでは、優先度の高いスレーブが昇格対象なため、本操作は必要ありません。
[server2]
=# SELECT pg_control_recovery();
   pg_control_recovery
--------------------------
 (0/1301F348,1,0/0,0/0,f)
(1 row)

[server3]
=# SELECT pg_control_recovery();
   pg_control_recovery
--------------------------
 (0/1301F170,1,0/0,0/0,f)
(1 row)

※WALはserver2が進んでいるため、server2を昇格させる。
  server1:1301F348
  server2:1301F170
(4) スレーブを新マスタへ昇格
旧スレーブ(server2)で実行します。
$ pg_ctl promote
※サーバログに下記内容が記載されること
"database system is ready to accept connections"

ただしこの時点ではsynchronous_standby_namesパラメータに値が設定されている場合、新マスタで更新処理ができない状態です。

(5) 新マスタを非同期に切り替え
synchronous_standby_namesパラメータの設定を''に設定し、リロードで反映します。
新マスタ(server2)で実行します。
$ vi $PGDATA/postgresql.conf

[編集後]
synchronous_standby_names = ''

$ pg_ctl reload
これで更新処理ができる状態になりました。

(6) 新マスタの死活監視にて正常を確認
$ pg_isready -h server2 -U postgres -d postgres
server2:5432 - accepting connections

以上でファイルオーバーは完了です。

5.4.3.2. フェイルバック(複数スレーブ)

pg_basebackupを使用したフェイルバックについて記載します。 昇格されなかったスレーブは、新マスタのスレーブとして運用するために再設定が必要です。 旧マスタから新スレーブへの構築は初期構築手順とほぼ同じです。

マスタ障害発生によるフェイルオーバ後、旧マスタを新スレーブとしたレプリケーション構成図

_images/FailBack_direct.png

■パラメータ

pg_basebackupに必要な設定を記載します。

表 5.23 pg_basebackupに必要な設定
サーバ 設定ファイル パラメータ 設定値 内容
マスタ postgresql.conf listen_address 0.0.0.0 全てのIPアドレス(v4)からの接続を受け付ける
マスタ postgresql.conf max_wal_senders 2 WALストリームオプションを付与する場合は、2以上を設定

■pg_basebackupコマンド

pg_basebackupコマンドの主なオプションは次の通りです。

表 5.24 pg_basebackupのオプション
オプション 内容
-D <directory>
出力を書き出すディレクトリを指定。
-X <method>
--xlog-method=<method>
必要なWALファイルをバックアップに含める。method(収集方式)は以下から選択。
  • fetch :WALファイルは最後に収集
  • stream:バックアップ作成中に同時にWALをストリームで収集
運用中にpg_basebackupを実行する場合には stream を指定する。
fetch (最後に収集)では、必要なWALファイルが削除される可能性があるため。
-S <slot_name>
--slot=<slot_name>
WALストリーミングの収集に指定したレプリケーションスロットを使用。
-X stream とセットで指定。
必要なWALファイルが削除されるのを防ぐ事を目的とする。
運用中にpg_basebackupを実行する場合に使用を検討する。
以下に注意する。
  • 事前にレプリケーションスロットを作成する必要がある
  • マスタのWAL領域の空きが十分である事を確認する
-R
--write-recovery-conf
最低限のrecovery.confを作成。必要に応じてrecovery.confを加筆修正。
-r <rate>
--max-rate=<rate>
サーバから転送されるデータの最大転送速度を指定。
運用中にpg_basebackupを実行する場合に使用を検討する。
転送速度を抑える事で、マスタに対する影響を制限する事を目的とする。
-P
進行状況報告を有効化。
pg_basebackup処理中におおよその進行状況を報告する。
運用中に実行した場合、データベースクラスタのサイズが増加して進行状況が100%を超える場合がある。
-v
冗長モードを有効化。
進行状況報告も有効な場合、現在処理中のファイル名を出力。

pg_basebackupでレプリケーションスロットが使用できます。 WAL収集方式に stream を指定する事でWALをほぼ確保できますが、スロットを指定する事でより確実になります。 スロットを使用する運用であれば、この段階で作成するのが有力です。

■フェイルバック手順

次のような状況を想定しています。

  • server1:旧マスタ  → 新スレーブ
  • server2:旧スレーブ → 新マスタ
  • server3:旧スレーブ → 新スレーブ

pg_basebackupにスロットを指定する場合を記載します。


(1) レプリケーションスロットの作成
新マスタ(server2)にて実行します。
$ psql

[server1:旧マスタ→新スレーブ用のレプリケーションスロット]
=# SELECT pg_create_physical_replication_slot('slot_server1',true);  -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,144/EEFC8940)
(1 行)

[server3:旧スレーブ→新スレーブ用のレプリケーションスロット]
=# SELECT pg_create_physical_replication_slot('slot_server3',true);  -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server3,144/EEFC8940)
(1 行)

(2) recovery.confの修正
旧スレーブ(server3)にて実行します。primary_conninfoに記載する接続先マスタの情報を新マスタ(server2)に書き換えます。
またレプリケーションスロット名が変更されている場合、変更後の名前に合わせます。
$ vi $PGDATA/recovery.conf

[編集前]
primary_conninfo = 'user=rep_user host=server1 port=5432 application_name=slave_server3 asslmode=prefer sslcompression=1'

[編集前]
primary_conninfo = 'user=rep_user host=server2 port=5432 application_name=slave_server3 asslmode=prefer sslcompression=1'

(3) 新スレーブの起動
旧スレーブ(server3)にて実行します。recovery.conf修正反映し、新スレーブとして再起動します。
$ pg_ctl restart

(4) 旧マスタのデータベースクラスタを削除
旧マスタ(server1)にて実行します。
$ rm -rf $PGDATA/*
※$PGDATA以外に表領域を作成している場合、そのファイルも削除します。
※$PGDATAディレクトリを削除する場合は、postgresユーザで$PGDATAディレクトリを作成できるよう
 親ディレクトリのオーナーまたはパーミッションを設定します。
(5) pg_basebackupコマンドにて、新マスタからデータベースクラスタをコピー
旧マスタ(server1)にて実行します。
$ pg_basebackup -h server2 -U rep_user -D $PGDATA -X stream -S slot_server1 -P -v -R
transaction log start point: 144/F3000028 on timeline 17
pg_basebackup: starting background WAL receiver
10503022/10503022 kB (100%), 1/1 tablespace
transaction log end point: 144/F3000130
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: base backup completed
(6) recovery.confの修正
旧マスタ(server1)にて実行します。
pg_basebackupにて-Rオプションを指定した事で、recovery.confが作成されます。
スロットを指定している事から、primary_slot_nameの指定があります。以下を追記します。
  • recovery_target_timelineパラメータ
  • primary_conninfパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=rep_user host=server2 port=5432 sslmode=prefer sslcompression=1'
primary_slot_name = 'slot_server1'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=rep_user host=server2 port=5432 application_name=slave_server1 asslmode=prefer sslcompression=1'
primary_slot_name = 'slot_server1'
recovery_target_timeline = latest
(7) 旧マスタのpostgresql.confの修正
旧マスタ(server1)にて、パラメータの調整を行います。
  • synchronous_standby_namesパラメータの無効化
  • pg_statsinfoの無効化(有効化されている場合)
同期レプリケーションの設定を無効化します。
新スレーブのshared_preload_librariesパラメータに pg_statsinfoが設定されている場合は、書き込みができずにエラーが発生します。
pg_statsinfoの指定を削除します。
$ vi $PGDATA/postgresql.conf

[編集後]
synchronous_standby_names = ''
shared_preload_libraries = ''
(6) 新スレーブの起動
新スレーブを起動します。
$ pg_ctl start
(7) 新マスタでのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 1866
usesysid         | 16384
usename          | rep_user
application_name | slave_server3
client_addr      | ::1
client_hostname  |
client_port      | 46840
backend_start    | 2018-02-13 14:06:04.475996+09
backend_xmin     |
state            | streaming            --- ストリーミング中
sent_lsn         | 0/B067570
write_lsn        | 0/B067570
flush_lsn        | 0/B067570
replay_lsn       | 0/B067570
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async                --- 非同期
-[ RECORD 2 ]----+------------------------------
pid              | 1776
usesysid         | 16384
usename          | rep_user
application_name | slave_server1
client_addr      | ::1
client_hostname  |
client_port      | 46838
backend_start    | 2018-02-13 14:05:53.580727+09
backend_xmin     |
state            | streaming            --- ストリーミング中
sent_lsn         | 0/B067570
write_lsn        | 0/B067570
flush_lsn        | 0/B067570
replay_lsn       | 0/B067570
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async                --- 非同期

=# SELECT slot_name,active FROM pg_replication_slots;
-[ RECORD 1 ]----
slot_name | slot1
active    | t                --- アクティブ
-[ RECORD 2 ]----
slot_name | slot2
active    | t                --- アクティブ
(8) レプリケーション方式を同期式に変更
新マスタにて、synchronous_standby_namesパラメータを設定し、リロードで反映します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_standby_names = ''

[編集後]
synchronous_standby_names = 'ANY 1 (slave_server1, slave_server3)'

$ pg_ctl reload

$ psql -h server2 -U postgres postgres -c "SELECT * FROM pg_stat_replication" -x
-[ RECORD 1 ]----+------------------------------
pid              | 1866
usesysid         | 16384
usename          | rep_user
application_name | slave2
client_addr      | ::1
client_hostname  |
client_port      | 46840
backend_start    | 2018-02-13 14:06:04.475996+09
backend_xmin     |
state            | streaming            --- ストリーミング中
sent_lsn         | 0/B067570
write_lsn        | 0/B067570
flush_lsn        | 0/B067570
replay_lsn       | 0/B067570
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | quorum                 --- クォーラムコミット
-[ RECORD 2 ]----+------------------------------
pid              | 1776
usesysid         | 16384
usename          | rep_user
application_name | slave1
client_addr      | ::1
client_hostname  |
client_port      | 46838
backend_start    | 2018-02-13 14:05:53.580727+09
backend_xmin     |
state            | streaming            --- ストリーミング中
sent_lsn         | 0/B067570
write_lsn        | 0/B067570
flush_lsn        | 0/B067570
replay_lsn       | 0/B067570
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | quorum                 --- クォーラムコミット

これにて、以下の構成に復旧しました。

  • レプリケーションスロット使用
  • 同期式レプリケーション構成

5.4.3.3. スイッチオーバ(複数スレーブ)

スイッチオーバについて記載します。

■スイッチオーバ手順

計画停止におけるマスタ/スレーブの切り替え手順です。 pg_basebackupやpg_rewindが不要であるためシンプルな手順です。

次のような状況を想定しています。

  • server1:マスタ  → 新スレーブ
  • server2:スレーブ → 新マスタ
  • server3:スレーブ → 新スレーブ
計画停止にてマスタとスレーブの役割を切り替えたレプリケーション構成図
_images/SwitchOver_direct.png
(1) マスタの正常停止
マスタ(server1)にて実行します。
$ pg_ctl stop -m fast
(2) スレーブの昇格
スレーブ(server2)にて実行します。

以下の場合にはpg_rewind不要です。
  • 計画停止
  • 同期レプリケーションのインスタンス障害(物理障害なし)
$ pg_ctl promote

以降の手順はフェイルオーバの場合と同様であるため省略します。

    1. レプリケーションスロットの作成
    1. recovery.confの作成
    1. 旧マスタのpostgresql.confの修正
    1. 新スレーブの起動
    1. 新マスタでのレプリケーション確認
    1. レプリケーション方式を同期式に変更
(9) レプリケーションスロットの削除(スレーブ)
フェイルオーバとの違いとしては、新スレーブ(server1)に旧マスタ時代のスロットが残る事があります。
restart_lsn列に値が残っている状態では、マスタのVACUUM処理を阻害するなどの
悪影響の可能性があるため、削除します。
スロットの削除は関数で行うため、スレーブでも実行可能です。
$ psql

=# SELECT pg_drop_replication_slot('slot_server2');
pg_create_physical_replication_slot
-------------------------------------

(1 row)

=# SELECT pg_drop_replication_slot('slot_server3');
pg_create_physical_replication_slot
-------------------------------------

(1 row)

=# SELECT slot_name FROM pg_replication_slots ;
(0 rows)

以上でスイッチオーバが完了しました。

5.4.3.4. スイッチバック(複数スレーブ)

pg_rewindを使用したスイッチバックについて記載します。

pg_rewindはタイムラインのずれたレプリケーションを再同期させる機能です。 実行後、ターゲットクラスタはソースクラスタと置き換えられた状態になります。 そのためpg_rewind後の操作は、通常のフェイルオーバ時と同じです。 タイムラインの分岐点からソースクラスタのWALを適用するため、更新量が少なければpg_basebackupによる複製より高速です。 これによりフェイルオーバ時、旧マスタを容易に新スレーブとして起動させることができます。

スイッチオーバー後に役割を元に戻したレプリケーション構成図
_images/SwitchBack.png

■関連パラメータ

pg_rewindに必要な設定を記載します。

表 5.25 pg_rewindに必要な設定
サーバ 設定ファイル パラメータ 設定値 内容
マスタ postgresql.conf full_page_writes on チェックポイント後の更新時、ディスクページの全内容をWALに書き込む。
マスタ postgresql.conf wal_log_hints on ヒントビット更新時もfull_page_writesを実行する。

■pg_rewindコマンド

pg_rewindコマンドのの主なオプションは次の通りです。

表 5.26 pg_rewindのオプション
オプション 内容
D <ターゲットクラスタ> pg_rewindを実行し、ソースクラスタの内容に置き換えるクラスタを指定する。
source-server="<ソースクラスタ>"

同期対象であるソースクラスタを指定します。主に次の接続文字列を使用します。

  • host:ソースクラスタのホスト名またはIPアドレス
  • port:ソースクラスタのポート番号
  • dbname:ソースクラスタの接続先データベース名
  • user:ソースクラスタの接続先ユーザ
P 進行状況をレポートとして表示する。

■スイッチバック手順

※事前にマスタ/スレーブで(1) 関連パラメータの設定がされていることを前提とします。
(1) 旧マスタの正常停止
pg_rewindを使用するには正常停止する必要があります。
停止した旧マスタを一旦起動した後、正常停止させます。
障害により正常に起動や停止ができない状態ではpg_rewindは使用できません。
その場合は、pg_basebackupを使用します。
$ pg_ctl start -w
$ pg_ctl stop -m fast -w
(2) pg_rewindの実行

 旧マスタでpg_rewindを実行する。
$ pg_rewind -D $PGDATA --source-server="host=server2 port=5432"
servers diverged at WAL position 0/5015B70 on timeline 1
rewinding from last common checkpoint at 0/5015B00 on timeline 1
Done!
(3) スレーブのrecovery.confを編集
旧マスタの$PGDATA配下にrecovery.confを作成し、以下を設定します。
$ vi $PGDATA/recovery.conf

[編集後]
standby_mode = 'on'
primary_conninfo = 'host=server2 port=5432 user=rep_user'
recovery_target_timeline = 'latest'
(4) 新スレーブを起動し、新マスタとタイムラインIDが揃っていることを確認する。
タイムラインIDの取得には、pg_controldataコマンドを使用する。
厳密には最新チェックポイント実行時のタイムラインIDであるため、
タイムラインIDが揃っていないときは、マスタにてチェックポイント実行後、再確認します。
ssh経由で実行する事で任意のノードから全データベースクラスタの情報が取得できる。
[新マスタ]

$ export LANG=C
$ pg_controldata | grep " TimeLineID"
Latest checkpoint's TimeLineID:       2

$ ssh server1 $PGHOME/bin/pg_controldata $PGDATA | grep " TimeLineID"
Latest checkpoint's TimeLineID:       2

■pg_rewind使用時の注意点

  • 正常停止が必要

    pg_rewindを実行するデータベースクラスタは正常終了しなければいけません。
    物理障害等により正常停止できない場合、pg_rewindは使用できません。pg_basebackupを使用します。
  • 同一タイムラインの場合は実施不可

    pg_promoteを実行せずに旧スレーブを新マスタにした場合、新マスタのタイムラインIDは変わらないため、
    新マスタと旧マスタのタイムラインIDは同じ状態です。
    この場合は、pg_rewindは実行できません。
  • 実行時期と所要時間の関係

    pg_rewindによるフェイルバックの所要時間は2つの要素から構成されます。

    Step1. pg_rewindによる巻き戻し (旧マスタのWALを使用)
    Step2. WAL適用による追い付き (新マスタのWALを使用)

    新マスタで大量更新がある場合は、Step1は短時間で終了してもStep2で時間がかかります。
    結果として、pg_basebackupの方が効率が良い場合もあり得ます。
    また新マスタ昇格時のWALが削除されている場合は、後述するようにStep2でエラーとなる可能性もあります。その場合はpg_basebackupが必要となります。
    pg_rewindはフェイルオーバー後、あまり時間を置かずに実行する事がポイントです。
  • 旧マスタのWAL削除

    旧マスタの巻き戻しに必要な旧マスタのWALが削除されているいる場合、pg_rewindは失敗します。
    例えば旧マスタが障害により大量更新の途中で異常終了した場合などに発生します。
    pg_rewind実行時に次のようなエラーが発生します。
    could not open file "/home/pg96/pg96_data/pg_xlog/0000000D00000002000000CF": No such file or directory
    could not find previous WAL record at 2/CF000140
    Failure, exiting
    
    pg_rewindが成功するかどうかは検証(dry-runオプション)にて事前に確認する事ができます。
$ pg_rewind -D $PGDATA --source-server="host=server2 port=5432" --dry-run
servers diverged at WAL position 0/5015B70 on timeline 1
rewinding from last common checkpoint at 0/5015B00 on timeline 1
Done!

| メッセージはdry-runオプションが無い場合と同じです。
| pg_rewindでエラーが発生する場合(スレーブのWAL削除)は、この検証にて確認できますが、
| pg_rewindでエラーが発生しないで、後から発生する場合(マスタのWAL削除)は検知できません。
| 検証の仕様について認識下さい。
  • 新マスタのWAL削除

    pg_rewind後、新スレーブは新マスタのWALを適用することで、新マスタと同期します。
    新マスタに昇格時のWALファイルが残っていない場合、新スレーブは追い付きができず、次のエラーがサーバログに出力され続けます。
    対策としてレプリケーションスロットの有効化が有力ですが、新マスタのWAL領域の枯渇にご注意下さい。
    ERROR:  requested WAL segment 0000000D00000000000000F3 has already been removed
    
  • タイムラインの巻き戻し

    pg_rewindはPostgreSQL9.6からタイムラインの巻き戻しができるよになっています。
    これによりスプリットブレインが発生しても、新マスタをスレーブに戻すことが可能です。
  • pg_rewindが不要な場合

    pg_rewindはターゲットとソースクラスタのタイムラインIDが分岐した場合に実行が必要です。
    そのためタイムラインが枝分かれしなかった場合、pg_rewindを実行する必要はありません。
    例えばpg_rewind実行時に次のようなメッセージが出た場合、pg_rewindは実行せずに、以降の操作を継続します。
    servers diverged at WAL position 0/503A428 on timeline 2
    no rewind required
    

5.4.3.5. スレーブ障害による対処(複数スレーブ)

スレーブ障害時の緊急対応の必要性は、非同期モードが同期モードかによって異なります。

非同期モードの場合は、スレーブ障害がマスタの更新処理を阻害しないため、緊急対応は必要ありません。
とはいえ、シングル状態であるため早期にレプリケーション構成に復旧します。

同期モードの場合、稼働しているスレーブ数が、同期対象のスレーブ数より下回った場合、スレーブ障害によりマスタの更新処理が阻害されハング状態となります
$ psql -h server1 -U postgres postgres -c "INSERT INTO test1_t VALUES ( 1 )"
Cancel request sent
WARNING:  canceling wait for synchronous replication due to user request
DETAIL:  ** The transaction has already committed locally, but might not have been replicated to the standby. **
INSERT 0 1
※Ctrl+Cをキーインする等、意図的にキャンセルしない限り、応答が返ってきません。
そのため、直ちに非同期に設定する、または同期対象のスレーブ数設定を減らす必要があります。
非同期への切り替え処理は以降に示す様に再起動不要であるため、即時対応が可能です。

非同期への切り替えではなく、スレーブの再起動を試みる方法も考えられます。その方が効率的のようにも考えられます。
即座に起動できればその通りですが、起動に時間がかかる、あるいは物理的な障害で起動できない状態である事も考えられます。
そのような試行錯誤より、まずは確実にマスタのハング状態解消を優先します。

マスタの復旧後は、スレーブの復旧を試みます。
物理障害により起動できない場合は、フェイルバック処理と同様の作業を行います。
_images/slave_failure.png
(1) スレーブの疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
スレーブにて実行します。
$ pg_ctl -w -m immediate stop
(2) スレーブの死活監視にて異常を検知
死活監視の方法には多数ありますが、ここではpg_isreadyコマンドを使用します。
pg_isreadyコマンドはクライアントツールですので、任意のノードから実行できます。
$ pg_isready -h server2 -U postgres -d postgres
    server2:5432 - no response
(3) 非同期モードに切り替え
synchronous_standby_namesパラメータを''に設定する事で、非同期の設定となります。

$ vi $PGDATA/postgresql.conf

[変更前]
synchronous_standby_names = 'FIRST X (standby_name1, standby_name2, ...)'

[変更後]
synchronous_standby_names = ''

$ pg_ctl reload
(4) 非同期モードの確認
レプリケーションモードが非同期(async)に変更された事を確認します。
$ psql -At -c "SELECT sync_state FROM pg_stat_replication;"
async

これでマスタが更新処理が可能な状態に復旧しました。 ただしシングル状態であるため、フェイルオーバと同様の作業を行います。

5.4.4. 障害時運用手順(カスケード構成)

■前提 以降の手順では次の前提とします。

  • PostgreSQL 10.1
  • マスタ、スレーブ1、スレーブ2の3台のカスケード構成(ホスト名をそれぞれ server1,server2,server3と表記する)
  • スレーブ1が同期モード(synchronous_commit=on, synchronous_standby_names='server2')
  • スレーブ2が非同期モード(synchronous_commit=off, synchronous_standby_names='')
  • スレーブはホットスタンバイ機能により参照可能 (hot_standby = on)
  • レプリケーション用のユーザは repuser
  • マスタ/スレーブとも、portは5432を使用
  • レプリケーションスロット使用
  • マスタ/スレーブとも、環境変数PGDATA,PGPORTは設定済み
  • 死活監視は実際の運用ではクラスタソフトを使用するのが一般的ですが、ここでは便宜上手動で実施
  • サーバのNICはパブリックのみ
  • 仮想IPについては考慮しない

■対処一覧 大別すると3種類の対処方法が考えられます。

表 5.27 障害別の対処
ID 障害箇所 障害状況 pg_basebackupとpg_rewindの使い分け
1
マスタ
マスタとスレーブの関係が崩れており再構成が必要
  • 物理障害
  • promoteを伴わないスレーブのマスタ化(recovery.conf削除)
pg_basebackupコマンドを使用してフェイルバック
2
マスタ
マスタとスレーブの関係は巻き戻しで復旧可能
  • 非同期レプリケーションのインスタンス障害(差異がある状態で昇格)
  • スプリットブレイン状態での旧マスタへの更新
pg_rewindコマンドを使用してスイッチバック
3
マスタ
マスタとスレーブの切り替え可能
  • 計画停止
  • 同期レプリケーションのインスタンス障害(物理障害なし)
pg_rewindコマンドを使用しないでスイッチバック
4
スレーブ1
マスタとスレーブの連携再開可能
  • スレーブの障害
同期式の場合は非同期式に切り替え
5
スレーブ1
マスタとスレーブの連携再開不可能
  • スレーブの障害
pg_basebackupコマンドを使用してフェイルバック
6
スレーブ2
マスタとスレーブの連携再開可能
  • スレーブの障害
pg_rewindコマンドを使用しないでフェイルバック
7
スレーブ2
マスタとスレーブの連携再開不可能
  • スレーブの障害
pg_basebackupコマンドを使用してフェイルバック
8
マスタ、スレーブ1
マスタとスレーブ2の連携再開可能
  • マスタ、スレーブ1の障害
pg_rewindコマンドを使用しないでフェイルバック
9
マスタ、スレーブ1
マスタとスレーブ2の連携再開不可能
  • スレーブの障害
pg_basebackupコマンドを使用してフェイルバック

5.4.4.1. フェイルオーバ

フェイルオーバについて記載します。

5.4.4.2. 同期スレーブへのフェイルオーバ

マスタ(server1)にて、障害が発生した場合にスレーブ1(server2)を新マスタへ昇格した構成図
_images/FailOver_cascade1.png
(1) マスタ(server1)の疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
マスタ(server1)にて実行します。
$ pg_ctl -w -m immediate stop
$ kill -9 `head -1 $PGDATA/postmaster.pid`
(2) マスタ(server1)の死活監視にて異常を検知
死活監視の方法には多数ありますが、ここではpg_isreadyコマンドを使用します。
pg_isreadyコマンドはクライアントツールですので、任意のノードから実行できます。
$ pg_isready -h server1 -U postgres -d postgres
server1:5432 - no response
(3) スレーブ1(server2)を新マスタへ昇格
スレーブ1(server2)で実行します。
$ pg_ctl promote
※サーバログに下記内容が記載されること
"database system is ready to accept connections"
(4) 新マスタ(server2)の死活監視にて正常を確認
$ pg_isready -h server2 -U postgres -d postgres
server2:5432 - accepting connections
(5) スレーブ2(server3)の死活監視を確認
$ pg_isready -h server3 -U postgres -d postgres
server3:5432 - accepting connections
(6) 新マスタ(server2)にてデータ更新が行えることを確認
(7) スレーブ2(server3)にてデータが伝播されていることを確認

以上でファイルオーバーは完了です。

5.4.4.3. 非同期スレーブへのフェイルオーバ

マスタ(server1)/スレーブ1(server2)サイトに障害が発生した場合のスレーブ2(server3)を新マスタへ昇格した構成図
_images/FailOver_cascade2.png
(1) マスタ(server1)の疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
マスタ(server1)にて実行します。
$ pg_ctl -w -m immediate stop
$ kill -9 `head -1 $PGDATA/postmaster.pid`
(2) スレーブ1(server2)の疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
スレーブ1(server2)にて実行します。
$ pg_ctl -w -m immediate stop
$ kill -9 `head -1 $PGDATA/postmaster.pid`
(3) マスタ(server1)、スレーブ1(server2)の死活監視にて異常を検知
死活監視の方法には多数ありますが、ここではpg_isreadyコマンドを使用します。
pg_isreadyコマンドはクライアントツールですので、任意のノードから実行できます。
$ pg_isready -h server1 -U postgres -d postgres
server1:5432 - no response
$ pg_isready -h server2 -U postgres -d postgres
server2:5432 - no response
(4) スレーブ2(server3)を新マスタへ昇格
スレーブ2(server3)で実行します。
$ pg_ctl promote
※サーバログに下記内容が記載されること
"database system is ready to accept connections"
(5) 新マスタ(server3)の死活監視にて正常を確認
$ pg_isready -h server3 -U postgres -d postgres
server3:5432 - accepting connections
(6) 新マスタ(server3)にてデータ更新が行えることを確認

以上でファイルオーバーは完了です。

5.4.4.4. フェイルバック

フェイルバックについて記載します。

5.4.4.5. 旧マスタを同期モードスレーブとしてフェイルバック

pg_basebackupを使用したフェイルバックについて記載します。 初期構築手順とほぼ同じです。

マスタ障害発生によるフェイルオーバ後、旧マスタを同期モード新スレーブとしたレプリケーション構成図

_images/FailBack_cascade1.png

■関連パラメータ

pg_basebackupに必要な設定を記載します。

表 5.28 pg_basebackupに必要な設定
サーバ 設定ファイル パラメータ 設定値 内容
マスタ postgresql.conf listen_address 0.0.0.0 全てのIPアドレス(v4)からの接続を受け付ける
マスタ postgresql.conf max_wal_senders 2 WALストリームオプションを付与する場合は、2以上を設定

■pg_basebackupコマンド

pg_basebackupコマンドの主なオプションは次の通りです。

表 5.29 pg_basebackupのオプション
オプション 内容
-D <directory>
出力を書き出すディレクトリを指定。
-X <method>
--wal-method=<method>
必要なWALファイルをバックアップに含める。method(収集方式)は以下から選択。
  • fetch :WALファイルは最後に収集
  • stream:バックアップ作成中に同時にWALをストリームで収集
運用中にpg_basebackupを実行する場合には stream を指定する。
fetch (最後に収集)では、必要なWALファイルが削除される可能性があるため。
-S <slot_name>
--slot=<slot_name>
WALストリーミングの収集に指定したレプリケーションスロットを使用。
-X stream とセットで指定。
必要なWALファイルが削除されるのを防ぐ事を目的とする。
運用中にpg_basebackupを実行する場合に使用を検討する。
以下に注意する。
  • 事前にレプリケーションスロットを作成する必要がある
  • マスタのWAL領域の空きが十分である事を確認する
-R
--write-recovery-conf
最低限のrecovery.confを作成。必要に応じてrecovery.confを加筆修正。
-r <rate>
--max-rate=<rate>
サーバから転送されるデータの最大転送速度を指定。
運用中にpg_basebackupを実行する場合に使用を検討する。
転送速度を抑える事で、マスタに対する影響を制限する事を目的とする。
-P
進行状況報告を有効化。
pg_basebackup処理中におおよその進行状況を報告する。
運用中に実行した場合、データベースクラスタのサイズが増加して進行状況が100%を超える場合がある。
-v
冗長モードを有効化。
進行状況報告も有効な場合、現在処理中のファイル名を出力。

pg_basebackupでレプリケーションスロットが使用できます。 WAL収集方式に stream を指定する事でWALをほぼ確保できますが、スロットを指定する事でより確実になります。 スロットを使用する運用であれば、この段階で作成するのが有力です。

■フェイルバック手順

pg_basebackupにスロットを指定する場合を記載します。

(1) 旧マスタ(server1)のデータベースクラスタを削除
旧マスタ(server1)にて実行します。
$ rm -rf $PGDATA/*
※$PGDATA以外に表領域を作成している場合、そのファイルも削除します。
※$PGDATAディレクトリを削除する場合は、postgresユーザで$PGDATAディレクトリを作成できるよう
親ディレクトリのオーナーまたはパーミッションを設定します。
(2) レプリケーションスロットの作成
新マスタ(server2)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server2',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server2,0/96000090)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server2
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/96000090     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(3) pg_basebackupコマンドにて、新マスタ(server2)からデータベースクラスタをコピー
旧マスタ(server1)にて実行します。
$ pg_basebackup -h server2 -U repuser -D $PGDATA -X stream -S slot_server2 -P -v -R
pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/99000028 on timeline 32
pg_basebackup: starting background WAL receiver
138336/138336 kB (100%), 1/1 tablespace
pg_basebackup: write-ahead log end point: 0/99000130
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: base backup completed
(4) recovery.confの修正
旧マスタ(server1)にて実行します。
pg_basebackupにて-Rオプションを指定した事で、recovery.confが作成されます。
スロットを指定している事から、primary_slot_nameの指定があります。以下を追記します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 application_name=server1 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'
recovery_target_timeline = 'latest'

起動
$ pg_ctl start
(5) 新マスタ(server2)のpostgresql.confの修正
新マスタ(server2)にて実行します。
  • synchronous_commitパラメータの有効化
  • synchronous_standby_namesパラメータの有効化
同期レプリケーションの設定を有効化します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = off
synchronous_standby_names = ''

[編集後]
synchronous_commit = on
synchronous_standby_names = 'server1'

設定を反映
$ pg_ctl reload
(6) レプリケーションスロットの作成
新スレーブ1(server1)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server1',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,0/9A000060)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/9A000060     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(7) スレーブ2(server3)にてrecovery.conf修正
スレーブ2(server3)にて実行します。
  • primary_conninfoパラメータの「host=」部分を修正
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'
recovery_target_timeline = 'latest'

再起動
$ pg_ctl restart
(8) 新マスタ(server2)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 46558
usesysid         | 16384
usename          | repuser
application_name | server1
client_addr      | <server1IP>
client_hostname  |
client_port      | 64897
backend_start    | 2018-02-09 11:22:23.535181+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/9A000140
write_lsn        | 0/9A000140
flush_lsn        | 0/9A000140
replay_lsn       | 0/9A000140
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                --- 同期

 =# SELECT slot_name,active FROM pg_replication_slots;
 -[ RECORD 1 ]-------+-------------
 slot_name           | slot_server2
 active              | t                --- アクティブ
(9) 新スレーブ(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 97277
usesysid         | 16384
usename          | repuser
application_name | server3
client_addr      | <server3IP>
client_hostname  |
client_port      | 52882
backend_start    | 2018-02-09 11:55:20.971553+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/9A000140
write_lsn        | 0/9A000140
flush_lsn        | 0/9A000140
replay_lsn       | 0/9A000140
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期

これにて、以下の構成に復旧しました。

  • レプリケーションスロット使用
  • server2(マスタ) =同期=> server1(スレーブ1) =非同期=> server3(スレーブ2) のカスケードレプリケーション構成

5.4.4.6. 旧マスタをインスタンス障害からフェイルバック

pg_basebackupを使用したフェイルバックについて記載します。 初期構築手順とほぼ同じです。

マスタインスタンス障害発生によるフェイルオーバ後、旧マスタを同期モード新スレーブとしたレプリケーション構成図

_images/FailBack_cascade1.png

■フェイルバック手順

(1) スレーブ2(server3)を一旦停止
スレーブ2(server3)にて実行します。
$ pg_ctl stop
(2) レプリケーションスロットの作成
新マスタ(server2)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server2',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server2,0/96000090)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server2
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/96000090     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(3) recovery.confの作成
旧マスタ(server1)にて実行します。
  • 以下を設定します。
$ vi $PGDATA/recovery.conf

standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 application_name=server1 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'
recovery_target_timeline = 'latest'
(4) 旧マスタ(server1)のpostgresql.confの修正
旧マスタ(server1)にて、パラメータの調整を行います。
  • synchronous_commitパラメータの無効化
  • synchronous_standby_namesパラメータの無効化
  • pg_statsinfoの無効化(有効化されている場合)
同期レプリケーションの設定を無効化します。
新スレーブのshared_preload_librariesパラメータに pg_statsinfoが設定されている場合は、書き込みができずにエラーが発生します。
pg_statsinfoの指定を削除します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = on
synchronous_standby_names = '*'
shared_preload_libraries = 'pg_stat_statements,pg_statsinfo'

[編集後]
synchronous_commit = off
synchronous_standby_names = ''
shared_preload_libraries = ''
(5) 新スレーブ(server1)の起動
新スレーブ(server1)を起動します。
$ pg_ctl start
(6) 新マスタ(server2)のpostgresql.confの修正
新マスタ(server2)にて実行します。
  • synchronous_commitパラメータの有効化
  • synchronous_standby_namesパラメータの有効化
同期レプリケーションの設定を有効化します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = off
synchronous_standby_names = ''

[編集後]
synchronous_commit = on
synchronous_standby_names = 'server1'

設定を反映
$ pg_ctl reload
(7) レプリケーションスロットの作成
新スレーブ1(server1)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server1',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,0/9A000060)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/9A000060     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(8) スレーブ2(server3)にてrecovery.conf修正
スレーブ2(server3)にて実行します。
  • primary_conninfoパラメータの「host=」部分を修正
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'
recovery_target_timeline = 'latest'

再起動
$ pg_ctl restart
(9) 新マスタ(server2)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 12505
usesysid         | 16384
usename          | repuser
application_name | server1
client_addr      | <server1IP>
client_hostname  |
client_port      | 58195
backend_start    | 2018-02-07 21:55:00.722782+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                --- 同期
(10) 新スレーブ(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 86651
usesysid         | 16384
usename          | repuser
application_name | server3
client_addr      | <server3IP>
client_hostname  |
client_port      | 18206
backend_start    | 2018-02-07 22:13:40.199786+09
backend_xmin     | 606
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期

これにて、以下の構成に復旧しました。

  • レプリケーションスロット使用
  • server2(マスタ) =同期=> server1(スレーブ1) -非同期-> server3(スレーブ2) のカスケードレプリケーション構成

5.4.4.7. 旧マスタ/旧スレーブ1をフェイルバック

pg_basebackupを使用したフェイルバックについて記載します。 初期構築手順とほぼ同じです。

マスタ(server1)/スレーブ1(server2)サイトに障害が発生し、フェイルオーバ後、 マスタ(server1)/スレーブ1(server2)を元の状態に復旧したレプリケーション構成図

_images/FailBack_cascade2.png

pg_basebackupに必要な設定については、以下と同様であるため省略します。

「旧マスタを同期モードスレーブとしてフェイルバック」

pg_basebackupでレプリケーションスロットが使用できます。 WAL収集方式に stream を指定する事でWALをほぼ確保できますが、スロットを指定する事でより確実になります。 スロットを使用する運用であれば、この段階で作成するのが有力です。

■フェイルバック手順

pg_basebackupにスロットを指定する場合を記載します。

(1) 旧マスタ(server1)のデータベースクラスタを削除
旧マスタ(server1)にて実行します。
$ rm -rf $PGDATA/*
※$PGDATA以外に表領域を作成している場合、そのファイルも削除します。
※$PGDATAディレクトリを削除する場合は、postgresユーザで$PGDATAディレクトリを作成できるよう
親ディレクトリのオーナーまたはパーミッションを設定します。
(2) 旧スレーブ1(server2)のデータベースクラスタを削除
旧スレーブ1(server2)にて実行します。
$ rm -rf $PGDATA/*
※$PGDATA以外に表領域を作成している場合、そのファイルも削除します。
※$PGDATAディレクトリを削除する場合は、postgresユーザで$PGDATAディレクトリを作成できるよう
親ディレクトリのオーナーまたはパーミッションを設定します。
(3) レプリケーションスロットの作成
新マスタ(server3)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server3',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server3,0/AD000090)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server3
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/AD000090     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(4) pg_basebackupコマンドにて、新マスタ(server3)からデータベースクラスタをコピー
旧マスタ(server1)にて実行します。
$ pg_basebackup -h server3 -U repuser -D $PGDATA -X stream -S slot_server3 -P -v -R
pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/3B000028 on timeline 2
pg_basebackup: starting background WAL receiver
203852/203852 kB (100%), 1/1 tablespace
pg_basebackup: write-ahead log end point: 0/3B000130
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: base backup completed
(5) recovery.confの修正
旧マスタ(server1)にて実行します。
pg_basebackupにて-Rオプションを指定した事で、recovery.confが作成されます。
スロットを指定している事から、primary_slot_nameの指定があります。以下を追記します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server3 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server3'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server3 port=5432 application_name=server1 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server3'
recovery_target_timeline = 'latest'
(6) 新スレーブ1(server1)の起動
新スレーブ1(server1)を起動します。
$ pg_ctl start
(7) レプリケーションスロットの作成
新スレーブ1(server1)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server1',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,0/AE000028)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/AE000028     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(8) pg_basebackupコマンドにて、新スレーブ1(server1)からデータベースクラスタをコピー
スレーブ2(server2)にて実行します。
$ pg_basebackup -h server1 -U repuser -D $PGDATA -X stream -S slot_server1 -P -v -R
pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/3B000028 on timeline 2
pg_basebackup: starting background WAL receiver
203852/203852 kB (100%), 1/1 tablespace
pg_basebackup: write-ahead log end point: 0/3B000130
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: base backup completed
(9) recovery.confの修正
スレーブ2(server2)にて実行します。
pg_basebackupにて-Rオプションを指定した事で、recovery.confが作成されます。
スロットを指定している事から、primary_slot_nameの指定があります。以下を追記します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 application_name=server2 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'
recovery_target_timeline = 'latest'
(10) 新スレーブ2(server2)の起動
新スレーブ2(server2)を起動します。
$ pg_ctl start
(11) マスタ(server3)の停止
マスタ(server3)を停止します。
$ pg_ctl stop
(12) スレーブ1(server1)を新マスタへ昇格
スレーブ1(server1)で実行します。
$ pg_ctl promote
※サーバログに下記内容が記載されること
"database system is ready to accept connections"
(13) レプリケーションスロットの作成
新スレーブ1(server2)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server2',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server2,0/AE000028)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server2
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/AE000028     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(14) 新スレーブ2(server3)にてrecovery.conf作成
新スレーブ2(server3)にてrecovery.confを作成します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータの追加
  • recovery_target_timelineパラメータの追加
$ mv $PGDATA/recovery.done $PGDATA/recovery.conf
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'
recovery_target_timeline = 'latest'

起動
$ pg_ctl start
(15) 新マスタ(server1)のpostgresql.confの修正
新マスタ(server1)にて実行します。
  • synchronous_commitパラメータの有効化
  • synchronous_standby_namesパラメータの有効化
同期レプリケーションの設定を有効化します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = off
synchronous_standby_names = ''

[編集後]
synchronous_commit = on
synchronous_standby_names = 'server2'

設定を反映
$ pg_ctl reload
(16) 新マスタ(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 12505
usesysid         | 16384
usename          | repuser
application_name | server2
client_addr      | <server2IP>
client_hostname  |
client_port      | 58195
backend_start    | 2018-02-07 21:55:00.722782+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                --- 同期

 =# SELECT slot_name,active FROM pg_replication_slots;
 -[ RECORD 1 ]-------+-------------
 slot_name           | slot_server1
 active              | t                --- アクティブ
(17) 新スレーブ1(server2)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 86651
usesysid         | 16384
usename          | repuser
application_name | server3
client_addr      | <server3IP>
client_hostname  |
client_port      | 18206
backend_start    | 2018-02-07 22:13:40.199786+09
backend_xmin     | 606
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期

 =# SELECT slot_name,active FROM pg_replication_slots;
   slot_name   | active
 --------------+--------
  slot_server2 | t
 (1 row)
(18) レプリケーションスロットの削除(スレーブ)
新スレーブに旧マスタ時代のスロットが残る事があります。
restart_lsn列に値が残っている状態では、マスタのVACUUM処理を阻害するなどの
悪影響の可能性があるため、削除します。
スロットの削除は関数で行うため、スレーブでも実行可能です。
スレーブ2(server3)にて実行します。
$ psql

=# SELECT slot_name,active FROM pg_replication_slots;
  slot_name   | active
--------------+--------
 slot_server3 | f
(1 row)

=# SELECT pg_drop_replication_slot('slot_server3');
pg_create_physical_replication_slot
-------------------------------------

(1 row)

=# SELECT slot_name FROM pg_replication_slots;
(0 rows)

これにて、以下の構成に復旧しました。

  • レプリケーションスロット使用
  • server1(マスタ) =同期=> server2(スレーブ1) =非同期=> server3(スレーブ2) のカスケードレプリケーション構成

5.4.4.8. スイッチオーバ

スイッチオーバについて記載します。

5.4.4.9. 同期スレーブへのスイッチオーバ

■スイッチオーバ手順

計画停止におけるマスタ/スレーブの切り替え手順です。 pg_basebackupやpg_rewindが不要であるためシンプルな手順です。

計画停止にてマスタとスレーブ1の役割を切り替えたレプリケーション構成図
_images/SwitchOver_cascade1.png
(1) マスタ(server1)の正常停止
マスタ(server1)にて実行します。
$ pg_ctl stop -m fast
(2) スレーブ(server2)の昇格
スレーブ(server2)にて実行します。

以下の場合にはpg_rewind不要です。
  • 計画停止
  • 同期レプリケーションのインスタンス障害(物理障害なし)
$ pg_ctl promote
(3) レプリケーションスロットの作成
新マスタ(server2)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server2',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server2,0/B40000C8)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server2
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/B40000C8     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(4) 旧マスタ(server1)のrecovery.confの作成
以下を記載します。
$ vi $PGDATA/recovery.conf

standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 application_name=server1 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'
recovery_target_timeline = 'latest'
(5) 旧マスタ(server1)のpostgresql.confの修正
旧マスタ(server1)にて、パラメータの調整を行います。
  • synchronous_commitパラメータの無効化
  • synchronous_standby_namesパラメータの無効化
  • pg_statsinfoの無効化(有効化されている場合)
同期レプリケーションの設定を無効化します。
新スレーブのshared_preload_librariesパラメータに pg_statsinfoが設定されている場合は、書き込みができずにエラーが発生します。
pg_statsinfoの指定を削除します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = on
synchronous_standby_names = '*'
shared_preload_libraries = 'pg_stat_statements,pg_statsinfo'

[編集後]
synchronous_commit = off
synchronous_standby_names = ''
shared_preload_libraries = ''

再起動
$ pg_ctl restart
(6) レプリケーションスロットの作成
新スレーブ(server1)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server1',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,0/B4000028)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/B4000028     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(7) 新マスタ(server2)のpostgresql.confの修正
新マスタ(server2)にて実行します。
  • synchronous_commitパラメータの有効化
  • synchronous_standby_namesパラメータの有効化
同期レプリケーションの設定を有効化します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = off
synchronous_standby_names = ''

[編集後]
synchronous_commit = on
synchronous_standby_names = 'server1'

設定を反映
$ pg_ctl reload
(8) スレーブ2(server3)にてrecovery.conf修正
スレーブ2(server3)にて実行します。
  • primary_conninfoパラメータの「host=」部分を修正
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=12079 sslmode=disable sslcompression=1 target_session_attrs=any'

[編集後]
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=12079 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'
recovery_target_timeline = 'latest'

再起動
$ pg_ctl restart
(9) 新マスタ(server2)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 12505
usesysid         | 16384
usename          | repuser
application_name | server1
client_addr      | <server1IP>
client_hostname  |
client_port      | 58195
backend_start    | 2018-02-07 21:55:00.722782+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                --- 同期

 =# SELECT slot_name,active FROM pg_replication_slots;
 -[ RECORD 1 ]-------+-------------
 slot_name           | slot_server2
 active              | t                --- アクティブ
(10) 新スレーブ(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 86651
usesysid         | 16384
usename          | repuser
application_name | server3
client_addr      | <server3IP>
client_hostname  |
client_port      | 18206
backend_start    | 2018-02-07 22:13:40.199786+09
backend_xmin     | 606
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期

 =# SELECT slot_name,active FROM pg_replication_slots;
 -[ RECORD 1 ]-----------
 slot_name | slot_server1
 active    | f

以上でスイッチオーバが完了しました。

5.4.4.10. 非同期スレーブへのスイッチオーバ

■スイッチオーバ手順

計画停止におけるマスタ/スレーブの切り替え手順です。 pg_basebackupやpg_rewindが不要であるためシンプルな手順です。

計画停止にてマスタとスレーブ2の役割を切り替えたレプリケーション構成図
_images/SwitchOver_cascade2.png
(1) マスタ(server1)の正常停止
マスタ(server1)にて実行します。
$ pg_ctl stop -m fast
(2) スレーブ(server3)の昇格
スレーブ(server3)にて実行します。

以下の場合にはpg_rewind不要です。
  • 計画停止
  • レプリケーションのインスタンス障害(物理障害なし)
$ pg_ctl promote
(3) レプリケーションスロットの作成
新マスタ(server3)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server3',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server3,0/BA0000C8)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server3
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/BA0000C8     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(4) 旧マスタ(server1)のrecovery.confの作成
以下を記載します。
$ vi $PGDATA/recovery.conf

standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server3 port=5432 application_name=server1 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server3'
recovery_target_timeline = 'latest'
(5) 旧マスタ(server1)のpostgresql.confの修正
旧マスタ(server1)にて、パラメータの調整を行います。
  • synchronous_commitパラメータの無効化
  • synchronous_standby_namesパラメータの無効化
  • pg_statsinfoの無効化(有効化されている場合)
同期レプリケーションの設定を無効化します。
新スレーブのshared_preload_librariesパラメータに pg_statsinfoが設定されている場合は、書き込みができずにエラーが発生します。
pg_statsinfoの指定を削除します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = on
synchronous_standby_names = '*'
shared_preload_libraries = 'pg_stat_statements,pg_statsinfo'

[編集後]
synchronous_commit = off
synchronous_standby_names = ''
shared_preload_libraries = ''
(6) 新スレーブ1(server1)の起動
新スレーブ1(server1)を起動します。
$ pg_ctl start
(7) レプリケーションスロットの作成
新スレーブ1(server1)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server1',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,0/BA000028)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/BA000028     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(8) スレーブ2(server2)にてrecovery.conf修正
スレーブ2(server2)にて実行します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 application_name=server1 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'
recovery_target_timeline = 'latest'

再起動
$ pg_ctl restart
(9) 新マスタ(server3)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 12505
usesysid         | 16384
usename          | repuser
application_name | server1
client_addr      | <server1IP>
client_hostname  |
client_port      | 58195
backend_start    | 2018-02-07 21:55:00.722782+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | async                --- 非同期

 =# SELECT slot_name,active FROM pg_replication_slots;
 -[ RECORD 1 ]-------+-------------
 slot_name           | slot_server3
 active              | t                --- アクティブ
(10) 新スレーブ1(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 86651
usesysid         | 16384
usename          | repuser
application_name | server2
client_addr      | <server2IP>
client_hostname  |
client_port      | 18206
backend_start    | 2018-02-07 22:13:40.199786+09
backend_xmin     | 606
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期

 =# SELECT slot_name,active FROM pg_replication_slots;
 -[ RECORD 1 ]-------+-------------
 slot_name           | slot_server1
 active              | t                --- アクティブ

以上でスイッチオーバが完了しました。

5.4.4.11. スイッチバック

スイッチバックについて記載します。

5.4.4.12. 同期モードスレーブからマスタへのスイッチバック

同期モードスレーブになっていた旧マスタをマスタに復帰させるスイッチバックついて記載します。

同期モードスレーブになっていた旧マスタをマスタとして復帰させたレプリケーション構成図

_images/SwitchBack_cascade1.png

■スイッチバック手順

(1) マスタ(server2)の停止
$ pg_ctl stop
(2) スレーブ1(server1)の昇格
スレーブ1(server1)にて実行します。
$ pg_ctl promote
(3) 旧マスタ(server2)のrecovery.confの作成
以下を記載します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ cp $PGDATA/recovery.done $PGDATA/recovery.conf
$ vi $PGDATA/recovery.conf

standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 application_name=server2 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'
recovery_target_timeline = 'latest'
(4) 新マスタ(server1)のpostgresql.confの修正
新マスタ(server1)にて実行します。
  • synchronous_commitパラメータの有効化
  • synchronous_standby_namesパラメータの有効化
同期レプリケーションの設定を有効化します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = off
synchronous_standby_names = ''

[編集後]
synchronous_commit = on
synchronous_standby_names = 'server2'

設定を反映
$ pg_ctl reload
(5) 新スレーブ1(server2)の起動
$ pg_ctl start
(6) スレーブ2(server3)のrecovery.confの作成
以下を記載します。
  • primary_conninfoパラメータの「host=」部分を修正
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ修正
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'
recovery_target_timeline = 'latest'
(7) スレーブ2(server3)の再起動
$ pg_ctl restart
(8) 新マスタ(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 12505
usesysid         | 16384
usename          | repuser
application_name | server2
client_addr      | <server2IP>
client_hostname  |
client_port      | 58195
backend_start    | 2018-02-07 21:55:00.722782+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                --- 同期
(9) スレーブ1(server2)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 86651
usesysid         | 16384
usename          | repuser
application_name | server3
client_addr      | <serrver3IP>
client_hostname  |
client_port      | 18206
backend_start    | 2018-02-07 22:13:40.199786+09
backend_xmin     | 606
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期

以上でスイッチバックが完了しました。

5.4.4.13. 非同期モードスレーブからマスタへのスイッチバック

非同期モードスレーブからマスタへスイッチバックついて記載します。

非同期モードスレーブになっていた旧マスタをマスタとして復帰させたレプリケーション構成図

_images/SwitchBack_cascade2.png

■スイッチバック手順

(1) マスタ(server3)の停止
$ pg_ctl stop
(2) スレーブ1(server1)の昇格
スレーブ1(server1)にて実行します。
$ pg_ctl promote
(3) レプリケーションスロットの作成
新スレーブ1(server2)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server2',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server2,0/BB0000C8)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server2
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/BB0000C8     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(4) 旧マスタ(server3)のrecovery.confの作成
以下を記載します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ cp $PGDATA/recovery.done $PGDATA/recovery.conf
$ vi $PGDATA/recovery.conf

standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'
recovery_target_timeline = 'latest'

再起動
$ pg_ctl restart
(5) 新マスタ(server1)のpostgresql.confの修正
新マスタ(server1)にて実行します。
  • synchronous_commitパラメータの有効化
  • synchronous_standby_namesパラメータの有効化
同期レプリケーションの設定を有効化します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = off
synchronous_standby_names = ''

[編集後]
synchronous_commit = on
synchronous_standby_names = 'server2'

$ pg_ctl reload
(6) 新マスタ(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 12505
usesysid         | 16384
usename          | repuser
application_name | server2
client_addr      | <server2IP>
client_hostname  |
client_port      | 58195
backend_start    | 2018-02-07 21:55:00.722782+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                --- 同期

=# SELECT slot_name,active FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
active              | t                --- アクティブ
(7) スレーブ1(server2)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 86651
usesysid         | 16384
usename          | repuser
application_name | server3
client_addr      | <server3IP>
client_hostname  |
client_port      | 18206
backend_start    | 2018-02-07 22:13:40.199786+09
backend_xmin     | 606
state            | streaming           --- ストリーミング中
sent_lsn         | 0/6A000818
write_lsn        | 0/6A000818
flush_lsn        | 0/6A000818
replay_lsn       | 0/6A000818
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期

=# SELECT slot_name,active FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server2
active              | t                --- アクティブ
(8) レプリケーションスロットの削除(スレーブ)
フェイルオーバとの違いとしては、新スレーブに旧マスタ時代のスロットが残る事があります。
restart_lsn列に値が残っている状態では、マスタのVACUUM処理を阻害するなどの
悪影響の可能性があるため、削除します。
スロットの削除は関数で行うため、スレーブでも実行可能です。
スレーブ2(server3)にて実施します。
$ psql

=# SELECT slot_name FROM pg_replication_slots;
  slot_name
--------------
 slot_server3
(1 row)

=# SELECT pg_drop_replication_slot('slot_server3');
pg_create_physical_replication_slot
-------------------------------------

(1 row)

=# SELECT slot_name FROM pg_replication_slots;
(0 rows)

以上でスイッチバックが完了しました。

5.4.4.14. pg_rewindを使用したスイッチバック

pg_rewindを使用したスイッチバックについて記載します。

pg_rewindはタイムラインのずれたレプリケーションを再同期させる機能です。 実行後、ターゲットクラスタはソースクラスタと置き換えられた状態になります。 そのためpg_rewind後の操作は、通常のスイッチオーバ時と同じです。 タイムラインの分岐点からソースクラスタのWALを適用するため、更新量が少なければpg_basebackによる複製より高速です。 これによりスイッチオーバ時、旧マスタを容易に新スレーブとして起動させることができます。

スイッチオーバー後、pg_rewindで旧マスタを新スレーブとして戻したレプリケーション構成図

_images/pg_rewind_cascade1.png _images/pg_rewind_cascade2.png _images/pg_rewind_cascade3.png

■関連パラメータ

pg_rewindに必要な設定を記載します。

表 5.30 pg_rewindに必要な設定
サーバ 設定ファイル パラメータ 設定値 内容
pg_rewind実行サーバ postgresql.conf full_page_writes on チェックポイント後の更新時、ディスクページの全内容をWALに書き込む。
pg_rewind実行サーバ postgresql.conf wal_log_hints on ヒントビット更新時もfull_page_writesを実行する。

■pg_rewindコマンド

pg_rewindコマンドの主なオプションは次の通りです。

表 5.31 pg_rewindのオプション
オプション 内容
D <ターゲットクラスタ> pg_rewindを実行し、ソースクラスタの内容に置き換えるクラスタを指定する。
source-server="<ソースクラスタ>"

同期対象であるソースクラスタを指定します。主に次の接続文字列を使用します。

  • host:ソースクラスタのホスト名またはIPアドレス
  • port:ソースクラスタのポート番号
  • dbname:ソースクラスタの接続先データベース名
  • user:ソースクラスタの接続先ユーザ
P 進行状況をレポートとして表示する。

■フェイルバック手順

※事前にマスタで関連パラメータの設定がされていることを前提とします。
(1) マスタの正常停止
pg_rewindを使用するには正常停止する必要があります。
停止した旧マスタを一旦起動した後、正常停止させます。
障害により正常に起動や停止ができない状態ではpg_rewindは使用できません。
その場合は、pg_basebackupを使用します。
$ pg_ctl stop -m fast
(2) スレーブ(server2)の昇格
スレーブ(server2)にて実行します。
  • 計画停止
  • 同期レプリケーションのインスタンス障害(物理障害なし)
$ pg_ctl promote
(3) 旧マスタ(server1)のpostgresql.confの修正
旧マスタ(server1)にて、パラメータの調整を行います。
  • synchronous_commitパラメータの無効化
  • synchronous_standby_namesパラメータの無効化
  • pg_statsinfoの無効化(有効化されている場合)
同期レプリケーションの設定を無効化します。
新スレーブのshared_preload_librariesパラメータに pg_statsinfoが設定されている場合は、書き込みができずにエラーが発生します。
pg_statsinfoの指定を削除します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = on
synchronous_standby_names = '*'
shared_preload_libraries = 'pg_stat_statements,pg_statsinfo'

[編集後]
synchronous_commit = off
synchronous_standby_names = ''
shared_preload_libraries = ''
(4) スプリットブレイン状態状態を作るため、旧マスタ(server1)を再度起動し、
データを更新する
$ pg_ctl start
$ psql
=# select * from test;
 col1
------
    1
    3
    2
(3 rows)

=# insert into test values(4);
INSERT 0 1
=# select * from test;
 col1
------
    1
    3
    2
    4
(4 rows)
(5) pg_rewindの実行のため、旧マスタ(server1)を正常停止する。
$ pg_ctl stop -m fast
(6) 旧マスタ(server1)にてpg_rewindの実行
$ pg_rewind -D $PGDATA --source-server="host=server2 port=5432"
servers diverged at WAL location 0/D0000098 on timeline 45
rewinding from last common checkpoint at 0/D0000028 on timeline 45
Done!
(7) レプリケーションスロットの作成
新マスタ(server2)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server2',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server2,0/D00000C8)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server2
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/D00000C8     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(8) recovery.confの作成
旧マスタ(server1)にて実行します。
  • 以下を設定します。
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ cp $PGDATA/recovery.done $PGDATA/recovery.conf
$ vi $PGDATA/recovery.conf

standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 application_name=server1 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'
recovery_target_timeline = 'latest'
(9) 新スレーブ(server1)の起動
新スレーブ(server1)を起動します。
$ pg_ctl start
(10) 新スレーブ(server1)にてデータ確認
pg_rewindにより、データが正常に巻き戻されていることを確認します。
$ psql

=# select * from test;
 col1
------
    1
    3
    2
(3 rows)
(11) 新マスタ(server2)のpostgresql.confの修正
新マスタ(server2)にて実行します。
  • synchronous_commitパラメータの有効化
  • synchronous_standby_namesパラメータの有効化
同期レプリケーションの設定を有効化します。
$ vi $PGDATA/postgresql.conf

[編集前]
synchronous_commit = off
synchronous_standby_names = ''

[編集後]
synchronous_commit = on
synchronous_standby_names = 'server1'

設定を反映
$ pg_ctl reload
(12) レプリケーションスロットの作成
新スレーブ1(server1)にて実行します。
$ psql

=# SELECT pg_create_physical_replication_slot('slot_server1',true);    -- 第2パラメータにtrueを指定
pg_create_physical_replication_slot
-------------------------------------
(slot_server1,0/D0000028)
(1 行)

=# \x on
拡張表示は on です。

=# SELECT * FROM pg_replication_slots;
-[ RECORD 1 ]-------+-------------
slot_name           | slot_server1
plugin              |
slot_type           | physical
datoid              |
database            |
temporary           | f
active              | f              -- まだスロットは使用されてないため false
active_pid          |
xmin                |
catalog_xmin        |
restart_lsn         | 0/D0000028     -- trueの指定により、作成直後からrestat_lsnを認識
confirmed_flush_lsn |
(13) スレーブ2(server3)にてrecovery.conf修正
スレーブ2(server3)にて実行します。
  • primary_conninfoパラメータの「host=」部分を修正
  • primary_conninfoパラメータにapplication_nameを追加(任意/デフォルトはwalreceiver)
  • primary_slot_nameパラメータ追加
  • recovery_target_timelineパラメータ追加
$ vi $PGDATA/recovery.conf

[編集前]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=5432 sslmode=disable sslcompression=1 target_session_attrs=any'

[編集後]
standby_mode = 'on'
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=5432 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'
recovery_target_timeline = 'latest'

再起動
$ pg_ctl restart
(14) 新マスタ(server2)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 82328
usesysid         | 16384
usename          | repuser
application_name | server1
client_addr      | 172.16.25.111
client_hostname  |
client_port      | 63206
backend_start    | 2018-02-28 22:40:16.337761+09
backend_xmin     |
state            | streaming           --- ストリーミング中
sent_lsn         | 0/D0019538
write_lsn        | 0/D0019538
flush_lsn        | 0/D0019538
replay_lsn       | 0/D0019538
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                --- 同期
(15) 新スレーブ(server1)でのレプリケーション確認

pg_stat_replicationsビューを参照して、レプリケーション構成である事を確認します。
pg_replication_slotsビューを参照して、スロットがアクティブである事を確認します。
$ psql

=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 51369
usesysid         | 16384
usename          | repuser
application_name | server3
client_addr      | 172.16.25.143
client_hostname  |
client_port      | 47281
backend_start    | 2018-02-28 23:02:39.56715+09
backend_xmin     |
sent_lsn         | 0/D0019538
write_lsn        | 0/D0019538
flush_lsn        | 0/D0019538
replay_lsn       | 0/D0019538
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async               --- 非同期
(16) マスタ、スレーブとタイムラインIDが揃っていることを確認する。
タイムラインIDの取得には、pg_controldataコマンドを使用する。
厳密には最新チェックポイント実行時のタイムラインIDであるため、
タイムラインIDが揃っていないときは、マスタにてチェックポイント実行後、再確認します。
[マスタ(server2)]
$ export LANG=C
$ pg_controldata | grep " TimeLineID"
Latest checkpoint's TimeLineID:      46

[スレーブ1(server1)]
$ export LANG=C
$ pg_controldata | grep " TimeLineID"
Latest checkpoint's TimeLineID:      46

[スレーブ2(server3)]
$ export LANG=C
$ pg_controldata | grep " TimeLineID"
Latest checkpoint's TimeLineID:      46

■pg_rewind使用時の注意点

  • 正常停止が必要

    pg_rewindを実行するデータベースクラスタは正常終了しなければいけません。
    物理障害等により正常停止できない場合、pg_rewindは使用できません。pg_basebackupを使用します。
  • 同一タイムラインの場合は実施不可

    pg_promoteを実行せずに旧スレーブを新マスタにした場合、新マスタのタイムラインIDは変わらないため、
    新マスタと旧マスタのタイムラインIDは同じ状態です。
    この場合は、pg_rewindは実行できません。
  • 実行時期と所要時間の関係

    pg_rewindによるフェイルバックの所要時間は2つの要素から構成されます。

    Step1. pg_rewindによる巻き戻し (旧マスタのWALを使用)
    Step2. WAL適用による追い付き (新マスタのWALを使用)

    新マスタで大量更新がある場合は、Step1は短時間で終了してもStep2で時間がかかります。
    結果として、pg_basebackupの方が効率が良い場合もあり得ます。
    また新マスタ昇格時のWALが削除されている場合は、後述するようにStep2でエラーとなる可能性もあります。その場合はpg_basebackupが必要となります。
    pg_rewindはフェイルオーバー後、あまり時間を置かずに実行する事がポイントです。
  • 旧マスタのWAL保管

    旧マスタの巻き戻しに必要な旧マスタのWALが削除されている場合、pg_rewindは失敗します。
    例えば旧マスタが障害により大量更新の途中で異常終了した場合などに発生します。
    pg_rewind実行時に次のようなエラーが発生します。
    could not open file "/home/pg96/pg96_data/pg_xlog/0000000D00000002000000CF": No such file or directory
    could not find previous WAL record at 2/CF000140
    Failure, exiting
    
    pg_rewindが成功するかどうかは検証(dry-runオプション)にて事前に確認する事ができます。
$ pg_rewind -D $PGDATA --source-server="host=server2 port=5432" --dry-run
servers diverged at WAL position 0/5015B70 on timeline 1
rewinding from last common checkpoint at 0/5015B00 on timeline 1
Done!

| メッセージはdry-runオプションが無い場合と同じです。
| pg_rewindでエラーが発生する場合(スレーブのWAL削除)は、この検証にて確認できますが、
| pg_rewindでエラーが発生しないで、後から発生する場合(マスタのWAL削除)は検知できません。
| 検証の仕様について認識下さい。
  • 新マスタのWAL保管

    pg_rewind後、新スレーブは新マスタのWALを適用することで、新マスタと同期します。
    新マスタに昇格時のWALファイルが残っていない場合、新スレーブは追い付きができず、次のエラーがサーバログに出力され続けます。
    対策としてレプリケーションスロットの有効化が有力ですが、新マスタのWAL領域の枯渇にご注意下さい。
    ERROR:  requested WAL segment 0000000D00000000000000F3 has already been removed
    
  • タイムラインの巻き戻し

    pg_rewindはPostgreSQL9.6からタイムラインの巻き戻しができるよになっています。
    これによりスプリットブレインが発生しても、新マスタをスレーブに戻すことが可能です。
  • pg_rewindが不要な場合

    pg_rewindはターゲットとソースクラスタのタイムラインIDが分岐した場合に実行が必要です。
    そのためタイムラインが枝分かれしなかった場合、pg_rewindを実行する必要はありません。
    例えばpg_rewind実行時に次のようなメッセージが出た場合、pg_rewindは実行せずに、以降の操作を継続します。
    servers diverged at WAL position 0/503A428 on timeline 2
    no rewind required
    

5.4.4.15. スレーブ障害による対処

スレーブ障害時の緊急対応の必要性は、非同期モードが同期モードかによって異なります。

非同期モードの場合は、スレーブ障害がマスタの更新処理を阻害しないため、緊急対応は必要ありません。
とはいえ、シングル状態であるため早期にレプリケーション構成に復旧します。

同期モードの場合は、スレーブ障害によりマスタの更新処理が阻害されハング状態となります
$ psql -h server1 -U postgres postgres -c "INSERT INTO test1_t VALUES ( 1 )"
Cancel request sent
WARNING:  canceling wait for synchronous replication due to user request
DETAIL:  ** The transaction has already committed locally, but might not have been replicated to the standby. **
INSERT 0 1
※Ctrl+Cをキーインする等、意図的にキャンセルしない限り、応答が返ってきません。
そのため、直ちに非同期に設定する必要があります。
非同期への切り替え処理は以降に示す様に再起動不要であるため、即時対応が可能です。

非同期への切り替えではなく、スレーブの再起動を試みる方法も考えられます。その方が効率的のようにも考えられます。
即座に起動できればその通りですが、起動に時間がかかる、あるいは物理的な障害で起動できない状態である事も考えられます。
そのような試行錯誤より、まずは確実にマスタのハング状態解消を優先します。

マスタの復旧後は、スレーブの復旧を試みます。
物理障害により起動できない場合は、フェイルバック処理と同様の作業を行います。
_images/slave_failure_cascade1.png _images/slave_failure_cascade2.png
(1) スレーブの疑似障害発生
immediateオプションにて停止、またはpostgresプロセスのkillする事で擬似障害を発生させます。
スレーブ1(server2)にて実行します。
$ pg_ctl -w -m immediate stop
(2) スレーブ1(server2)の死活監視にて異常を検知
死活監視の方法には多数ありますが、ここではpg_isreadyコマンドを使用します。
pg_isreadyコマンドはクライアントツールですので、任意のノードから実行できます。
$ pg_isready -h server2 -U postgres -d postgres
server2:5432 - no response
(3) 非同期モードに切り替え
synchronous_standby_namesパラメータを''に設定する事で、非同期の設定となります。
マスタ(server1)にて実行します。
$ vi $PGDATA/postgresql.conf

[変更前]
synchronous_standby_names = '*'

[変更後]
synchronous_standby_names = ''

$ pg_ctl reload

これでマスタが更新処理が可能な状態に復旧しました。

(4) スレーブ2(server3)を非同期モードで復活
recovery.confファイルを修正します。
  • primary_conninfoパラメータの「host=」部分を修正
  • primary_slot_nameパラメータが設定されている場合は、修正
$ vi $PGDATA/recovery.conf

[変更前]
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server2 port=12079 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server2'

[変更後]
primary_conninfo = 'user=repuser passfile=''/home/p101/.pgpass'' host=server1 port=12079 application_name=server3 sslmode=disable sslcompression=1 target_session_attrs=any'
primary_slot_name = 'slot_server1'

$ pg_ctl restart
(5) スレーブ2(server3)にてデータが伝播されることを確認

同期モードのスレーブも復活させたい場合は、フェイルバックと同様の作業を行います。

5.5. まとめ

5.5.1. SR機能拡張の歴史

PostgreSQL 9.0でSR機能が実装されて以降、メジャーバージョン毎にSR関連の新機能を実装しています。

  • "ミスオペレーション"を"ミスオペ"と略記する場合があります。

  • "レプリケーションスロット"と表記した場合は物理型を指します。論理型の場合は明記します。

  • "レプリケーションスロット"を"スロット"と略記する場合があります。

    表 5.32 SR機能拡張の歴史
    バージョン 分野または目的 概要 関連パラメータ
    9.1
    管理性の向上
    データ保護
    同期モードの実装
    COMMIT時にWALの転送完了までを保証する。
    データ保護とパフォーマンスとのトレードオフ
    [マスタのpostgresql.conf]
    synchronous_standby_names
    9.1
    管理性向上
    昇格処理の明瞭化
    pg_ctl promote コマンドによる昇格
    明確なコマンドとなり分かり易くなった。
    従来はtrigger_fileで設定したパスにtouchコマンド等でファイルを作成する方式であった。

    9.1
    管理性向上
    SR状況把握の簡易化
    pg_stat_replicationビューの追加
    マスタでの、スレーブへのWAL転送および適用の状況把握方法が簡易になった。wal_receiver_status_interval間隔で反映。
    [スレーブのpostgresql.conf]
    wal_receiver_status_interval
    9.1
    管理性向上
    SR状況把握の簡易化
    pg_last_xact_replay_timestamp関数の追加
    スレーブにて最終適用された時間を取得する

    9.2
    データ保護とパフォーマンスの調整
    同期方式にremote_writeの追加
    synchronous_commitの選択肢として、on/off/localに加え、remote_writeが追加された。
    スレーブのメモリに書き込むまでを保証する。その時点でOSがハングした場合、WALは損失する。
    [マスタのpostgresql.conf]
     synchronous_commit
    9.2
    高可用性の向上
    カスケードレプリケーションの実装
    スレーブにぶら下がる2段目のSR構成が可能となった。
    マスタの負荷を限定。

    9.2
    管理性の向上
    SR状況把握の簡易化
    pg_xlog_location_diff関数の追加
    スレーブの転送や適用がどの程度遅れているかを取得するのが容易になった。
    従来は pg_stat_replicationビューを参照していたが、ログの位置をバイト数に換算する計算が必要だった。
    本関数により16進の差分をバイト数として取得できる。

    9.3
    管理性の向上
    役割交換の効率化
    スイッチバックの実装
    historyファイルの転送により実装

    9.3
    管理性の向上
    カスケードレプリケーションの管理性向上
    カスケードのSR構成において、スレーブの新マスタ昇格後に、スレーブとSRの継続が可能に

    9.3
    障害時間の短縮
    昇格処理の高速化
    昇格処理にて、リカバリのみ実行し、チェックポイントを省略する事で時間短縮を実現。
    従来はチェックポイントも昇格処理にて実行していた。

    9.4
    管理性の向上
    ミスオペ対策
    遅延レプリケーションの実装
    スレーブでの適用を一定時間(recovery_min_apply_delay)遅らせる事で、マスタで発生したミスオペの伝搬を防ぐ。以下の事項に注意。
    • 一定時間内にマスタでのミスオペを検知して対応が必要
    • 昇格時には遅延分の適用が必要であるため時間を要する
    [スレーブのrecovery.conf]
     recovery_min_apply_delay
    9.4
    管理性の向上
    WAL保持の保証
    レプリケーションスロットの実装
    スレーブに必要なWALをマスタで保持し続ける事を保証。
    特に複数スレーブ構成にて効果的。以下の事項に注意。
    • マスタのWAL領域が溢れないように監視を検討
    • 不要スロットは削除(残存しているとWALを保持)
    [マスタのpostgresql.conf]
     max_replication_slots
    [スレーブのpostgresql.conf]
     hot_standby_feedback
    9.4
    論理レプリケーション
    論理レプリケーションの関数の実装
    行レベルの変更内容を出力する関数が実装された。
    • pg_logical_slot_get_changes関数
    • pg_logical_slot_peek_changes関数
    必要な設定
    • wal_levelをlogicalに設定
    • 論理レプリケーションスロットを作成
    [マスタのpostgresql.conf]
    wal_level
     max_replication_slot
    9.5
    WAL転送効率の向上
    WAL圧縮機能の実装
    WALを圧縮する事で、転送効率の向上する。
    圧縮/解凍によるオーバーヘッドとのトレードオフだが、通常はメリットの方が大きい。
    SRに特化した機能では無いが、特に次のSR構成での適用が効果的と考えられる。
    • 同期モードまたは完全同期モード
    • スレーブを遠隔地に配置(ディザスタ・リカバリ)
    [マスタのpostgresql.conf]
     wal_compression
    9.5
    管理性の向上
    SR構築の効率化
    pg_rewindコマンドの実装
    昇格した新マスタと旧マスタを再同期する事で、SR構成を実装。物理的な障害でない場合は、pg_rewindにて対応可能な可能性がある。
    新マスタから旧マスタへ差分バックアップを転送および旧マスタのWALを適用。差分バックアップの転送はpg_basebackupと同様であるため、同様の設定が必要。
    [マスタのpostgresql.conf]
     wal_log_hints
     full_page_writes
     max_wal_senders
    [マスタのpg_hba.conf]
     replication疑似データベースとの認証設定
    9.5
    管理性の向上
    レプリケーション関連メッセージの出力
    ログ監視の利便性が向上。ただしあまり多くは出力されない。
    [マスタのpostgresql.conf]
     log_replication_commands
    9.5
    管理性の向上
    継続的アーカイブ
    スレーブでのアーカイブ出力機能
    スレーブにおいて自分のWALを出力する事で、昇格時に途切れること無く継続的なアーカイブが可能。
    [スレーブのpostgresql.conf]
     archive_mode
    9.6
    スケールアウト
    参照負荷分散
    完全同期レプリケーション機能の実装
    スレーブへの適用完了までを保証。
    スレーブ参照時にマスタと同一データが保証される事で、参照負荷分散によるスケールアウトが期待される。
    [マスタのpostgresql.conf]
     synchronous_commit
    9.6
    データ保護の多重化
    複数同期スレーブ構成機能
    従来は複数スレーブの内、同期モードが設定できるのは1台のみであったが、その制限が無くなった。
    同時に複数スレーブに対して同期モードが設定できる。
    [マスタのpostgresql.conf]
    synchronous_standby_names
    9.6
    管理性の向上
    SR状況把握の簡易化
    pg_stat_wal_receiverビューの追加
    従来はマスタにてpg_stat_replicationビューが参照できたが、スレーブにてpg_stat_wal_receiverビューが参照できる。
    スレーブからレプリケーションの状況が把握が容易に。

    9.6
    管理性の向上
    監視手法の多様化
    pg_control_recovery関数の追加
    制御ファイル情報の内、リカバリに関する情報を取得。
    従来はpg_controldataコマンドで制御ファイルの情報を取得したが、SELECT文で取得できる事で管理手法の選択肢が増えた。

    9.6
    管理性の向上
    役割交換の簡易化
    pg_rewindの機能拡張
    タイムラインIDの変更後にも対応できるようになった。
    マスタに適用する事でスレーブに戻す事が可能。

    10.0
    管理性の向上
    一時レプリケーションスロット
    レプリケーションスロットを一時的に作成できるようになった。
    pg_basebackupコマンド実行時のみ使用する場合に有効。
    一時的なスロットかどうかはpg_replication_slotsカタログのtemporary列で判断する。

    10.0
    管理性の向上
    Quorum-based同期レプリケーション
    synchronous_standby_namesにてスレーブのリストに対してANYを指定する事で、同期レプリケーションとなるスレーブが定足数に応じて任意に選択される。
    [マスタのpostgresql.conf]
    synchronous_standby_names
    10.0
    WAL対象の拡充
    ハッシュインデックスのWAL出力
    従来は非推奨だったハッシュインデックスの活用が増える可能性あり。
    SRの機能ではないが、関連事項として掲載。
    [マスタのpostgresql.conf]
    synchronous_standby_names

6. ロジカルレプリケーション

6.1. ロジカルレプリケーションの概要

6.1.1. 概要

PostgreSQL 10.0よりロジカルレプリケーション機能がサポートされました。 ロジカルレプリケーションでは、テーブルデータに対する論理的な変更内容を用いて、サーバ間のデータレプリケーションを実現します。 PostgreSQL 9.0よりサポートされているストリーミングレプリケーションでは、データベースを構成するファイルの変更内容を物理的に複製することで、 サーバ間のデータレプリケーションを実現します。 従来のストリーミングレプリケーションの物理的なレプリケーションと対比する形で、ロジカルレプリケーションと呼称されています。

6.1.2. 特徴

ロジカルレプリケーションは、PostgreSQL 9.0よりサポートされているストリーミングレプリケーションと比較すると以下の特徴を持ちます。

  • 任意のテーブルのみをレプリケーション可能
  • 特定の処理(例えばINSERTのみ)のみをレプリケーション可能
  • 異なるPostgreSQLバージョン間でのレプリケーション可能
  • レプリケーション先でテーブルデータの更新やインデックス定義が可能

ただし、レプリケーション先のテーブルデータを更新した場合や、特定の処理のみをレプリケーションする場合は、 レプリケーション元とレプリケーション先でデータが異なる状態になります。 上記のような場合にデータの整合性はユーザが保証する必要があります。

6.1.3. アーキテクチャ

ロジカルレプリケーションでは、Publisher(パブリッシャ)/Subscriber(サブスクライバ)モデルを採用しており、 レプリケーション元のサーバはPublisher、レプリケーション先のサーバはSubscriberと呼称されます。 Publisher上には、レプリケーション対象とするテーブルの論理集合であるPublication(パブリケーション)を定義し、 Subscriber上には、レプリケーション対象するパブリケーションとその接続情報であるSubscription(サブスクリプション)を定義します。 ロジカルレプリケーションにおける各用語の説明を下表に記載します。

表 6.1 用語説明
No. 用語 説明
1 Publisher(パブリッシャ) レプリケーション元となるサーバ
2 Subscriber(サブスクライバ)
レプリケーション先となるサーバ
3 Publication(パブリケーション) レプリケーション対象とするテーブルの定義。CREATE PUBLICATIONコマンドで作成。
4 Subscription(サブスクリプション) レプリケーション対象するパブリケーションとその接続定義。CREATE SUBSCRIPTIONコマンドで作成。
5 Replication Slot(レプリケーションスロット)
レプリケーションの状態を保持するオブジェクト。Subscription作成時に、Publisher上に作成される。
デフォルトでは作成されるReplication Slotの名前は、Subscriptionの名前と同じに設定される。

ロジカルレプリケーションは、下記の2段階でレプリケーションが実施されます。

  1. 初期データのスナップショット取得
  2. データ変更内容の送受信によるデータ同期

動作は下図のイメージとなります。

ロジカルレプリケーション概要図

PublisherからSubscriberへの「2. データ変更内容の送受信によるデータ同期」は下図の処理で実施されます。

ロジカルレプリケーション動作
①. PublisherのディスクにWALが書き込まれる
②. Publisherのwalsenderプロセスが更新内容を論理的な変更内容(論理変更)に変換
③. Subscriberのlogical replication workerプロセスに論理変更を送信
④. Subscriberのlogical replication workerプロセスは論理変更をテーブルに適用

6.1.4. ユースケース

ロジカルレプリケーションは以下のようなケースで有用と考えます。

  1. 複数のDBサーバ上のデータを1台のDBサーバに統合したいケース(データ分析用途など)
  2. 任意のテーブルデータのみをレプリケーションさせたいケース(システム間のデータの連携など)
  3. 異なるバージョン間でレプリケーションさせたいケース(バージョンアップ時のデータ移行など)

ロジカルレプリケーションの典型的な利用例がPostgreSQLの文書 [1] に記載されていますので、ご参照下さい。

[1]PostgreSQL 10.0文書 - 第31章 論理レプリケーション

6.1.5. 制限事項

ロジカルレプリケーションの制限事項は以下の通りです。(PostgreSQL 10時点)

表 6.2 ロジカルレプリケーション制限事項
No. 制限事項 補足
1 データベーススキーマおよびDDLコマンドはレプリケーションされない データベーススキーマは、pg_dump --schema-onlyを利用して移行可能
2 シーケンスはレプリケーションされない
シーケンスはレプリケーションされないが、
シーケンスによって裏付けされたSERIAL型や識別列のデータは、テーブルデータの一部としてレプリケーション可能
3 TRUNCATEコマンドはレプリケーションされない DELETEコマンドで回避することは可能
4 ラージオブジェクトはレプリケーションされない 通常のテーブルにデータを格納する以外の回避方法なし
5 テーブル以外のオブジェクトはレプリケーションできない ビュー、マテリアライズドビュー、パーティションのルートテーブル(親テーブル)、 外部テーブルはレプリケーションしようとするとエラーになる

制限事項の詳細は、PostgreSQLの文書 [2] に記載されていますので、ご参照下さい。

[2]PostgreSQL 10.0文書 - 第31章 論理レプリケーション 31.4. 制限事項

6.2. ロジカルレプリケーションの設定

ロジカルレプリケーションの設定手順について確認します。
ロジカルレプリケーションでは、レプリケーション対象とするテーブルを以下から選択することが可能です。
  1. データベース内の全てのテーブルをレプリケーション(本書では「データベース単位の設定」と呼称)
  2. データベースの任意のテーブルのみをレプリケーション(本書では「テーブル単位の設定」と呼称)
レプリケーション対象とするテーブルは、Publication作成時に指定します。
Publicationの作成コマンドであるCREATE PUBLICATIONコマンドの構文は以下の通りです。
CREATE PUBLICATION name
    [ FOR TABLE [ ONLY ] table_name [ * ] [, ...]
      | FOR ALL TABLES ]
    [ WITH ( publication_parameter [= value] [, ... ] ) ]
CREATE PUBLICATIONコマンド実行時に「FOR ALL TABLES」を指定した場合は、
データベース内の全てのテーブルがレプリケーション対象になり、
「FOR TABLE」では、レプリケーション対象とするテーブルを指定します。

「CREATE PUBLICATION」コマンドの仕様は、PostgreSQLの文書 [1] に記載されていますので、ご参照下さい。

[1]PostgreSQL 10.0文書 - SQLコマンド CREATE PUBLICATION

■ 環境情報
手順は以下環境を前提に記載しております。

表 6.3 環境情報
項目 説明
PostgreSQLバージョン
10.1
OSバージョン
CentOS 7.4
サーバ構成(各ホスト名) 2サーバ(node1,node2)

6.2.1. データベース単位の設定

6.2.1.1. 検証目的

データベース内の全てのテーブルをレプリケーションする際の設定手順について確認します。

6.2.1.2. 検証内容

本検証では、下記のレプリケーション設定においてデータベース単位での、
柔軟なデータ連携(データレプリケーション)が実現可能かの検証を実施しました。

6.2.1.3. 検証環境

ロジカルレプリケーション(データベース単位)

6.2.1.4. 検証手順

■ 事前準備

ロジカルレプリケーションの環境を設定するためnode1およびnode2において、
PostgreSQL10のインストールやデータベースクラスタ初期化・設定を実施します。
  1. 環境準備

該当サーバにsshにて接続します。

(node1,node2のサーバにおいて実施)
ssh接続を用いて、
該当環境へ接続
ユーザ: root
パスワード: xxxxxxx
  1. ホスト名の設定

名前解決のために各サーバのホスト名を設定します。

(node1,node2のサーバにおいて実施)
# vi /etc/hosts
[下記をファイル末尾に追加]
192.168.56.101 node1
192.168.56.102 node2
  1. 5432ポートの解放

PostgreSQL間の通信のため、5432ポートを解放します。

(node1,node2のサーバにおいて実施)
# firewall-cmd --permanent --add-port=5432/tcp
# firewall-cmd --reload

以下のコマンド5432でポートが解放されていることを確認します。

(node1,node2のサーバにおいて実施)
# firewall-cmd --list-ports
5432/tcp
  1. PostgreSQL10のyumレポジトリ追加

PostgreSQL10のyumレポジトリ用のパッケージをインストールします。

(node1,node2のサーバにおいて実施)
# yum install -y https://download.postgresql.org/pub/repos/yum/10/redhat/rhel-7-x86_64/pgdg-centos10-10-1.noarch.rpm

以下のコマンドで該当のパッケージがインストールされたことを確認します。

(node1,node2のサーバにおいて実施)
# yum list installed pgdg-centos10.noarch
[省略]
インストール済みパッケージ
pgdg-centos10.noarch          10-1          installed
  1. PostgreSQLのインストール

yumコマンドを用いて、PostgreSQLをインストールします。

(node1,node2のサーバにおいて実施)
#  yum install postgresql10 postgresql10-server postgresql10-libs postgresql10-contrib

以下のコマンドでパッケージをインストールされたことを確認します。

(node1,node2のサーバにおいて実施)
# yum list installed postgresql10 postgresql10-server postgresql10-libs postgresql10-contrib
[省略]
インストール済みパッケージ
postgresql10.x86_64                              10.1-1PGDG.rhel7          @pgdg10-updates-testing
postgresql10-contrib.x86_64                      10.1-1PGDG.rhel7          @pgdg10-updates-testing
postgresql10-libs.x86_64                         10.1-1PGDG.rhel7          @pgdg10-updates-testing
postgresql10-server.x86_64                       10.1-1PGDG.rhel7          @pgdg10-updates-testing
  1. 環境変数の設定

postgresユーザの環境変数の設定を行います。

(node1,node2のサーバにおいて実施)
# su - postgres
$ vi ~/.bash_profile
[下記をファイル末尾に追加]
export PATH=${PATH}:/usr/pgsql-10/bin
export PGHOME=/usr/pgsql-10
export PGDATA=/var/lib/pgsql/10/data
export PGDATABASE=postgres
export PGPORT=5432
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/usr/pgsql-10/lib

以下のコマンドで環境変数の設定を確認します。($PGDATAの確認を例示しています。)

(node1,node2のサーバにおいて実施)
$ exit
ログアウト
# su - postgres
$ echo $PATH
/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/usr/pgsql-10/bin
$ echo $PGDATA
/var/lib/pgsql/10/data
  1. データベースクラスタの作成

データベースクラスタの作成を実施します。

(node1,node2のサーバにおいて実施)
$ initdb -D $PGDATA -A trust -U postgres --no-locale --encoding UTF8
  1. postgresql.conf修正

postgresql.confに以下を設定します。

(node1,node2のサーバにおいて実施)
$ vi $PGDATA/postgresql.conf
[下記の修正を加える]
listen_addresses = '*'             # 全てのネットワークインターフェースへの接続を受け付ける設定
wal_level = 'logical'               # ロジカルレプリケーションに必要なWAL出力設定
max_wal_senders = 10                # wal senderプロセス数の最大数を10に設定(デフォルト値)
max_replication_slots = 10          # レプリケーションスロットの最大数を10に設定(デフォルト値)
log_line_prefix = '[%m][%d][%h][%u][%e][%p] ' # ログメッセージの解析時に必要な情報を付与
  1. pg_hba.conf

node1,node2間でのデータベース接続を許可します。

(node1,node2のサーバにおいて実施)
$ vi $PGDATA/pg_hba.conf
[下記の修正を加える]
host    all             all          192.168.56.0/24            trust
※ 上記の設定は、SubscriberがPublisherのデータベースに接続する際に利用されます。
   そのため、Publisherにのみ上記設定が必要となります。
  1. PostgreSQL起動

PostgreSQLを起動します。

(node1,node2のサーバにおいて実施)
$ pg_ctl start

以下のコマンドでPostgreSQLの起動を確認します。

(node1,node2のサーバにおいて実施)
$ pg_ctl status
pg_ctl: サーバが動作中です(PID: 3517)
/usr/pgsql-10/bin/postgres

■ ロジカルレプリケーションの動作確認(データベース単位)

  1. 動作確認用のデータベース作成

本検証で利用するデータベースを作成します。

(node1,node2のサーバにおいて実施)
$ createdb logicalreptest

以下のコマンドでデータベースが作成されたことを確認します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres -l
                               List of databases
      Name      |  Owner   | Encoding | Collate | Ctype |   Access privileges
----------------+----------+----------+---------+-------+-----------------------
 logicalreptest | postgres | UTF8     | C       | C     |
[省略]
  1. 動作確認用のテーブル作成

作成したデータベースにテスト用のテーブルを作成します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE TABLE t1logical (c1 INT, PRIMARY KEY (c1));
=# CREATE TABLE t2logical (c1 INT, PRIMARY KEY (c1));

以下のコマンドで作成したテーブルを確認します。

=# \d
           List of relations
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+----------
 public | t1logical | table | postgres
 public | t2logical | table | postgres
(2 rows)
  1. Publication作成

node1上にデータベース内の全テーブル(作成したlogicalとt2logicalテーブル)をレプリケーション対象とするPublicationを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE PUBLICATION pub_node1_logicaldb FOR ALL TABLES;
以下のコマンドでPublicationが作成されたことを確認します。
作成したpublicationではデータベース内の全テーブルをレプリケーション対象としているため、
All tables列がtrueであることを確認します。
(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node1_logicaldb | postgres | t          | t       | t       | t
(1 row)
  1. Subscription作成

node2上にnode1上に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node2_logicaldb CONNECTION 'host=node1 dbname=logicalreptest port=5432 user=postgres' PUBLICATION pub_node1_logicaldb;

以下のコマンドでSubscriptionが作成されたことを確認します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node2_logicaldb | postgres | t       | {pub_node1_logicaldb}
(1 row)
  1. ロジカルレプリケーション簡易動作検証

テスト用に作成した2つのテーブルにレコードをそれぞれINSERTします。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# INSERT INTO t1logical VALUES (1);
=# INSERT INTO t2logical VALUES (2);
=# SELECT * FROM t1logical;
 c1
----
1
(1 rows)
=# SELECT * FROM t2logical;
 c1
----
2
(1 rows)

Subscription側のテーブルにレコードがINSERTされていることを確認します。(レプリケーションされていることを確認)

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t1logical;
 c1
----
1   <-- node1と同じ結果がレプリケーションされていること
(1 rows)
----
2   <-- node1と同じ結果がレプリケーションされていること
(1 rows)

6.2.2. テーブル単位の設定

6.2.2.1. 検証目的

データベース内の任意のテーブルのみをレプリケーションする際の設定手順について確認します。

6.2.2.2. 検証内容

本検証では、下記のレプリケーション設定においてテーブル単位での
柔軟なデータ連携(データレプリケーション)が実現可能かの検証を実施しました。

6.2.2.3. 検証環境

ロジカルレプリケーション(テーブル単位)

6.2.2.4. 検証手順

データベース単位でレプリケーションの構築手順について確認します。

■ ロジカルレプリケーションの動作確認(テーブル単位)

  1. 動作確認用のデータベース作成

本検証で利用するデータベースを作成します。

(node1,node2のサーバにおいて実施)
$ createdb logicalreptest2

以下のコマンドでデータベースが作成されたことを確認します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres -l
                                List of databases
      Name       |  Owner   | Encoding | Collate | Ctype |   Access privileges
-----------------+----------+----------+---------+-------+-----------------------
 logicalreptest2 | postgres | UTF8     | C       | C     |
[省略]
  1. 動作確認用のテーブル作成

作成したデータベースに検証用のテーブルを作成します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres logicalreptest2
=# CREATE TABLE t3logical (c1 INT, PRIMARY KEY (c1));

以下のコマンドでテーブルが作成されたことを確認します。

(node1,node2のサーバにおいて実施)
=# \d
           List of relations
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+----------
 public | t3logical | table | postgres
(1 row)
  1. Publication作成

node1上にt3logicalテーブルのみをレプリケーション対象とするPublicationを作成します。 (FOR TABLEにt3logicalを指定)

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest2
=# CREATE PUBLICATION pub_node1_t3logical FOR TABLE t3logical;

以下のコマンドでPublicationが作成されたことを確認します。 作成したPublicationでは、All tables列がfalseであることを確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest2
=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node1_t3logical | postgres | f          | t       | t       | t
(1 row)
  1. Subscription作成

node2上にPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest2
=# CREATE SUBSCRIPTION sub_node2_t3logical CONNECTION 'host=node1 dbname=logicalreptest2 port=5432 user=postgres' PUBLICATION pub_node1_t3logical;

以下のコマンドでSubscriptionが作成されたことを確認します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest2
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node2_t3logical | postgres | t       | {pub_node1_t3logical}
(1 row)
  1. ロジカルレプリケーション簡易動作検証

テスト用に作成したテーブルにレコードをINSERTします。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest2
=# INSERT INTO t3logical VALUES (1);
=# INSERT INTO t3logical VALUES (2);
=# SELECT * FROM t3logical;
 c1
----
1
2
(2 rows)

Subscription側のテーブルにもレコードがINSERTされていることを確認します。(レプリケーションされていることを確認)

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest2
=# SELECT * FROM t3logical;
 c1
----
1           <-- node1と同じ結果がレプリケーションされていること
2
(2 rows)

6.2.3. レプリケーション対象とする更新処理の限定

6.2.3.1. 検証目的

任意のテーブルをロジカルレプリケーションの対象とした状態で特定の更新処理のみを
レプリケーション対象とする際の手順を確認します。

6.2.3.2. 検証内容

本検証では、下記のレプリケーション設定において更新処理の限定が実現可能かの検証を実施しました。

6.2.3.3. 検証環境

レプリケーション対象とする更新処理の限定

6.2.3.4. 検証手順

■ 初期状態

  1. レプリケーション状態の確認

現在のレプリケーション状態を確認します。

(node1,node2のサーバにおいて実施)
$ psql -x -U postgres -c "SELECT * FROM pg_stat_replication"
(0 rows)
  1. テスト用データベース作成

本検証で利用するデータベースを作成します。

(node1,node2のサーバにおいて実施)
$ createdb testlogicalre

以下のコマンドでデータベースが作成された事を確認します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres -l
                               List of databases
     Name      |  Owner   | Encoding | Collate | Ctype |   Access privileges
---------------+----------+----------+---------+-------+-----------------------
 testlogicalre | postgres | UTF8     | C       | C     |                    <-- 作成したDBを確認
[省略]
  1. テスト用テーブル作成

作成したデータベースにテスト用のテーブルを作成します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# CREATE TABLE t1logical (c1_no integer PRIMARY KEY, c2_date date);
=# SELECT * FROM t1logical;
 c1_no | c2_date
-------+---------
(0 rows)

以下のコマンドで作成したテーブルを確認します。

(node1,node2のサーバにおいて実施)
=# \dt
           List of relations
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+----------
 public | t1logical | table | postgres
(1 row)

■ ロジカルレプリケーションでのレプリケーション内容の変更

  1. PublicationとSubscriptionを作成

node1に作成したt1logicalテーブルに対し、insertとupdateをレプリケーション対象とするPublicationを作成します。

(node1のサーバにおいて実施)
=# CREATE PUBLICATION pub_test_node1 FOR TABLE t1logical WITH (publish='insert, update');           <-- 指定した更新処理のみをレプリケーション
=# SELECT * FROM pg_publication_tables;
    pubname     | schemaname | tablename
----------------+------------+-----------
 pub_test_node1 | public     | t1logical
(1 row)
以下のコマンドでPublicationが作成された事を確認します。
作成したpublicationでは対象のテーブルのinsertとupdateをレプリケーション対象としているため、
InsertsとUpdates列がtrueであることを確認します。
(node1のサーバにおいて実施)
=# \dRp
                         List of publications
      Name      |  Owner   | All tables | Inserts | Updates | Deletes
----------------+----------+------------+---------+---------+---------
 pub_test_node1 | postgres | f          | t       | t       | f                                                                     <-- DELETEはレプリケーション対象に含めない
(1 row)

node2にnode1に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
=# CREATE SUBSCRIPTION sub_test_node2 CONNECTION 'host=node1 dbname=testlogicalre port=5432 user=postgres' PUBLICATION pub_test_node1;
=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate | srsublsn
---------+---------+------------+-----------
   16435 |   16430 | r          | 0/181A380
(1 row)

以下のコマンドでSubscriptionが作成された事を確認します。

(node2のサーバにおいて実施)
# \dRs
                 List of subscriptions
      Name      |  Owner   | Enabled |   Publication
----------------+----------+---------+------------------
 sub_test_node2 | postgres | t       | {pub_test_node1}
(1 row)
  1. レプリケーション状態確認
現在のレプリケーション状態を確認します。
作成したPublicationとSubscriptionが正常にレプリケーションされているかを
application_name、state、sync_state項目で確認します。
(node1のサーバにおいて実施)
$ psql -x -U postgres -c "SELECT * FROM pg_stat_replication"
-[ RECORD 1 ]----+------------------------------
pid              | 1264
usesysid         | 10
usename          | postgres
application_name | sub_test_node2                                                                   <-- subscription_nameが表示される
client_addr      | 192.168.56.102
client_hostname  |
client_port      | 53076
backend_start    | 2017-12-20 10:44:04.503952+09
backend_xmin     |
state            | streaming                                                                                <-- streaming が表示される
sent_lsn         | 0/181A380
write_lsn        | 0/181A380
flush_lsn        | 0/181A380
replay_lsn       | 0/181A380
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async                                                                                    <-- async(非同期)

■ ロジカルレプリケーション動作確認

  1. テーブルにデータを追加

テスト用に作成したPublication側のテーブルにレコードをINSERTします。

(node1のサーバにおいて実施)
=# INSERT INTO t1logical VALUES (1,'2017/12/20');
=# SELECT * FROM t1logical;
 c1_no |  c2_date
-------+------------
     1 | 2017-12-20
(1 row)

Subscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
=# SELECT * FROM t1logical;
 c1_no |  c2_date
-------+------------
     1 | 2017-12-20
(1 row)
  1. テーブルのデータを更新

Publication側のテーブルのレコードをUPDATEします。

(node1のサーバにおいて実施)
=# UPDATE t1logical SET c2_date = '2017/12/25' WHERE c1_no = 1;
=# SELECT * FROM t1logical;
 c1_no |  c2_date
-------+------------
     1 | 2017-12-25
(1 row)

Subscription側のテーブルでもレコードがUPDATEされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
=# SELECT * FROM t1logical;
 c1_no |  c2_date
-------+------------
     1 | 2017-12-25
(1 row)
  1. テーブルのデータを削除

Publication側のテーブルのレコードをDELETEします。

(node1のサーバにおいて実施)
=# DELETE FROM t1logical WHERE c1_no = 1;
=# SELECT * FROM t1logical;
 c1_no | c2_date
-------+---------
(0 rows)                                                                                                                    <-- 指定のデータが削除される

Subscription側のテーブルではレコードがDELETEされていない事を確認します。(レプリケーションされていない事を確認)

(node2のサーバにおいて実施)
=# SELECT * FROM t1logical;
 c1_no |  c2_date
-------+------------
     1 | 2017-12-25                                                                                                 <-- DELETEはレプリケーションされていない
(1 row)

6.2.4. 設定するパラメータ

ロジカルレプリケーションの構築に必要な設定について説明します。

6.2.4.1. 実行時パラメータ

ロジカルレプリケーションに関係する実行時パラメータは以下のとおりです。

表 6.4 ロジカルレプリケーションに関係する実行時パラメータ
パラメータ名 パラメータの説明 設定するサーバ
wal_level WALに書かれる情報量。ロジカルレプリケーションでは「logical」を設定する。
ロジカルレプリケーションのPublisher、
ストリーミングレプリケーションのマスタ
max_wal_senders wal senderプロセスの最大数
ロジカルレプリケーションのPublisher、
ストリーミングレプリケーションのマスタ
max_logical_replication_workers logical replication workerプロセスの最大数 ロジカルレプリケーションのSubscriber
max_worker_processes バックグラウンドプロセスの最大数 ロジカルレプリケーションのPublisher、Subscriber
max_sync_workers_per_subscription Subscriptionあたりのワーカープロセスの並列度 ロジカルレプリケーションのSubscriber
max_replication_slots レプリケーションスロットの最大数
ロジカルレプリケーションのPublisher
ストリーミングレプリケーションのマスタ

6.2.4.2. 実行時パラメータと起動プロセスの関係

いくつかの実行時パラメータはロジカルレプリケーション稼働時に起動されるプロセス数との関係を理解して設定値を決める必要があります。ここでは以下の実行時パラメータについて考察します。

  • max_wal_senders
  • max_logical_replication_workers
  • max_worker_processes
  • max_sync_workers_per_subscription
  • max_replication_slots

■初期状態

本章は以下の環境を利用した検証結果を元に解説します。

_images/logical_replica_failure_environment.png
  • ロジカルレプリケーションは、Publisher1台、Subscriber1台で構成
  • ロジカルレプリケーションのPublisher(サーバ1。IPアドレス 192。168。127。31)では、データベースクラスタ内に1つのデータベースを作成
    • pubdb: レプリケーション対象のテーブルを個別定義するPublication pub1を作成
    • pub1のレプリケーション対象としてテーブルdata1を設定
  • ロジカルレプリケーションのSubscriber(サーバ2。IPアドレス 192.168.127.32)では、データベースクラスタ内に1つのデータベースを作成
    • subdb: Publisherのpubdbデータベースに定義したPublicationから変更データを受け取るSubscription sub1およびテーブルdata1を作成

ここでPublisher、Subscriberのプロセスを確認すると、以下のプロセスが起動していることがわかります。

  • logical replication launcher: Publisher, Subscriberに1つずつ
  • wal sender: Publisherに1つ
  • logical replication worker: は Subscriberに1つ
(サーバ1において実施)
$ ps aux | grep postgres
postgres  2503  0.0  0.8 389296 16492 pts/0    S    15:15   0:00 /usr/pgsql-10/bin/postgres
postgres  2504  0.0  0.1 242108  1936 ?        Ss   15:15   0:00 postgres: logger process
postgres  2506  0.0  0.1 389448  3664 ?        Ss   15:15   0:00 postgres: checkpointer process
postgres  2507  0.0  0.1 389296  3428 ?        Ss   15:15   0:00 postgres: writer process
postgres  2508  0.0  0.3 389296  6324 ?        Ss   15:15   0:00 postgres: wal writer process
postgres  2509  0.0  0.1 389752  3048 ?        Ss   15:15   0:00 postgres: autovacuum launcher process
postgres  2510  0.0  0.1 244360  2204 ?        Ss   15:15   0:00 postgres: stats collector process
postgres  2511  0.0  0.1 389588  2492 ?        Ss   15:15   0:00 postgres: bgworker: logical replication launcher
postgres  2513  0.0  0.3 392428  5788 ?        Ss   15:15   0:00 postgres: wal sender process repusr1 192.168.127.32(40906) idle

(サーバ2において実施)
$ ps aux | grep postgres
postgres  2387  0.0  0.8 389296 16492 pts/0    S    15:15   0:00 /usr/pgsql-10/bin/postgres
postgres  2388  0.0  0.1 242108  1932 ?        Ss   15:15   0:00 postgres: logger process
postgres  2390  0.0  0.2 389448  3928 ?        Ss   15:15   0:00 postgres: checkpointer process
postgres  2391  0.0  0.1 389296  3428 ?        Ss   15:15   0:00 postgres: writer process
postgres  2392  0.0  0.3 389296  6324 ?        Ss   15:15   0:00 postgres: wal writer process
postgres  2393  0.0  0.1 389752  3076 ?        Ss   15:15   0:00 postgres: autovacuum launcher process
postgres  2394  0.0  0.1 244360  2200 ?        Ss   15:15   0:00 postgres: stats collector process
postgres  2395  0.0  0.1 389588  2756 ?        Ss   15:15   0:00 postgres: bgworker: logical replication launcher
postgres  2398  0.0  0.3 397072  6320 ?        Ss   15:15   0:00 postgres: bgworker: logical replication worker for subscription 16394

■Publication,Subscriptionを追加した時の起動プロセス

Publisher,Subscriberにそれぞれテーブルdata2を追加し、テーブルdata2を複製するPublication,Subscriptionを作成します。

(サーバ1において実施)
pubdb=> CREATE TABLE data2 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
pubdb=> CREATE PUBLICATION pub2 FOR TABLE data2;

(サーバ2において実施)
subdb=> CREATE TABLE data2 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
subdb=# CREATE SUBSCRIPTION sub2 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub2;
NOTICE:  created replication slot "sub2" on publisher
CREATE SUBSCRIPTION

Publisherの動的統計情報ビューpg_stat_replicationを見ると、今回追加したレプリケーション(RECORD 1)元々設定済のレプリケーション(RECORD 2)の情報が確認できます。

(サーバ1において実施)
pubdb=# SELECT * FROM pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 2692
usesysid         | 16385
usename          | repusr1
application_name | sub2
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 40909
backend_start    | 2017-12-14 16:22:28.821828+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/16D7B60
write_lsn        | 0/16D7B60
flush_lsn        | 0/16D7B60
replay_lsn       | 0/16D7B60
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 2 ]----+------------------------------
pid              | 2513
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 40906
backend_start    | 2017-12-14 15:15:46.183256+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/16D7B60
write_lsn        | 0/16D7B60
flush_lsn        | 0/16D7B60
replay_lsn       | 0/16D7B60
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

ここでPublisher、Subscriberのプロセスを確認すると以下のプロセスが起動しており、wal sender, logical replication workerがそれぞれSubscription単位に起動していることがわかります。

  • logical replication launcher: Publisher, Subscriberに1つずつで変化なし
  • wal sender: Publisherに2つ
  • logical replication worker: Subscriberに2つ
(サーバ1において実施)
$ ps aux | grep postgres
postgres  2503  0.0  0.8 389296 16492 pts/0    S    15:15   0:00 /usr/pgsql-10/bin/postgres
postgres  2504  0.0  0.1 242108  1936 ?        Ss   15:15   0:00 postgres: logger process
postgres  2506  0.0  0.2 389448  4188 ?        Ss   15:15   0:00 postgres: checkpointer process
postgres  2507  0.0  0.1 389296  3428 ?        Ss   15:15   0:00 postgres: writer process
postgres  2508  0.0  0.3 389296  6324 ?        Ss   15:15   0:00 postgres: wal writer process
postgres  2509  0.0  0.1 389752  3048 ?        Ss   15:15   0:00 postgres: autovacuum launcher process
postgres  2510  0.0  0.1 244360  2204 ?        Ss   15:15   0:00 postgres: stats collector process
postgres  2511  0.0  0.1 389588  2492 ?        Ss   15:15   0:00 postgres: bgworker: logical replication launcher
postgres  2513  0.0  0.3 392428  5788 ?        Ss   15:15   0:00 postgres: wal sender process repusr1 192.168.127.32(40906) idle
postgres  2692  0.0  0.2 392292  5252 ?        Ss   16:22   0:00 postgres: wal sender process repusr1 192.168.127.32(40909) idle

(サーバ2において実施)
$ ps aux | grep postgres
postgres  2387  0.0  0.8 389296 16492 pts/0    S    15:15   0:00 /usr/pgsql-10/bin/postgres
postgres  2388  0.0  0.1 242108  1932 ?        Ss   15:15   0:00 postgres: logger process
postgres  2390  0.0  0.2 389448  4192 ?        Ss   15:15   0:00 postgres: checkpointer process
postgres  2391  0.0  0.1 389296  3428 ?        Ss   15:15   0:00 postgres: writer process
postgres  2392  0.0  0.3 389296  6324 ?        Ss   15:15   0:00 postgres: wal writer process
postgres  2393  0.0  0.1 389752  3076 ?        Ss   15:15   0:00 postgres: autovacuum launcher process
postgres  2394  0.0  0.1 244360  2200 ?        Ss   15:15   0:00 postgres: stats collector process
postgres  2395  0.0  0.1 389588  2756 ?        Ss   15:15   0:00 postgres: bgworker: logical replication launcher
postgres  2398  0.0  0.3 397072  6320 ?        Ss   15:15   0:00 postgres: bgworker: logical replication worker for subscription 16394
postgres  2571  0.0  0.3 397072  5992 ?        Ss   16:22   0:00 postgres: bgworker: logical replication worker for subscription 16400

■初期データコピー実行中の起動プロセス

次にロジカルレプリケーションのSubscription作成時に実行される初期データのコピー処理による起動プロセスの増減を確認します。ここではベンチマークツールpgbenchを使ってあらかじめデータベースpubdbに1000万件の初期データを持つテーブルpgbench_accountsを作成します。

(サーバ1において実施)
$ pgbench -i -s 10 -U pubusr1 pubdb

Publisherのデータベースpubdbに接続し、新たなPublication pub3を作成します。

(サーバ1において実施)
pubdb=> CREATE PUBLICATION pub3 FOR TABLE pgbench_accounts;

Subscriberのデータベースsubdbに接続し、レプリケーション対象テーブルを作成します。

(サーバ2において実施)
subdb=> CREATE TABLE pgbench_accounts (aid INT PRIMARY KEY, bid INT, abalance INT, filler character(84));

データベースsubdbにSubscription sub3を作成し、ロジカルレプリケーションを開始します。

(サーバ2において実施)
subdb=# CREATE SUBSCRIPTION sub3 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub3;

Subscription sub3を作成した直後にPublisher,Subscriberの起動プロセスを確認すると、wal sender, logical replication workerがそれぞれSubscriptionに加えて初期データコピーを実行するプロセス分増えていることがわかります。

  • logical replication launcher: Publisher, Subscriberに1つずつで変化なし
  • wal sender: Publisherに4つ。うち1つは初期データコピー用のプロセス
  • logical replication worker: Subscriberに4つ。うち1つは初期データコピー用のプロセス
(サーバ1において実施)
$ ps aux | grep postgres
postgres  2503  0.0  0.6 389296 12532 ?        S    12月14   0:01 /usr/pgsql-10/bin/postgres
postgres  2504  0.0  0.0 242108  1576 ?        Ss   12月14   0:00 postgres: logger process
postgres  2506  0.0  2.2 389612 42160 ?        Ss   12月14   0:00 postgres: checkpointer process
postgres  2507  0.0  0.1 389296  2892 ?        Ss   12月14   0:00 postgres: writer process
postgres  2508  0.0  0.3 389296  5836 ?        Ss   12月14   0:02 postgres: wal writer process
postgres  2509  0.0  0.1 389752  2564 ?        Ss   12月14   0:00 postgres: autovacuum launcher process
postgres  2510  0.0  0.0 244360  1816 ?        Ss   12月14   0:02 postgres: stats collector process
postgres  2511  0.0  0.1 389588  2120 ?        Ss   12月14   0:00 postgres: bgworker: logical replication launcher
postgres  2513  0.0  1.9 425924 36336 ?        Ss   12月14   0:24 postgres: wal sender process repusr1 192.168.127.32(40906) idle
postgres  2692  0.0  1.9 425852 35920 ?        Ss   12月14   0:24 postgres: wal sender process repusr1 192.168.127.32(40909) idle
postgres  5731  0.0  0.2 392292  5248 ?        Ss   10:15   0:00 postgres: wal sender process repusr1 192.168.127.32(40958) idle
postgres  5732 19.8  0.4 392592  8788 ?        Ss   10:15   0:01 postgres: wal sender process repusr1 192.168.127.32(40959) COPY

(サーバ2において実施)
$ ps aux | grep postgres
postgres  2387  0.0  0.8 389296 16492 ?        S    12月14   0:01 /usr/pgsql-10/bin/postgres
postgres  2388  0.0  0.1 242108  2000 ?        Ss   12月14   0:00 postgres: logger process
postgres  2390  0.0  2.3 389612 43740 ?        Ss   12月14   0:00 postgres: checkpointer process
postgres  2391  0.0  0.1 389296  3428 ?        Ss   12月14   0:00 postgres: writer process
postgres  2392  0.0  0.3 389296  6324 ?        Ss   12月14   0:01 postgres: wal writer process
postgres  2393  0.0  0.1 389752  3092 ?        Ss   12月14   0:00 postgres: autovacuum launcher process
postgres  2394  0.0  0.1 244360  2200 ?        Ss   12月14   0:02 postgres: stats collector process
postgres  2395  0.0  0.1 389588  2756 ?        Ss   12月14   0:00 postgres: bgworker: logical replication launcher
postgres  2398  0.0  0.3 397072  6320 ?        Ss   12月14   0:03 postgres: bgworker: logical replication worker for subscription 16394
postgres  2571  0.0  0.3 397072  5992 ?        Ss   12月14   0:03 postgres: bgworker: logical replication worker for subscription 16400
postgres  5597  0.0  0.3 397072  5728 ?        Ss   10:15   0:00 postgres: bgworker: logical replication worker for subscription 16418
postgres  5598 83.0  3.5 397536 67052 ?        Rs   10:15   0:06 postgres: bgworker: logical replication worker for subscription 16418 sync 16413

なお、初期データのコピーが完了するとコピー用のプロセス(上記のPID 5732,5598)が終了し、wal sender,logical replication workerの数がSubscriptionと同じ3つに変化することも確認できました。

■max_worker_processesと起動プロセスの関係

max_worker_processesはバックグラウンドプロセスの最大数を定義する実行時パラメータでデフォルト値は8です。ロジカルレプリケーションに関係するプロセスもバックグラウンドプロセスとして起動するため、このパラメータとの関係を検証します。

まず、Subscriberのmax_worker_processesを3に変更して再起動すると、サーバログに以下の様なエラーメッセージが出力されます。

(サーバ2のサーバログ)
2017-12-15 11:00:44.951 JST [5755] LOG:  logical replication apply worker for subscription "sub1" has started
2017-12-15 11:00:44.963 JST [5756] LOG:  logical replication apply worker for subscription "sub2" has started
2017-12-15 11:00:44.966 JST [5754] WARNING:  out of background worker slots
2017-12-15 11:00:44.966 JST [5754] HINT:  You might need to increase max_worker_processes.

この時、Subscriberの動的統計情報ビューpg_stat_activityでバックグラウンドプロセスの情報を表示すると、logical replication launcher と logical replication worker がそれぞれバックグラウンドプロセスとして起動していることがわかります。

(サーバ2において実施)
postgres=# select datname,pid,usename,backend_start,wait_event_type,wait_event,state,backend_type from pg_stat_activity where backend_type = 'background worker';
 datname | pid  | usename  |         backend_start         | wait_event_type |     wait_event      | state |   backend_type
---------+------+----------+-------------------------------+-----------------+---------------------+-------+-------------------
         | 5790 | postgres | 2017-12-15 11:01:16.733998+09 | Activity        | LogicalLauncherMain |       | background worker
 subdb   | 5791 | postgres | 2017-12-15 11:01:16.742653+09 | Activity        | LogicalApplyMain    | idle  | background worker
 subdb   | 5792 | postgres | 2017-12-15 11:01:16.750161+09 | Activity        | LogicalApplyMain    | idle  | background worker
 subdb   | 5793 | postgres | 2017-12-15 11:01:16.760481+09 | Activity        | LogicalApplyMain    | idle  | background worker

一方でPublisherのmax_worker_processesを3に変更して再起動してもサーバログにエラーメッセージは出力されません。この時のPublisherのバックグラウンドプロセスの情報を表示すると、logical replication launcher だけがカウントされ、wal senderはカウントされていません。

(サーバ1において実施)
pubdb=#  select datname,pid,usename,backend_start,wait_event_type,wait_event,state,backend_type from pg_stat_activity where backend_type = 'background worker';
 datname | pid  | usename  |         backend_start         | wait_event_type |     wait_event      | state |   backend_type
---------+------+----------+-------------------------------+-----------------+---------------------+-------+-------------------
         | 2511 | postgres | 2017-12-14 15:15:28.478386+09 | Activity        | LogicalLauncherMain |       | background worker

このように、Publisherのwal senderとSubscriberのlogical replication workerはともにSubscriptionの数に合わせて増加しますが、Publisherのwal senderはmax_worker_processesで管理するバックグラウンドプロセスとは異なる扱いであることがわかりました。

6.2.4.3. 考察

検証結果を踏まえて以下の実行時パラメータの決め方について考察します。

■max_wal_senders

  • ロジカルレプリケーションでの起動数とストリーミングレプリケーションのスレーブ数を考慮して以下の数が必要です。
    • 必要数 = (ストリーミングレプリケーションのスレーブ数) + (ロジカルレプリケーションのSubscription数 ✕ 2)
    • ロジカルレプリケーションの初期データ同期プロセスが起動する分を考慮して2倍する。
    • 初期データ同期プロセスは max_sync_workers_per_subscription(デフォルト2)に応じて変化するため、ここを増やす場合は✕2の部分も変化する。
    • PostgreSQL10.1のデフォルト値は10。上記必要数で不足しそうなら変更しておく。

■max_logical_replication_workers

  • ロジカルレプリケーションでの起動数を考慮して以下の数が必要です。
    • 必要数 = このサーバに作成するSubscriptionの数 ✕ 2
    • ロジカルレプリケーションの初期データ同期時に増えるプロセス数を考慮して2倍する。
    • 初期データ同期プロセスは max_sync_workers_per_subscription(デフォルト2)に応じて変化するため、ここを増やす場合は✕2の部分も変化する。
    • PostgreSQL10.1のデフォルト値は4。上記必要数で不足しそうなら変更しておく。

■max_worker_processes

  • ロジカルレプリケーションでの起動数を考慮して以下の数が必要です。
    • 必要数 = このサーバに作成するSubscriptionの数 ✕ 2 + 1(=logical replication launcherの分) + その他のバックグラウンドプロセス(任意)
    • 初期データ同期プロセスは max_sync_workers_per_subscription(デフォルト2)に応じて変化するため、ここを増やす場合は✕2の部分も変化する。
    • PostgreSQL10.1のデフォルト値は8。上記必要数で不足しそうなら変更しておく。

■max_sync_workers_per_subscription

  • PostgreSQL10.1のデフォルト値は2で、この場合1つがレプリケーション用に確保されるため初期データ同期の並列度は1で固定されます。
  • 初期データ同期の並列度は1テーブルにつき1までです。複数テーブルに対する同期処理の並列度は上がりますが、同じテーブルに対する並列度は上がりません。

■max_replication_slots

  • ロジカルレプリケーションでの作成数と合わせて以下の数が必要です。
    • 必要数 = ロジカルレプリケーションで作成するSubscriptionの数 ✕ 2 + ストリーミングレプリケーションで作成するレプリケーションスロットの数(任意)
    • 初期データ同期プロセスは一時的にレプリケーションスロットを作成するため、ここを増やす場合は✕2の部分も変化する。
    • PostgreSQL10.1のデフォルト値は10。上記必要数で不足しそうなら変更しておく。

6.3. ロジカルレプリケーションの応用

6.3.1. primary keyが定義されていないテーブルのレプリケーション設定

6.3.1.1. 検証目的

primary keyが定義されていないテーブルにおけるロジカルレプリケーションの設定方法を確認します。

ロジカルレプリケーションでは、UPDATEもしくはDELTEをレプリケーションさせるために、 変更したレコードを特定するための情報であるREPLICA IDENTITYをPublication側のテーブルに設定する必要があります。 REPLICA IDENTITYが指定されていないテーブルがPublicationに追加された場合、UPDATEもしくはDELETEが行われるとエラーが発生します。

デフォルトではprimary keyが定義されたテーブルの場合、primary keyがREPLICA IDENTITYとして自動設定されます。 また、REPLICA IDENTITYにユニークインデックス(NOT NULLかつ部分インデックスや遅延可能インデックスではない)を設定することで、UPDATEとDELETEのレプリケーションが可能です。 テーブルに上記のキーが存在しない場合は、REPLICA IDENTITYにFULLを設定することで、UPDATEとDELETEのレプリケーションが可能です。[1]

REPLICA IDENTITYは、ロジカルレプリケーション利用時において、WALファイルに書き込む情報量に影響します。 REPLICA IDENTITYに指定された列では、変更前の古いレコードがWALファイルに書き込まれます。 FULLを指定した場合には、古いレコードを全てWALファイルに書き込みため、非効率です。

REPLICA IDENTITYの設定方法の詳細は、PostgreSQL 文書をご確認下さい。[2]

[1]PostgreSQL 10.0文書 - 第31章 論理レプリケーション 31.1. パブリケーション
[2]PostgreSQL 10.0文書 - SQLコマンド ALTER TABLE

6.3.1.2. 検証内容

primary keyが定義されていないテーブルにおけるレプリケーションが可能な否かを確認するため、 下表のパターンでロジカルレプリケーションの設定を実施しました。 パターンと動作確認結果は下表の通りです。(○:レプリケーション可能、×:レプリケーション不可)

表 6.5 primary keyが定義されていないテーブルの動作確認
テーブル構成 INSERT UPDATE DELETE 備考
primary keyが定義されたテーブル
-
unique制約(not null制約)とreplica identityにuniqueインデックスを指定
-
上記に該当しないテーブル
×
×
Publication側のテーブルにDELETEおよびUPDATEを実施した場合、エラーになる
上記に該当しないテーブルにreplica identityにfullを指定
-

6.3.1.3. 検証環境

primary keyが定義されていないテーブルのレプリケーション設定

6.3.1.4. 検証手順

  1. 動作確認用のテーブル作成

テスト用のテーブルを作成します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# CREATE TABLE t8logical (c1 INT, c2 TEXT, PRIMARY KEY (c1));
=# CREATE TABLE t9logical (c1 INT NOT NULL, c2 TEXT);
=# CREATE UNIQUE INDEX t9logical_ukey ON t9logical (c1);
=# ALTER TABLE t9logical REPLICA IDENTITY USING INDEX t9logical_ukey;
=# CREATE TABLE t10logical (c1 INT, c2 TEXT);
=# CREATE TABLE t11logical (c1 INT, c2 TEXT);
=# ALTER TABLE t11logical REPLICA IDENTITY FULL;

以下のコマンドでテーブルが作成されたことを確認します。

(node1,node2のサーバにおいて実施)
=# \d
            List of relations
 Schema |    Name    | Type  |  Owner
--------+------------+-------+----------
 public | t10logical | table | postgres
 public | t11logical | table | postgres
 public | t8logical  | table | postgres
 public | t9logical  | table | postgres
(4 rows)
  1. Publication作成

node1上の各テーブルをレプリケーション対象とするPublicationを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# CREATE PUBLICATION pub_node1_t8logical FOR TABLE t8logical;
=# CREATE PUBLICATION pub_node1_t9logical FOR TABLE t9logical;
=# CREATE PUBLICATION pub_node1_t10logical FOR TABLE t10logical;
=# CREATE PUBLICATION pub_node1_t11logical FOR TABLE t11logical;

以下のコマンドでPublicationが作成されたことを確認します。

(node1のサーバにおいて実施)
=# \dRp
                            List of publications
         Name         |  Owner   | All tables | Inserts | Updates | Deletes
----------------------+----------+------------+---------+---------+---------
 pub_node1_t10logical | postgres | f          | t       | t       | t
 pub_node1_t11logical | postgres | f          | t       | t       | t
 pub_node1_t8logical  | postgres | f          | t       | t       | t
 pub_node1_t9logical  | postgres | f          | t       | t       | t
(5 rows)
  1. Subscription作成

node2上にnode1に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# CREATE SUBSCRIPTION sub_node2_t8logical CONNECTION 'host=node1 dbname=logicalreptest3 port=5432 user=postgres' PUBLICATION pub_node1_t8logical;
=# CREATE SUBSCRIPTION sub_node2_t9logical CONNECTION 'host=node1 dbname=logicalreptest3 port=5432 user=postgres' PUBLICATION pub_node1_t9logical;
=# CREATE SUBSCRIPTION sub_node2_t10logical CONNECTION 'host=node1 dbname=logicalreptest3 port=5432 user=postgres' PUBLICATION pub_node1_t10logical;
=# CREATE SUBSCRIPTION sub_node2_t11logical CONNECTION 'host=node1 dbname=logicalreptest3 port=5432 user=postgres' PUBLICATION pub_node1_t11logical;

以下のコマンドでSubscriptionが作成されたことを確認します。

(node2のサーバにおいて実施)
=# \dRs
                   List of subscriptions
     Name         |  Owner   | Enabled |      Publication
----------------------+----------+---------+------------------------
 sub_node2_t10logical | postgres | t       | {pub_node1_t10logical}
 sub_node2_t11logical | postgres | t       | {pub_node1_t11logical}
 sub_node2_t8logical  | postgres | t       | {pub_node1_t8logical}
 sub_node2_t9logical  | postgres | t       | {pub_node1_t9logical}
(4 rows)
  1. ロジカルレプリケーション動作検証確認

primary keyが定義されたテーブルに対して、INSERT,UPDATE,DELETEを実施します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# INSERT INTO t8logical VALUES (1, 'MERRY CHRISTMAS');
=# INSERT INTO t8logical VALUES (2, 'CHRISTMAS DAY');
=# UPDATE t8logical SET c2 = 'HAPPY NEW YEAR' WHERE c1 = 2;
=# DELETE FROM t8logical WHERE c1 = 1;
=# SELECT * FROM t8logical;
 c1 |       c2
----+----------------
  2 | HAPPY NEW YEAR
(1 row)

Subscription側にデータが反映されていることを確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# SELECT * FROM t8logical;
 c1 |       c2
----+----------------
  2 | HAPPY NEW YEAR
(1 row)

unique制約(not null制約)とreplica identityにuniqueインデックスが定義されたテーブルに対して、INSERT,UPDATE,DELETEを実施します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# INSERT INTO t9logical VALUES (1, 'MERRY CHRISTMAS');
=# INSERT INTO t9logical VALUES (2, 'CHRISTMAS DAY');
=# UPDATE t9logical SET c2 = 'HAPPY NEW YEAR' WHERE c1 = 2;
=# DELETE FROM t9logical WHERE c1 = 1;
=# SELECT * FROM t9logical;
 c1 |       c2
----+----------------
  2 | HAPPY NEW YEAR
(1 row)

Subscription側にデータが反映されていることを確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# SELECT * FROM t9logical;
 c1 |       c2
----+----------------
  2 | HAPPY NEW YEAR
(1 row)

上記に該当しないテーブルに対して、INSERT,UPDATE,DELETEを実施します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# INSERT INTO t10logical VALUES (1, 'MERRY CHRISTMAS');
=# INSERT INTO t10logical VALUES (2, 'CHRISTMAS DAY');
=# UPDATE t10logical SET c2 = 'HAPPY NEW YEAR' WHERE c1 = 2;
ERROR:  cannot update table "t10logical" because it does not have a replica identity and publishes updates
HINT:  To enable updating the table, set REPLICA IDENTITY using ALTER TABLE.
※ REPLICA IDENTITYが指定されていない場合は、UPDATEできない旨がメッセージとして出力される。
=# DELETE FROM t10logical WHERE c1 = 1;
ERROR:  cannot delete from table "t10logical" because it does not have a replica identity and publishes deletes
HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.
=# SELECT * FROM t10logical;
 c1 |       c2
----+-----------------
  1 | MERRY CHRISTMAS
  2 | CHRISTMAS DAY
(2 rows)

Subscription側にデータが反映されないことを確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# SELECT * FROM t10logical;
 c1 |       c2
----+-----------------
 1 | MERRY CHRISTMAS
 2 | CHRISTMAS DAY
(2 rows)

上記に該当しないテーブルにreplica identityにfullを指定したテーブルに対して、INSERT,UPDATE,DELETEを実施します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# INSERT INTO t11logical VALUES (1, 'MERRY CHRISTMAS');
=# INSERT INTO t11logical VALUES (2, 'CHRISTMAS DAY');
=# UPDATE t11logical SET c2 = 'HAPPY NEW YEAR' WHERE c1 = 2;
=# DELETE FROM t11logical WHERE c1 = 1;
=# SELECT * FROM t11logical;
 c1 |       c2
----+----------------
  2 | HAPPY NEW YEAR
(1 row)

Subscription側にデータが反映されていることを確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest3
=# SELECT * FROM t11logical;
 c1 |       c2
----+----------------
  2 | HAPPY NEW YEAR
(1 row)

6.3.2. 同期レプリケーション

6.3.2.1. 検証目的

ロジカルレプリケーションにおいて同期レプリケーションを設定する手順を確認します。

6.3.2.2. 検証内容

本検証では、下記のレプリケーション設定において同期モードが実現可能かの検証を実施しました。

6.3.2.3. 検証環境

同期レプリケーション

6.3.2.4. 検証手順

■ 初期状態

  1. レプリケーション状態の確認

現在のレプリケーション状態を確認します。

(node1,node2のサーバにおいて実施)
$ psql -x -U postgres -c "SELECT * FROM pg_stat_replication"
(0 rows)
  1. テスト用データベース作成

本検証で利用するデータベースを作成します。

(node1,node2のサーバにおいて実施)
$ createdb testlogicaldb

以下のコマンドでデータベースが作成された事を確認します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres -l
                               List of databases
     Name      |  Owner   | Encoding | Collate | Ctype |   Access privileges
---------------+----------+----------+---------+-------+-----------------------
 testlogicaldb | postgres | UTF8     | C       | C     |                    <-- 作成したDBを確認
[省略]
  1. テスト用テーブル作成

作成したデータベースにテスト用のテーブルを作成します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres testlogicaldb
=# CREATE TABLE t1logical (c1_no integer PRIMARY KEY, c2_date_time timestamp);
=# SELECT * FROM t1logical;
 c1_no | c2_date_time
-------+--------------
(0 rows)

以下のコマンドで作成したテーブルを確認します。

(node1,node2のサーバにおいて実施)
=# \dt
           List of relations
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+----------
 public | t1logical | table | postgres
(1 row)

■ ロジカルレプリケーションでの作成

  1. PublicationとSubscriptionを作成

node1に作成したt1logicalテーブルをレプリケーション対象とするPublicationを作成します。

(node1のサーバにおいて実施)
=# CREATE PUBLICATION test_slot1 FOR TABLE t1logical;
=# SELECT * FROM pg_publication_tables;
  pubname   | schemaname | tablename
------------+------------+-----------
 test_slot1 | public     | t1logical
(1 row)

以下のコマンドでPublicationが作成された事を確認します。

=# \dRp
                       List of publications
    Name    |  Owner   | All tables | Inserts | Updates | Deletes
------------+----------+------------+---------+---------+---------
 test_slot1 | postgres | f          | t       | t       | t                 <-- オールテーブルではない
(1 row)

node2にnode1に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
=# CREATE SUBSCRIPTION test_slot1 CONNECTION 'host=node1 dbname=testlogicaldb port=5432 user=postgres' PUBLICATION test_slot1;
=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   24755 |   24750 | r          | 2/912A8A10
(1 row)

以下のコマンドでSubscriptionが作成された事を確認します。

# \dRs
             List of subscriptions
    Name    |  Owner   | Enabled | Publication
------------+----------+---------+--------------
 test_slot1 | postgres | t       | {test_slot1}
(1 row)
  1. レプリケーション状態確認
現在のレプリケーション状態を確認します。
作成したPublicationとSubscriptionが正常にレプリケーションされているかを
application_name、state、sync_state項目で確認します。(sync_stateは現時点ではasyncとなります。)
(node1のサーバにおいて実施)
$ psql -x -U postgres -c "SELECT * FROM pg_stat_replication"
-[ RECORD 1 ]----+------------------------------
pid              | 1667
usesysid         | 10
usename          | postgres
application_name | test_slot1                                                                               <-- subscription_nameが表示される
client_addr      | 192.168.43.11
client_hostname  |
client_port      | 35564
backend_start    | 2018-01-05 10:39:02.914132+09
backend_xmin     |
state            | streaming                                                                                <-- streaming が表示される
sent_lsn         | 2/912A8A10
write_lsn        | 2/912A8A10
flush_lsn        | 2/912A8A10
replay_lsn       | 2/912A8A10
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async                                                                                    <-- async(現時点では非同期)

■ 同期レプリケーションの設定

  1. コマンドラインで設定

コマンドラインからレプリケーションの設定を同期レプリケーションにします。

(node1のサーバにおいて実施)
$ psql -U postgres testlogicaldb
=# ALTER SYSTEM SET synchronous_standby_names = 'test_slot1';               <-- application_nameを指定する

設定ファイルで同期レプリケーションに変更された事を確認します。

(node1のサーバにおいて実施)
$ cat /var/lib/pgsql/10/data/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by ALTER SYSTEM command.
synchronous_standby_names = 'test_slot1'                                                    <-- 同期設定にapplication_nameが設定されている

設定ファイルの内容を反映します。

(node1のサーバにおいて実施)
$ pg_ctl reload

PostgreSQL起動時にsynchronous_standby_namesパラメータが読み込まれている事を確認します。

(nodeのサーバにおいてサーバ実施)
$ tail /var/lib/pgsql/10/data/log/postgresql-2018-01-05.log                 <-- ログで設定が読み込まれていることを確認
LOG:  received SIGHUP, reloading configuration files
LOG:  parameter "synchronous_standby_names" changed to "test_slot1"
LOG:  standby "test_slot1" is now a synchronous standby with priority 1
現在のレプリケーション状態を確認します。
sync_stateがsyncとなっている事を確認します。
(node1のサーバにおいて実施)
$ psql -x -U postgres -c "SELECT * FROM pg_stat_replication"
-[ RECORD 1 ]----+------------------------------
pid              | 1667
usesysid         | 10
usename          | postgres
application_name | test_slot1
client_addr      | 192.168.43.11
client_hostname  |
client_port      | 35564
backend_start    | 2018-01-05 10:39:02.914132+09
backend_xmin     |
state            | streaming
sent_lsn         | 2/912A8A10
write_lsn        | 2/912A8A10
flush_lsn        | 2/912A8A10
replay_lsn       | 2/912A8A10
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 1
sync_state       | sync                                                                                             <-- sync(同期)になる

■ ロジカルレプリケーション動作確認

  1. テーブルにデータを追加

テスト用に作成したPublication側のテーブルにレコードをINSERTします。

(node1のサーバにおいて実施)
$ psql -U postgres testlogicaldb
=# INSERT INTO t1logical VALUES (1,localtimestamp);
=# SELECT * FROM t1logical;
 c1_no |        c2_date_time
-------+----------------------------
     1 | 2018-01-05 11:07:36.590471
(1 row)

Subscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
=# SELECT * FROM t1logical;
 c1_no |        c2_date_time
-------+----------------------------
     1 | 2018-01-05 11:07:36.590471
(1 row)
  1. テーブルのデータを更新

Publication側のテーブルのレコードをUPDATEします。

(node1のサーバにおいて実施)
=# UPDATE t1logical SET c2_date_time = localtimestamp WHERE c1_no = 1;
=# SELECT * FROM t1logical;
 c1_no |        c2_date_time
-------+----------------------------
     1 | 2018-01-05 11:10:14.419084
(1 row)

Subscription側のテーブルでもレコードがUPDATEされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
=# SELECT * FROM t1logical;
 c1_no |        c2_date_time
-------+----------------------------
     1 | 2018-01-05 11:10:14.419084
(1 row)
  1. テーブルのデータを削除

Publication側のテーブルのレコードをDELETEします。

(node1のサーバにおいて実施)
=# DELETE FROM t1logical WHERE c1_no = 1;
=# SELECT * FROM t1logical;
 c1_no | c2_date_time
-------+--------------
(0 rows)                                                                                                                    <-- 指定のデータが削除される

Subscription側のテーブルでもレコードがDELETEされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
=# SELECT * FROM t1logical;
 c1_no | c2_date_time
-------+--------------
(0 rows)                                                                                                                    <-- 指定のデータが削除される
  1. テーブルロックを実施し同期レプリケーションを確認

Subscription側のデータベース、テーブルのoidを確認します。

(node2のサーバにおいて実施)
=# SELECT oid, datname FROM pg_database;
  oid  |    datname
-------+---------------
 24748 | testlogicaldb
[省略]

=# SELECT oid, datname FROM pg_database;
 relid |    relname
-------+-----------------
 24750 | t1logical
[省略]

Subscription側のロック情報を確認します。

(node2のサーバにおいて実施)
=# SELECT locktype, database, relation, mode FROM pg_locks;
  locktype  | database | relation |      mode
------------+----------+----------+-----------------
 relation   |    24748 |    11577 | AccessShareLock                                 <-- ロックされていない
 virtualxid |          |          | ExclusiveLock
(2 rows)

Subscription側のテーブルをトランザクション内でロック状態のままにします。(ロックモード : EXCLUSIVE MODE)

(node2のサーバにおいて実施)
=# BEGIN;
=# LOCK t1logical IN ACCESS EXCLUSIVE MODE ;                                                <-- テーブルをロック

Subscription側のロック情報を確認します。

(node2のサーバにおいて実施)
=# SELECT locktype, database, relation, mode FROM pg_locks;
   locktype    | database | relation |        mode
---------------+----------+----------+---------------------
 relation      |    24748 |    11577 | AccessShareLock
 virtualxid    |          |          | ExclusiveLock
 transactionid |          |          | ExclusiveLock
 relation      |    24748 |    24750 | AccessExclusiveLock                  <-- テーブルをロック
[省略]
Subscription側のテーブルがロック状態のまま、Publication側のテーブルにレコードをINSERTします。
Subscription側(スタンバイ)からCommitの応答があるまで待機状態となります。
(ただし、60秒以上応答がない場合は、タイムアウトのログが出力されます。)
(node1のサーバにおいて実施)
=# INSERT INTO t1logical VALUES (1,localtimestamp);                                 <-- セカンダリから commit 応答がないためプロンプトが止まる
(node1のサーバにおいて実施)
$ tail /var/lib/pgsql/10/data/log/postgresql-2018-01-05.log
LOG:  terminating walsender process due to replication timeout              <-- 設定値 wal_sender_timeout (default 60s)待ってログが出力される
[Ctrl+c]
Cancel request sent
WARNING:  canceling wait for synchronous replication due to user request
DETAIL:  The transaction has already committed locally, but might not have been replicated to the standby.
INSERT 0 1
Subscription側のテーブルロックを解除するとレプリケーションが動作します。
Subscription側のテーブルでもレコードがINSERTされている事を確認します。
(node2のサーバにおいて実施)
=# COMMIT; <-- ロック解除
=# SELECT locktype, database, relation, mode FROM pg_locks;
  locktype  | database | relation |      mode
------------+----------+----------+-----------------
 relation   |    24748 |    11577 | AccessShareLock                                 <-- ロックされていない
 virtualxid |          |          | ExclusiveLock
(2 rows)
=# SELECT * FROM t1logical ;
 c1_no |        c2_date_time
-------+----------------------------
     1 | 2018-01-05 13:44:52.134441                                                                 <-- レプリケーションされている
(1 row)

6.3.3. 複数サブスクリプション

6.3.3.1. 検証目的

ロジカルレプリケーションで複数のSubscriptionにレプリケーションが可能か確認します。

6.3.3.2. 検証内容

本検証では、3ノードで下記構成のレプリケーションが実現可能かの検証を実施しました。

6.3.3.3. 検証環境

複数サブスクリプション

6.3.3.4. 検証手順

■ ロジカルレプリケーションの動作確認(テーブル単位)

  1. 動作確認用のデータベース作成

本検証で利用するデータベースを作成します。

(node1,node2,node3のサーバにおいて実施)
$ createdb logicalreptest

以下のコマンドでデータベースが作成された事を確認します。

(node1,node2,node3のサーバにおいて実施)
$ psql -U postgres -l
                               List of databases
      Name      |  Owner   | Encoding | Collate | Ctype |   Access privileges
----------------+----------+----------+---------+-------+-----------------------
 logicalreptest | postgres | UTF8     | C       | C     |
[省略]
  1. 動作確認用のテーブル作成

作成したデータベースにテスト用のテーブルを作成します。

(node1,node2,node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE TABLE t1logical (c1 INT, PRIMARY KEY (c1));
=# CREATE TABLE t2logical (c1 INT, PRIMARY KEY (c1));
=# CREATE TABLE t3logical (c1 INT, PRIMARY KEY (c1));

以下のコマンドで作成したテーブルを確認します。

(node1,node2,node3のサーバにおいて実施)
=# \dt
           List of relations
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+----------
 public | t1logical | table | postgres
 public | t2logical | table | postgres
 public | t3logical | table | postgres
(3 rows)
  1. 1セット目Publication、Subscription作成

node1に作成したt1logicalテーブルをレプリケーション対象とするPublicationを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE PUBLICATION pub_node1_t1logical FOR TABLE t1logical;

以下のコマンドでPublicationが作成された事を確認します。

(node1のサーバにおいて実施)
=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node1_t1logical | postgres | f          | t       | t       | t
(1 row)

node2にnode1に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node2_t1logical CONNECTION 'host=node1 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node1_t1logical;

以下のコマンドでSubscriptionが作成された事を確認します。

(node2のサーバにおいて実施)
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node2_t1logical | postgres | t       | {pub_node1_t1logical}
(1 row)

node3にnode1に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node3_t1logical CONNECTION 'host=node1 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node1_t1logical;

以下のコマンドでSubscriptionが作成された事を確認します。

(node3のサーバにおいて実施)
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node3_t1logical | postgres | t       | {pub_node1_t1logical}
(1 row)
  1. 2セット目Publication、Subscription作成

node2に作成したt2logicalテーブルをレプリケーション対象とするPublicationを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE PUBLICATION pub_node2_t2logical FOR TABLE t2logical;

以下のコマンドでPublicationが作成された事を確認します。

(node2のサーバにおいて実施)
=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node2_t2logical | postgres | f          | t       | t       | t
(1 row)

node3にnode2に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node3_t2logical CONNECTION 'host=node2 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node2_t2logical;

以下のコマンドでSubscriptionが作成された事を確認します。

(node3のサーバにおいて実施)
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node3_t1logical | postgres | t       | {pub_node1_t1logical}
 sub_node3_t2logical | postgres | t       | {pub_node2_t2logical}
(2 rows)

node1にnode2に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node1_t2logical CONNECTION 'host=node2 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node2_t2logical;

以下のコマンドでSubscriptionが作成された事を確認します。

(node1のサーバにおいて実施)
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node1_t2logical | postgres | t       | {pub_node2_t2logical}
(1 row)
  1. 3セット目Publication、Subscription作成

node3に作成したt3logicalテーブルをレプリケーション対象とするPublicationを作成します。

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE PUBLICATION pub_node3_t3logical FOR TABLE t3logical;

以下のコマンドでPublicationが作成された事を確認します。

=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node3_t3logical | postgres | f          | t       | t       | t
(1 row)

node3に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node2_t3logical CONNECTION 'host=node3 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node3_t3logical;

以下のコマンドでSubscriptionが作成された事を確認します。

=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node2_t1logical | postgres | t       | {pub_node1_t1logical}
 sub_node2_t3logical | postgres | t       | {pub_node3_t3logical}
(2 rows)

node3に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node1_t3logical CONNECTION 'host=node3 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node3_t3logical;

以下のコマンドでSubscriptionが作成された事を確認します。

(node1のサーバにおいて実施)
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node1_t2logical | postgres | t       | {pub_node2_t2logical}
 sub_node1_t3logical | postgres | t       | {pub_node3_t3logical}
(2 rows)
  1. 1セット目 ロジカルレプリケーション動作検証

node1のPublication側のテーブル(t1logical)にレコードをINSERTします。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# INSERT INTO t1logical VALUES (1);
=# INSERT INTO t1logical VALUES (2);
=# SELECT * FROM t1logical;
 c1
----
1
2
(2 rows)

node2のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t1logical;
 c1
----
1           <-- node1と同じ結果がレプリケーションされていること
2
(2 rows)

node3のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t1logical;
 c1
----
1           <-- node1と同じ結果がレプリケーションされていること
2
(2 rows)
  1. 2セット目 ロジカルレプリケーション動作検証

node2のPublication側のテーブル(t2logical)にレコードをINSERTします。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# INSERT INTO t2logical VALUES (3);
=# INSERT INTO t2logical VALUES (4);
=# SELECT * FROM t2logical;
 c1
----
3
4
(2 rows)

node3のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t2logical;
 c1
----
3           <-- node2と同じ結果がレプリケーションされていること
4
(2 rows)

node1のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t2logical;
 c1
----
3           <-- node2と同じ結果がレプリケーションされていること
4
(2 rows)
  1. 3セット目 ロジカルレプリケーション動作検証

node3のPublication側のテーブル(t3logical)にレコードをINSERTします。

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# INSERT INTO t3logical VALUES (5);
=# INSERT INTO t3logical VALUES (6);
=# SELECT * FROM t3logical;
 c1
----
5
6
(2 rows)

node2のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t3logical;
 c1
----
5           <-- node3同じ結果がレプリケーションされていること
6
(2 rows)

node1のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t3logical;
 c1
----
5           <-- node3と同じ結果がレプリケーションされていること
6
(2 rows)

■ 動作確認

  1. VIEWでのステータス確認

node1での各Publication/Subscriptionの定義を確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest

・Publicationに設定したテーブルを確認
=# SELECT * FROM pg_publication_tables;
       pubname       | schemaname | tablename
---------------------+------------+-----------
 pub_node1_t1logical | public     | t1logical
(1 row)

・レプリケーションしている状態の確認
=# SELECT pid, application_name, client_addr, state, sync_state FROM pg_stat_replication;
 pid  |  application_name   |  client_addr  |   state   | sync_state
------+---------------------+---------------+-----------+------------
 1291 | sub_node2_t1logical | node2         | streaming | async
 1298 | sub_node3_t1logical | node3         | streaming | async
(2 rows)

・Subscriptionの設定情報
=# SELECT subslotname, subpublications, subconninfo FROM pg_subscription;
     subslotname     |    subpublications    |                                 subconninfo
---------------------+-----------------------+-----------------------------------------------------------------------------
 sub_node1_t2logical | {pub_node2_t2logical} | host=node2 dbname=logicalreptest port=5432 user=postgres password=postgres
 sub_node1_t3logical | {pub_node3_t3logical} | host=node3 dbname=logicalreptest port=5432 user=postgres password=postgres
(2 rows)

node2での各Publication/Subscriptionの定義を確認します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest

・Publicationに設定したテーブルを確認
=# SELECT * FROM pg_publication_tables;
       pubname       | schemaname | tablename
---------------------+------------+-----------
 pub_node2_t2logical | public     | t2logical
(1 row)

・レプリケーションしている状態の確認
=# SELECT pid, application_name, client_addr, state, sync_state FROM pg_stat_replication;
 pid  |  application_name   |  client_addr  |   state   | sync_state
------+---------------------+---------------+-----------+------------
 1323 | sub_node1_t2logical | node1         | streaming | async
 1344 | sub_node3_t2logical | node3         | streaming | async
(2 rows)

・Subscriptionの設定情報
=# SELECT subslotname, subpublications, subconninfo FROM pg_subscription;
     subslotname     |    subpublications    |                                 subconninfo
---------------------+-----------------------+-----------------------------------------------------------------------------
 sub_node2_t3logical | {pub_node3_t3logical} | host=node3 dbname=logicalreptest port=5432 user=postgres password=postgres
 sub_node2_t1logical | {pub_node1_t1logical} | host=node1 dbname=logicalreptest port=5432 user=postgres password=postgres
(2 rows)

node3での各Publication/Subscriptionの定義を確認します。

(node3において実施)
$ psql -U postgres logicalreptest

・Publicationに設定したテーブルを確認
=# SELECT * FROM pg_publication_tables;
       pubname       | schemaname | tablename
---------------------+------------+-----------
 pub_node3_t3logical | public     | t3logical
(1 row)

・レプリケーションしている状態の確認
=# SELECT pid, application_name, client_addr, state, sync_state FROM pg_stat_replication;
 pid  |  application_name   |  client_addr  |   state   | sync_state
------+---------------------+---------------+-----------+------------
 1346 | sub_node1_t3logical | node1         | streaming | async
 1348 | sub_node2_t3logical | node2         | streaming | async
(2 rows)

・Subscriptionの設定情報
=# SELECT subslotname, subpublications, subconninfo FROM pg_subscription;
     subslotname     |    subpublications    |                                subconninfo
---------------------+-----------------------+----------------------------------------------------------------------------
 sub_node3_t1logical | {pub_node1_t1logical} | host=node1 dbname=logicalreptest port=5432 user=postgres password=postgres
 sub_node3_t2logical | {pub_node2_t2logical} | host=node2 dbname=logicalreptest port=5432 user=postgres password=postgres
(2 rows)

6.3.4. カスケード構成

6.3.4.1. 検証目的

ロジカルレプリケーションを利用したカスケード構成の挙動を確認します。

6.3.4.2. 検証内容

本検証では、下記構成のレプリケーションが実現可能かの検証を実施しました。

6.3.4.3. 検証環境

カスケード構成

6.3.4.4. 検証手順

■ ロジカルレプリケーションの動作確認(テーブル単位)

  1. 動作確認用のデータベース作成

本検証で利用するデータベースを作成します。

(node1,node2,node3のサーバにおいて実施)
$ createdb logicalreptest

以下のコマンドでデータベースが作成された事を確認します。

(node1,node2,node3のサーバにおいて実施)
$ psql -U postgres -l
                               List of databases
      Name      |  Owner   | Encoding | Collate | Ctype |   Access privileges
----------------+----------+----------+---------+-------+-----------------------
 logicalreptest | postgres | UTF8     | C       | C     |
[省略]
  1. 動作確認用のテーブル作成

作成したデータベースにテスト用のテーブルを作成します。

(node1,node2,node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE TABLE t1logical (c1 INT, PRIMARY KEY (c1));

以下のコマンドで作成したテーブルを確認します。

(node1,node2,node3のサーバにおいて実施)
=# \d
           List of relations
 Schema |   Name    | Type  |  Owner
--------+-----------+-------+----------
 public | t1logical | table | postgres
(1 row)
  1. Publication作成

node1に作成したt1logicalテーブルをレプリケーション対象とするPublicationを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE PUBLICATION pub_node1_t1logical FOR TABLE t1logical;

以下のコマンドでPublicationが作成された事を確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node1_t1logical | postgres | f          | t       | t       | t
(1 row)
  1. Subscription作成

node2にnode1に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node2_t1logical CONNECTION 'host=node1 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node1_t1logical;

以下のコマンドでSubscriptionが作成された事を確認します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node2_t1logical | postgres | t       | {pub_node1_t1logical}
(1 row)
  1. Publication作成
node1に作成したPublicationとレプリケーションする対象となっていたテーブル(t1logical)を
新たにレプリケーション対象としたPublicationをnode2に作成します。
(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE PUBLICATION pub_node2_t1logical FOR TABLE t1logical;

以下のコマンドでPublicationが作成された事を確認します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node2_t1logical | postgres | f          | t       | t       | t
(1 row)
  1. Subscription作成

node3にnode2に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node3_t1logical CONNECTION 'host=node2 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node2_t1logical;

以下のコマンドでSubscriptionが作成された事を確認します。

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# \dRs
                      List of subscriptions
        Name         |  Owner   | Enabled |      Publication
---------------------+----------+---------+-----------------------
 sub_node3_t1logical | postgres | t       | {pub_node2_t1logical}
(1 row)
  1. ロジカルレプリケーション簡易動作検証

node1のPublication側のテーブルにレコードをINSERTします。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# INSERT INTO t1logical VALUES (1);
=# INSERT INTO t1logical VALUES (2);
=# SELECT * FROM t1logical;
 c1
----
1
2
(2 rows)
  1. ロジカルレプリケーション簡易動作検証確認

node2のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(node1からレプリケーションされている事を確認)

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t1logical;
 c1
----
1           <-- node1と同じ結果がレプリケーションされていること
2
(2 rows)

node3のSubscription側のテーブルにもレコードがINSERTされている事を確認します。(node2からレプリケーションされている事を確認)

(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# SELECT * FROM t1logical;
 c1
----
1           <-- node1と同じ結果がレプリケーションされていること
2
(2 rows)

■ 動作確認

  1. VIEWでのステータス確認

node1でPublicationの定義を確認します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest

・Publicationに設定したテーブルを確認
=# SELECT * FROM pg_publication_tables;
       pubname       | schemaname | tablename
---------------------+------------+-----------
 pub_node1_t1logical | public     | t1logical
(1 row)

・レプリケーションしている状態の確認
=# SELECT pid, application_name, client_addr, state, sync_state FROM pg_stat_replication;
 pid  |  application_name   |  client_addr  |   state   | sync_state
------+---------------------+---------------+-----------+------------
 31121| sub_node2_t1logical | node2         | streaming | async
(1 rows)

node2でのPublication/Subscriptionの定義を確認します。

(node2のサーバにおいて実施)
$ psql -U postgres logicalreptest

・Publicationに設定したテーブルを確認
=# SELECT * FROM pg_publication_tables;
       pubname       | schemaname | tablename
---------------------+------------+-----------
 pub_node2_t1logical | public     | t1logical
(1 row)

・レプリケーションしている状態の確認
=# SELECT pid, application_name, client_addr, state, sync_state FROM pg_stat_replication;
 pid  |  application_name   |  client_addr  |   state   | sync_state
------+---------------------+---------------+-----------+------------
 9185 | sub_node3_t1logical | node3         | streaming | async
(1 rows)

・Subscriptionの設定情報
=# SELECT subslotname, subpublications, subconninfo FROM pg_subscription;
     subslotname     |    subpublications    |                                 subconninfo
---------------------+-----------------------+-----------------------------------------------------------------------------
 sub_node2_t1logical | {pub_node1_t1logical} | host=node1 dbname=logicalreptest port=5432 user=postgres password=postgres
(1 rows)

node3のSubscriptionの定義を確認します。

(node3において実施)
$ psql -U postgres logicalreptest

・レプリケーションしている状態の確認
=# SELECT pid, application_name, client_addr, state, sync_state FROM pg_stat_replication;
(0 rows)

・Subscriptionの設定情報
=# SELECT subslotname, subpublications, subconninfo FROM pg_subscription;
     subslotname     |    subpublications    |                                subconninfo
---------------------+-----------------------+----------------------------------------------------------------------------
 sub_node3_t1logical | {pub_node2_t1logical} | host=node2 dbname=logicalreptest port=5432 user=postgres password=postgres
(1 row)

■ 追加検証(PublicationとSubscriptionの関係をループさせた場合の挙動)

  1. Publication作成
node2に作成したPublicationとレプリケーションする対象となっていたテーブル(t1logical)を
新たにレプリケーション対象としたPublicationをnode3に作成します。
(node3のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE PUBLICATION pub_node3_t1logical FOR TABLE t1logical;

以下のコマンドでPublicationが作成された事を確認します。

$ psql -U postgres logicalreptest
=# \dRp
                           List of publications
        Name         |  Owner   | All tables | Inserts | Updates | Deletes
---------------------+----------+------------+---------+---------+---------
 pub_node3_t1logical | postgres | f          | t       | t       | t
(1 row)
  1. Subscription作成

node1にnode3に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# CREATE SUBSCRIPTION sub_node1_t1logical CONNECTION 'host=node3 dbname=logicalreptest port=5432 user=postgres password=postgres' PUBLICATION pub_node3_t1logical;
  1. ロジカルレプリケーション簡易動作検証(ループ時)
レプリケーション元とレプリケーション先がループしているため、
初期データのコピー時に一意制約違反が発生し、5秒毎にエラーメッセージが出力されます。
(node1のサーバにおいて実施)
$ psql -U postgres logicalreptest
=# INSERT INTO t1logical VALUES (1);
(PostgreSQLログのエラーメッセージ)
[2018-01-31 17:18:03.060 JST][][][00000][444] LOG:  logical replication table synchronization worker for subscription "sub_node1_t1logical", table "t1logical" has started
[2018-01-31 17:18:03.072 JST][][][23505][444] ERROR:  duplicate key value violates unique constraint "t1logical_pkey"
[2018-01-31 17:18:03.072 JST][][][23505][444] DETAIL:  Key (c1)=(1) already exists.
[2018-01-31 17:18:03.072 JST][][][23505][444] CONTEXT:  COPY t1logical, line 1
[2018-01-31 17:18:03.073 JST][][][00000][227] LOG:  worker process: logical replication worker for subscription 16393 sync 16385 (PID 444) exited with exit code 1

6.3.5. パーティショニングとの組み合わせ

6.3.5.1. 検証構成

パーティショニングとの組み合わせ

6.3.5.2. 組み合わせた場合の利点

パーティショニングは大規模なテーブルに対し、データを複数テーブルに分割して格納する事で性能や運用性を向上させることができます。
また、ロジカルレプリケーションは論理的な変更内容を伝播させてレプリケーションさせる事で、複数サーバからデータを複製先のデータベースに集約させる事ができます。
これらを組み合わせる事で、複数サーバのデータを1つの集約サーバ上でパーティションテーブルとして管理する事が可能となります。

6.3.5.3. 検証内容

■ レプリケーション対象としたパーティショニングテーブルがSubscriberに反映されることを確認する。
■ Publisherと同等の処理がSubscriberのパーティショニングテーブルに行われる事を確認する。
■ 複数サーバの子テーブルを1つのサーバのパーティショニングテーブルに統合する構成が組めるか確認する。

6.3.5.4. 検証結果

■ 環境構築

  1. レプリケーション状態の確認

現在のレプリケーション状態を確認します。

(node1,node2のサーバにおいて実施)
$ psql -x -U postgres -c "SELECT * FROM pg_stat_replication;"
(0 rows)
  1. 試験用データベース作成

本検証で利用するデータベースを作成します。

(node1,node2のサーバにおいて実施)
$ createdb testlogicalre

以下のコマンドでデータベースが作成された事を確認します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres -l
                               List of databases
     Name      |  Owner   | Encoding | Collate | Ctype |   Access privileges
---------------+----------+----------+---------+-------+-----------------------
 testlogicalre | postgres | UTF8     | C       | C     |
[省略]
  1. 試験用テーブル作成(pgbench)
作成したデータベースにpgbenchを使用してテスト用のテーブルを作成します。
その後に、対象のテーブル(pgbench_history)をパーティション構文を用いてパーティショニング化します。
(node1,node2のサーバにおいて実施)
$ pgbench -i -U postgres testlogicalre
$ psql -U postgres testlogicalre
=# DROP TABLE pgbench_history;
=# CREATE TABLE pgbench_history(tid integer, bid integer, aid integer, delta integer, mtime timestamp without time zone, filler character(22)) PARTITION BY RANGE (mtime);
=# CREATE TABLE pgbench_history_201801 PARTITION OF pgbench_history FOR VALUES FROM ('2018-01-01') TO ('2018-02-01');
=# CREATE TABLE pgbench_history_201712 PARTITION OF pgbench_history FOR VALUES FROM ('2017-12-01') TO ('2018-01-01');
=# CREATE TABLE pgbench_history_201711 PARTITION OF pgbench_history FOR VALUES FROM ('2017-11-01') TO ('2017-12-01');
=# ALTER TABLE pgbench_history_201801 ADD CONSTRAINT pgbench_history_201801_pkey PRIMARY KEY(tid, aid);
=# ALTER TABLE pgbench_history_201712 ADD CONSTRAINT pgbench_history_201712_pkey PRIMARY KEY(tid, aid);
=# ALTER TABLE pgbench_history_201711 ADD CONSTRAINT pgbench_history_201711_pkey PRIMARY KEY(tid, aid);
=# SELECT relname, n_live_tup AS rowcount FROM pg_stat_all_tables WHERE relname LIKE 'pgbench_history%' ORDER BY relname;
        relname         | rowcount
------------------------+----------
 pgbench_history_201711 |        0
 pgbench_history_201712 |        0
 pgbench_history_201801 |        0
(3 rows)
  1. PublicationとSubscriptionを作成

node1に作成したパーティションの子テーブルをレプリケーション対象とするPublicationを作成します。

(node1のサーバにおいて実施)
=# CREATE PUBLICATION pub_node1_parttables FOR TABLE pgbench_history_201711;
=# ALTER PUBLICATION pub_node1_parttables ADD TABLE pgbench_history_201712;
=# ALTER PUBLICATION pub_node1_parttables ADD TABLE pgbench_history_201801;
=# SELECT * FROM pg_publication_tables;
       pubname        | schemaname |       tablename
----------------------+------------+------------------------
 pub_node1_parttables | public     | pgbench_history_201801
 pub_node1_parttables | public     | pgbench_history_201712
 pub_node1_parttables | public     | pgbench_history_201711
(3 rows)

以下のコマンドでPublicationが作成された事を確認します。

(node1のサーバにおいて実施)
=# \dRp
                           List of publications
         Name         |  Owner   | All tables | Inserts | Updates | Deletes
----------------------+----------+------------+---------+---------+---------
 pub_node1_parttables | postgres | f          | t       | t       | t
(1 row)

node2にnode1に作成したPublicationとレプリケーションするSubscriptionを作成します。

 (node2のサーバにおいて実施)
=# CREATE SUBSCRIPTION sub_node2_parttables CONNECTION 'host=node1 dbname=testlogicalre port=5432 user=postgres' PUBLICATION pub_node1_parttables;
=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   18381 |   18372 | r          | 0/109C2570
   18381 |   18369 | r          | 0/109C2570
   18381 |   18366 | r          | 0/109C25A8
(3 rows)

以下のコマンドでSubscriptionが作成された事を確認します。

(node2のサーバにおいて実施)
=# \dRs
                       List of subscriptions
         Name         |  Owner   | Enabled |      Publication
----------------------+----------+---------+------------------------
 sub_node2_parttables | postgres | t       | {pub_node1_parttables}
(1 row)

■ ロジカルレプリケーション簡易動作確認

  1. テーブルデータ追加

pgbenchで動作させるスクリプトを作成します。

(node1のサーバにおいて実施)
$ vi part_test.sql
-----
\set aid random(1, 90 * :scale)
\set bid random(1, 1 * :scale)
\set tid random(1, 10000 * :scale)
\set delta random(-5000, 5000)
BEGIN;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CAST('2018-01-31' AS date) - CAST(:aid AS integer));
UPDATE pgbench_history SET filler = to_char(:bid + :delta, '9999') WHERE tid = :tid AND aid = :aid;
END;
-----

node1のPublication側のテーブルに対して、pgbenchでスクリプトを実行します。

(node1のサーバにおいて実施)
$ pgbench -U postgres -c 1 -t 1000 -f part_test.sql testlogicalre

レコードがパーティショニングテーブルに振り分けられている事を確認します。

(node1のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# ANALYZE;
=# SELECT relname, n_live_tup AS rowcount FROM pg_stat_all_tables WHERE relname LIKE 'pgbench_history%' ORDER BY relname;
        relname         | rowcount
------------------------+----------
 pgbench_history_201711 |      348
 pgbench_history_201712 |      332
 pgbench_history_201801 |      320
(3 rows)

Publication側のパーティショニングテーブルのレコード情報を確認します。

(node1のサーバにおいて実施)
=# SELECT * FROM pgbench_history_201711 ORDER BY tid LIMIT 3;
 tid | bid | aid | delta |        mtime        |         filler
-----+-----+-----+-------+---------------------+------------------------
  27 |   1 |  75 |  2772 | 2017-11-17 00:00:00 |  2773
 125 |   1 |  90 |  1513 | 2017-11-02 00:00:00 |  1514
 146 |   1 |  81 | -3348 | 2017-11-11 00:00:00 | -3347
(3 rows)

(node1のサーバにおいて実施)
=# SELECT * FROM pgbench_history_201712 ORDER BY tid LIMIT 3;
 tid | bid | aid | delta |        mtime        |         filler
-----+-----+-----+-------+---------------------+------------------------
  66 |   1 |  35 | -4563 | 2017-12-27 00:00:00 | -4562
 156 |   1 |  55 |  1206 | 2017-12-07 00:00:00 |  1207
 231 |   1 |  51 |  -894 | 2017-12-11 00:00:00 |  -893
(3 rows)

(node1のサーバにおいて実施)
=# SELECT * FROM pgbench_history_201801 ORDER BY tid LIMIT 3;
 tid | bid | aid | delta |        mtime        |         filler
-----+-----+-----+-------+---------------------+------------------------
  13 |   1 |  22 |   339 | 2018-01-09 00:00:00 |   340
  22 |   1 |  29 | -1540 | 2018-01-02 00:00:00 | -1539
  41 |   1 |  13 | -4569 | 2018-01-18 00:00:00 | -4568
(3 rows)

パーティショニングテーブルの予測行数がnode1と一致しているか否かを確認します。

(node2のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# ANALYZE;
=# SELECT relname, n_live_tup AS rowcount FROM pg_stat_all_tables WHERE relname LIKE 'pgbench_history%' ORDER BY relname;
        relname         | rowcount
------------------------+----------
 pgbench_history_201711 |      348
 pgbench_history_201712 |      332
 pgbench_history_201801 |      320
(3 rows)

Subscription側のパーティショニングテーブルでnode1とレコードが一致している事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
=# SELECT * FROM pgbench_history_201711 ORDER BY tid LIMIT 3;
 tid | bid | aid | delta |        mtime        |         filler
-----+-----+-----+-------+---------------------+------------------------
  27 |   1 |  75 |  2772 | 2017-11-17 00:00:00 |  2773
 125 |   1 |  90 |  1513 | 2017-11-02 00:00:00 |  1514
 146 |   1 |  81 | -3348 | 2017-11-11 00:00:00 | -3347
(3 rows)

=# SELECT * FROM pgbench_history_201712 ORDER BY tid LIMIT 3;
 tid | bid | aid | delta |        mtime        |         filler
-----+-----+-----+-------+---------------------+------------------------
  66 |   1 |  35 | -4563 | 2017-12-27 00:00:00 | -4562
 156 |   1 |  55 |  1206 | 2017-12-07 00:00:00 |  1207
 231 |   1 |  51 |  -894 | 2017-12-11 00:00:00 |  -893
(3 rows)

=# SELECT * FROM pgbench_history_201801 ORDER BY tid LIMIT 3;
 tid | bid | aid | delta |        mtime        |         filler
-----+-----+-----+-------+---------------------+------------------------
  13 |   1 |  22 |   339 | 2018-01-09 00:00:00 |   340
  22 |   1 |  29 | -1540 | 2018-01-02 00:00:00 | -1539
  41 |   1 |  13 | -4569 | 2018-01-18 00:00:00 | -4568
(3 rows)
  1. レプリケーション状態確認
現在のレプリケーション状態を確認します。
作成したPublicationとSubscriptionが正常にレプリケーションされているかを
application_name、state、sync_state項目で確認します。
(node1のサーバにおいて実施)
$ psql -x -U postgres -c "SELECT * FROM pg_stat_replication;"
-[ RECORD 1 ]----+------------------------------
pid              | 1446
usesysid         | 10
usename          | postgres
application_name | sub_node2_parttables             <-- subscriptionの名前が表示される
client_addr      | 192.168.56.102
client_hostname  |
client_port      | 52428
backend_start    | 2018-02-05 10:21:33.710471+09
backend_xmin     |
state            | streaming                <-- streaming が表示される
sent_lsn         | 0/10A29160
write_lsn        | 0/10A29160
flush_lsn        | 0/10A29160
replay_lsn       | 0/10A29160
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async            <-- async(非同期)

■ テーブル追加

  1. テーブル作成

新たにパーティショニングテーブルを作成します。

(node1,node2のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# CREATE TABLE pgbench_history_201802 PARTITION OF pgbench_history FOR VALUES FROM ('2018-02-01') TO ('2018-03-01');
=# ALTER TABLE pgbench_history_201802 ADD CONSTRAINT pgbench_history_201802_pkey PRIMARY KEY(tid, aid);

以下のコマンドで作成したテーブルを確認します。

=# \dt
                 List of relations
 Schema |          Name          | Type  |  Owner
--------+------------------------+-------+----------
 public | pgbench_accounts       | table | postgres
 public | pgbench_branches       | table | postgres
 public | pgbench_history        | table | postgres
 public | pgbench_history_201711 | table | postgres
 public | pgbench_history_201712 | table | postgres
 public | pgbench_history_201801 | table | postgres
 public | pgbench_history_201802 | table | postgres
 public | pgbench_tellers        | table | postgres
(8 rows)
  1. 既存のロジカルレプリケーションにテーブルを追加

作成したパーティショニングテーブルを既存のPublicationに追加します。

(node1のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# ALTER PUBLICATION pub_node1_parttables ADD TABLE pgbench_history_201802;
=# SELECT * FROM pg_publication_tables;
       pubname        | schemaname |       tablename
----------------------+------------+------------------------
 pub_node1_parttables | public     | pgbench_history_201801
 pub_node1_parttables | public     | pgbench_history_201712
 pub_node1_parttables | public     | pgbench_history_201711
 pub_node1_parttables | public     | pgbench_history_201802
(4 rows)

更新したPublicationの情報をSubscriptionに反映させます。

(node2のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   18381 |   18372 | r          | 0/109C2570
   18381 |   18369 | r          | 0/109C2570
   18381 |   18366 | r          | 0/109C25A8
(3 rows)

=# ALTER SUBSCRIPTION sub_node2_parttables REFRESH PUBLICATION;
=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   18381 |   18382 | r          | 0/10A3EB98
   18381 |   18372 | r          | 0/109C2570
   18381 |   18369 | r          | 0/109C2570
   18381 |   18366 | r          | 0/109C25A8
(4 rows)

■ ロジカルレプリケーション動作確認

  1. 新規テーブルにデータ追加

新たに作成したPublication側のパーティショニングテーブルにレコードをINSERTします。

(node1のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# SELECT * FROM pgbench_history_201802;
 tid | bid | aid | delta | mtime | filler
-----+-----+-----+-------+-------+--------
(0 rows)
=# INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (7777, 1, 12, 4321, '2018-02-11 00:00:00');
=# SELECT * FROM pgbench_history_201802;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 7777 |   1 |  12 |  4321 | 2018-02-11 00:00:00 |
(1 row)

Subscription側のパーティショニングテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node2のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# SELECT * FROM pgbench_history_201802;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 7777 |   1 |  12 |  4321 | 2018-02-11 00:00:00 |
(1 row)

■ 子テーブルの取り外し/取り付け

  1. パーティションからテーブルを取り外し

node1のパーティショニングテーブルの取り外しとPublicationからの削除を行います。

(node1のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# SELECT * FROM pgbench_history WHERE tid = 7777;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 7777 |   1 |  24 |  4321 | 2018-02-11 00:00:00 |
(1 row)

=# SELECT * FROM pg_publication_tables;
       pubname        | schemaname |       tablename
----------------------+------------+------------------------
 pub_node1_parttables | public     | pgbench_history_201801
 pub_node1_parttables | public     | pgbench_history_201712
 pub_node1_parttables | public     | pgbench_history_201711
 pub_node1_parttables | public     | pgbench_history_201802
(4 rows)

=# ALTER TABLE pgbench_history DETACH PARTITION pgbench_history_201802;
=# ALTER PUBLICATION pub_node1_parttables DROP TABLE pgbench_history_201802;
=# SELECT * FROM pgbench_history WHERE tid = 7777;
 tid | bid | aid | delta | mtime | filler
-----+-----+-----+-------+-------+--------
(0 rows)

=# SELECT * FROM pg_publication_tables;
       pubname        | schemaname |       tablename
----------------------+------------+------------------------
 pub_node1_parttables | public     | pgbench_history_201801
 pub_node1_parttables | public     | pgbench_history_201712
 pub_node1_parttables | public     | pgbench_history_201711
(3 rows)

node2のパーティショニングテーブルの取り外しと更新したPublicationの情報のSubscriptionへの反映を行います。

(node2のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# SELECT * FROM pgbench_history WHERE tid = 7777;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 7777 |   1 |  24 |  4321 | 2018-02-11 00:00:00 |
(1 row)

=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   18381 |   18382 | r          | 0/10A3EB98
   18381 |   18372 | r          | 0/109C2570
   18381 |   18369 | r          | 0/109C2570
   18381 |   18366 | r          | 0/109C25A8
(4 rows)

=# ALTER TABLE pgbench_history DETACH PARTITION pgbench_history_201802;
=# ALTER SUBSCRIPTION sub_node2_parttables REFRESH PUBLICATION;
=# SELECT * FROM pgbench_history WHERE tid = 7777;
 tid | bid | aid | delta | mtime | filler
-----+-----+-----+-------+-------+--------
(0 rows)

=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   18381 |   18372 | r          | 0/109C2570
   18381 |   18369 | r          | 0/109C2570
   18381 |   18366 | r          | 0/109C25A8
(3 rows)
  1. パーティションにテーブルを取り付け

node1のパーティショニングテーブルの取り付けとPublicationへの追加を行います。

(node1のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# TRUNCATE TABLE pgbench_history_201802;
=# ALTER TABLE pgbench_history ATTACH PARTITION pgbench_history_201802 FOR VALUES FROM ('2018-02-01') TO ('2018-03-01');
=# ALTER PUBLICATION pub_node1_parttables ADD TABLE pgbench_history_201802;
=# SELECT * FROM pg_publication_tables;
       pubname        | schemaname |       tablename
----------------------+------------+------------------------
 pub_node1_parttables | public     | pgbench_history_201801
 pub_node1_parttables | public     | pgbench_history_201712
 pub_node1_parttables | public     | pgbench_history_201711
 pub_node1_parttables | public     | pgbench_history_201802
(4 rows)

node2のパーティショニングテーブルの取り付けと更新したPublicationの情報のSubscriptionへの反映を行います。

(node2のサーバにおいて実施)
$ psql -U postgres testlogicalre
=# TRUNCATE TABLE pgbench_history_201802;
=# ALTER TABLE pgbench_history ATTACH PARTITION pgbench_history_201802 FOR VALUES FROM ('2018-02-01') TO ('2018-03-01');
=# ALTER SUBSCRIPTION sub_node2_parttables REFRESH PUBLICATION;
=# SELECT * FROM pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   18381 |   18382 | r          | 0/10A523F8
   18381 |   18372 | r          | 0/109C2570
   18381 |   18369 | r          | 0/109C2570
   18381 |   18366 | r          | 0/109C25A8
(4 rows)

■ 子テーブルをレプリケーションし、パーティショニングを構成

子テーブルをレプリケーションし、パーティショニングを構成
  1. 試験用データベース作成

本検証で利用するデータベースを作成します。

(node1のサーバにおいて実施)
$ createdb testlogicalre2

(node2のサーバにおいて実施)
$ createdb testlogicalre2_c1
$ createdb testlogicalre2_c2
$ createdb testlogicalre2_c3

以下のコマンドでデータベースが作成された事を確認します。

(node1のサーバにおいて実施)
$ psql -U postgres -p 5432 -l
                               List of databases
      Name      |  Owner   | Encoding | Collate | Ctype |   Access privileges
----------------+----------+----------+---------+-------+-----------------------
 testlogicalre2 | postgres | UTF8     | C       | C     |
[省略]

(node2のサーバにおいて実施)
$ psql -U postgres -p 5432 -l
                                 List of databases
       Name        |  Owner   | Encoding | Collate | Ctype |   Access privileges
-------------------+----------+----------+---------+-------+-----------------------
 testlogicalre2_c1 | postgres | UTF8     | C       | C     |
 testlogicalre2_c2 | postgres | UTF8     | C       | C     |
 testlogicalre2_c3 | postgres | UTF8     | C       | C     |
[省略]
  1. 試験用テーブル作成(pgbench)
node1で作成したデータベースにpgbenchを使用してテスト用のテーブルを作成します。
その後に、対象のテーブル(pgbench_history)をパーティション構文を用いてパーティショニング化します。
(node1のサーバにおいて実施)
$ pgbench -i -U postgres testlogicalre2
$ psql -U postgres testlogicalre2
=# DROP TABLE pgbench_history;
=# CREATE TABLE pgbench_history(tid integer, bid integer, aid integer, delta integer, mtime timestamp without time zone, filler character(22)) PARTITION BY RANGE (mtime);
=# CREATE TABLE pgbench_history_201711 PARTITION OF pgbench_history FOR VALUES FROM ('2017-11-01') TO ('2017-12-01');
=# CREATE TABLE pgbench_history_201712 PARTITION OF pgbench_history FOR VALUES FROM ('2017-12-01') TO ('2018-01-01');
=# CREATE TABLE pgbench_history_201801 PARTITION OF pgbench_history FOR VALUES FROM ('2018-01-01') TO ('2018-02-01');
=# ALTER TABLE pgbench_history_201711 ADD CONSTRAINT pgbench_history_201711_pkey PRIMARY KEY(tid, aid);
=# ALTER TABLE pgbench_history_201712 ADD CONSTRAINT pgbench_history_201712_pkey PRIMARY KEY(tid, aid);
=# ALTER TABLE pgbench_history_201801 ADD CONSTRAINT pgbench_history_201801_pkey PRIMARY KEY(tid, aid);
=# SELECT relname, n_live_tup AS rowcount FROM pg_stat_all_tables WHERE relname LIKE 'pgbench_history%' ORDER BY relname;
        relname         | rowcount
------------------------+----------
 pgbench_history_201711 |        0
 pgbench_history_201712 |        0
 pgbench_history_201801 |        0
(3 rows)

node2で作成した各データベースにテーブルを作成します。

(node2のサーバにおいて実施)
$ psql -U postgres -p 5432 testlogicalre2_c1
=# CREATE TABLE pgbench_history_201711(tid integer, bid integer, aid integer, delta integer, mtime timestamp without time zone, filler character(22));
=# ALTER TABLE pgbench_history_201711 ADD CONSTRAINT pgbench_history_201711_pkey PRIMARY KEY(tid, aid);

$ psql -U postgres -p 5432 testlogicalre2_c2
=# CREATE TABLE pgbench_history_201712(tid integer, bid integer, aid integer, delta integer, mtime timestamp without time zone, filler character(22));
=# ALTER TABLE pgbench_history_201712 ADD CONSTRAINT pgbench_history_201712_pkey PRIMARY KEY(tid, aid);

$ psql -U postgres -p 5432 testlogicalre2_c3
=# CREATE TABLE pgbench_history_201801(tid integer, bid integer, aid integer, delta integer, mtime timestamp without time zone, filler character(22));
=# ALTER TABLE pgbench_history_201801 ADD CONSTRAINT pgbench_history_201801_pkey PRIMARY KEY(tid, aid);
  1. PublicationとSubscriptionを作成

node2の各データベースに作成したテーブルをレプリケーション対象とするPublicationをそれぞれ作成します。

(node2のサーバにおいて実施)
$ psql -U postgres -p 5432 testlogicalre2_c1
=# CREATE PUBLICATION pub_node2_parttables1 FOR TABLE pgbench_history_201711;
$ psql -U postgres -p 5432 testlogicalre2_c2
=# CREATE PUBLICATION pub_node2_parttables2 FOR TABLE pgbench_history_201712;
$ psql -U postgres -p 5432 testlogicalre2_c3
=# CREATE PUBLICATION pub_node2_parttables3 FOR TABLE pgbench_history_201801;

node1にnode2に作成したPublicationとレプリケーションするSubscriptionを作成します。

(node1のサーバにおいて実施)
$ psql -U postgres -p 5432 testlogicalre2
=# CREATE SUBSCRIPTION sub_node1_parttables1 CONNECTION 'host=node2 dbname=testlogicalre2_c1 port=5432 user=postgres' PUBLICATION pub_node2_parttables1;
=# CREATE SUBSCRIPTION sub_node1_parttables2 CONNECTION 'host=node2 dbname=testlogicalre2_c2 port=5432 user=postgres' PUBLICATION pub_node2_parttables2;
=# CREATE SUBSCRIPTION sub_node1_parttables3 CONNECTION 'host=node2 dbname=testlogicalre2_c3 port=5432 user=postgres' PUBLICATION pub_node2_parttables3;
  1. ロジカルレプリケーション簡易動作確認

node2の各データベースのPublication側のテーブルにレコードをINSERTします。

(node2のサーバにおいて実施)
$ psql -U postgres -p 5432 testlogicalre2_c1
=# INSERT INTO pgbench_history_201711 (tid, bid, aid, delta, mtime) VALUES (1111, 1, 12, 4321, '2017-11-11 00:00:00');
=# SELECT * FROM pgbench_history_201711;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 1111 |   1 |  12 |  4321 | 2017-11-11 00:00:00 |
(1 row)

$ psql -U postgres -p 5432 testlogicalre2_c2
=# INSERT INTO pgbench_history_201712 (tid, bid, aid, delta, mtime) VALUES (2222, 2, 34, 8765, '2017-12-11 00:00:00');
=# SELECT * FROM pgbench_history_201712;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 2222 |   2 |  34 |  8765 | 2017-12-11 00:00:00 |
(1 row)

$ psql -U postgres -p 5432 testlogicalre2_c3
=# INSERT INTO pgbench_history_201801 (tid, bid, aid, delta, mtime) VALUES (3333, 3, 56, 2109, '2018-01-11 00:00:00');
=# SELECT * FROM pgbench_history_201801;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 3333 |   3 |  56 |  2109 | 2018-01-11 00:00:00 |
(1 row)

Subscription側のテーブルにもレコードがINSERTされている事を確認します。(レプリケーションされている事を確認)

(node1のサーバにおいて実施)
$ psql -U postgres -p 5432 testlogicalre2
=# SELECT * FROM pgbench_history;
 tid  | bid | aid | delta |        mtime        | filler
------+-----+-----+-------+---------------------+--------
 3333 |   3 |  56 |  2109 | 2018-01-11 00:00:00 |
 2222 |   2 |  34 |  8765 | 2017-12-11 00:00:00 |
 1111 |   1 |  12 |  4321 | 2017-11-11 00:00:00 |
(3 rows)

6.4. ロジカルレプリケーションの運用

6.4.1. 監視

ロジカルレプリケーションが稼働する環境を監視する際に利用する情報について説明します。

6.4.1.1. 前提とする環境

本章は以下の環境を利用した検証結果を元に解説します。なお、ストリーミングレプリケーションと共用する情報(動的統計情報ビュー pg_stat_replication 等)もあるため、それぞれの表示形式の違いが比較できるようにロジカルレプリケーションとストリーミングレプリケーションを両方利用する環境を利用します。

_images/logical_replica_monitor_environment.png
  • PostgreSQLサーバは3台構成
  • ロジカルレプリケーションは、Publisher1台(サーバ1。IPアドレス 192。168。127。31)、Subscriber1台(サーバ2。IPアドレス 192。168。127。32)で構成
  • ストリーミングレプリケーションは、マスタ1台(Publisherと同一)、スタンバイ1台(サーバ3。IPアドレス 192。168。127。33)で構成
  • ロジカルレプリケーションのPublisherでは、データベースクラスタ内に2つのデータベースを作成
    • pubdb: レプリケーション対象のテーブルを個別定義するPublicationを作成
    • pubdb_all_table: データベース内に存在する全テーブルをレプリケーション対象とするPublicationを作成
  • ロジカルレプリケーションのSubscriberでは、データベースクラスタ内に2つのデータベースを作成
    • subdb: Publisherのpubdbデータベースに定義したPublicationから変更データを受け取るSubscriptionを作成
    • subdb_all_table: Publisherのpubdb_all_tableデータベースに定義したPublicationから変更データを受け取るSubscriptionを作成

6.4.1.2. ロジカルレプリケーションの設定

6.4.1.2.1. Publisherで確認できる情報

Publisherのサーバにおいて、Publicationが作成されたデータベースに接続して確認できる情報は以下のとおりです。

■システムカタログ pg_publication [1]

接続中のデータベースに定義されているPublicationの情報を確認できます。 Publicationの名前、所有者に加えて、レプリケーションが行われる更新処理の種別がわかります。 また、FOR ALL TABLES句を指定して作成したPublicationは、puballtables = t となります。

(サーバ1において実施)
pubdb=# select oid, * from pg_publication;
  oid  | pubname | pubowner | puballtables | pubinsert | pubupdate | pubdelete
-------+---------+----------+--------------+-----------+-----------+-----------
 16392 | pub1    |    16386 | f            | t         | t         | t
 16399 | pub2    |    16386 | f            | t         | t         | t
 16471 | pub3    |       10 | f            | t         | t         | t

pubdb_all_table=# select oid, * from pg_publication;
  oid  |    pubname    | pubowner | puballtables | pubinsert | pubupdate | pubdelete
-------+---------------+----------+--------------+-----------+-----------+-----------
 16475 | pub_all_table |       10 | t            | t         | t         | t
[1]PostgreSQL 10.0文書 - 51.40. pg_publication

■システムカタログ pg_publication_rel [2]

接続中のデータベースに定義されているPublicationがレプリケーション対象とするテーブルを確認できます。

(サーバ1において実施)
pubdb=# select * from pg_publication_rel;
 prpubid | prrelid
---------+---------
   16392 |   16387
   16399 |   16394
   16471 |   16465
   16392 |   16496

pubdb_all_table=# select * from pg_publication_rel;
 prpubid | prrelid
---------+---------

ただし、Publication、テーブルのOIDしか保有しないため、実際に利用するには pg_publication, pg_class と結合して、 それぞれの名称を取得する必要があるでしょう。また、FOR ALL TABLES句を指定して作成したPublicationでは レプリケーション対象となっているテーブルが表示されません。

[2]PostgreSQL 10.0文書 - 51.41. pg_publication_rel

■システムカタログ pg_publication_tables [3]

接続中のデータベースに定義されているPublicationがレプリケーション対象とするテーブルを確認できます。

(サーバ1において実施)
pubdb=# select * from pg_publication_tables;
 pubname | schemaname | tablename
---------+------------+-----------
 pub1    | public     | data1
 pub1    | public     | data1_1
 pub2    | public     | data2
 pub3    | public     | data3

pubdb_all_table=# select * from pg_publication_tables;
    pubname    | schemaname | tablename
---------------+------------+-----------
 pub_all_table | public     | data1
 pub_all_table | public     | data2

前述のpg_publication_relと比較して、Publication、テーブルの名前が直接確認できること、 FOR ALL TABLES句を指定して作成したPublicationでもレプリケーション対象のテーブルが確認できることから 実際の運用ではこちらを活用するとよいでしょう。

[3]PostgreSQL 10.0文書 - 51.78. pg_publication_tables

■psqlのメタコマンド

psqlコマンドのメタコマンド「dRp+」でもPublicationの情報を確認できます。

(サーバ1において実施)
pubdb=# \dRp+
                  Publication pub1
  Owner  | All tables | Inserts | Updates | Deletes
---------+------------+---------+---------+---------
 pubusr1 | f          | t       | t       | t
 Tables:
    "public.data1"
    "public.data1_1"

                  Publication pub2
  Owner  | All tables | Inserts | Updates | Deletes
---------+------------+---------+---------+---------
 pubusr1 | f          | t       | t       | t
 Tables:
    "public.data2"

                  Publication pub3
  Owner   | All tables | Inserts | Updates | Deletes
----------+------------+---------+---------+---------
 postgres | f          | t       | t       | t
 Tables:
    "public.data3"

pubdb_all_table=# \dRp+
              Publication pub_all_table
  Owner   | All tables | Inserts | Updates | Deletes
----------+------------+---------+---------+---------
 postgres | t          | t       | t       | t

システムカタログpg_publication, pg_publication_relの情報がまとめて表示されますが、 FOR ALL TABLES句を指定して作成したPublicationのテーブルは表示されません。

■システムカタログpg_replication_slots [4]

Publisherに自動作成されるロジカルレプリケーションスロットの情報を確認できます。

(サーバ1において実施)
pubdb=# select * from pg_replication_slots;
   slot_name   |  plugin  | slot_type | datoid |    database     | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn
---------------+----------+-----------+--------+-----------------+-----------+--------+------------+------+--------------+-------------+---------------------
 sub3          | pgoutput | logical   |  16384 | pubdb           | f         | t      |      31511 |      |          684 | 0/56083970  | 0/560839A8
 sub2          | pgoutput | logical   |  16384 | pubdb           | f         | t      |      31509 |      |          684 | 0/56083970  | 0/560839A8
 sub1          | pgoutput | logical   |  16384 | pubdb           | f         | t      |      32366 |      |          684 | 0/56083970  | 0/560839A8
 sub_all_table | pgoutput | logical   |  16474 | pubdb_all_table | f         | t      |      31512 |      |          684 | 0/56083970  | 0/560839A8

Publicationの情報は接続するデータベースに存在するものしか見えませんが、レプリケーションスロットはデータベースクラスタ内に存在するものが全て表示されます。 なお、slot_type はロジカルレプリケーションスロットでは logical 、ストリーミングレプリケーションスロットは physical と表示されます。

[4]PostgreSQL 10.0文書 - 51.80. pg_replication_slots
6.4.1.2.2. Subscriberで確認できる情報

Subscriberのサーバにおいて、Subscriptionが作成されたデータベースに接続して確認できる情報は以下のとおりです。

■システムカタログ pg_subscription [5]

データベースに存在するSubscriptionの情報を確認できます。 Subscriptionの名前、所有者に加えて、接続先のデータベース、Publicationの名前等がわかります。 なお、pg_publicationでは接続したデータベース内のPublicationのみが見えましたが、pg_subscriptionでは接続したデータベースに関わらず、 全てのSubscriptionが表示されます。

(サーバ2において実施)
subdb=# select * from pg_subscription;
 subdbid |    subname    | subowner | subenabled |                               subconninfo                                |  subslotname  | subsynccommit | subpublications
---------+---------------+----------+------------+--------------------------------------------------------------------------+---------------+---------------+-----------------
   16384 | sub2          |       10 | t          | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1           | sub2          | off           | {pub2}
   16384 | sub3          |       10 | t          | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1           | sub3          | off           | {pub3}
   16477 | sub_all_table |       10 | t          | host=192.168.127.31 dbname=pubdb_all_table user=repusr1 password=repusr1 | sub_all_table | off           | {pub_all_table}
   16384 | sub1          |       10 | t          | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1           | sub1          | off           | {pub1}
[5]PostgreSQL 10.0文書 - 51.52. pg_subscription

■システムカタログ pg_subscription_rel [6]

接続中のデータベースに定義されているSubscriptionがレプリケーション対象とするテーブルを確認できます。 また、対象テーブルの他に、レプリケーションのステータス(i = initialize, d = data is being copied, s = synchronized, r = ready (normal replication))やLSNが表示されます。

(サーバ2において実施)
subdb=# select * from pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   16463 |   16455 | r          | 0/55FB63D0
   16400 |   16395 | r          | 0/16D7B60
   16476 |   16470 | r          | 0/55FDE4A0
   16463 |   16490 | r          | 0/56068D38

subdb_all_table=# select * from pg_subscription_rel;
 srsubid | srrelid | srsubstate |  srsublsn
---------+---------+------------+------------
   16484 |   16479 | r          | 0/5602B818
   16484 |   16485 | r          | 0/56048B00

ただし、Subscription、テーブルのOIDしか保有しないため、実際に利用するには pg_subscription, pg_class と結合して、それぞれの名称を取得する必要があるでしょう。

[6]PostgreSQL 10.0文書 - 51.53. pg_subscription_rel

■psqlのメタコマンド

psqlコマンドのメタコマンド「dRs+」でもSubscriptionの情報を確認できます。 システムカタログpg_subscriptionと同様の情報が見やすく整形されて表示されます。

(サーバ2において実施)
subdb=# \dRs+
                                                    List of subscriptions
 Name |  Owner   | Enabled | Publication | Synchronous commit |                            Conninfo
------+----------+---------+-------------+--------------------+----------------------------------------------------------------
 sub1 | postgres | t       | {pub1}      | off                | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1
 sub2 | postgres | t       | {pub2}      | off                | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1
 sub3 | postgres | t       | {pub3}      | off                | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1

subdb_all_table=# \dRs+
                                                                List of subscriptions
  Name         |  Owner   | Enabled |   Publication   | Synchronous commit |                                 Conninfo
---------------+----------+---------+-----------------+--------------------+--------------------------------------------------------------------------
 sub_all_table | postgres | t       | {pub_all_table} | off                | host=192.168.127.31 dbname=pubdb_all_table user=repusr1 password=repusr1

なお、pg_subscriptionではデータベースクラスタ内のSUBSCRIPTIONが全て表示されたのに対し、メタコマンド「dRs+」では接続中のデータベース内のSubscriptionしか表示されません。

6.4.1.3. ロジカルレプリケーションの稼働状態

6.4.1.3.1. Publisherで確認できる情報

Publisherのサーバにおいて確認できる情報は以下のとおりです。

■サーバログ

CREATE PUBLICATION文を実行した時点ではPublisherのサーバログには何も表示されません。 SubscriberでCREATE SUBSCRIPTION文を実行した時点で以下のログが出力されます。

(サーバ1のサーバログ)
2018-02-06 11:24:40.990 JST [20086] LOG:  starting logical decoding for slot "sub_all_table"
2018-02-06 11:24:40.990 JST [20086] DETAIL:  streaming transactions committing after 0/56084A90, reading WAL from 0/56084A58

■動的統計情報ビュー pg_stat_replication [7]

ストリーミングレプリケーションと同様に、ロジカルレプリケーションの稼働状況を確認できます。 接続中のデータベースに関わらず、データベースクラスタ全体の情報が表示されます。

(サーバ1において実施)
postgres=# \x
postgres=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 20086
usesysid         | 16385
usename          | repusr1
application_name | sub_all_table
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46452
backend_start    | 2018-02-06 11:24:40.988512+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/58000140
write_lsn        | 0/58000140
flush_lsn        | 0/58000140
replay_lsn       | 0/58000140
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 2 ]----+------------------------------
pid              | 31511
usesysid         | 16385
usename          | repusr1
application_name | sub3
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46353
backend_start    | 2018-01-17 11:07:07.204636+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/58000140
write_lsn        | 0/58000140
flush_lsn        | 0/58000140
replay_lsn       | 0/58000140
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 3 ]----+------------------------------
pid              | 32366
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46411
backend_start    | 2018-01-17 15:01:47.434102+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/58000140
write_lsn        | 0/58000140
flush_lsn        | 0/58000140
replay_lsn       | 0/58000140
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 4 ]----+------------------------------
pid              | 31509
usesysid         | 16385
usename          | repusr1
application_name | sub2
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46351
backend_start    | 2018-01-17 11:07:07.188398+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/58000140
write_lsn        | 0/58000140
flush_lsn        | 0/58000140
replay_lsn       | 0/58000140
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 5 ]----+------------------------------
pid              | 21625
usesysid         | 16385
usename          | repusr1
application_name | nk_PGECons3
client_addr      | 192.168.127.33
client_hostname  |
client_port      | 55519
backend_start    | 2018-02-06 16:00:40.749454+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/58000140
write_lsn        | 0/58000140
flush_lsn        | 0/58000140
replay_lsn       | 0/58000140
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

ここでは RECORD 1〜4 でサーバ1-2間のロジカルレプリケーション、RECORD 5 でサーバ1-3間のストリーミングレプリケーションの情報が表示されています。ロジカルレプリケーションとストリーミングレプリケーションはほぼ同じ形式で見えますが、application_nameはそれぞれ以下の情報が表示されています。

  • ロジカルレプリケーション:Subscriptionの名前
  • ストリーミングレプリケーション: スレーブのrecovery.confに記載したapplication_name

なお、Subscriptionを停止させると、pg_stat_replicationから該当するレプリケージョンの情報が表示されなくなります。

(サーバ2において実施)
subdb=# ALTER SUBSCRIPTION sub1 DISABLE;
ALTER SUBSCRIPTION
pubdb=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 20086
usesysid         | 16385
usename          | repusr1
application_name | sub_all_table
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46452
backend_start    | 2018-02-06 11:24:40.988512+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580074E0
write_lsn        | 0/580074E0
flush_lsn        | 0/580074E0
replay_lsn       | 0/580074E0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 2 ]----+------------------------------
pid              | 31511
usesysid         | 16385
usename          | repusr1
application_name | sub3
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46353
backend_start    | 2018-01-17 11:07:07.204636+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580074E0
write_lsn        | 0/580074E0
flush_lsn        | 0/580074E0
replay_lsn       | 0/580074E0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 3 ]----+------------------------------
pid              | 31509
usesysid         | 16385
usename          | repusr1
application_name | sub2
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46351
backend_start    | 2018-01-17 11:07:07.188398+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580074E0
write_lsn        | 0/580074E0
flush_lsn        | 0/580074E0
replay_lsn       | 0/580074E0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 4 ]----+------------------------------
pid              | 21625
usesysid         | 16385
usename          | repusr1
application_name | nk_PGECons3
client_addr      | 192.168.127.33
client_hostname  |
client_port      | 55519
backend_start    | 2018-02-06 16:00:40.749454+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580074E0
write_lsn        | 0/580074E0
flush_lsn        | 0/580074E0
replay_lsn       | 0/580074E0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

ロジカルレプリケーションもストリーミングレプリケーションも、pg_stat_replicationの件数を定期的に確認することで稼働状況を監視するとよいでしょう。

[7]PostgreSQL 10.0文書 - 表28.5 pg_stat_replicationビュー
6.4.1.3.2. Subscriberで確認できる情報

Subscriberのサーバにおいて確認できる情報は以下のとおりです。

■サーバログ

CREATE SUBSCRIPTION文を実行した時点で以下のログが出力されます。

(サーバ2のサーバログ)
2018-02-06 11:20:29.883 JST [4245] LOG:  logical replication apply worker for subscription "sub_all_table" has started
2018-02-06 11:20:29.888 JST [4246] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data1" has started
2018-02-06 11:20:29.899 JST [4247] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data2" has started
2018-02-06 11:20:29.901 JST [4246] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data1" has finished
2018-02-06 11:20:29.912 JST [4247] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data2" has finished

「logical replication table synchronization worker for subscription (Subscription名), table (テーブル名) has finished」と出力されていれば、レプリケーション対象のテーブルに対する初期データの転送が完了しており正常にロジカルレプリケーションが開始したことが確認できます。

■動的統計情報ビュー pg_stat_subscription [8]

Subscription単位にロジカルレプリケーションの稼働状況を確認できます。 接続中のデータベースに関わらず、データベースクラスタ全体の情報が表示されます。

(サーバ2において実施)
subdb_all_table=# select * from pg_stat_subscription;
 subid |    subname    |  pid  | relid | received_lsn |      last_msg_send_time       |     last_msg_receipt_time     | latest_end_lsn |        latest_end_time
-------+---------------+-------+-------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------
 16400 | sub2          | 31707 |       | 0/58000140   | 2018-02-06 16:00:07.649126+09 | 2018-02-06 16:00:07.482495+09 | 0/58000140     | 2018-02-06 16:00:07.649126+09
 16476 | sub3          | 31709 |       | 0/58000140   | 2018-02-06 16:00:07.649515+09 | 2018-02-06 16:00:07.482694+09 | 0/58000140     | 2018-02-06 16:00:07.649515+09
 16463 | sub1          | 32626 |       | 0/58000140   | 2018-02-06 16:00:07.649203+09 | 2018-02-06 16:00:07.482386+09 | 0/58000140     | 2018-02-06 16:00:07.649203+09
 16508 | sub_all_table |  4267 |       | 0/58000140   | 2018-02-06 16:00:07.649259+09 | 2018-02-06 16:00:07.482608+09 | 0/58000140     | 2018-02-06 16:00:07.649259+09
[8]PostgreSQL 10.0文書 - 表28.7 pg_stat_subscription View

6.4.2. 障害時の動作確認

ロジカルレプリケーションが稼働する環境で障害が発生した場合の挙動を確認します。

6.4.2.1. 前提とする環境

本章は以下の環境を利用した検証結果を元に解説します。

_images/logical_replica_failure_environment.png
  • ロジカルレプリケーションは、Publisher1台、Subscriber1台で構成
  • ロジカルレプリケーションのPublisher(サーバ1。IPアドレス 192。168。127。31)では、データベースクラスタ内に1つのデータベースを作成
    • pubdb: レプリケーション対象のテーブルを個別定義するPublication pub1を作成
  • ロジカルレプリケーションのSubscriber(サーバ2。IPアドレス 192。168。127。32)では、データベースクラスタ内に1つのデータベースを作成
    • subdb: Publisherのpubdbデータベースに定義したPublicationから変更データを受け取るSubscription sub1を作成

6.4.2.2. 障害ケース1 ロジカルレプリケーション関連プロセスの異常終了

ロジカルレプリケーションに関連する以下のプロセスが異常終了した時の挙動を確認します。

  • logical replication launcher
  • wal sender
  • logical replication worker

■初期状態

Publisherでは logical replication launcher と wal sender のプロセスが起動しています。 また、pg_stat_replicationでロジカルレプリケーションが稼働中であることが確認できます。

(サーバ1において実施)
-bash-4.2$ ps aux | grep postgres
postgres  2179  0.0  0.8 389356 16552 ?        S     1月09   0:56 /usr/pgsql-10/bin/postgres
postgres  2180  0.0  0.1 242164  2404 ?        Ss    1月09   0:00 postgres: logger process
postgres  2182  0.0  0.3 389496  5716 ?        Ss    1月09   0:01 postgres: checkpointer process
postgres  2183  0.0  0.2 389356  4092 ?        Ss    1月09   0:20 postgres: writer process
postgres  2184  0.0  0.3 389356  6744 ?        Ss    1月09   0:23 postgres: wal writer process
postgres  2185  0.0  0.1 389812  3352 ?        Ss    1月09   0:38 postgres: autovacuum launcher process
postgres  2186  0.0  0.1 244420  2620 ?        Ss    1月09   1:09 postgres: stats collector process
postgres  2187  0.0  0.1 389648  3060 ?        Ss    1月09   0:01 postgres: bgworker: logical replication launcher
postgres 21882  0.0  0.3 392540  5780 ?        Ss    2月06   0:00 postgres: wal sender process repusr1 192.168.127.32(46474) idle

pubdb=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 21882
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46474
backend_start    | 2018-02-06 16:40:08.541313+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/5800DBB0
write_lsn        | 0/5800DBB0
flush_lsn        | 0/5800DBB0
replay_lsn       | 0/5800DBB0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

また、Subscriberでは logical replication launcher と logical replication worker のプロセスが起動しており、 pg_stat_subscriptionからもロジカルレプリケーションが稼働中であることが確認できます。

(サーバ2において実施)
-bash-4.2$ ps aux | grep postgres
postgres  5405  0.0  0.3 397072  6264 ?        Ss    2月06   0:02 postgres: bgworker: logical replication worker for subscription 16463
postgres 31698  0.0  0.8 389296 16496 ?        S     1月17   0:47 /usr/pgsql-10/bin/postgres
postgres 31699  0.0  0.1 242108  2008 ?        Ss    1月17   0:00 postgres: logger process
postgres 31701  0.0  0.2 389448  4724 ?        Ss    1月17   0:00 postgres: checkpointer process
postgres 31702  0.0  0.1 389296  3436 ?        Ss    1月17   0:15 postgres: writer process
postgres 31703  0.0  0.3 389296  6332 ?        Ss    1月17   0:18 postgres: wal writer process
postgres 31704  0.0  0.1 389752  3132 ?        Ss    1月17   0:32 postgres: autovacuum launcher process
postgres 31705  0.0  0.1 244360  2264 ?        Ss    1月17   1:07 postgres: stats collector process
postgres 31706  0.0  0.1 389588  2760 ?        Ss    1月17   0:01 postgres: bgworker: logical replication launcher

subdb=# select * from pg_stat_subscription;
-[ RECORD 1 ]---------+------------------------------
subid                 | 16463
subname               | sub1
pid                   | 5405
relid                 |
received_lsn          | 0/5800DBB0
last_msg_send_time    | 2018-02-07 13:45:31.222443+09
last_msg_receipt_time | 2018-02-07 13:45:31.05565+09
latest_end_lsn        | 0/5800DBB0
latest_end_time       | 2018-02-07 13:45:31.222443+09

■logical replication launcherの停止

Publisherのlogical replication launcher をKillコマンドで強制終了すると再度別のプロセスIDで起動されていることがわかります。

(サーバ1において実施)
-bash-4.2$ kill -9 2187
-bash-4.2$ ps aux | grep postgres
postgres  2179  0.0  0.8 389356 16552 ?        S     1月09   0:56 /usr/pgsql-10/bin/postgres
postgres  2180  0.0  0.1 242168  2404 ?        Ss    1月09   0:00 postgres: logger process
postgres 28990  0.0  0.1 389356  2152 ?        Ss   13:56   0:00 postgres: checkpointer process
postgres 28991  0.0  0.1 389356  2156 ?        Ss   13:56   0:00 postgres: writer process
postgres 28992  0.0  0.1 389356  2116 ?        Ss   13:56   0:00 postgres: wal writer process
postgres 28993  0.0  0.1 389784  3004 ?        Ss   13:56   0:00 postgres: autovacuum launcher process
postgres 28994  0.0  0.1 244288  1988 ?        Ss   13:56   0:00 postgres: stats collector process
postgres 28995  0.0  0.1 389656  2520 ?        Ss   13:56   0:00 postgres: bgworker: logical replication launcher

この時Publisher、Subscriberのログにはそれぞれ以下のメッセージが出力されています。

(サーバ1のサーバログ)
2018-02-07 13:56:05.609 JST [2179] LOG:  worker process: logical replication launcher (PID 2187) was terminated by signal 9: Killed
2018-02-07 13:56:05.609 JST [2179] LOG:  terminating any other active server processes
2018-02-07 13:56:05.609 JST [21882] WARNING:  terminating connection because of crash of another server process
2018-02-07 13:56:05.609 JST [21882] DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
2018-02-07 13:56:05.609 JST [21882] HINT:  In a moment you should be able to reconnect to the database and repeat your command.
2018-02-07 13:56:05.614 JST [2185] WARNING:  terminating connection because of crash of another server process
2018-02-07 13:56:05.614 JST [2185] DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
2018-02-07 13:56:05.614 JST [2185] HINT:  In a moment you should be able to reconnect to the database and repeat your command.
2018-02-07 13:56:05.620 JST [28988] FATAL:  the database system is in recovery mode
2018-02-07 13:56:05.621 JST [2179] LOG:  all server processes terminated; reinitializing
2018-02-07 13:56:05.652 JST [28989] LOG:  database system was interrupted; last known up at 2018-02-07 13:45:23 JST
2018-02-07 13:56:05.692 JST [28989] LOG:  database system was not properly shut down; automatic recovery in progress
2018-02-07 13:56:05.693 JST [28989] LOG:  redo starts at 0/5800DAD0
2018-02-07 13:56:05.693 JST [28989] LOG:  invalid record length at 0/5800DBB0: wanted 24, got 0
2018-02-07 13:56:05.693 JST [28989] LOG:  redo done at 0/5800DB78
2018-02-07 13:56:05.698 JST [2179] LOG:  database system is ready to accept connections
2018-02-07 13:56:10.640 JST [28998] LOG:  starting logical decoding for slot "sub1"
2018-02-07 13:56:10.640 JST [28998] DETAIL:  streaming transactions committing after 0/5800DB78, reading WAL from 0/5800DB78
2018-02-07 13:56:10.640 JST [28998] LOG:  logical decoding found consistent point at 0/5800DB78
2018-02-07 13:56:10.640 JST [28998] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
WARNING:  terminating connection because of crash of another server process
DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT:  In a moment you should be able to reconnect to the database and repeat your command.
2018-02-07 13:56:05.444 JST [5405] ERROR:  could not receive data from WAL stream: server closed the connection unexpectedly
                This probably means the server terminated abnormally
                before or while processing the request.
2018-02-07 13:56:05.446 JST [31698] LOG:  worker process: logical replication worker for subscription 16463 (PID 5405) exited with exit code 1
2018-02-07 13:56:05.450 JST [9938] LOG:  logical replication apply worker for subscription "sub1" has started
2018-02-07 13:56:05.453 JST [9938] ERROR:  could not connect to the publisher: FATAL:  the database system is in recovery mode
2018-02-07 13:56:05.454 JST [31698] LOG:  worker process: logical replication worker for subscription 16463 (PID 9938) exited with exit code 1
2018-02-07 13:56:10.463 JST [9939] LOG:  logical replication apply worker for subscription "sub1" has started

上記より、Publisherのlogical replication launcherが停止した場合、Publisherのインスタンス再起動、リカバリ処理が行われた後にレプリケーションが再開されることがわかりました。

同様に、Subscriberのlogical replication launcher をKillコマンドで強制終了した場合も、再度別のプロセスIDで起動されていることがわかります。また、この時Subscriberのインスタンス再起動、リカバリ処理が行われてからレプリケーションが再開されています。

(サーバ2において実施)
-bash-4.2$ kill -9 31706
-bash-4.2$ ps aux | grep postgres
postgres  9947  0.0  0.1 389296  2124 ?        Ss   13:57   0:00 postgres: checkpointer process
postgres  9948  0.0  0.1 389296  2128 ?        Rs   13:57   0:00 postgres: writer process
postgres  9949  0.0  0.1 389296  2108 ?        Rs   13:57   0:00 postgres: wal writer process
postgres  9950  0.0  0.1 389616  2980 ?        Ss   13:57   0:00 postgres: autovacuum launcher process
postgres  9951  0.0  0.1 244228  2008 ?        Ss   13:57   0:00 postgres: stats collector process
postgres  9952  0.0  0.1 389588  2760 ?        Ss   13:57   0:00 postgres: bgworker: logical replication launcher
postgres  9953  0.0  0.3 397084  6240 ?        Ss   13:57   0:00 postgres: bgworker: logical replication worker for subscription 16463
postgres 31698  0.0  0.8 389296 16500 ?        S     1月17   0:47 /usr/pgsql-10/bin/postgres
postgres 31699  0.0  0.1 242108  2008 ?        Ss    1月17   0:00 postgres: logger process

(サーバ1のサーバログ)
2018-02-07 13:57:43.391 JST [28998] LOG:  unexpected EOF on standby connection
2018-02-07 13:57:43.498 JST [29004] LOG:  starting logical decoding for slot "sub1"
2018-02-07 13:57:43.498 JST [29004] DETAIL:  streaming transactions committing after 0/5800DC58, reading WAL from 0/5800DC20
2018-02-07 13:57:43.498 JST [29004] LOG:  logical decoding found consistent point at 0/5800DC20
2018-02-07 13:57:43.498 JST [29004] DETAIL:  There are no running transactions.
(サーバ2のサーバログ)
2018-02-07 13:57:43.216 JST [31698] LOG:  worker process: logical replication launcher (PID 31706) was terminated by signal 9: Killed
2018-02-07 13:57:43.216 JST [31698] LOG:  terminating any other active server processes
2018-02-07 13:57:43.218 JST [31704] WARNING:  terminating connection because of crash of another server process
2018-02-07 13:57:43.218 JST [31704] DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
2018-02-07 13:57:43.218 JST [31704] HINT:  In a moment you should be able to reconnect to the database and repeat your command.
2018-02-07 13:57:43.226 JST [31698] LOG:  all server processes terminated; reinitializing
2018-02-07 13:57:43.250 JST [9946] LOG:  database system was interrupted; last known up at 2018-02-07 13:46:06 JST
2018-02-07 13:57:43.309 JST [9946] LOG:  recovered replication state of node 1 to 0/5800DA98
2018-02-07 13:57:43.309 JST [9946] LOG:  database system was not properly shut down; automatic recovery in progress
2018-02-07 13:57:43.310 JST [9946] LOG:  redo starts at 0/73706380
2018-02-07 13:57:43.310 JST [9946] LOG:  invalid record length at 0/73706460: wanted 24, got 0
2018-02-07 13:57:43.310 JST [9946] LOG:  redo done at 0/73706428
2018-02-07 13:57:43.315 JST [31698] LOG:  database system is ready to accept connections
2018-02-07 13:57:43.327 JST [9953] LOG:  logical replication apply worker for subscription "sub1" has started

■wal senderの停止

Publisherのwal senderをKillコマンドで強制終了します。

(サーバ1において実施)
-bash-4.2$ kill -9 29004
-bash-4.2$ ps aux | grep postgres
postgres  2179  0.0  0.8 389356 16552 ?        S     1月09   0:56 /usr/pgsql-10/bin/postgres
postgres  2180  0.0  0.1 242168  2404 ?        Ss    1月09   0:00 postgres: logger process
postgres 29085  0.0  0.1 389356  2156 ?        Ss   14:12   0:00 postgres: checkpointer process
postgres 29086  0.0  0.1 389356  2160 ?        Ss   14:12   0:00 postgres: writer process
postgres 29087  0.0  0.1 389356  2120 ?        Ss   14:12   0:00 postgres: wal writer process
postgres 29088  0.0  0.1 389784  3004 ?        Ss   14:12   0:00 postgres: autovacuum launcher process
postgres 29089  0.0  0.1 244288  1992 ?        Ss   14:12   0:00 postgres: stats collector process
postgres 29090  0.0  0.1 389656  2520 ?        Ss   14:12   0:00 postgres: bgworker: logical replication launcher
postgres 29093  0.0  0.3 392476  5952 ?        Ss   14:12   0:00 postgres: wal sender process repusr1 192.168.127.32(46482) idle

(サーバ1のサーバログ)
2018-02-07 14:12:19.446 JST [2179] LOG:  server process (PID 29004) was terminated by signal 9: Killed
2018-02-07 14:12:19.447 JST [2179] LOG:  terminating any other active server processes
2018-02-07 14:12:19.450 JST [28993] WARNING:  terminating connection because of crash of another server process
2018-02-07 14:12:19.450 JST [28993] DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
2018-02-07 14:12:19.450 JST [28993] HINT:  In a moment you should be able to reconnect to the database and repeat your command.
2018-02-07 14:12:19.456 JST [2179] LOG:  all server processes terminated; reinitializing
2018-02-07 14:12:19.485 JST [29083] LOG:  database system was interrupted; last known up at 2018-02-07 14:11:06 JST
2018-02-07 14:12:19.511 JST [29084] FATAL:  the database system is in recovery mode
2018-02-07 14:12:19.519 JST [29083] LOG:  database system was not properly shut down; automatic recovery in progress
2018-02-07 14:12:19.519 JST [29083] LOG:  redo starts at 0/5800DEE8
2018-02-07 14:12:19.519 JST [29083] LOG:  invalid record length at 0/5800DFC8: wanted 24, got 0
2018-02-07 14:12:19.519 JST [29083] LOG:  redo done at 0/5800DF90
2018-02-07 14:12:19.524 JST [2179] LOG:  database system is ready to accept connections
2018-02-07 14:12:24.529 JST [29093] LOG:  starting logical decoding for slot "sub1"
2018-02-07 14:12:24.529 JST [29093] DETAIL:  streaming transactions committing after 0/5800DF90, reading WAL from 0/5800DF90
2018-02-07 14:12:24.529 JST [29093] LOG:  logical decoding found consistent point at 0/5800DF90
2018-02-07 14:12:24.529 JST [29093] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-02-07 14:12:19.280 JST [9953] ERROR:  could not receive data from WAL stream: server closed the connection unexpectedly
                This probably means the server terminated abnormally
                before or while processing the request.
2018-02-07 14:12:19.281 JST [31698] LOG:  worker process: logical replication worker for subscription 16463 (PID 9953) exited with exit code 1
2018-02-07 14:12:19.285 JST [9998] LOG:  logical replication apply worker for subscription "sub1" has started
2018-02-07 14:12:19.344 JST [9998] ERROR:  could not connect to the publisher: FATAL:  the database system is in recovery mode
2018-02-07 14:12:19.345 JST [31698] LOG:  worker process: logical replication worker for subscription 16463 (PID 9998) exited with exit code 1
2018-02-07 14:12:24.354 JST [9999] LOG:  logical replication apply worker for subscription "sub1" has started

上記よりPublisherのwal senderが停止した場合も、Publisherインスタンスの再起動、リカバリ処理が行われた後にレプリケーションが再開されることがわかりました。

■logical replication workerの停止

Subscriberのlogical replication workerをKillコマンドで強制終了します。

(サーバ2において実施)
-bash-4.2$ kill -9 9999
-bash-4.2$ ps aux | grep postgres
postgres 10013  0.0  0.1 389296  2124 ?        Ss   14:16   0:00 postgres: checkpointer process
postgres 10014  0.0  0.1 389296  2128 ?        Ss   14:16   0:00 postgres: writer process
postgres 10015  0.0  0.1 389296  2104 ?        Ss   14:16   0:00 postgres: wal writer process
postgres 10016  0.0  0.1 389616  2980 ?        Ss   14:16   0:00 postgres: autovacuum launcher process
postgres 10017  0.0  0.1 244228  1968 ?        Ss   14:16   0:00 postgres: stats collector process
postgres 10018  0.0  0.1 389588  2760 ?        Ss   14:16   0:00 postgres: bgworker: logical replication launcher
postgres 10019  0.0  0.3 397052  6340 ?        Ss   14:16   0:00 postgres: bgworker: logical replication worker for subscription 16463
postgres 31698  0.0  0.8 389296 16500 ?        S     1月17   0:47 /usr/pgsql-10/bin/postgres
postgres 31699  0.0  0.1 242108  2008 ?        Ss    1月17   0:00 postgres: logger process

(サーバ1のサーバログ)
2018-02-07 14:16:13.087 JST [29093] LOG:  unexpected EOF on standby connection
2018-02-07 14:16:13.161 JST [29139] LOG:  starting logical decoding for slot "sub1"
2018-02-07 14:16:13.161 JST [29139] DETAIL:  streaming transactions committing after 0/5800E360, reading WAL from 0/5800E328
2018-02-07 14:16:13.161 JST [29139] LOG:  logical decoding found consistent point at 0/5800E328
2018-02-07 14:16:13.161 JST [29139] DETAIL:  Logical decoding will begin using saved snapshot.

(サーバ2のサーバログ)
2018-02-07 14:16:12.920 JST [31698] LOG:  worker process: logical replication worker for subscription 16463 (PID 9999) was terminated by signal 9: Killed
2018-02-07 14:16:12.920 JST [31698] LOG:  terminating any other active server processes
2018-02-07 14:16:12.924 JST [9950] WARNING:  terminating connection because of crash of another server process
2018-02-07 14:16:12.924 JST [9950] DETAIL:  The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
2018-02-07 14:16:12.924 JST [9950] HINT:  In a moment you should be able to reconnect to the database and repeat your command.
2018-02-07 14:16:12.930 JST [31698] LOG:  all server processes terminated; reinitializing
2018-02-07 14:16:12.957 JST [10012] LOG:  database system was interrupted; last known up at 2018-02-07 14:12:43 JST
2018-02-07 14:16:12.973 JST [10012] LOG:  recovered replication state of node 1 to 0/5800DEB0
2018-02-07 14:16:12.973 JST [10012] LOG:  database system was not properly shut down; automatic recovery in progress
2018-02-07 14:16:12.974 JST [10012] LOG:  redo starts at 0/73706710
2018-02-07 14:16:12.974 JST [10012] LOG:  invalid record length at 0/73706A30: wanted 24, got 0
2018-02-07 14:16:12.974 JST [10012] LOG:  redo done at 0/737069F8
2018-02-07 14:16:12.974 JST [10012] LOG:  last completed transaction was at log time 2018-02-07 14:15:21.893165+09
2018-02-07 14:16:12.980 JST [31698] LOG:  database system is ready to accept connections
2018-02-07 14:16:12.990 JST [10019] LOG:  logical replication apply worker for subscription "sub1" has started

上記より、Subscriberのlogical replication launcherが停止した場合もSubscriberインスタンスの再起動、リカバリ処理が行われた後にレプリケーションが再開されることがわかりました。

6.4.2.3. 障害ケース2 Publisherのノード停止

ロジカルレプリケーションのPublisherが停止した時の挙動を確認します。

■初期状態

Subscriberのpg_stat_subscriptionよりレプリケーションが稼働中であることを確認しました。

(サーバ2において実施)
subdb=# select * from pg_stat_subscription;
-[ RECORD 1 ]---------+------------------------------
subid                 | 16463
subname               | sub1
pid                   | 10084
relid                 |
received_lsn          | 0/5800ECB8
last_msg_send_time    | 2018-02-07 14:37:30.310381+09
last_msg_receipt_time | 2018-02-07 14:37:30.143584+09
latest_end_lsn        | 0/5800ECB8
latest_end_time       | 2018-02-07 14:37:30.310381+09

■Publisherのノードを停止

Publisherで起動中のPostgreSQLサーバを強制終了します。

(サーバ1において実施)
-bash-4.2$ pg_ctl -m f stop
サーバ停止処理の完了を待っています....完了
サーバは停止しました

次にSubscriberのpg_stat_subscriptionを確認すると、レコードは消えないもののpid, LSN等の情報が表示されなくなりました。

(サーバ2において実施)
subdb=# select * from pg_stat_subscription;
-[ RECORD 1 ]---------+------
subid                 | 16463
subname               | sub1
pid                   |
relid                 |
received_lsn          |
last_msg_send_time    |
last_msg_receipt_time |
latest_end_lsn        |
latest_end_time       |

また、この時のSubscriberのサーバログを確認すると、5秒毎に接続を試行してlogical replication workerプロセスがエラー終了する事象が繰り返されていました。

(サーバ2のサーバログ)
2018-02-07 14:55:44.067 JST [10084] LOG:  data stream from publisher has ended
2018-02-07 14:55:44.067 JST [10084] ERROR:  could not send end-of-streaming message to primary: no COPY in progress
2018-02-07 14:55:44.069 JST [10075] LOG:  worker process: logical replication worker for subscription 16463 (PID 10084) exited with exit code 1
2018-02-07 14:55:44.072 JST [10118] LOG:  logical replication apply worker for subscription "sub1" has started
2018-02-07 14:55:44.073 JST [10118] ERROR:  could not connect to the publisher: FATAL:  the database system is shutting down
2018-02-07 14:55:44.074 JST [10075] LOG:  worker process: logical replication worker for subscription 16463 (PID 10118) exited with exit code 1
2018-02-07 14:55:49.083 JST [10119] LOG:  logical replication apply worker for subscription "sub1" has started
2018-02-07 14:55:49.084 JST [10119] ERROR:  could not connect to the publisher: could not connect to server: Connection refused
                Is the server running on host "192.168.127.31" and accepting
                TCP/IP connections on port 5432?
2018-02-07 14:55:49.085 JST [10075] LOG:  worker process: logical replication worker for subscription 16463 (PID 10119) exited with exit code 1

この状態から、PublisherのPostgreSQLサーバを再度起動します。

(サーバ1において実施)
-bash-4.2$ pg_ctl start
サーバの起動完了を待っています....2018-02-07 14:58:44.785 JST [29300] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2018-02-07 14:58:44.785 JST [29300] LOG:  listening on IPv6 address "::", port 5432
2018-02-07 14:58:44.785 JST [29300] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2018-02-07 14:58:44.790 JST [29300] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2018-02-07 14:58:44.807 JST [29300] LOG:  redirecting log output to logging collector process
2018-02-07 14:58:44.807 JST [29300] HINT:  Future log output will appear in directory "log".
完了
サーバ起動完了

この時、Subscriberのpg_stat_subscriptionを見ると初期状態の様にレプリケーションが稼働中であることが確認できました。

(サーバ2において実施)
subdb=# select * from pg_stat_subscription;
-[ RECORD 1 ]---------+------------------------------
subid                 | 16463
subname               | sub1
pid                   | 10161
relid                 |
received_lsn          | 0/5800ED60
last_msg_send_time    | 2018-02-07 14:58:59.837546+09
last_msg_receipt_time | 2018-02-07 14:58:59.670736+09
latest_end_lsn        | 0/5800ED60
latest_end_time       | 2018-02-07 14:58:59.837546+09

この時Publisher、Subscriberのログにはそれぞれ以下のメッセージが出力されています。

(サーバ2のサーバログ)
2018-02-07 14:58:49.397 JST [10161] LOG:  logical replication apply worker for subscription "sub1" has started

(サーバ1のサーバログ)
2018-02-07 14:55:44.215 JST [2179] LOG:  received fast shutdown request
2018-02-07 14:55:44.226 JST [2179] LOG:  aborting any active transactions
2018-02-07 14:55:44.232 JST [2179] LOG:  worker process: logical replication launcher (PID 29090) exited with exit code 1
2018-02-07 14:55:44.232 JST [29085] LOG:  shutting down
2018-02-07 14:55:44.240 JST [29290] FATAL:  the database system is shutting down
2018-02-07 14:55:44.252 JST [2179] LOG:  database system is shut down
2018-02-07 14:58:44.809 JST [29302] LOG:  database system was shut down at 2018-02-07 14:55:44 JST
2018-02-07 14:58:44.813 JST [29300] LOG:  database system is ready to accept connections
2018-02-07 14:58:49.572 JST [29310] LOG:  starting logical decoding for slot "sub1"
2018-02-07 14:58:49.572 JST [29310] DETAIL:  streaming transactions committing after 0/5800EC80, reading WAL from 0/5800EC80
2018-02-07 14:58:49.572 JST [29310] LOG:  logical decoding found consistent point at 0/5800EC80
2018-02-07 14:58:49.572 JST [29310] DETAIL:  There are no running transactions.

この結果より、Publisherが停止した場合はレプリケーションが停止するが、再度起動すればレプリケーションも再開されることがわかりました。

6.4.2.4. 障害ケース3 Subscriberのノード停止

ロジカルレプリケーションのSubscriberが停止した時の挙動を確認します。

■初期状態

Publisherのpg_stat_replicationよりレプリケーションが稼働中であることを確認しました。

(サーバ1において実施)
pubdb=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 29213
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46484
backend_start    | 2018-02-07 14:33:55.842956+09
backend_xmin     |
state            | catchup
sent_lsn         | 0/5800EBD8
write_lsn        | 0/5800EBD8
flush_lsn        | 0/5800EBD8
replay_lsn       | 0/5800EBD8
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

■Subscriberのノードを停止

Subscriberで起動中のPostgreSQLサーバを強制終了します。

(サーバ2において実施)
-bash-4.2$ pg_ctl -m f stop
サーバ停止処理の完了を待っています....完了
サーバは停止しました

次にPublisherのpg_stat_replicationを確認すると、レプリケーションの情報が表示されなくなりました。 また、この時Publisherのサーバログには何も出力されていませんでした。

(サーバ1において実施)
pubdb=# select * from pg_stat_replication;
(0 rows)

なお、この状態のままPublisherで1件データをINSERTしても特に問題なく処理は完了します。

(サーバ1において実施)
pubdb=# insert into data1 values (6, 'pub6');
INSERT 0 1

この状態から、SubscriberのPostgreSQLサーバを再度起動します。

(サーバ2において実施)
-bash-4.2$ pg_ctl start
サーバの起動完了を待っています....2018-02-07 14:33:55.641 JST [10075] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2018-02-07 14:33:55.641 JST [10075] LOG:  listening on IPv6 address "::", port 5432
2018-02-07 14:33:55.642 JST [10075] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2018-02-07 14:33:55.642 JST [10075] LOG:  listening on Unix socket "/tmp/.s.PGSQL.5432"
2018-02-07 14:33:55.656 JST [10075] LOG:  redirecting log output to logging collector process
2018-02-07 14:33:55.656 JST [10075] HINT:  Future log output will appear in directory "log".
完了
サーバ起動完了

ここで、Publisher側のpg_stat_replicationを見ると初期状態の様にレプリケーションが復旧していることが確認できました。

(サーバ1において実施)
pubdb=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 29213
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46484
backend_start    | 2018-02-07 14:33:55.842956+09
backend_xmin     |
state            | catchup
sent_lsn         | 0/5800EBD8
write_lsn        | 0/5800EBD8
flush_lsn        | 0/5800EBD8
replay_lsn       | 0/5800EBD8
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

また、PublisherでINSERTしたレコードがSubscriberのテーブルにも転送されています。

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2  | c3
----+------+----
  1 | pub1 |
  2 | pub2 |
  3 | pub3 |
  4 | pub4 |
  5 | pub5 |
  6 | pub6 |

この時Publisher、Subscriberのログにはそれぞれ以下のメッセージが出力されています。

(サーバ1のサーバログ)
2018-02-07 14:33:55.845 JST [29213] LOG:  starting logical decoding for slot "sub1"
2018-02-07 14:33:55.845 JST [29213] DETAIL:  streaming transactions committing after 0/5800E858, reading WAL from 0/5800E820
2018-02-07 14:33:55.845 JST [29213] LOG:  logical decoding found consistent point at 0/5800E820
2018-02-07 14:33:55.845 JST [29213] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-02-07 14:33:55.658 JST [10077] LOG:  database system was shut down at 2018-02-07 14:29:59 JST
2018-02-07 14:33:55.659 JST [10077] LOG:  recovered replication state of node 1 to 0/5800E740
2018-02-07 14:33:55.662 JST [10075] LOG:  database system is ready to accept connections
2018-02-07 14:33:55.674 JST [10084] LOG:  logical replication apply worker for subscription "sub1" has started

この結果より、Subscriberが停止した場合はレプリケーションが停止するが、再度起動すればレプリケーションも再開されることがわかりました。

6.4.2.5. 障害ケース4 Publisher/Subscriber間のネットワーク断絶

ロジカルレプリケーションで利用するネットワークが断絶した時の挙動を確認します。

■初期状態

Publisherのpg_stat_replicationよりレプリケーションが稼働中であることを確認しました。

(サーバ1において実施)
pubdb=# select * from pg_stat_replication;
-[ RECORD 1 ]----+-----------------------------
pid              | 29310
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46522
backend_start    | 2018-02-07 14:58:49.56556+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/5800EE40
write_lsn        | 0/5800EE40
flush_lsn        | 0/5800EE40
replay_lsn       | 0/5800EE40
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

■ネットワークの断絶

Publisherでレプリケーションに使用するネットワークを停止します。 ここではOS(CentOS7.2)のNetworkManagerを利用して対象インタフェース(ens192)を停止しました。

(サーバ1において実施)
[root@nk_PGECons_1 ~]# nmcli connection down ens192
Connection 'ens192' successfully deactivated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/0)
[root@nk_PGECons_1 ~]# nmcli connection show
名前    UUID                                  タイプ          デバイス
ens192  fa64b28e-a7db-45f0-b7b1-0b2b298b20d2  802-3-ethernet  --
ens160  c0791a7a-43be-4254-847d-2b793b583bba  802-3-ethernet  ens160

しばらくの間はPublisherのpg_stat_replicationが初期状態のままですが、 1分ほど経過するとレプリケーションの情報が表示されなくなりました。

(サーバ1において実施)
pubdb=# select * from pg_stat_replication;
(0 rows)

この時のPublisherのログには以下のメッセージが出力されました。ここではwal_sender_timeoutパラメータをデフォルトの60秒で設定していたため、 タイムアウトを検知して非活動のレプリケーション接続を停止しています。

(サーバ1のサーバログ)
2018-02-07 15:41:12.504 JST [29310] LOG:  terminating walsender process due to replication timeout

■ネットワークの復旧

Publisherで停止したネットワークのインタフェースを復旧します。

(サーバ1において実施)
[root@nk_PGECons_1 ~]# nmcli connection up ens192
接続が正常にアクティベートされました (D-Bus アクティブパス: /org/freedesktop/NetworkManager/ActiveConnection/2)
[root@nk_PGECons_1 ~]# nmcli connection show
名前    UUID                                  タイプ          デバイス
ens192  fa64b28e-a7db-45f0-b7b1-0b2b298b20d2  802-3-ethernet  ens192
ens160  c0791a7a-43be-4254-847d-2b793b583bba  802-3-ethernet  ens160

Publisher側のpg_stat_replicationを確認すると、レプリケーションの情報が再び表示されるようになりました。

(サーバ1において実施)
pubdb=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 29665
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46523
backend_start    | 2018-02-07 15:44:40.511419+09
backend_xmin     |
state            | catchup
sent_lsn         | 0/5800F300
write_lsn        | 0/5800F300
flush_lsn        | 0/5800F300
replay_lsn       | 0/5800F300
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

Publisherのログには以下のメッセージが出力され、レプリケーションが再開されたことがわかります。

(サーバ1のサーバログ)
2018-02-07 15:44:40.514 JST [29665] LOG:  starting logical decoding for slot "sub1"
2018-02-07 15:44:40.514 JST [29665] DETAIL:  streaming transactions committing after 0/5800EE40, reading WAL from 0/5800EE08
2018-02-07 15:44:40.514 JST [29665] LOG:  logical decoding found consistent point at 0/5800EE08
2018-02-07 15:44:40.514 JST [29665] DETAIL:  There are no running transactions.

また、Subscriberのログは以下のメッセージが出力されており、ネットワークの復旧に伴って logical replication worker が再起動したことがわかります。

(サーバ2のサーバログ)
2018-02-07 15:44:40.338 JST [10161] ERROR:  could not receive data from WAL stream: server closed the connection unexpectedly
                This probably means the server terminated abnormally
                before or while processing the request.
2018-02-07 15:44:40.339 JST [10075] LOG:  worker process: logical replication worker for subscription 16463 (PID 10161) exited with exit code 1
2018-02-07 15:44:40.343 JST [10247] LOG:  logical replication apply worker for subscription "sub1" has started

これらの結果より、ネットワークの断絶により一時的にレプリケーションが停止した場合でも、ネットワークが復旧すればレプリケーションも自動的に再開されることがわかりました。

6.4.3. レプリケーション対象テーブルの追加/削除

ロジカルレプリケーションが稼働している環境で新たに作成したテーブルをレプリケーション対象として追加したり、テーブル定義を変更する手順を確認します。

6.4.3.1. 前提とする環境

本章は以下の環境を利用した検証結果を元に解説します。

_images/logical_replica_table_environment.png
  • PostgreSQLサーバはロジカルレプリケーションのPublisher1台、Subscriber1台で構成
  • ロジカルレプリケーションのPublisher(サーバ1。IPアドレス 192。168。127。31)では、新規に作成したデータベースpubdb,pubdb_all_tableのみを作成
  • ロジカルレプリケーションのSubscriber(サーバ2。IPアドレス 192。168。127。32)では、新規に作成したデータベースsubdb,subdb_all_tableのみを作成

6.4.3.2. レプリケーション対象のテーブルを追加する手順(データベース単位のPublication)

データベース単位のPublicationが作成されたデータベースに対して、テーブルを追加する手順を説明します。

■初期状態

Publisherのデータベースpubdb_all_tableに接続し、データベース単位のPublication pub_all_tableを作成します。

(サーバ1において実施)
pubdb_all_table=# CREATE PUBLICATION pub_all_table FOR ALL TABLES;
CREATE PUBLICATION
pubdb_all_table=# \dRp+
              Publication pub_all_table
  Owner   | All tables | Inserts | Updates | Deletes
----------+------------+---------+---------+---------
 postgres | t          | t       | t       | t

Pulisherのデータベースpubdb_all_tableにテーブルdata1を作成します。システムカタログpg_publication_tablesでテーブルdata1がPublication pub_all_tableのレプリケーション対象として認識されたことが確認できます。

(サーバ1において実施)
pubdb_all_table=# CREATE TABLE data1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
pubdb_all_table=# SELECT * FROM pg_publication_tables;
    pubname    | schemaname | tablename
---------------+------------+-----------
 pub_all_table | public     | data1

Subscriberのデータベースsubdb_all_tableに接続し、Subscriptionを作成します。なお、Subscription作成時点で接続先のPublicationの対象テーブルが存在しないとエラーになるため、あらかじめSubscriberのデータベースに同一名称のテーブルを作成しておいてください。

(サーバ2において実施)
subdb_all_table=# CREATE SUBSCRIPTION sub_all_table CONNECTION 'host=192.168.127.31 dbname=pubdb_all_table user=repusr1 password=repusr1' PUBLICATION pub_all_table;
ERROR:  relation "public.data1" does not exist  ★data1テーブルが存在しないとエラーとなる

subdb_all_table=# CREATE TABLE data1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
subdb_all_table=# CREATE SUBSCRIPTION sub_all_table CONNECTION 'host=192.168.127.31 dbname=pubdb_all_table user=repusr1 password=repusr1' PUBLICATION pub_all_table;
NOTICE:  created replication slot "sub_all_table" on publisher
CREATE SUBSCRIPTION

Subscriptionを作成するとSubscriberのログに初期データ転送のメッセージが出力され、ロジカルレプリケーションが開始されたことが確認できます。

(サーバ2のサーバログ)
2018-01-17 11:09:23.050 JST [31749] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data1" has started
2018-01-17 11:09:23.062 JST [31749] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data1" has finished

■テーブルの追加手順

初期状態から、Pulisherのデータベースpubdb_all_tableにテーブルdata2を作成します。FOR ALL TABLES句を付けてCRATEしたPublicationのため、作成したテーブルdata2が自動的にレプリケーション対象として認識されていることがわかります。

(サーバ1において実施)
pubdb_all_table=# CREATE TABLE data2 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
pubdb_all_table=# SELECT * FROM pg_publication_tables;
    pubname    | schemaname | tablename
---------------+------------+-----------
 pub_all_table | public     | data1
 pub_all_table | public     | data2

次に、Subscriberのデータベースsubdb_all_tableにもテーブルdata2を作成します。この時点ではSubscription sub_all_tableにテーブルdata2は認識されておらず、テーブルdata2のレプリケーションが開始されていないことがわかります。

(サーバ2において実施)
subdb_all_table=# CREATE TABLE data2 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
subdb_all_table=# select a3.subname, a2.relname, a1.srsubstate, a1.srsublsn from pg_subscription_rel as a1 left outer join pg_class as a2 on a1.srrelid = a2.oid left outer join pg_stat_subscription as a3 on a1.srsubid = a3.subid;
    subname    | relname | srsubstate |  srsublsn
---------------+---------+------------+------------
 sub_all_table | data1   | r          | 0/5602B818

既存のSubscriptionに新たなレプリケーション対象テーブルを認識させるために、ALTER SUBSCRIPTION文を実行します。再びpg_subscription_relを確認するとテーブルdata2が新たに認識されていることが確認できます。

(サーバ2において実施)
subdb_all_table=# ALTER SUBSCRIPTION sub_all_table REFRESH PUBLICATION;
ALTER SUBSCRIPTION
subdb_all_table=# select a3.subname, a2.relname, a1.srsubstate, a1.srsublsn from pg_subscription_rel as a1 left outer join pg_class as a2 on a1.srrelid = a2.oid left outer join pg_stat_subscription as a3 on a1.srsubid = a3.subid;
    subname    | relname | srsubstate |  srsublsn
---------------+---------+------------+------------
 sub_all_table | data1   | r          | 0/5602B818
 sub_all_table | data2   | d          |

6.4.3.3. レプリケーション対象のテーブルを追加する手順(テーブル単位のPublication)

テーブル単位のPublicationが作成されたデータベースに対して、テーブルを追加する手順を説明します。

■初期状態

Publisherのデータベースpubdbにテーブルdata1をCREATEした後、レプリケーション対象とするPublication pub1を作成します。

(サーバ1において実施)
pubdb=# CREATE TABLE data1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
pubdb=> CREATE PUBLICATION pub1 FOR TABLE data1;
CREATE PUBLICATION
pubdb=# \dRp+
                  Publication pub1
  Owner  | All tables | Inserts | Updates | Deletes
---------+------------+---------+---------+---------
 pubusr1 | f          | t       | t       | t
Tables:
    "public.data1"

SubscriberのデータベースsubdbにSubscriptionを作成し、レプリケーションを開始します。

(サーバ2において実施)
subdb=# CREATE TABLE data1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
subdb=# CREATE SUBSCRIPTION sub1 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub1;
NOTICE:  created replication slot "sub1" on publisher
CREATE SUBSCRIPTION

■テーブルの追加手順

初期状態から、Pulisherのデータベースpubdbにテーブルdata1_1を作成します。FOR ALL TABLES句を付けずにCRATEしたPublicationのため、作成したテーブルdata1_1がレプリケーション対象と認識されていないことがわかります。

(サーバ1において実施)
pubdb=# CREATE TABLE data1_1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
pubdb=# SELECT * FROM pg_publication_tables;
 pubname  | schemaname | tablename
----------+------------+-----------
 pub1     | public     | data1

テーブルdata1_1を既存のPublication pub1にレプリケーション対象として追加するには、ALTER PUBLICATION文を実行します。

(サーバ1において実施)
pubdb=# ALTER PUBLICATION pub1 ADD TABLE data1_1;
ALTER PUBLICATION
pubdb=# SELECT * FROM pg_publication_tables;
 pubname | schemaname | tablename
---------+------------+-----------
 pub1    | public     | data1
 pub1    | public     | data1_1

次に、Subscriberのデータベースsubdbにテーブルdata1_1を作成します。この時点ではSubscription sub1にテーブルdata1_1は認識されません。

(サーバ2において実施)
subdb=# CREATE TABLE data1_1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
subdb=# select a3.subname, a2.relname, a1.srsubstate, a1.srsublsn from pg_subscription_rel as a1 left outer join pg_class as a2 on a1.srrelid = a2.oid left outer join pg_stat_subscription as a3 on a1.srsubid = a3.subid;
 subname | relname | srsubstate |  srsublsn
---------+---------+------------+------------
 sub1    | data1   | r          | 0/55FB63D0

既存のSubscriptionに新たなレプリケーション対象テーブルを認識させるために、ALTER SUBSCRIPTION文を実行します。pg_subscription_relを確認するとテーブルdata1_1が新たに認識されていることが確認できます。

(サーバ2において実施)
subdb=# ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
ALTER SUBSCRIPTION
subdb=# select a3.subname, a2.relname, a1.srsubstate, a1.srsublsn from pg_subscription_rel as a1 left outer join pg_class as a2 on a1.srrelid = a2.oid left outer join pg_stat_subscription as a3 on a1.srsubid = a3.subid;
 subname | relname | srsubstate |  srsublsn
---------+---------+------------+------------
 sub1    | data1_1 | r          | 0/56068D38
 sub1    | data1   | r          | 0/55FB63D0

6.4.3.4. レプリケーション対象のテーブルを変更する手順

レプリケーションが実行されているテーブルに新たな列を追加する手順を説明します。

■初期状態

前述のPublication pub1, Subscription sub1を利用し、テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、テーブルdata1にはそれぞれ1件のデータが存在する状態とします。

(サーバ1において実施)
pubdb=# SELECT * FROM data1;
 c1 |  c2
----+------
  1 | pub1

(サーバ2において実施)
subdb=# SELECT * FROM data1;
 c1 |  c2
----+------
  1 | pub1

■列の追加手順

レプリケーション対象のテーブル定義に不一致があると更新競合が発生し、ロジカルレプリケーションの停止につながります。意図しないエラーを起こさぬようあらかじめALTER SUBSCRIPTION文でSubscriptionを一時停止しておくとよいでしょう。

(サーバ2において実施)
subdb=# ALTER SUBSCRIPTION sub1 DISABLE;
ALTER SUBSCRIPTION
subdb=# SELECT * FROM pg_stat_subscription;
 subid |    subname    |  pid  | relid | received_lsn |      last_msg_send_time       |     last_msg_receipt_time     | latest_end_lsn |        latest_end_time
-------+---------------+-------+-------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------
 16463 | sub1          |       |       |              |                               |                               |                |

この時、Subscriberのログには以下の様なメッセージが出力されます。

(サーバ2のサーバログ)
2018-01-17 14:51:52.437 JST [31708] LOG:  logical replication apply worker for subscription "sub1" will stop because the subscription was disabled

ロジカルレプリケーションを停止した後、Publisherのテーブルdata1にALTER TABLE文で列を追加します。また、ここで確認のため1件レコードを挿入しておきます。

(サーバ1において実施)
pubdb=# ALTER TABLE data1 ADD COLUMN c3 VARCHAR(5);
ALTER TABLE
pubdb=# INSERT INTO data1 VALUES (2, 'pub2', 'pub2');
INSERT 0 1
pubdb=# SELECT * FROM data1;
 c1 |  c2  |  c3
----+------+------
  1 | pub1 |
  2 | pub2 | pub2

次にSubscriberのテーブルdata1にもALTER TABLE文で同じ定義の列を追加します。Subscriptionを停止しているため2件目のレコードはまだ転送されていません。

(サーバ2において実施)
subdb=# ALTER TABLE data1 ADD COLUMN c3 VARCHAR(5);
ALTER TABLE
subdb=# SELECT * FROM data1;
 c1 |  c2  | c3
----+------+----
  1 | pub1 |

SubscriberでSubscriptionを再度有効化し、ロジカルレプリケーションを再開すると、この時点で2件目のレコードが転送されます。

(サーバ2において実施)
subdb=# ALTER SUBSCRIPTION sub1 ENABLE;
ALTER SUBSCRIPTION
subdb=# SELECT * FROM data1;
 c1 |  c2  |  c3
----+------+------
  1 | pub1 |
  2 | pub2 | pub2

Subscriberのpg_stat_subscritpionとログを再度確認すると、正常にレプリケーションが再開されていることがわかります。

(サーバ2において実施)
subdb=# SELECT * FROM pg_stat_subscription;
 subid |    subname    |  pid  | relid | received_lsn |      last_msg_send_time       |     last_msg_receipt_time     | latest_end_lsn |        latest_end_time
-------+---------------+-------+-------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------
 16463 | sub1          | 32626 |       | 0/56073298   | 2018-01-17 15:02:10.817291+09 | 2018-01-17 15:02:10.650377+09 | 0/56073298     | 2018-01-17 15:02:10.817291+09

(サーバ2のサーバログ)
2018-01-17 15:01:47.266 JST [32626] LOG:  logical replication apply worker for subscription "sub1" has started

6.4.3.5. レプリケーション対象のテーブルにインデックスを追加する手順

レプリケーションが実行されているテーブルにインデックスを追加する手順を説明します。

■初期状態

前述のPublication pub1, Subscription sub1およびテーブルdata1をそのまま利用します。初期状態でPublisher,Subscriberのテーブルdata1は以下の定義となっています。

(サーバ1において実施)
pubdb=# \d data1
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
 c3     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
Publications:
    "pub1"

(サーバ2において実施)
subdb=# \d data1;
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
 c3     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)

■Publisherのテーブルにインデックスを追加

初期状態からPublisherのテーブルdata1にのみインデックスをCREATEすると、Subscriberのテーブルdata1にはインデックスが追加されないことがわかります。

(サーバ1において実施)
pubdb=# CREATE INDEX data1_c2_idx ON data1 (c2);
CREATE INDEX
pubdb=# \d data1
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
 c3     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
    "data1_c2_idx" btree (c2)
Publications:
    "pub1"

(サーバ2において実施)
subdb=# \d data1;
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
 c3     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)

■Subscriberのテーブルにインデックスを追加

次にSubscriberのテーブルdata1にのみインデックスをCREATEすると、Subscriberのテーブルdata1にのみインデックスが追加されていることがわかります。

(サーバ2において実施)
subdb=# CREATE INDEX data1_c3_idx ON data1 (c3);
CREATE INDEX
subdb=# \d data1;
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
 c3     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
    "data1_c3_idx" btree (c3)

(サーバ1において実施)
pubdb=# \d data1
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
 c3     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
    "data1_c2_idx" btree (c2)
Publications:
    "pub1"

ロジカルレプリケーションにおいてインデックスはレプリケーション対象とならないため、インデックスの追加、削除が既存のレプリケーションに影響を及ぼすことはありませんが、Publisherで作成したインデックスがSubscriberでも必要であれば同じCREATE INDEX文を発行する必要があります。

6.4.4. レプリケーション対象外となる操作、オブジェクト

ストリーミングレプリケーションがデータベースクラスタへの操作、オブジェクトを全て複製するのと違い、PostgreSQL10のロジカルレプリケーションでは以下の様な操作、オブジェクトが複製されません。そのためこれらは必要に応じてPublisherとSubscriberで同じ操作を行う等の対処を求められます。

表 6.6 ロジカルレプリケーションで複製されない操作、オブジェクト
操作、オブジェクト 動作検証
CREATE TABLE, ALTER TABLE, CREATE INDEX等のDDL  
シーケンスの定義、データ
TRUNCATE
Large Object  
VIEW, MATERIALIZED VIEW  
FOREIGN TABLE  
UNLOGGED TABLE  
パーティショニングの親テーブル  

ここでは、TRUNCATEとシーケンスに対するロジカルレプリケーションの挙動を確認します。

6.4.4.1. 前提とする環境

本章は以下の環境を利用した検証結果を元に解説します。

_images/logical_replica_not_covered_environment.png
  • PostgreSQLサーバはロジカルレプリケーションのPublisher1台、Subscriber1台で構成
  • ロジカルレプリケーションのPublisher(サーバ1。IPアドレス 192。168。127。31)では、新規に作成したデータベースpubdbを作成
    • データベースpubdbには、レプリケーション対象のテーブルを個別定義するPublication pub1,pub3を作成
    • pub1,pub3のレプリケーション対象として、それぞれテーブルdata1,data3を設定
  • ロジカルレプリケーションのSubscriber(サーバ2。IPアドレス 192。168。127。32)では、新規に作成したデータベースsubdbを作成
    • データベースsubdbには、Publisherのpubdbデータベースに定義したPublicationから変更データを受け取るSubscription sub1,sub3およびテーブルdata1,data3を作成

6.4.4.2. TRUNCATE

■初期状態

初期状態でテーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、テーブルdata1にはそれぞれ5件のデータが存在する状態とします。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5

Publisherでテーブルdata1に対してTRUNCATE TABLEを実行します。

(サーバ1において実施)
pubdb=# TRUNCATE TABLE data1;
TRUNCATE TABLE
pubdb=# select * from data1;
 c1 | c2
----+----

この状態でSubscriberのテーブルdata1は変化していないことが確認できます。

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5

Subscriberでもテーブルdata1に対してTRUNCATE TABLEを実行することで、Publisherと一致します。

(サーバ2において実施)
subdb=# TRUNCATE TABLE data1;
TRUNCATE TABLE
subdb=# select * from data1;
 c1 | c2
----+----

(サーバ1において実施)
pubdb=# select * from data1;
 c1 | c2
----+----

TRUNCATE TABLEはWALにも記録されるデータ更新操作であり、ストリーミングレプリケーションで複製される操作ですが、ロジカルレプリケーションでは複製されないため注意が必要です。

6.4.4.3. シーケンス

ここではSERIAL列を含むテーブルをロジカルレプリケーションで複製した場合の挙動を確認します。

■初期状態

Publisherのデータベースpubdb、Subscriberのデータベースsubdbにそれぞれテーブルdata3を作成し、ロジカルレプリケーションで複製される状態にします。 また、テーブルdata3にはSERIAL列を定義します。

(サーバ1において実施)
pubdb=# CREATE TABLE data3 (c1 SERIAL PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
pubdb=# CREATE PUBLICATION pub3 FOR TABLE data3;
CREATE PUBLICATION

(サーバ2において実施)
subdb=# CREATE TABLE data3 (c1 SERIAL PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
subdb=# CREATE SUBSCRIPTION sub3 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub3;
NOTICE:  created replication slot "sub3" on publisher
CREATE SUBSCRIPTION

■SERIAL列のレプリケーション

Publisherのテーブルdata3にレコードを挿入します。挿入したレコードがSubscriberのテーブルdata3にも転送されます。

(サーバ1において実施)
pubdb=# INSERT INTO data3 (c2) VALUES ('pub1');
INSERT 0 1
pubdb=# SELECT * FROM data3;
 c1 |  c2
----+------
  1 | pub1

(サーバ2において実施)
subdb=# SELECT * FROM data3;
 c1 |  c2
----+------
  1 | pub1

この時点でPublisherに存在するシーケンスは現在値が1になっていますが、Subscriberに存在するシーケンスからは現在値が取得できません。 つまり、SERIAL列の値自体は複製されたものの、内部的に作成されたシーケンスのデータは複製されていません。

(サーバ1において実施)
pubdb=# SELECT currval('data3_c1_seq');
 currval
---------
       1

(サーバ2において実施)
subdb=# SELECT currval('data3_c1_seq');
ERROR:  currval of sequence "data3_c1_seq" is not yet defined in this session

ここでSubscriberのテーブルdata3に対してレコードを挿入するとキー重複でエラーになります。 これはSubscriberのシーケンスにPublisherのシーケンスデータと関係なく1を発行したためです。

(サーバ2において実施)
subdb=# INSERT INTO data3 (c2) VALUES ('pub1');
ERROR:  duplicate key value violates unique constraint "data3_pkey"
DETAIL:  Key (c1)=(1) already exists.
subdb=# SELECT currval('data3_c1_seq');
 currval
---------
       1

この状態で再度Subscriberのテーブルdata3にレコードを挿入すると成功します。これは先ほどINSERTに失敗した際にSubscriberのシーケンスは1にカウントアップされており、今回のINSERTで2を発行したためキー重複が発生せず挿入できたためです。

(サーバ2において実施)
subdb=# INSERT INTO data3 (c2) VALUES ('pub1');
INSERT 0 1
subdb=# SELECT * FROM data3;
 c1 |  c2
----+------
  1 | pub1
  2 | pub1
subdb=# SELECT currval('data3_c1_seq');
 currval
---------
       2

この時点でPublisherのシーケンスは現在値が1のため、このままPublisherのテーブルdata3にINSERTするとSubscriberのテーブルでキー重複が発生します。 そこで、Publisherのシーケンスの現在値を2に進めてからレコード挿入するとPublisherで挿入したレコードがSubscriberに複製されました。

(サーバ1において実施)
pubdb=# SELECT * FROM data3;
 c1 |  c2
----+------
  1 | pub1
pubdb=# SELECT currval('data3_c1_seq');
 currval
---------
       1
pubdb=# SELECT nextval('data3_c1_seq');
 nextval
---------
       2
pubdb=# INSERT INTO data3 (c2) VALUES ('pub1');
INSERT 0 1
pubdb=# SELECT * FROM data3;
 c1 |  c2
----+------
  1 | pub1
  3 | pub1

(サーバ1において実施)
subdb=# SELECT * FROM data3;
 c1 |  c2
----+------
  1 | pub1
  2 | pub1
  3 | pub1

このように、PublisherでSERIAL列に付与された値はSubscriberのテーブルに転送されますが、内部的に使用するシーケンスのデータは複製されないため、Subscriberで独自にレコードを挿入するとシーケンスのデータに不整合が生じます。そのため、SERIAL列を含むテーブルでロジカルレプリケーションを行う場合はSubscriberでのレコード更新は抑制した方がよいでしょう。

6.4.5. 更新競合時の動作

マスタしか更新できないストリーミングレプリケーションに対し、ロジカルレプリケーションはSubscriberも更新することができます。ただし、PublisherとSubscriberのテーブル間で以下の様な更新処理の競合が発生する可能性があります。

表 6.7 ロジカルレプリケーションで発生する更新処理の競合パターン
競合パターン 動作検証 (更新時) 動作検証 (初期データ同期時)
主キー違反/一意キー違反
CHECK制約違反
更新データが存在しない
削除データが存在しない
テーブルが存在しない
一部の列が存在しない
データ型変換エラー
テーブルのロック
更新対象レコードのロック

ここでは競合が発生した時の挙動と対応方法について検証します。

6.4.5.1. 前提とする環境

本章は以下の環境を利用した検証結果を元に解説します。

_images/logical_replica_conflict.png
  • PostgreSQLサーバはロジカルレプリケーションのPublisher1台、Subscriber1台で構成
  • ロジカルレプリケーションのPublisher(サーバ1。IPアドレス 192。168。127。31)では、新規に作成したデータベースpubdbを作成
    • データベースpubdbには、レプリケーション対象のテーブルを個別定義するPublication pub1を作成
    • pub1のレプリケーション対象として、それぞれテーブルdata1を設定
  • ロジカルレプリケーションのSubscriber(サーバ2。IPアドレス 192。168。127。32)では、新規に作成したデータベースsubdbを作成
    • データベースsubdbには、Publisherのpubdbデータベースに定義したPublicationから変更データを受け取るSubscription sub1およびテーブルdata1を作成

6.4.5.2. 更新時の競合発生と解消

前述の環境において実際に競合が発生した時の挙動と競合を解消させるための方法を競合発生のパターン毎に紹介します。

6.4.5.2.1. 主キー違反/一意キー違反

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、テーブルdata1にはc1列をPRIMARY KEYとし、それぞれ以下のデータが存在する状態とします。

(サーバ1において実施)
pubdb=# \d data1
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
Publications:
    "pub1"

pubdb=# select * from data1;
 c1 |  c2
----+-------
  1 | aaaaa

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+-------
  1 | aaaaa
  2 | sub

■競合の発生

Publisherのテーブルdata1にc1=2のレコードを挿入するとPublisherのINSERTは成功します。

(サーバ1において実施)
pubdb=# insert into data1 values (2, 'pub');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+-------
  1 | aaaaa
  2 | pub

この時Subscriberでは主キー違反による競合が発生し、レプリケーションが停止します。 Publisher、Subscriberのログには競合が解消するまで5秒毎に以下のメッセージが繰り返し出力されます。

(サーバ1のサーバログ)
2018-01-10 13:37:55.149 JST [5859] LOG:  starting logical decoding for slot "sub1"
2018-01-10 13:37:55.149 JST [5859] DETAIL:  streaming transactions committing after 0/55F82E30, reading WAL from 0/55F82DF8
2018-01-10 13:37:55.149 JST [5859] LOG:  logical decoding found consistent point at 0/55F82DF8
2018-01-10 13:37:55.149 JST [5859] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-01-10 13:37:49.965 JST [2132] ERROR:  duplicate key value violates unique constraint "data1_pkey"
2018-01-10 13:37:49.965 JST [2132] DETAIL:  Key (c1)=(2) already exists.
2018-01-10 13:37:49.968 JST [2131] WARNING:  out of background worker slots
2018-01-10 13:37:49.968 JST [2131] HINT:  You might need to increase max_worker_processes.
2018-01-10 13:37:49.968 JST [2123] LOG:  worker process: logical replication worker for subscription 16394 (PID 2132) exited with exit code 1

■競合の解消

Subscriberのテーブルでキーが重複するレコードを削除すると競合が解消し、レプリケーションが再開されます。 また、Publisher、Subscriberへのエラーメッセージも出力されなくなります。

(サーバ2において実施)
subdb=# delete from data1 where c1 = 2;
DELETE 1
subdb=# select * from data1;
 c1 |  c2
----+-------
  1 | aaaaa
  2 | pub
6.4.5.2.2. CHECK制約違反

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、Subscriberのテーブルdata1のc1列にCHECK制約を付与します。

(サーバ2において実施)
subdb=# ALTER TABLE data1 ADD CONSTRAINT data1_c1_check CHECK (c1 < 10);
ALTER TABLE
subdb=# \d+ data1
                                          Table "public.data1"
 Column |         Type         | Collation | Nullable | Default | Storage  | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
 c1     | integer              |           | not null |         | plain    |              |
 c2     | character varying(5) |           |          |         | extended |              |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
Check constraints:
    "data1_c1_check" CHECK (c1 < 10)

■競合の発生

Publisherのテーブルdata1に、Subscriberのみに追加したCHECK制約に違反するレコードを挿入すると、PublisherのINSERTは成功します。

(サーバ1において実施)
pubdb=# insert into data1 values (10, 'pub10');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+-------
  1 | aaaaa
  2 | pub
 10 | pub10

この時SubscriberではCHECK制約違反による競合が発生し、レプリケーションが停止します。 Publisher、Subscriberのログには競合が解消するまで5秒毎に以下のメッセージが繰り返し出力されます。

(サーバ1のサーバログ)
2018-01-10 14:05:32.268 JST [6057] LOG:  starting logical decoding for slot "sub1"
2018-01-10 14:05:32.268 JST [6057] DETAIL:  streaming transactions committing after 0/55F833E8, reading WAL from 0/55F833B0
2018-01-10 14:05:32.268 JST [6057] LOG:  logical decoding found consistent point at 0/55F833B0
2018-01-10 14:05:32.268 JST [6057] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-01-10 14:05:32.097 JST [6020] LOG:  logical replication apply worker for subscription "sub1" has started
2018-01-10 14:05:32.102 JST [6020] ERROR:  new row for relation "data1" violates check constraint "data1_c1_check"
2018-01-10 14:05:32.102 JST [6020] DETAIL:  Failing row contains (10, pub10).
2018-01-10 14:05:32.103 JST [2123] LOG:  worker process: logical replication worker for subscription 16394 (PID 6020) exited with exit code 1

■競合の解消

Subscriberのテーブルdata1のCHECK制約をDROPすると競合が解消し、レプリケーションが再開されます。 また、Publisher、Subscriberへのエラーメッセージも出力されなくなります。

(サーバ2において実施)
subdb=# ALTER TABLE data1 DROP CONSTRAINT data1_c1_check;
ALTER TABLE
6.4.5.2.3. 更新データが存在しない

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、Publisher,Subscriberのテーブルdata1はそれぞれ以下のレコードが存在する状態とします。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

(サーバ2において実施)
subdb=# select * from data1;
 c1 | c2
----+----

■競合の発生

Publisherでレコードを更新するとPublisherのUPDATEは成功しますが、Subscriberのレコードには変化がありません。また、Publisher,Subscriberのログにエラーメッセージも出力されていません。

(サーバ1において実施)
pubdb=# update data1 set c2 = 'pub1a';
UPDATE 1
pubdb=# select * from data1;
 c1 |  c2
----+-------
  1 | pub1a

(サーバ2において実施)
subdb=# select * from data1;
 c1 | c2
----+----

キーが一致するレコードが存在しない場合の競合は無視され、レプリケーションも停止しないことがわかります。

6.4.5.2.4. 削除データが存在しない

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、Publisher,Subscriberのテーブルdata1はそれぞれ以下のレコードが存在する状態とします。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

(サーバ2において実施)
subdb=# select * from data1;
 c1 | c2
----+----

■競合の発生

Publisherでレコードを削除するとPublisherのDELETEは成功しますが、Subscriberのレコードには変化がありません。また、Publisher,Subscriberのログにエラーメッセージも出力されていません。

(サーバ2において実施)
subdb=# delete from data1 where c1 = 1;
DELETE 1
subdb=# select * from data1;
 c1 | c2
----+----
subdb=# select * from data1;
 c1 | c2
----+----

キーが一致するレコードが存在しない場合の競合は無視され、レプリケーションも停止しないことがわかります。

6.4.5.2.5. テーブルが存在しない

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。

■競合の発生

Subscriberでテーブルdata1をDROPした後、Publisherでテーブルdata1にレコードを挿入すると、PublisherのINSERTは成功します。

(サーバ2において実施)
subdb=# DROP TABLE data1;
DROP TABLE

(サーバ1において実施)
pubdb=# insert into data1 values (1, 'pub1');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

この時Subscriberではテーブル不一致による競合が発生し、レプリケーションが停止します。 Publisher、Subscriberのログには競合が解消するまで5秒毎に以下のメッセージが繰り返し出力されます。

(サーバ1のサーバログ)
2018-01-10 15:09:10.872 JST [6331] LOG:  starting logical decoding for slot "sub1"
2018-01-10 15:09:10.872 JST [6331] DETAIL:  streaming transactions committing after 0/55F8B9E8, reading WAL from 0/55F8B9B0
2018-01-10 15:09:10.872 JST [6331] LOG:  logical decoding found consistent point at 0/55F8B9B0
2018-01-10 15:09:10.872 JST [6331] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-01-10 15:09:10.700 JST [6297] LOG:  logical replication apply worker for subscription "sub1" has started
2018-01-10 15:09:10.706 JST [6297] ERROR:  logical replication target relation "public.data1" does not exist
2018-01-10 15:09:10.708 JST [2123] LOG:  worker process: logical replication worker for subscription 16394 (PID 6297) exited with exit code 1

■競合の解消

Subscriberでテーブルdata1を再作成するとログにエラーは出力されなくなりますが、レプリケーションは再開されません。

(サーバ2において実施)
subdb=# CREATE TABLE data1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
subdb=# select * from data1;
 c1 | c2
----+----

これは、再作成したテーブルdata1がSubscription sub1からレプリケーション対象として認識されていないためと考えられます。

(サーバ2において実施)
subdb=# \dRs+
                                                    List of subscriptions
 Name |  Owner   | Enabled | Publication | Synchronous commit |                            Conninfo
------+----------+---------+-------------+--------------------+----------------------------------------------------------------
 sub1 | postgres | t       | {pub1}      | off                | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1

subdb=# select a3.subname, a2.relname, a1.srsubstate, a1.srsublsn from pg_subscription_rel as a1 left outer join pg_class as a2 on a1.srrelid = a2.oid left outer join pg_stat_subscription as a3 on a1.srsubid = a3.subid;
 subname |     relname      | srsubstate |  srsublsn
---------+------------------+------------+------------

Subscription sub1に対してALTER SUBSCRIPTION文を実行して、sub1にレプリケーション対象テーブルを再認識させると、テーブルdata1のレプリケーションが再開されます。

(サーバ2において実施)
subdb=# ALTER SUBSCRIPTION sub1 REFRESH PUBLICATION;
ALTER SUBSCRIPTION
subdb=# select a3.subname, a2.relname, a1.srsubstate, a1.srsublsn from pg_subscription_rel as a1 left outer join pg_class as a2 on a1.srrelid = a2.oid left outer join pg_stat_subscription as a3 on a1.srsubid = a3.subid;
 subname |     relname      | srsubstate |  srsublsn
---------+------------------+------------+------------
 sub1    | data1            | r          | 0/55F8C298
6.4.5.2.6. 一部の列が存在しない

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、Subscriberでテーブルdata1のc2列をDROPします。

(サーバ2において実施)
subdb=# ALTER TABLE data1 DROP COLUMN c2;
ALTER TABLE
subdb=# \d+ data1;
                                  Table "public.data1"
 Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description
--------+---------+-----------+----------+---------+---------+--------------+-------------
 c1     | integer |           | not null |         | plain   |              |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)

■競合の発生

Publisherでテーブルdata1に対してレコードを挿入すると、PublisherのINSERTは成功します。

(サーバ1において実施)
pubdb=# insert into data1 values (1, 'pub1');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

この時Subscriberでは列の不一致による競合が発生し、レプリケーションが停止します。 Publisher、Subscriberのログには競合が解消するまで5秒毎に以下のメッセージが繰り返し出力されます。

(サーバ1のサーバログ)
2018-01-10 15:53:11.464 JST [6480] LOG:  starting logical decoding for slot "sub1"
2018-01-10 15:53:11.464 JST [6480] DETAIL:  streaming transactions committing after 0/55F900C0, reading WAL from 0/55F90088
2018-01-10 15:53:11.464 JST [6480] LOG:  logical decoding found consistent point at 0/55F90088
2018-01-10 15:53:11.464 JST [6480] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-01-10 15:53:11.284 JST [6472] LOG:  logical replication apply worker for subscription "sub1" has started
2018-01-10 15:53:11.298 JST [6472] ERROR:  logical replication target relation "public.data1" is missing some replicated columns
2018-01-10 15:53:11.299 JST [6420] LOG:  worker process: logical replication worker for subscription 16394 (PID 6472) exited with exit code 1

■競合の解消

Subscriberでテーブルdata1のc2列を再度追加するとレプリケーションが再開されます。 また、Publisher、Subscriberへのエラーメッセージも出力されなくなります。

(サーバ2において実施)
subdb=# ALTER TABLE data1 ADD COLUMN c2 VARCHAR(5);
ALTER TABLE
subdb=# \d+ data1;
                                          Table "public.data1"
 Column |         Type         | Collation | Nullable | Default | Storage  | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
 c1     | integer              |           | not null |         | plain    |              |
 c2     | character varying(5) |           |          |         | extended |              |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

■Subscriberのテーブルに列が多く定義されている場合

前述のとおりSubscriberのテーブルにPublisherのテーブルの列が存在しない場合は競合が発生します。 逆にPublisherのテーブルに存在しない列がSubscriberのテーブルに存在した場合、以下のとおり競合は発生しません。 この時Subscriberのテーブルのみに定義された列にはNULLが設定されます。

(サーバ1において実施)
pubdb=# \d data1
                      Table "public.data1"
 Column |         Type         | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
 c1     | integer              |           | not null |
 c2     | character varying(5) |           |          |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
Publications:
    "pub1"

(サーバ2において実施)
subdb=# ALTER TABLE data1 ADD COLUMN c3 VARCHAR(5); <-- Publisherに存在しないc3列を追加する
ALTER TABLE
subdb=# \d+ data1;
                                          Table "public.data1"
 Column |         Type         | Collation | Nullable | Default | Storage  | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
 c1     | integer              |           | not null |         | plain    |              |
 c2     | character varying(5) |           |          |         | extended |              |
 c3     | character varying(5) |           |          |         | extended |              |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)

(サーバ1において実施)
pubdb=# insert into data1 values (1, 'pub1');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2  |  c3
----+------+------
  1 | pub1 | NULL  <-- c3列(Publisher未定義列)にはNULLが格納される
6.4.5.2.7. データ型変換エラー

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、Subscriberでテーブルdata1のc2列を異なるデータ型に変更します。

(サーバ2において実施)
subdb=# ALTER TABLE data1 ALTER COLUMN c2 TYPE integer USING c2::integer;
ALTER TABLE
subdb=# \d+ data1;
                                  Table "public.data1"
 Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description
--------+---------+-----------+----------+---------+---------+--------------+-------------
 c1     | integer |           | not null |         | plain   |              |
 c2     | integer |           |          |         | plain   |              |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)

■競合の発生

Publisherでテーブルdata1に対してレコードを挿入すると、PublisherのINSERTは成功します。

(サーバ1において実施)
pubdb=# insert into data1 values (1, 'pub1');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

この時Subscriberでは型の不一致による競合が発生し、レプリケーションが停止します。 Publisher、Subscriberのログには競合が解消するまで5秒毎に以下のメッセージが繰り返し出力されます。

(サーバ1のサーバログ)
2018-01-10 16:08:13.308 JST [6602] LOG:  starting logical decoding for slot "sub1"
2018-01-10 16:08:13.308 JST [6602] DETAIL:  streaming transactions committing after 0/55F943E8, reading WAL from 0/55F943B0
2018-01-10 16:08:13.308 JST [6602] LOG:  logical decoding found consistent point at 0/55F943B0
2018-01-10 16:08:13.308 JST [6602] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-01-10 16:08:13.136 JST [6597] LOG:  logical replication apply worker for subscription "sub1" has started
2018-01-10 16:08:13.142 JST [6597] ERROR:  invalid input syntax for integer: "pub1"
2018-01-10 16:08:13.142 JST [6597] CONTEXT:  processing remote data for replication target relation "public.data1" column "c2", remote type character varying
, local type character varying
2018-01-10 16:08:13.143 JST [6420] LOG:  worker process: logical replication worker for subscription 16394 (PID 6597) exited with exit code 1

■競合の解消

Subscriberでc2列のデータ型を元に戻すとレプリケーションが再開されます。 また、Publisher、Subscriberへのエラーメッセージも出力されなくなります。

(サーバ2において実施)
subdb=# ALTER TABLE data1 ALTER COLUMN c2 TYPE VARCHAR(5);
ALTER TABLE
subdb=# \d+ data1;
                                          Table "public.data1"
 Column |         Type         | Collation | Nullable | Default | Storage  | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
 c1     | integer              |           | not null |         | plain    |              |
 c2     | character varying(5) |           |          |         | extended |              |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

■データ型の暗黙的変換が可能な場合

PublisherとSubscriberのテーブルで列のデータ型が相違する場合でも暗黙的な変換が可能であれば以下のとおり競合は発生しません。

(サーバ2において実施)
subdb=# ALTER TABLE data1 ALTER COLUMN c2 TYPE date;
ALTER TABLE
subdb=# \d+ data1;
                                  Table "public.data1"
 Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description
--------+---------+-----------+----------+---------+---------+--------------+-------------
 c1     | integer |           | not null |         | plain   |              |
 c2     | date    |           |          |         | plain   |              |
Indexes:
    "data1_pkey" PRIMARY KEY, btree (c1)

(サーバ1において実施)
pubdb=# insert into data1 values (1, '2018/1/1');
INSERT 0 1
pubdb=# select * from data1;
 c1 |    c2
----+----------
  1 | 2018/1/1

(サーバ2において実施)
subdb=# select * from data1;
 c1 |     c2
----+------------
  1 | 2018-01-01  <-- 暗黙的な型変換が可能なレコードはレプリケーションされる(TEXT型 -> DATE型)
6.4.5.2.8. テーブルのロック

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。

■競合の発生

Subscriberでレプリケーション対象のテーブルdata1を明示的にロックします。

(サーバ2において実施)
subdb=# BEGIN;
BEGIN
subdb=# LOCK TABLE data1 IN EXCLUSIVE MODE;
LOCK TABLE

Publisherでテーブルdata1に対してレコードを挿入すると、PublisherのINSERTは成功しますが、Subscriberのテーブルdata1には反映されません。 この時Publisher、Subscriberのログにはエラーメッセージは出力されておらず、レプリケーションがロック解放を待機した状態となっています。

(サーバ1において実施)
pubdb=# insert into data1 values (1, 'pub1');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

(サーバ2において実施)
subdb=# select * from data1;
 c1 | c2
----+----

■競合の解消

Subscriberでトランザクションを終了し、ロックを解放すると、レプリケーションが再開され、データが転送されます。

(サーバ2において実施)
subdb=# COMMIT;
COMMIT
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
6.4.5.2.9. 更新対象レコードのロック

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。

■競合の発生

Subscriberでテーブルdata1のレコードをSELECT FOR UPDATEで明示的にロックします。

(サーバ2において実施)
subdb=# BEGIN;
BEGIN
subdb=# SELECT * FROM data1 WHERE c1 = 1 FOR UPDATE;
LOCK TABLE

Publisherで同一のレコードを更新すると、PublisherのUPDATEは成功しますが、Subscriberのテーブルdata1には反映されません。 この時Publisher、Subscriberのログにはエラーメッセージは出力されておらず、レプリケーションがロック解放を待機した状態となっています。

(サーバ1において実施)
pubdb=# update data1 SET c2 = 'pub11' where c1 = 1;
UPDATE 1
pubdb=# select * from data1;
 c1 |  c2
----+-------
  1 | pub11

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

■競合の解消

Subscriberでトランザクションを終了し、ロックを解放すると、レプリケーションが再開され、データが転送されます。

(サーバ2において実施)
subdb=# COMMIT;
COMMIT
subdb=# select * from data1;
 c1 |  c2
----+-------
  1 | pub11

6.4.5.3. 初期データ同期時の競合発生と解消

ロジカルレプリケーションの開始時に実行される初期データ同期処理においても更新時と同様の競合が発生する可能があります。以下で初期データ同期時に競合が発生した時の挙動と競合を解消させるための方法をパターン毎に紹介します。

6.4.5.3.1. 主キー違反/一意キー違反

■初期状態

Publisher,Subscriberのテーブルdata1にはc1列をPRIMARY KEYとし、それぞれ以下のデータが存在する状態とします。

(サーバ1において実施)
pubdb=# select * from data1;
c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+------
  2 | pub2

■競合の発生

SubscriberでSubscription sub1を作成し、ロジカルレプリケーションを開始します。この時Subscriberでは主キー違反による競合が発生し、レプリケーションが停止します。

(サーバ2において実施)
subdb=# CREATE SUBSCRIPTION sub1 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub1;
NOTICE:  created replication slot "sub1" on publisher
CREATE SUBSCRIPTION

Publisher、Subscriberのログには競合が解消するまで5秒毎に以下のメッセージが繰り返し出力されます。

(サーバ1のサーバログ)
2018-01-11 15:53:32.764 JST [10254] LOG:  logical decoding found consistent point at 0/55FB4C50
2018-01-11 15:53:32.764 JST [10254] DETAIL:  There are no running transactions.
2018-01-11 15:53:32.772 JST [10255] LOG:  starting logical decoding for slot "sub1"
2018-01-11 15:53:32.772 JST [10255] DETAIL:  streaming transactions committing after 0/55FB4C88, reading WAL from 0/55FB4C50
2018-01-11 15:53:32.772 JST [10255] LOG:  logical decoding found consistent point at 0/55FB4C50
2018-01-11 15:53:32.772 JST [10255] DETAIL:  There are no running transactions.
2018-01-11 15:53:32.792 JST [10256] LOG:  logical decoding found consistent point at 0/55FB4C88
2018-01-11 15:53:32.792 JST [10256] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-01-11 15:53:32.602 JST [10275] LOG:  logical replication apply worker for subscription "sub1" has started
2018-01-11 15:53:32.608 JST [10276] LOG:  logical replication table synchronization worker for subscription "sub1", table "data1" has started
2018-01-11 15:53:32.630 JST [10276] ERROR:  duplicate key value violates unique constraint "data1_pkey"
2018-01-11 15:53:32.630 JST [10276] DETAIL:  Key (c1)=(2) already exists.
2018-01-11 15:53:32.630 JST [10276] CONTEXT:  COPY data1, line 2
2018-01-11 15:53:32.631 JST [6420] LOG:  worker process: logical replication worker for subscription 16447 sync 16424 (PID 10276) exited with exit code 1

■競合の解消

Subscriberのテーブルでキーが重複するレコードを削除すると競合が解消し、レプリケーションが再開されます。 また、Publisher、Subscriberへのエラーメッセージも出力されなくなります。

(サーバ2において実施)
subdb=# delete from data1 where c1 = 2;
DELETE 1
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
6.4.5.3.2. CHECK制約違反

主キー違反/一意キー違反による競合と同じ挙動になると推測されるため割愛します。

6.4.5.3.3. 更新データが存在しない

初期データ同期では発生しないケースのため割愛します。

6.4.5.3.4. 削除データが存在しない

初期データ同期では発生しないケースのため割愛します。

6.4.5.3.5. テーブルが存在しない

Publisher,Subscriberのテーブルdata1にはc1列をPRIMARY KEYとし、それぞれ以下のデータが存在する状態とします。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+------
  2 | pub2

SubscriberでテーブルをDROPしてから、Subscriptionを作成するとエラーとなります。Subscriberのデータベースにテーブルが存在しない状態ではSubscriptionを作成できないため、初期データ同期時には発生しないケースとなります。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3

(サーバ2において実施)
subdb=# drop table data1;
DROP TABLE
subdb=# CREATE SUBSCRIPTION sub1 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub1;
ERROR:  relation "public.data1" does not exist
6.4.5.3.6. 一部の列が存在しない

■初期状態

Publisherにはテーブルdata1を作成し、レコードが3件存在する状態にします。Subscriberには列が少ないテーブルdata1を作成します。

(サーバ1において実施)
pubdb=# CREATE TABLE data1 (c1 INT PRIMARY KEY, c2 VARCHAR(5));
CREATE TABLE
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3

(サーバ2において実施)
subdb=# CREATE TABLE data1 (c1 INT PRIMARY KEY);
CREATE TABLE

■競合の発生

SubscriberでSubscription sub1を作成し、ロジカルレプリケーションを開始します。この時Subscriberでは列定義の相違による競合が発生し、レプリケーションが停止します。

(サーバ2において実施)
subdb=# CREATE SUBSCRIPTION sub1 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub1;
NOTICE:  created replication slot "sub1" on publisher
CREATE SUBSCRIPTION

Publisher、Subscriberのログには5秒毎に以下のメッセージが繰り返し出力されます。

(サーバ1のサーバログ)
2018-01-11 16:04:15.836 JST [10365] LOG:  starting logical decoding for slot "sub1"
2018-01-11 16:04:15.836 JST [10365] DETAIL:  streaming transactions committing after 0/55FB5AC0, reading WAL from 0/55FB5A88
2018-01-11 16:04:15.836 JST [10365] LOG:  logical decoding found consistent point at 0/55FB5A88
2018-01-11 16:04:15.836 JST [10365] DETAIL:  There are no running transactions.
2018-01-11 16:04:15.849 JST [10366] LOG:  logical decoding found consistent point at 0/55FB5AC0
2018-01-11 16:04:15.849 JST [10366] DETAIL:  There are no running transactions.

(サーバ2のサーバログ)
2018-01-11 16:04:15.666 JST [10385] LOG:  logical replication apply worker for subscription "sub1" has started
2018-01-11 16:04:15.673 JST [10386] LOG:  logical replication table synchronization worker for subscription "sub1", table "data1" has started
2018-01-11 16:04:15.686 JST [10386] ERROR:  logical replication target relation "public.data1" is missing some replicated columns
2018-01-11 16:04:15.688 JST [6420] LOG:  worker process: logical replication worker for subscription 16454 sync 16449 (PID 10386) exited with exit code 1

Subscriberでテーブルdata1を参照しても、初期データは同期されていません。

(サーバ2において実施)
subdb=# select * from data1;
 c1
----

■競合の解消

更新時の競合発生と同じく、Subscriberでテーブルdata1のc2列を追加するとレプリケーションが再開されます。

6.4.5.3.7. データ型変換エラー

一部の列が存在しないパターンと同じ挙動になると推測されるため割愛します。

6.4.5.3.8. テーブルのロック

■初期状態

Publisher,Subscriberのテーブルdata1にはそれぞれ以下のデータが存在する状態とします。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3

(サーバ2において実施)
subdb=# select * from data1;
 c1 | c2
----+----

■競合の発生

Subscriberでテーブルdata1をLOCK TABLE 〜 IN EXCLUSIVE MODEで明示的にロックします。

(サーバ2において実施)
subdb=# BEGIN;
BEGIN
subdb=# LOCK TABLE data1 IN EXCLUSIVE MODE;
LOCK TABLE

Subscriberで上記のトランザクションを実行したまま、別のコンソールでSubscriptionを作成します。この時レプリケーションはロックを待機するため、テーブルdata1には初期データが同期されていないことがわかります。

(サーバ2において実施)
subdb=# CREATE SUBSCRIPTION sub1 CONNECTION 'host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub1;
NOTICE:  created replication slot "sub1" on publisher
CREATE SUBSCRIPTION
subdb=# select * from data1;
 c1 | c2
----+----

■競合の解消

Subscriberでトランザクションを終了するとロックが解放されて、レプリケーションの初期データ同期が開始されます。

(サーバ2において実施)
subdb=# COMMIT;
COMMIT
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
6.4.5.3.9. 更新対象レコードのロック

初期データ同期では発生しないケースのため割愛します。

6.4.5.4. WALのスキップによる競合の解消

前述の様にSubscriberで競合の原因となる要素を取り除く方法とは別に、競合が発生するWALのSubscriberへの適用をスキップする方法について以下に紹介します。

■初期状態

テーブルdata1のデータがPublisher,Subscriber間でレプリケーションされている状態とします。また、テーブルdata1にはレコードが5件存在し、PublisherのWALのLSNは「0/55FB6948」となっています。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5
pubdb=# SELECT pg_current_wal_lsn() ;
pg_current_wal_lsn
--------------------
0/55FB6948

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5
subdb=# SELECT oid, subname FROM pg_subscription ;
  oid  | subname
-------+---------
 16463 | sub1
subdb=# SELECT * FROM pg_replication_origin_status;
 local_id | external_id | remote_lsn | local_lsn
----------+-------------+------------+------------
        1 | pg_16463    | 0/55FB6948 | 0/7360A120

■競合の発生

Subscriberでテーブルdata1に6件目のレコードを挿入した後、Publisherで同一キーのレコードを挿入し、競合を発生させます。

(サーバ2において実施)
subdb=# insert into data1 values (6, 'sub6');
INSERT 0 1
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5
  6 | sub6

(サーバ1において実施)
pubdb=# insert into data1 values (6, 'pub6');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5
  6 | pub6

競合が発生しているため、PublisherのLSNまでSubscriberのLSN(remote_lsn)が到達していません。

(サーバ1において実施)
pubdb=# SELECT pg_current_wal_lsn() ;
pg_current_wal_lsn
--------------------
0/55FB6DB0

・SUBSCRIPTION側でremote_lsnを確認

(サーバ2において実施)
subdb=# SELECT * FROM pg_replication_origin_status;
 local_id | external_id | remote_lsn | local_lsn
----------+-------------+------------+------------
        1 | pg_16463    | 0/55FB6910 | 0/7360A120

■競合の解消

Subscriberでpg_replication_origin_advance関数を実行しLSNを進めることで、競合が発生しているWALをスキップさせます。この時点でSubscriberのサーバログにはレプリケーションが停止した際のエラーメッセージが出力されなくなります。

(サーバ2において実施)
subdb=# SELECT pg_replication_origin_advance ('pg_16463', '0/55FB6DB0');
pg_replication_origin_advance
-------------------------------

SubscriberでLSNを再度確認するとPublisherのLSNと一致していることがわかります。なお、競合発生直前に挿入したレコード(c1=6, c2=sub6)はそのまま残されています。

(サーバ2において実施)
subdb=# SELECT * FROM pg_replication_origin_status;
 local_id | external_id | remote_lsn | local_lsn
----------+-------------+------------+------------
        1 | pg_16463    | 0/55FB6DB0 | 0/7360A120

subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5
  6 | sub6

この状態でPublisherに新しいレコードを挿入すると、Subscriberのc1=6のレコードは残されたままレプリケーションは再開されており、Publisherでレコード(c1=6,c2=pub6)を挿入したWALのみがスキップされたことがわかります。

(サーバ1において実施)
pubdb=# insert into data1 values (7, 'pub7');
INSERT 0 1
pubdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5
  6 | pub6
  7 | pub7

(サーバ2において実施)
subdb=# select * from data1;
 c1 |  c2
----+------
  1 | pub1
  2 | pub2
  3 | pub3
  4 | pub4
  5 | pub5
  6 | sub6
  7 | pub7

6.4.6. ストリーミングレプリケーションとの併用

ここでは、ロジカルレプリケーションとストリーミングレプリケーションを併用する環境を想定し、以下の観点を確認します。

  • ストリーミングレプリケーションを併用する環境の構築手順
  • ストリーミングレプリケーションを併用する環境で障害が発生した場合の運用手順

6.4.6.1. 前提とする環境

本章は以下の環境を利用した検証結果を元に解説します。

_images/logical_replica_srmix_before.png
  • PostgreSQLサーバは3台構成
  • ロジカルレプリケーションは、Publisher1台(サーバ1。IPアドレス 192。168。127。31)、Subscriber1台(サーバ2。IPアドレス 192。168。127。32)で構成
  • ストリーミングレプリケーションは、マスタ1台(Publisherと同一)、スタンバイ1台(サーバ3。IPアドレス 192。168。127。33)で構成
  • ロジカルレプリケーションのPublisherでは、データベースクラスタ内に2つのデータベースを作成
    • pubdb: レプリケーション対象のテーブルを個別定義するPublicationを作成
    • pubdb_all_table: データベース内に存在する全テーブルをレプリケーション対象とするPublicationを作成
  • ロジカルレプリケーションのSubscriberでは、データベースクラスタ内に2つのデータベースを作成
    • subdb: Publisherのpubdbデータベースに定義したPublicationから変更データを受け取るSubscriptionを作成
    • subdb_all_table: Publisherのpubdb_all_tableデータベースに定義したPublicationから変更データを受け取るSubscriptionを作成

6.4.6.2. ストリーミングレプリケーションを併用する環境の構築手順

既にロジカルレプリケーションが稼働している環境に対してストリーミングレプリケーションを稼働させる手順は、新規でストリーミングレプリケーションを稼働させる手順と変わりません。(後で「SR環境の設定手順」への相互参照リンクを貼る)の手順にしたがってストリーミングレプリケーションのマスタ、スレーブを構築して下さい。

6.4.6.3. ストリーミングレプリケーションを併用する環境で障害が発生した場合の運用手順

■初期状態

Publisherにはデータベースpubdbとpubdb_all_tableが存在します。また、pubdbデータベース、pubdb_all_tableデータベースには、Publicationがそれぞれ2つ、1つ存在します。

(サーバ1において実施)
pubdb=# \dRp+
Publication pub1
-[ RECORD 1 ]-------
Owner      | pubusr1
All tables | f
Inserts    | t
Updates    | t
Deletes    | t

Tables:
    "public.data1"
    "public.data1_1"

Publication pub2
-[ RECORD 1 ]-------
Owner      | pubusr1
All tables | f
Inserts    | t
Updates    | t
Deletes    | t

Tables:
    "public.data2"

pubdb_all_table=# \dRp+
Publication pub_all_table
-[ RECORD 1 ]--------
Owner      | postgres
All tables | t
Inserts    | t
Updates    | t
Deletes    | t

ロジカルレプリケーションのPublisherとストリーミングレプリケーションのマスタを兼ねるサーバ1の動的統計情報ビューpg_stat_replicationには、ロジカルレプリケーションの情報(RECORD 1〜3)、ストリーミングレプリケーションの情報(RECORD 4)が確認できます。

(サーバ1において実施)
pubdb=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 2054
usesysid         | 16385
usename          | repusr1
application_name | sub_all_table
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46604
backend_start    | 2018-02-08 13:42:42.520503+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580126B0
write_lsn        | 0/580126B0
flush_lsn        | 0/580126B0
replay_lsn       | 0/580126B0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 2 ]----+------------------------------
pid              | 1948
usesysid         | 16385
usename          | repusr1
application_name | sub2
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46526
backend_start    | 2018-02-08 13:36:25.199985+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580126B0
write_lsn        | 0/580126B0
flush_lsn        | 0/580126B0
replay_lsn       | 0/580126B0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 3 ]----+------------------------------
pid              | 1850
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 46524
backend_start    | 2018-02-08 13:15:08.479523+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580126B0
write_lsn        | 0/580126B0
flush_lsn        | 0/580126B0
replay_lsn       | 0/580126B0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 4 ]----+------------------------------
pid              | 1846
usesysid         | 16385
usename          | repusr1
application_name | nk_PGECons3
client_addr      | 192.168.127.33
client_hostname  |
client_port      | 55520
backend_start    | 2018-02-08 13:14:36.673678+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/580126B0
write_lsn        | 0/580126B0
flush_lsn        | 0/580126B0
replay_lsn       | 0/580126B0
write_lag        | 00:00:00.00054
flush_lag        | 00:00:00.000541
replay_lag       | 00:00:00.000541
sync_priority    | 0
sync_state       | async

サーバ1のシステムカタログpg_replication_slotsでは、ロジカルレプリケーションのSubscriptionと合わせて自動作成されたレプリケーションスロットの情報が確認できます。

(サーバ1において実施)
pubdb=# select * from pg_replication_slots;
-[ RECORD 1 ]-------+----------------
slot_name           | sub2
plugin              | pgoutput
slot_type           | logical
datoid              | 16384
database            | pubdb
temporary           | f
active              | t
active_pid          | 1948
xmin                |
catalog_xmin        | 701
restart_lsn         | 0/58017AA8
confirmed_flush_lsn | 0/58017AE0
-[ RECORD 2 ]-------+----------------
slot_name           | sub1
plugin              | pgoutput
slot_type           | logical
datoid              | 16384
database            | pubdb
temporary           | f
active              | t
active_pid          | 1850
xmin                |
catalog_xmin        | 701
restart_lsn         | 0/58017AA8
confirmed_flush_lsn | 0/58017AE0
-[ RECORD 3 ]-------+----------------
slot_name           | sub_all_table
plugin              | pgoutput
slot_type           | logical
datoid              | 16474
database            | pubdb_all_table
temporary           | f
active              | t
active_pid          | 2054
xmin                |
catalog_xmin        | 701
restart_lsn         | 0/58017AA8
confirmed_flush_lsn | 0/58017AE0

pubdbデータベース、pubdb_all_tableデータベースそれぞれのdata1テーブルにはあらかじめ1件データをINSERTしておきます。

(サーバ1において実施)
pubdb=# select * from data1;
 c1 |  c2
----+-------
  1 | pub1
pubdb_all_table=# select * from data1;
 c1 |  c2
----+------
  1 | pub1

Subscriberのsubdbデータベース、subdb_all_tableデータベースには、Subscriptionがそれぞれ2つ、1つ存在します。

(サーバ2において実施)
subdb=# \dRs+
                                                    List of subscriptions
 Name |  Owner   | Enabled | Publication | Synchronous commit |                            Conninfo
------+----------+---------+-------------+--------------------+----------------------------------------------------------------
 sub1 | postgres | t       | {pub1}      | off                | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1
 sub2 | postgres | t       | {pub2}      | off                | host=192.168.127.31 dbname=pubdb user=repusr1 password=repusr1

subdb_all_table=# \dRs+
                                                                List of subscriptions
     Name      |  Owner   | Enabled |   Publication   | Synchronous commit |                                 Conninfo
---------------+----------+---------+-----------------+--------------------+--------------------------------------------------------------------------
 sub_all_table | postgres | t       | {pub_all_table} | off                | host=192.168.127.31 dbname=pubdb_all_table user=repusr1 password=repusr1

ロジカルレプリケーションのSubscriberであるサーバ2の動的統計情報ビューpg_stat_subscriptionには、ロジカルレプリケーションの情報が3件確認できます。

(サーバ2において実施)
subdb_all_table=# select * from pg_stat_subscription;
 subid |    subname    |  pid  | relid | received_lsn |      last_msg_send_time       |     last_msg_receipt_time     | latest_end_lsn |        latest_end_time
-------+---------------+-------+-------+--------------+-------------------------------+-------------------------------+----------------+-------------------------------
 16463 | sub1          | 11038 |       | 0/58011F60   | 2018-02-08 13:42:47.590367+09 | 2018-02-08 13:42:47.423435+09 | 0/58011F60     | 2018-02-08 13:42:47.590367+09
 24677 | sub2          | 11072 |       | 0/58011F60   | 2018-02-08 13:42:47.59058+09  | 2018-02-08 13:42:47.42367+09  | 0/58011F60     | 2018-02-08 13:42:47.59058+09
 24678 | sub_all_table | 11158 |       | 0/58011F60   | 2018-02-08 13:42:47.590534+09 | 2018-02-08 13:42:47.423605+09 | 0/58011F60     | 2018-02-08 13:42:47.590534+09

ストリーミングレプリケーションのスレーブであるサーバ3では動的統計情報ビューpg_stat_wal_receiverでストリーミングレプリケーションの情報が確認できます。

(サーバ3において実施)
pubdb=# select * from pg_stat_wal_receiver;
-[ RECORD 1 ]---------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
pid                   | 26152
status                | streaming
receive_start_lsn     | 0/58000000
receive_start_tli     | 1
received_lsn          | 0/58016A78
received_tli          | 1
last_msg_send_time    | 2018-02-08 13:52:22.063552+09
last_msg_receipt_time | 2018-02-08 13:52:21.953994+09
latest_end_lsn        | 0/58016A78
latest_end_time       | 2018-02-08 13:52:22.063552+09
slot_name             |
conninfo              | user=repusr1 password=******** dbname=replication host=192.168.127.31 port=5432 application_name=nk_PGECons3 fallback_application_name=walreceiver sslmode=prefer sslcompression=1 krbsrvname=postgres target_session_attrs=any

また、サーバ3にはマスタであるサーバ1で定義されているPublicationがストリーミングレプリケーションで複製されています。

(サーバ3において実施)
pubdb=# \dRp+
Publication pub1
-[ RECORD 1 ]-------
Owner      | pubusr1
All tables | f
Inserts    | t
Updates    | t
Deletes    | t

Tables:
    "public.data1"
    "public.data1_1"

Publication pub2
-[ RECORD 1 ]-------
Owner      | pubusr1
All tables | f
Inserts    | t
Updates    | t
Deletes    | t

Tables:
    "public.data2"
pubdb_all_table=# \dRp+
Publication pub_all_table
-[ RECORD 1 ]--------
Owner      | postgres
All tables | t
Inserts    | t
Updates    | t
Deletes    | t

ただし、サーバ3はサーバ2とのロジカルレプリケーションは確立していないため、サーバ3の動的統計情報ビューpg_stat_replication,pg_replication_slotsを見ても、サーバ1と同じ情報は存在しません。

(サーバ3において実施)
pubdb_all_table=# select * from pg_replication_slots;
(0 rows)
pubdb_all_table=# select * from pg_stat_replication;
(0 rows)

■ストリーミングレプリケーションのマスタを切り替える手順

初期状態の構成でストリーミングレプリケーションのマスタに障害が発生した状態を想定し、昇格したスレーブへロジカルレプリケーションを切り替える手順を説明します。切替後の環境は以下の様になります。

_images/logical_replica_srmix_after.png

まず、ストリーミングレプリケーションのマスタ(サーバ1)のPostgreSQLサーバを強制終了します。

(サーバ1において実施)
-bash-4.2$ pg_ctl -m f stop
サーバ停止処理の完了を待っています....完了
サーバは停止しました

ストリーミングレプリケーションのスレーブ(サーバ3)でpg_ctl promoteコマンドを実行し、マスタに昇格します。

(サーバ3において実施)
-bash-4.2$ pg_ctl promote
waiting for server to promote....完了
server promoted

サーバ3のログでマスタとして接続を受け付け始めたことが確認でき、ストリーミングレプリケーションのマスタ切替は完了します。

(サーバ3のサーバログ)
2018-02-08 15:16:34.693 JST [26148] LOG:  received promote request
2018-02-08 15:16:34.693 JST [26148] LOG:  redo done at 0/58020F38
2018-02-08 15:16:34.693 JST [26148] LOG:  last completed transaction was at log time 2018-02-08 14:24:19.546571+09
2018-02-08 15:16:34.694 JST [26148] LOG:  selected new timeline ID: 2
2018-02-08 15:16:34.734 JST [26148] LOG:  archive recovery complete
2018-02-08 15:16:34.738 JST [26146] LOG:  database system is ready to accept connections

次に、サーバ3を新たなPublisherとしたロジカルレプリケーションを開始させてみます。

まず、サーバ2でALTER SUBSCRIPTION文を実行して既存のSubscriptionの接続先を変更してみました。

(サーバ2において実施)
subdb=# ALTER SUBSCRIPTION sub1 CONNECTION 'host=192.168.127.33 dbname=pubdb user=repusr1 password=repusr1';
ALTER SUBSCRIPTION

Subscriberのログを見るとサーバ3にレプリケーションスロット sub1 が存在しないことが原因で失敗しています。

(サーバ2のサーバログ)
2018-02-08 15:27:04.296 JST [11029] LOG:  worker process: logical replication worker for subscription 24678 (PID 12728) exited with exit code 1
2018-02-08 15:27:04.299 JST [12729] LOG:  logical replication apply worker for subscription "sub1" has started
2018-02-08 15:27:04.302 JST [12729] ERROR:  could not start WAL streaming: ERROR:  replication slot "sub1" does not exist

また、既存のSubscriptionを再作成しようとしても、レプリケーションスロット sub1 が存在しないことで削除も拒否されます。

(サーバ2において実施)
subdb=# DROP SUBSCRIPTION sub1;
ERROR:  could not drop the replication slot "sub1" on publisher
DETAIL:  The error was: ERROR:  replication slot "sub1" does not exist

SubscriptionのDROP時にはレプリケーションスロットも合わせてDROPしようとします。今回の様にレプリケーションスロットが存在しないSubscriptionが残存する場合は、Subscriptionを一時停止(DISABLE)し、レプリケーションスロットとの対応を無効化(slot_name = NONE)してからDROPする必要があります。

(サーバ2において実施)
subdb=# ALTER SUBSCRIPTION sub1 DISABLE;
ALTER SUBSCRIPTION
subdb=# ALTER SUBSCRIPTION sub1 SET (slot_name = NONE);
ALTER SUBSCRIPTION
subdb=# DROP SUBSCRIPTION sub1;
DROP SUBSCRIPTION
subdb_all_table=# ALTER SUBSCRIPTION sub_all_table DISABLE;
ALTER SUBSCRIPTION
subdb_all_table=# ALTER SUBSCRIPTION sub_all_table SET (slot_name = NONE);
ALTER SUBSCRIPTION
subdb_all_table=# DROP SUBSCRIPTION sub_all_table;
DROP SUBSCRIPTION

SubscriptionをDROPした後、新たにSubscriptionを作り直すと、LRが開始されます。

(サーバ2において実施)
subdb=# CREATE SUBSCRIPTION sub1 CONNECTION 'host=192.168.127.33 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub1 WITH (copy_data = false);
NOTICE:  created replication slot "sub1" on publisher
CREATE SUBSCRIPTION
subdb=# CREATE SUBSCRIPTION sub2 CONNECTION 'host=192.168.127.33 dbname=pubdb user=repusr1 password=repusr1' PUBLICATION pub2 WITH (copy_data = false);
NOTICE:  created replication slot "sub2" on publisher
CREATE SUBSCRIPTION

(サーバ2のサーバログ)
2018-02-08 15:53:08.879 JST [13210] LOG:  logical replication apply worker for subscription "sub1" has started
2018-02-08 15:57:37.386 JST [13231] LOG:  logical replication apply worker for subscription "sub2" has started
2018-02-08 15:57:37.393 JST [13232] LOG:  logical replication table synchronization worker for subscription "sub2", table "data2" has started
2018-02-08 15:57:37.405 JST [13232] LOG:  logical replication table synchronization worker for subscription "sub2", table "data2" has finished

なお、CREATE SUBSCRIPTION文にWITH (copy_data = false)を付与すると、ロジカルレプリケーション開始時の初期データコピーがスキップされます。試しにWITH (copy_data = false) を付けずに実行すると、初期データのコピー時に残存するデータとキー重複を起こし、ロジカルレプリケーションが開始されません。

(サーバ2において実施)
subdb_all_table=# CREATE SUBSCRIPTION sub_all_table CONNECTION 'host=192.168.127.33 dbname=pubdb_all_table user=repusr1 password=repusr1' PUBLICATION pub_all_table;
NOTICE:  created replication slot "sub_all_table" on publisher
CREATE SUBSCRIPTION

(サーバ2のサーバログ)
2018-02-08 16:00:04.913 JST [13245] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data1" has started
2018-02-08 16:00:04.923 JST [13246] LOG:  logical replication table synchronization worker for subscription "sub_all_table", table "data2" has started
2018-02-08 16:00:04.928 JST [13245] ERROR:  duplicate key value violates unique constraint "data1_pkey"
2018-02-08 16:00:04.928 JST [13245] DETAIL:  Key (c1)=(1) already exists.
2018-02-08 16:00:04.928 JST [13245] CONTEXT:  COPY data1, line 1
2018-02-08 16:00:04.929 JST [11029] LOG:  worker process: logical replication worker for subscription 24685 sync 16479 (PID 13245) exited with exit code 1
2018-02-08 16:00:04.939 JST [13246] ERROR:  duplicate key value violates unique constraint "data2_pkey"
2018-02-08 16:00:04.939 JST [13246] DETAIL:  Key (c1)=(1) already exists.
2018-02-08 16:00:04.939 JST [13246] CONTEXT:  COPY data2, line 1

SRの新マスタとなったサーバ3の動的統計情報ビューpg_stat_replicationを確認すると、サーバ3⇒サーバ2のロジカルレプリケーションが確立していることがわかります。

(サーバ3において実施)
pubdb_all_table=# select * from pg_stat_replication;
-[ RECORD 1 ]----+------------------------------
pid              | 26661
usesysid         | 16385
usename          | repusr1
application_name | sub_all_table
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 53059
backend_start    | 2018-02-08 16:07:25.52522+09
backend_xmin     |
state            | catchup
sent_lsn         | 0/58022DA0
write_lsn        | 0/58022DA0
flush_lsn        | 0/58022DA0
replay_lsn       | 0/58022DA0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 2 ]----+------------------------------
pid              | 26503
usesysid         | 16385
usename          | repusr1
application_name | sub2
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 52951
backend_start    | 2018-02-08 15:57:37.444994+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/58022DA0
write_lsn        | 0/58022DA0
flush_lsn        | 0/58022DA0
replay_lsn       | 0/58022DA0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async
-[ RECORD 3 ]----+------------------------------
pid              | 26486
usesysid         | 16385
usename          | repusr1
application_name | sub1
client_addr      | 192.168.127.32
client_hostname  |
client_port      | 52949
backend_start    | 2018-02-08 15:53:08.937385+09
backend_xmin     |
state            | streaming
sent_lsn         | 0/58022DA0
write_lsn        | 0/58022DA0
flush_lsn        | 0/58022DA0
replay_lsn       | 0/58022DA0
write_lag        |
flush_lag        |
replay_lag       |
sync_priority    | 0
sync_state       | async

6.4.6.4. 考察

ロジカルレプリケーションとストリーミングレプリケーションを併用する場合、更新対象を一元化してデータの整合性を担保しやすくするため、ストリーミングレプリケーションのマスタとロジカルレプリケーションのPublisherは同じサーバにした方がよいと考えます。

この場合、ストリーミングレプリケーションのマスタがスレーブに切り替わってもPublicationを再定義する必要はありません。

一方でSubscriptionは再作成が必要で、古いSubscriptionはレプリケーションスロットとの対応を無効化してから削除する必要があります。 また、新たなSubscriberを作成する際は以下のいずれかの方法が選択できます。

  • CREATE SUBSCRIPTION文にWITH (copy_data = false)を付けて初期データコピーを回避する。
  • SubscriberのテーブルをあらかじめTRUNCATE TABLEで空にしてからCREATE SUBSCRIPTIONを実行し、初期データコピーをやり直す。

前者は初期データコピーの処理が不要でロジカルレプリケーションを再開するまでの時間を短縮できますが、マスタの切替えからSubscriptionの再作成までに発生する更新を複製できないデメリットがありますので、運用形態によって選択する必要がありそうです。

6.5. まとめ

本章ではまずPostgreSQLのロジカルレプリケーションの特徴、アーキテクチャ、ユースケース、制限事項を整理し、実際の構築例を交えてロジカルレプリケーションを設定する具体的な手順を説明しました。ストリーミングレプリケーションではマスタのベースバックアップからデータベースクラスタ単位で一致するスレーブを作成したのに対し、ロジカルレプリケーションではPublication/Subscriptionといったオブジェクトによって、テーブル単位や更新種別単位での柔軟な複製形態でのレプリケーションを構成できることがわかりました。

また、実行時パラメータについては稼働するプロセス数との関係を検証した結果を踏まえて妥当な値を算出する方法を提示しました。アーキテクチャがストリーミングレプリケーションと共通な部分があるため一部の実行時パラメータはストリーミングレプリケーションで使用するプロセス数を考慮した設定が必要であることがわかりました。

次に同期レプリケーション、複数Subscriptionへのレプリケーション、カスケード構成といった応用的な使い方を検証し、ロジカルレプリケーションでもストリーミングレプリケーションと同様の構成がとれることが確認できました。また、パーティショニングを組み合わせることで複数サーバのデータを1つのサーバに集約するデータ統合の用途にも応用できることが確認できました。

さらにロジカルレプリケーションを実際に運用するシーンを想定し、監視方法や障害発生時の挙動、レプリケーション開始後のテーブル追加、定義変更の手順について検証しました。ストリーミングレプリケーションと違いロジカルレプリケーションではSubscriberを更新できる代わりに整合性をユーザが担保しなければならないため、更新時の競合が発生した場合もユーザ側で検知、解消する必要があります。ただし、現状では競合が発生した際の対応方法は限られており、運用も容易ではないため、基本的に競合が発生しない設計、運用を心がけるべきです。つまり、Subscriberには検索用のインデックスを付与する程度に留めてデータの更新は行わないのが望ましいと考えます。また、ストリーミングレプリケーションとロジカルレプリケーションを併用する環境での障害を想定した検証では、Subscriptionを新しいPublicationにつなぎ直してロジカルレプリケーションを再開するまでの具体的な手順を確認できました。

ロジカルレプリケーションは、アーキテクチャの面でストリーミングレプリケーションと共通する部分がありますが、データベースクラスタ単位で複製されるストリーミングレプリケーションよりも柔軟にテーブル単位、更新種別単位で複製できるようになり、レプリケーションの活用範囲が広がったといえます。その一方で、運用面ではロジカルレプリケーションの対象とならない操作やオブジェクトが存在することや、更新時の競合が発生した時の運用が全てユーザに任されている点など現状では制約事項も多く、運用面の負荷が軽減される機能強化が今後求められます。

7. Bi-Directional Replication (BDR)

7.1. はじめに

7.1.1. BDRの特徴

7.1.1.1. BDRとは

BDR(Bi-Directional Replication)は2ndQuadrant社によって開発された、オープンソース(PostgreSQL License)のマルチマスタ・レプリケーションシステムです。 双方向の非同期論理レプリケーションを使用し、地理的に分散したクラスタで使用するために設計されています。

マルチマスタについて

一般的なRDBMSの冗長化構成(マスタスレーブ構成)においては、更新処理を実行可能なサーバをマスタと呼称します。 マルチマスタとは、同一のデータを保持している複数のDB間(クラスタ)において、 更新処理を実施可能なサーバが複数台存在する構成を指します。 BDRでは双方向にレプリケーションを実施することで、複数のサーバへの更新を可能にしています。

7.1.1.2. ユースケース

BDRは以下のようなケースで有用です。

  1. 遠隔地や高レイテンシ環境でクラスタを構成している場合。
  2. 各ノードが書き込み処理を実施する場合。
  3. クラスタ間でデータが非同期であることを許容できる場合。

例としては以下のようにレスポンスを向上させるために各地でアプリケーション及びDBを動作させるようなケースが考えられます。

BDRユースケース

図 7.1 BDRユースケース

7.1.1.3. メカニズム

BDRでは「Logical Decoding」により、WALから論理的な変更点を抽出し、 各ノードで適用することで双方向レプリケーションを実現しています。 (Logical Decoding及びWALの送受信はバックグラウンドワーカープロセスが実施します)

従来のトリガーを用いた双方向レプリケーションの場合、 下図のように書き込みが余分に発生(変更記録、変更反映)してしまいます。

トリガベースメカニズム

図 7.2 トリガベースメカニズム

一方、BDRでは余分な書き込みが発生せずパフォーマンス的に有利となっています。

BDRメカニズム

図 7.3 BDRメカニズム

利用されているPostgreSQLのメカニズムの一覧です。

表 7.1 利用メカニズムの一覧
No. 機能 概要
1 Event Triggers 一つのテーブルに接続され、DMLイベントのみを補足する通常のトリガとは異なり、特定データベースのDDLイベントを捕捉可能。
2 Logical Decoding SQLを介して実行された更新処理を外部コンシューマへストリーミングするための機能。更新結果はロジカルレプリケーションスロットで識別され、ストリームに送出される。
3 Replication Slots マスタのデータベースの変更をスレーブ側に同じ順序で適用するための機能。
4 Background Workers ユーザ提供のコードを別プロセスで実行するように拡張する機能。Background Workerプロセスはpostgresプロセスによって起動、終了、監視される。
5 Commit Timestamps トランザクションがいつコミットされたかを確認するための機能。
6 Replication Origins レプリケーションの進行状況を追跡するための機能。双方向レプリケーションにてループの防止などが可能。
7 DDL event capture 実行されるDDLコマンドを返す機能。
8 generic WAL messages for logical decoding テキストあるいはバイナリのメッセージをWALに挿入できる仕組み(API)。Logical Decoding機能によって読み出されることを想定している。

7.1.1.4. 整合性

マルチマスタ構成を取る場合、各ノードが持つ情報に不整合が発生しないように管理する仕組みが必要となります。 BDRでは結果整合性(eventually consistent)と呼ばれる一貫性モデルを採用し、整合性を確保します。

  • デフォルトでは競合が発生した場合、最後の更新処理が適用されます。(last_update_wins)
  • 競合結果はテーブル「bdr.bdr_conflict_history」で確認可能です。
結果整合性

図 7.4 結果整合性

7.1.1.5. シーケンス

BDRでは一意な値を払い出すために、「グローバルシーケンス」、 「ステップ/オフセットシーケンス」という2つの手法を紹介しています。

  1. グローバルシーケンス

グローバルシーケンスでは、各ノードに予め値の塊(chunk)を一定数ずつ割り振ることで値の重複を回避しています。

  • chunkを消費するとvoting処理(下記参照)を行い、新たにchunkを割り振ります。
  • グローバルシーケンスは廃止予定です。(下記のステップ/オフセットシーケンスを推奨)

voting処理

"chunk"と呼ばれるシーケンス番号のまとまりをノードに割り当てる処理をvoting処理と呼びます。 "chunk"が複数のノードに割り当てられないことを確認するため、ノード間で投票処理が行われており、正常に機能させるためには奇数台のノードが必要です。 過半数のノードが停止している場合は、投票処理にて過半数に到達しなくなるため、新しい"chunk"がノードに割り当てられません。 そして、"chunk"が枯渇した場合、nextvalの実行に失敗してしまいます。

グローバルシーケンス

図 7.5 グローバルシーケンス

  1. ステップ/オフセットシーケンス

ステップ/オフセットシーケンスでは、各ノードで通常のPostgreSQLシーケンスを使用します。 各シーケンスを同じ量だけ増分されるようにし、値が重複しないように設定します。

  • 設計時には注意が必要です。
  • 増分する値をある程度大きく取らないとノードの追加に対応できません。(例: 10の増分であればノード数の追加は10台まで)
ステップ/オフセットシーケンス

図 7.6 ステップ/オフセットシーケンス

7.1.1.6. 比較表

類似機能及び製品との机上比較の結果です。

表 7.2 類似機能及び製品との机上比較
No. 比較項目 BDR SR(Hot Standby) Slony ロジカルレプリケーション
1 マルチマスタ × × ×
2 選択的レプリケーション ×
3 競合検知 × ×
4 カスケーディング ×
5 WALベースレプリケーション ×
6 DDLレプリケーション × ×
7 自動レプリカ新テーブル × ×
8 シーケンスレプリケーション ×
9 プライマリキー更新 ×
10 同期コミット ×
11 外部デーモン不使用 ×
12 レプリカへの書き込み ×

https://2ndquadrant.com/en/resources/bdr/

7.1.1.7. サポート

サポートについては以下が存在します。

表 7.3 サポート
No. サポート 概要
1 無償サポート
BDRコミュニティへのメール、BDRのGoogleグループが存在。
2 有償サポート
2ndQuadrant社によるサポートを受けることが可能です。
※ 2ndQuadrant社について
BDRの製造元で、PostgreSQLの専門家(コミッター等)が多数在籍する企業です。
PostgreSQLのコンサルティングサービス等を提供しています。

7.2. 調査、検証の目的

机上の情報整理および検証について、以下を主な目的としています。

  • 情報整理
    • BDRを使用したマルチマスタ環境を構築、運用するにあたり、各種機能やパラメータの理解に必要な情報の提供
  • 検証
    • BDRの動作(競合時等)や障害発生時の対処法を調査
    • 更新性能の検証

7.3. 調査、検証の前提

表 7.4 調査、検証の前提
項目 説明
PostgreSQLバージョン
9.4.10
※ 調査時点ではBDRは9.6に対応していなかったため
BDRバージョン
1.0.2
OS CentOS 7.1
構成 2ノード構成

ダウンロードモジュールは以下になります。

  • postgresql-bdr94-bdr.x86_64 0:1.0.2-1_2ndQuadrant.el7.centos
  • postgresql-bdr94.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos
  • postgresql-bdr94-contrib.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos
  • postgresql-bdr94-libs.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos
  • postgresql-bdr94-server.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos

以下は構成図になります。

調査、検証環境構成図

図 7.7 調査、検証環境構成図

7.4. BDR環境構築時の設定と設定手順

7.4.1. BDR環境の設定手順

7.4.1.1. 概要

PostgreSQL BDR (Bi-Directional Replication)を利用したマルチマスタ環境の構築手順について確認します。

また、マルチマスタ環境構築後、各ノードに対して更新が実行可能かを確認します。

http://bdr-project.org/docs/next/installation-packages.html#INSTALLATION-PACKAGES-REDHAT

こちらの環境 で検証を実施しました。

7.4.1.2. 対象試験サーバ

表 7.5 対象試験サーバ
サーバ名 IPアドレス
node1 192.168.0.10
node2 192.168.0.12

7.4.1.3. 事前確認

  1. yumが利用可能な状態であること
  • パッケージを入手する際にyumを使用

設定手順

指定がない部分は、node1・node2両方で実施します。

  1. 検証環境準備

BDR検証を実施するための環境を準備します。

ssh接続を用いて、該当環境へ接続
ユーザ: root
パスワード: xxxxxxx
  1. hostsの設定

DNSの名前解決のために各サーバのホスト名を設定します。

# vi /etc/hosts
[下記をファイル末尾に追加]
192.168.0.10 node1
192.168.0.12 node2
  1. 関連ポートの開放

PostgreSQL間のBDR接続のために5432ポートの開放をします。

# firewall-cmd --permanent --add-port=5432/tcp
# firewall-cmd --reload

以下のコマンドで確認します。

# firewall-cmd --list-ports
5432/tcp
  1. BDRレポジトリの登録

BDRレポジトリ用のRPMをインストールします。

# RHEL/CentOS users only:
# yum install http://packages.2ndquadrant.com/postgresql-bdr94-2ndquadrant/yum-repo-rpms/postgresql-bdr94-2ndquadrant-redhat-latest.noarch.rpm

以下のコマンドでインストール済み一覧を確認します。

# yum list installed postgresql-bdr94-2ndquadrant-redhat.noarch 0:1.0-3
[省略]
インストール済みパッケージ
  postgresql-bdr94-2ndquadrant-redhat.noarch            1.0-3            installed
  1. BDRインストール

登録したBDRレポジトリからBDRをインストールします。

# yum install postgresql-bdr94-bdr
[省略]
インストール:
  postgresql-bdr94-bdr.x86_64 0:1.0.2-1_2ndQuadrant.el7.centos

依存性関連をインストールしました:
  postgresql-bdr94.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos
  postgresql-bdr94-contrib.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos
  postgresql-bdr94-libs.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos
  postgresql-bdr94-server.x86_64 0:9.4.10_bdr1-1_2ndQuadrant.el7.centos

完了しました!

以下のコマンドでインストール済み一覧を確認します。

# yum list installed postgresql-bdr94-bdr.x86_64 0:1.0.2-1_2ndQuadrant.el7.centos
[省略]
インストール済みパッケージ
  postgresql-bdr94-bdr.x86_64 1.0.2-1_2ndQuadrant.el7.centos @postgresql-bdr94-2ndquadrant-redhat
  1. 環境変数設定

データベースクラスタとコマンド実行ファイルに環境変数の設定をします。

$ vi ~/.bash_profile
[下記の修正を加える]
#PGDATA=/var/lib/pgsql/9.4-bdr/data
PGDATA=/var/lib/pgsql/2ndquadrant_bdr/data/
[下記をファイル末尾に追加]
export PATH=/usr/pgsql-9.4/bin:$PATH

以下のコマンドで設定した環境変数を確認します。

$ exit
ログアウト
# su - postgres
$ echo $PATH
/usr/pgsql-9.4/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin
$ echo $PGDATA
/var/lib/pgsql/2ndquadrant_bdr/data/
  1. データベースクラスタ作成

BDR検証用に新規でデータベースクラスタを作成します。

$ mkdir -p $HOME/2ndquadrant_bdr/
$ initdb -D $HOME/2ndquadrant_bdr/data -A trust -U postgres --no-locale
  1. BDRのパラメータ設定(postgresql.conf)

BDRのパラメータ設定(postgresql.conf)をします。

$ vi /var/lib/pgsql/2ndquadrant_bdr/data/postgresql.conf
[下記の修正を加える]
listen_addresses = '*'
shared_preload_libraries = 'bdr'
wal_level = 'logical'
track_commit_timestamp = on
max_connections = 100
max_wal_senders = 10
max_replication_slots = 10
# Make sure there are enough background worker slots for BDR to run
max_worker_processes = 10
log_line_prefix = '%m d=%d p=%p a=%a%q '

# These aren't required, but are useful for diagnosing problems
#log_error_verbosity = verbose
#log_min_messages = debug1
# Useful options for playing with conflicts
#bdr.default_apply_delay=2000   # milliseconds
#bdr.log_conflicts_to_table=on
  1. BDRのクライアント認証設定(pg_hba.conf)

BDRのクライアント認証設定(pg_hba.conf)をします。

$ vi /var/lib/pgsql/2ndquadrant_bdr/data/pg_hba.conf
[下記の修正を加える]
host    all             all                192.168.0.10/32            trust
host    all             all                192.168.0.12/32            trust
local   replication   postgres                  trust
host    replication   postgres     0.0.0.0/0    trust
host    replication   postgres     ::1/128      trust
  1. PostgreSQL起動
設定が環境しましたらPostgreSQLを起動します。
起動時にBDRのバックグランドワーカの登録メッセージが出力される事を確認します。
$ pg_ctl start
サーバは起動中です。
-bash-4.2$ < 2016-10-19 01:47:01.891 JST >LOG:  バックグラウンドワーカ"bdr supervisor"を登録しています
< 2016-10-19 01:47:02.041 JST >LOG:  ログ出力をログ収集プロセスにリダイレクトし ています
< 2016-10-19 01:47:02.041 JST >ヒント:  ここからのログ出力はディレクトリ"pg_log"に現れます。

以下のコマンドでPostgreSQLの起動状況を確認します。

$ pg_ctl status
pg_ctl: サーバが動作中です(PID: 10948)
/usr/pgsql-9.4/bin/postgres
  1. データベース作成

BDRの動作確認用のデータベースを作成します。

$ createdb bdrtest

以下のコマンドで作成したデータベースへの接続を確認します。

$ psql bdrtest
psql (9.4.9)
"help" でヘルプを表示します.

bdrtest=# \q
-bash-4.2$
  1. BDRモジュール登録

BDRに必要な拡張モジュールを登録します。

$ psql -U postgres bdrtest

=# CREATE EXTENSION btree_gist;
=# CREATE EXTENSION bdr;

以下のコマンドでインストール済みの拡張モジュールを確認します。

=# \dx
                             インストール済みの拡張の一覧
    名前    | バージョン |  スキーマ  |                     説明
------------+------------+------------+-----------------------------------------------
 bdr        | 1.0.1.0    | pg_catalog | Bi-directional replication for PostgreSQL
 btree_gist | 1.0        | public     | support for indexing common datatypes in GiST
 plpgsql    | 1.0        | pg_catalog | PL/pgSQL procedural language
(3 )
  1. BDRグループ作成

node1でBDRグループを作成するため、「bdr.bdr_group_create」関数を実行します。

$ psql -U postgres bdrtest
=# SELECT bdr.bdr_group_create(
     local_node_name := 'node1',
     node_external_dsn := 'host=node1 port=5432 dbname=bdrtest');

グループが作成されたことを確認するため、「bdr.bdr_node_join_wait_for_ready」関数を実行します。

=# SELECT bdr.bdr_node_join_wait_for_ready();
 bdr_node_join_wait_for_ready
------------------------------

(1 )
  1. ノード登録

node2でノード登録するために、「bdr.bdr_group_join」関数を実行します。

$ psql -U postgres bdrtest
=# SELECT bdr.bdr_group_join(
     local_node_name := 'node2',
     node_external_dsn := 'host=node2 port=5432 dbname=bdrtest',
     join_using_dsn := 'host=node1 port=5432 dbname=bdrtest'
   );

ノードが登録されたことを確認するため、「bdr.bdr_node_join_wait_for_ready」関数を実行します。

=# SELECT bdr.bdr_node_join_wait_for_ready();
  bdr_node_join_wait_for_ready
 ------------------------------

 (1 )
  1. ノード状態の確認

ノードの状態を「bdr.bdr_nodes」テーブルの情報から確認します。

$ psql -U postgres bdrtest
=# \x
=# SELECT * FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_sysid         | 6369044607312522907
node_timeline      | 1
node_dboid         | 16385
node_status        | r
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_init_from_dsn |
node_read_only     | f
-[ RECORD 2 ]------+------------------------------------
node_sysid         | 6369045470046586386
node_timeline      | 1
node_dboid         | 16385
node_status        | r
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest
node_read_only     | f
  1. 簡易動作検証(node1)
node1でBDRの簡易動作検証を実施します。
node1で作成されたテーブルとデータがnode2にレプリケーションされていることを確認します。
$ psql -U postgres bdrtest
=# CREATE TABLE t1bdr (c1 INT, PRIMARY KEY (c1));
=# INSERT INTO t1bdr VALUES (1);
=# INSERT INTO t1bdr VALUES (2);
  1. 簡易動作検証(node2)
node2でBDRの簡易動作検証の結果を確認します。
node1と同じテーブルとデータが表示できていれば、構築したBDR環境に問題はありません。
node1と同様の簡易動作検証をnode2からも実施して下さい。
$ psql -U postgres bdrtest
=# SELECT * FROM t1bdr;
 c1
----
  1
  2
(2 行)
=# INSERT INTO t1bdr VALUES (3);

7.5. BDR動作検証

7.5.1. ノードの追加・切り離し

7.5.1.1. 検証の目的

ノードの追加/切り離しをオンライン(DB停止)なしで実行できるか否かを確認します。

7.5.1.2. 検証内容

本検証では、2台で構成されたBDRクラスタに対して、下記を実施しました。

  1. ノード切り離し
  2. ノード追加
ノードの追記・切り離し

「ノード切り離し」および「ノード追加」時に他ノード(上図のnode1)にトランザクションを実行し、トランザクションにエラーが発生するか否かを確認しました。

7.5.1.3. 検証環境

こちらの環境 で検証を実施しました。

7.5.1.4. 検証手順

7.5.1.4.1. ノード切り離し
  1. BDRの各ノード状態確認

BDRの各ノード間のレプリケーションが正常に動作していることを「bdr.bdr_nodes」テーブルを用いて確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                                    <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                                    <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest  <-- node1から作成されていることを確認
  1. 検証用テーブルとデータの作成

pgbebchを利用して、検証時に利用するテーブルとデータを作成します。

(node1にて実施)
$ pgbench -i -s 10 bdrtest
  1. トランザクション実行

pgbenchを利用して、ノード1にトランザクションを継続的に実行します。 ノード2を切り離した発生した場合に、ノード1に対して実行したトランザクションにエラーが発生するか否かを確認します。

$ pgbench -h node1 -c 10 -t 100000 bdrtest
  starting vacuum...end.
  1. ノード切り離し

ノード2を切り離しするため、「bdr.bdr_part_by_node_names」関数を実行します。

(node1のデータベースに接続)
=# SELECT bdr.bdr_part_by_node_names(ARRAY['node2']);
bdr_part_by_node_names
------------------------
(1 row)
  1. ノードの切り離し結果確認

ノードの切り離し結果を「bdr.bdr_nodes」テーブルを用いて確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | k                                    <-- k(削除)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest
  1. システムカタログの残データ削除

本手順は推奨される手順ではありませんが、削除状態のノードが「bdr.bdr_nodes」テーブルに残っていると削除したノードの追加が実行できないため暫定的な対処です。 BDRのIssuesで「bdr.bdr_connections」のデータも削除する事が提案されていたので、こちらも暫定的な手順ですが実行します。 詳細は下記をご参照下さい。

(node1のデータベースに接続)
=# DELETE FROM bdr.bdr_connections USING bdr.bdr_nodes WHERE node_status = 'k' AND
(node_sysid, node_timeline, node_dboid) = (conn_sysid, conn_timeline, conn_dboid);
=# DELETE FROM bdr.bdr_nodes where node_status = 'k';
  1. ノードの切り離し結果確認

ノードの切り離し結果を「bdr.bdr_nodes」テーブルを用いて確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r
node_init_from_dsn |
  1. BDRの無効化

ノード2のデータベースから「bdr.remove_bdr_from_local_node」関数を用いてBDRを削除し、BDR拡張機能を削除します。

(node2のデータベースに接続)
=# SELECT bdr.remove_bdr_from_local_node(true);
WARNING:  forcing deletion of possibly active BDR node
NOTICE:  removing BDR from node
NOTICE:  BDR removed from this node. You can now DROP EXTENSION bdr and,
if this is the last BDR node on this PostgreSQL instance, remove bdr from shared_preload_libraries.
remove_bdr_from_local_node
----------------------------

=# DROP EXTENSION bdr;
DROP EXTENSION
  1. トランザクション状態実行状態確認
手順1で実行したpgbenchにエラーが発生してないことを確認します。本検証ではエラーは発生しませんでした。
7.5.1.4.2. ノード追加

切り離したBDRノードを再度追加する場合、既存ノードのデータベースと追加するノードのデータベースのスキーマおよびデータを同期させる必要があります。 ノード間のデータコピーには、論理コピーと物理コピーの2つの手法があります。

表 7.6 コピー取得方法
項番 コピー取得 説明 備考
1 bdr.bdr_group_join 関数実行 ユーザが指定したノード内データベースのスキーマとデータダンプを取得 pg_dumpコマンドに相当
2 bdr_init_copyコマンド ユーザが指定したノード上の全てのデータベースのコピーを取得 pg_basebackupコマンドに相当

■論理コピーによるノード追加

  1. トランザクション実行

pgbenchを利用して、ノード1にトランザクションを継続的に実行します。 ノード2に復旧する際に、ノード1に対して実行したトランザクションの停止が必要か否かを確認します。

$ pgbench -h node1 -c 10 -t 100000 bdrtest
starting vacuum...end.
  1. ノード2のPostgreSQL起動

ノード2上で動作するPostgreSQLを起動させます。

$ pg_ctl start
  1. データベース削除

BDRで利用したデータベースを削除します。

(node2のデータベースに接続)
=# DROP DATABASE bdrtest ;                ※ 接続が残っており、削除できない場合はPostgreSQLを再起動
DROP DATABASE
  1. データベースの再作成

BDRで利用するデータべースを再度作成します。

(node2のデータベースに接続)
=# CREATE DATABASE bdrtest;
  1. BDR有効化

無効化したBDRを再度有効化します。

(node2のデータベースに接続)
=# CREATE EXTENSION btree_gist;
CREATE EXTENSION
=# CREATE EXTENSION bdr;
CREATE EXTENSION
  1. ノードの追加

ノードを追加(復旧)させるため、「bdr.bdr_group_join」関数を実行します。

(node2のデータベースに接続)
=# SELECT bdr.bdr_group_join(
     local_node_name := 'node2',
     node_external_dsn := 'host=node2 port=5432 dbname=bdrtest',
     join_using_dsn := 'host=node1 port=5432 dbname=bdrtest'
   );

bdr_group_join
----------------

(1 )
  1. ノード追加の確認待ち

ノードが追加されたことを確認するため、「bdr.bdr_node_join_wait_for_ready」関数を実行します。

(node2のデータベースに接続)
=# SELECT bdr.bdr_node_join_wait_for_ready();
※ トランザクションが実行中の場合、上記関数の結果が戻りません。

node1のPostgreSQLログファイルに下記メッセージが出力された後、復旧の処理が開始されません。

(node1のログメッセージ抜粋)
LOG:  logical decoding found initial starting point at 0/BB399BF0
DETAIL:  10 transactions need to finish.

pgbenchコマンドで実行中のトランザクションを停止すると、下記ログメッセージが出力され、復旧処理が開始されます。

(node1のログメッセージ抜粋)
LOG:  logical decoding found consistent point at 0/B2895648
DETAIL:  There are no running transactions.
LOG:  exported logical decoding snapshot: "00046FE0-1" with 0 transaction IDs
  1. BDRの各ノード状態確認

ノード2が追加されたことを「bdr.bdr_nodes」テーブルの情報から確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest                  <-- node1から作成されていることを確認

■ 物理コピーによるノード追加

  1. トランザクション実行

pgbenchを利用して、ノード1にトランザクションを継続的に実行します。 ノード2に追加する際に、ノード1に対して実行したトランザクションの停止が必要か否かを確認します。

$ pgbench -h node1 -c 10 -t 100000 bdrtest
starting vacuum...end.
  1. ノード2のPostgreSQL停止確認

ノード2上で動作するPostgreSQLが停止していることを確認します。

(node2にて実施)
$ pg_ctl status
pg_ctl: no server running
  1. 物理コピーの取得

ノード2上で「bdr_init_copy」コマンドを実行し、ノード1上のコピーを取得します。

(node2にて実施)
$ rm -rf $PGDATA/*
$ bdr_init_copy -D $PGDATA -n node2 -h node1 -p 5432 -d bdrtest
  --local-host=node2 --local-port=5432 --local-dbname=bdrtest
  bdr_init_copy: starting ...
  Getting remote server identification ...
  Detected 1 BDR database(s) on remote server
  Updating BDR configuration on the remote node:
  bdrtest: creating replication slot ...
  bdrtest: creating node entry for local node ...
  Creating base backup of the remote node...
  50357/50357 kB (100%), 1/1 tablespace
  Creating restore point on remote node ...
  Bringing local node to the restore point ...
  トランザクションログをリセットします。
  Initializing BDR on the local node:
  bdrtest: adding the database to BDR cluster ...
  All done

node1のPostgreSQLログファイルに下記メッセージが出力された後、追加の処理が開始されません。

(node1のログメッセージ抜粋)
LOG:  logical decoding found initial starting point at 0/BB399BF0
DETAIL:  10 transactions need to finish.

pgbenchコマンドで実行中のトランザクションを停止すると、下記ログメッセージが出力され、復旧処理が開始されます。

(node1のログメッセージ抜粋
LOG:  logical decoding found consistent point at 0/B2895648
DETAIL:  There are no running transactions.
STATEMENT:  SELECT pg_create_logical_replication_slot('bdr_25434_6369931070716042622_2_25434__', 'bdr');
  1. BDRの各ノード状態確認

ノード2が追加されたことを「bdr.bdr_nodes」テーブルの情報から確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest                  <-- node1から作成されていることを確認

7.5.1.5. 検証結果

切り離したノードの情報がシステムカタログに残ってしまうため、データ操作が禁止されているシステムカタログのデータ削除が必要でした。

システムカタログのデータ操作は禁止されていますが、切り離したノードの情報がシステムカタログに残っている場合、 ノード追加時に下記のメッセージが出力され、ノードが追加が実施できないため、本検証では暫定対処としてシステムカタログのデータ削除を実施しております。

ERROR:  System identification mismatch between connection and slot
詳細:  Connection for bdr (6369029438929838565,1,16385,) resulted in slot on node bdr (6369034270871134883,2,16385,) instead of expected node
LOG:  ワーカプロセス: bdr db: bdrtest (PID 17232)は終了コード1で終了しました

ノード追加にはプロセスの再起動は必要ありませんが、トランザクションの停止が必要でした。

7.5.2. グローバルシーケンス設定

7.5.2.1. 検証の目的

BDRに実装されたグローバルシーケンスの利用方法について確認します。 グルーバルシーケンスを利用することでノード毎に払い出されるシーケンス番号を独立させ、ノード間のシーケンス番号の競合を防ぐことができるか否かを確認します。

7.5.2.2. 検証環境

こちらの環境 で検証を実施しました。

7.5.2.3. 検証手順

  1. 初期状態

BDRの各ノード間のレプリケーションが正常に動作していることを「bdr.bdr_nodes」テーブルを用いて確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                                    <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                                    <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest  <-- node1から作成されていることを確認
  1. グローバルシーケンス動作確認

node1のデータベースに接続し、serial型を列に持つgstestテーブルを作成します。 serial型を持つテーブルを作成すると、自動的にシーケンスが作成されます。 グローバルシーケンス作成のため、テーブル作成時に「SET LOCAL default_sequenceam = 'bdr';」を指定する必要があります。

(node1のデータベースに接続)
=#
BEGIN;
    SET LOCAL default_sequenceam = 'bdr';                 <-- グローバルシーケンス作成用の設定
    CREATE TABLE gstest (
      id serial primary key,
      hogehoge text
    );
COMMIT;

参考. 上記DDL実行時にnode1に出力されるログメッセージ
LOG:  DDL LOCK TRACE: attempting to acquire in mode <ddl_lock> for (bdr (xxxxxxxx))
--------
STATEMENT:  CREATE TABLE gstest (
              id serial primary key,
              hogehoge text
            );
LOG:  DDL LOCK TRACE: attempting to acquire in mode <write_lock> (upgrading from <ddl_lock>) for (bdr (xxxxxxxxx,))
>STATEMENT:  CREATE TABLE gstest (
              id serial primary key,
              hogehoge text
            );
--------
  1. グルーバルシーケンス作成確認

node1およびnode2のデータベースに接続し、グローバルシーケンスが作成されたことを確認します。

(node1のデータベースに接続)
=# \ds
List of relations
-[ RECORD 1 ]---------
Schema | public
Name   | gstest_id_seq
Type   | sequence
Owner  | postgres
(node2のデータベースに接続)
=# \ds
List of relations
-[ RECORD 1 ]---------
Schema | public
Name   | gstest_id_seq
Type   | sequence
Owner  | postgres

※ 下記のSQLで詳細情報(pg_classの情報)を確認可能です。

=# SELECT * FROM pg_class
   INNER JOIN pg_seqam ON (pg_class.relam = pg_seqam.oid)
   WHERE pg_seqam.seqamname = 'bdr' AND relkind = 'S';
  1. グローバルシーケンス利用

node1のデータベースに接続し、1件のデータを投入します。

(node1のデータベースに接続)
=# INSERT INTO gstest(hogehoge) VALUES ('test1');
INSERT 0 1

node2のデータベースに接続し、1件のデータを投入します。

(node2のデータベースに接続)
=# INSERT INTO gstest(hogehoge) VALUES ('test2');
INSERT 0 1
  1. シーケンス値の競合有無確認

node1およびnode2のシーケンスの値に競合が発生していないことを確認します。 本検証では、node1に2が割り当てられ、node2には100001が割り当てられました。

(node1のデータベースに接続)
=# SELECT * FROM gstest;
   id   | hogehoge
--------+----------
      2 | test1                                    <-- node1には2のシーケンスが割り当てられる
 100001 | test2                                    <-- node2には100001のシーケンスが割り当てられる
(2 rows)
  1. グローバルシーケンス利用

node1に200,000件のデータを投入し、シーケンスの値が100,000を超えた場合、node2の100,001と競合しないか否かを確認しました。

(node1において実施)
$ cat /tmp/gs.sh
#!/bin/sh

for i in `seq 1 200000`
  do
      psql -d bdrtest -c "INSERT into gstest(hogehoge) VALUES ('test${i}');"
      done
$ sh /tmp/gs.sh
INSERT 0 1
[省略]
  1. グローバルの状態確認

node1のシーケンスが100,000を利用したのち、node1に割り振られた値を確認しました。

(node1のデータベースに接続)
=# SELECT * FROM gstest WHERE 99999 <= id;

   id   |  hogehoge
--------+------------
  99999 | test99997
 100000 | test99998
 100001 | test2
 150001 | test99999                -< 100,000を超えると、node1のシーケンスが150,001に割り振られる。
 150002 | test100000
 150003 | test100001
 150004 | test100002
 150005 | test100003
 150006 | test100004

7.5.2.4. 検証結果

グローバルシーケンスを用いることでノード間のシーケンス番号の競合を抑止できることが確認できました。 グルーバルシーケンスについてはいくつかの制限事項がマニュアルに記載されておりますので、注意して下さい。

  1. 1(デフォルト値)のINCREMENTだけがサポートされています。
  2. MINVALUEとMAXVALUEはデフォルトでロックされており、変更することはできません。
  3. CACHE指令はサポートされていません。

7.5.3. 選択的レプリケーション

選択的レプリケーションについて記載します。

7.5.3.1. 検証目的

テーブル単位での選択的レプリケーションの可否を確認します。 選択的レプリケーションの可否は互いのデータベース間での任意のテーブルのデータ状態を元に判断します。 また、選択的レプリケーションを実現する際の変更手順を明確にする事を目的とします。

7.5.3.2. 検証内容

選択的レプリケーション検証概要図

選択的レプリケーション検証内容

  • 双方向レプリケーションの動作確認
  • 選択的レプリケーション(node1)の動作確認
  • 選択的レプリケーション(node2)の動作確認

7.5.3.3. 検証環境

こちらの環境 で検証を実施しました。

7.5.3.4. 検証手順

  1. 選択的レプリケーション環境構築

選択的レプリケーションの動作確認の為に、下記の検証環境を構築します。

(1)データベース作成

テストを実施する為のデータベースを作成します。

(node1,node2において実施)
=# CREATE DATABASE rep_test;

(2)BDRの有効化

作成したデータベース上でBDR機能の有効化を実施します。

(node1,node2のデータベースにおいて実施)
=# CREATE EXTENSION btree_gist;
=# CREATE EXTENSION bdr;

(3)BDRグループの作成

BDRノードのクラスタに「bdr.bdr_group_create」を利用して最初のノードを作成します。

(node1のデータベースに接続)
=# SELECT bdr.bdr_group_create(
     local_node_name := 'node1',
     node_external_dsn := 'host=node1 port=5432 dbname=rep_test',
     replication_sets:= ARRAY['default','node1']);                   ※ replications_retにnode1を追加

(4)ノード登録(node2)

既存のBDRノードのクラスタに「bdr.bdr_group_join」を利用してノードを登録します。 これにより、全てのノード間でレプリケーションが開始されます。

(node2のデータベースに接続)
=# SELECT bdr.bdr_group_join(
     local_node_name := 'node2',
     node_external_dsn := 'host=node2 port=5432 dbname=rep_test',
     join_using_dsn := 'host=node1 port=5432 dbname=rep_test',
     replication_sets:= ARRAY['default','node2']);                   ※ replications_retにnode2を追加

(5)ノードの状態確認

BDRグループ内のノードのメンバシップを「bdr.bdr_nodes」を利用して確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+-------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=rep_test
node_status        | r                                       <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+-------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=rep_test
node_status        | r                                       <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=rep_test    <-- node1から作成されていることを確認

(6)レプリケーションセットの確認

指定したノードのレプリケーションセットを「bdr.connection_get_replication_sets」を利用して確認します。

(node1のデータベースに接続)
=# SELECT bdr.connection_get_replication_sets('node1');
-[ RECORD 1 ]-------------------+----------------
connection_get_replication_sets | {default,node1}        <-- node1が追加されていること

=# SELECT bdr.connection_get_replication_sets('node2');
-[ RECORD 1 ]-------------------+----------------
connection_get_replication_sets | {default,node2}        <-- node2が追加されていること
  1. 双方向レプリケーション対象テーブルの動作確認

まずは、通常の双方向レプリケーション動作を確認します。 片方のテーブルにデータを挿入した時にもう一方のテーブルにもデータが挿入されています。

(1)テーブル(all_rep)作成

テストを実施する為のテーブル(all_rep)を作成します。

(node1のデータベースに接続)
=# CREATE TABLE all_rep(id int, PRIMARY KEY(id));
CREATE TABLE

(2)レプリケーションセット確認

指定したテーブルのレプリケーションセットを「bdr.connection_get_replication_sets」を利用して確認します。

(node1のデータベースに接続)
=# SELECT bdr.table_get_replication_sets('all_rep');
-[ RECORD 1 ]--------------+--------------
table_get_replication_sets | {default,all}    <-- defaultが含まれていること

(3)データ追加

node1のテーブルにデータを挿入します。

(node1のデータベースに接続)
=# INSERT INTO all_rep VALUES (1);
INSERT 0 1

(4)レプリケーション確認

node2のテーブルにもデータがレプリケーションされている事を確認します。

(node2のデータベースに接続)
=# SELECT id FROM all_rep;
 id
----
1                                             <-- データがレプリケーションされていること
(1 row)
  1. レプリケーション対象外テーブルの作成(node1のみ)

次に、選択的レプリケーション動作を確認します。 指定したテーブルではレプリケーション動作が行われなくなりますので、任意のテーブルのみレプリケーションさせる事が出来ます。

(1)テーブル(node1_only)の作成

テストを実施する為のテーブル(node1_only)を作成します。

(node1のデータベースに接続)
=# CREATE TABLE node1_only(id int, PRIMARY KEY(id));
CREATE TABLE

(2)レプリケーションセット変更

指定したテーブルのレプリケーションセットを「bdr.table_set_replication_sets」を利用して'node1'に設定し、レプリケーション対象を'node1'のみにします。 設定したレプリケーションセットを確認するには、「bdr.table_get_replication_sets」を利用します。

(node1のデータベースに接続)
=# SELECT bdr.table_set_replication_sets('node1_only', ARRAY['node1']);
-[ RECORD 1 ]--------------+-
table_set_replication_sets |

=# SELECT bdr.table_get_replication_sets('node1_only');
-[ RECORD 1 ]--------------+------------
table_get_replication_sets | {node1,all}      <-- node1が追加されていること

(3)データ追加

node1のテーブルにデータを挿入します。

(node1のデータベースに接続)
=# INSERT INTO node1_only VALUES (1);
INSERT 0 1

(4)レプリケーション確認

node2のテーブルにはデータがレプリケーションされていない事を確認します。

(node2のデータベースに接続)
=# SELECT id FROM node1_only;
 id
----                                          <-- レプリケーションされていないこと
(0 )
  1. レプリケーション対象外テーブルの作成(node2のみ)

同様に、もう一方からの選択的レプリケーション動作も確認します。

(1)テーブル(node2_only)の作成

テストを実施する為のテーブル(node2_only)を作成します。

(node2のデータベースに接続)
=# CREATE TABLE node2_only(id int, PRIMARY KEY(id));
CREATE TABLE

(2)レプリケーションセット変更

指定したテーブルのレプリケーションセットを「bdr.table_set_replication_sets」を利用して'node2'に設定し、レプリケーション対象を'node2'のみにします。 設定したレプリケーションセットを確認するには、「bdr.table_get_replication_sets」を利用します。

(node2のデータベースに接続)
=# SELECT bdr.table_set_replication_sets('node2_only', ARRAY['node2']);
-[ RECORD 1 ]--------------+-
table_set_replication_sets |

=# SELECT bdr.table_get_replication_sets('node2_only');
-[ RECORD 1 ]--------------+------------
table_get_replication_sets | {node2,all}      <-- node2が追加されていること

(3)データ追加

node2のテーブルにデータを挿入します。

(node2のデータベースに接続)
=# INSERT INTO node2_only VALUES (1);
INSERT 0 1

(4)レプリケーション確認

node1のテーブルにはデータがレプリケーションされていない事を確認します。

(node1のデータベースに接続)
=# SELECT id FROM node2_only;
 id
----                                          <-- レプリケーションされていないこと
(0 )

7.5.3.5. 検証結果

今回の検証結果では、レプリケーションセットに任意のノードを指定する事で選択的レプリケーションが実現される事が確認出来ました。 また、選択的レプリケーション実現の為の設定変更手順についても確認出来ました。

ただし、レプリケーションセットにテーブルを追加する場合は、過去のデータまで反映しないので、手動で同期が必要です。

選択的レプリケーションの主な注意事項は下記になります。

  1. DDLはレプリケーションセットの設定に関係なく常にすべてのノードに影響します。
  2. TRUNCATEは常にレプリケートされます。望ましくない場合は、DELETEを利用する必要があります。
  3. レプリケーションセットの設定は、初期ノード追加(結合時)にはテーブルデータのコピーに影響を与えません。
  4. レプリケーションセットにテーブルを追加した場合も以前のデータ内容はノードに同期されません。 通常、管理者は、テーブルをレプリケーションセットに追加した後、手動で同期する必要があります。

7.5.4. 更新処理競合時の動作

7.5.4.1. 検証目的

BDRはマルチマスタ構成する各ノードに対して、参照処理と更新処理を実行することが可能です。 複数ノードに対して同時に更新処理が実施された場合、各ノードに対して実行された更新処理が競合する事象が発生する場合があります。

BDR競合が発生した場合、最後の更新処理が適用されます(last_update_wins)。また、競合結果はテーブル「bdr.bdr_conflict_history」で確認可能です。

下記を明らかにするため検証を実施しました。

  1. 更新処理が競合した場合の挙動
  2. 競合発生時にシステムカタログに記録される情報

7.5.4.2. 検証内容

BDR動作検証概要図

BDR動作検証概要図

下表の競合発生時の動作を検証しました。

表 7.7 検証した競合のパターン
項番 分類 説明
1 PRIMARY KEYまたはUNIQUE制約
2つの操作が同じUNIQUE KEYを持つ行に影響を及ぼす
行の競合を検証します。
2 外部キー制約
外部キー制約が定義されたテーブルにおいて、
制約に違反するデータ削除によって引き起こされる競合を検証します。
3 排他制約
BDRでは排他制約をサポートしていないために、
排他制約において競合が発生した場合を検証します。
4 グローバルなデータ
ノードのグローバル(PostgreSQLシステム全体)の
データ(ロールなど)が異なる場合での競合を検証します。
5 ロックの競合とデッドロックの中断 BDR適用プロセスとロックの競合について検証します。
6 その他
自動的に解決出来ないデータの相違が発生した場合に
手動で調整を行う方法を検証します。

7.5.4.3. 検証環境

こちらの環境 で検証を実施しました。

7.5.4.4. 検証手順

  1. 競合ログオプションの有効化確認

競合発生時にテーブル「bdr.bdr_conflict_history」に情報を記録するため、bdr.log_conflicts_to_tableパラメータがonに設定されていることを確認します。

※ offになっていたら、postgresql.confに「bdr.log_conflicts_to_table=on」を追記し、再読み込みを実行して下さい。

node1/node2のデータベースに接続し、パラメータを確認するために下記コマンドを実行します。

(node1にて実施)
=# SHOW bdr.log_conflicts_to_table;
bdr.log_conflicts_to_table
----------------------------
on
(1 )
(node2にて実施)
=# SHOW bdr.log_conflicts_to_table;
bdr.log_conflicts_to_table
----------------------------
on
(1 )
  1. レプリケーション反映時間の設定

ネットワーク遅延などの実際の環境を想定して検証を行うため、bdr.default_apply_delayパラメータを設定します。 このパラメータを設定する事でレプリケーションの反映を設定した時間(ミリ秒)遅らせる事が出来ます。

※ 今回の検証では、競合の発生を確認しやすくするために"2s"を設定しています。

node1/node2のデータベースに接続し、パラメータを確認するために下記コマンドを実行します。

(node1にて実施)
=# SHOW bdr.default_apply_delay;
bdr.default_apply_delay
----------------------------
2s
(1 )
(node2にて実施)
=# SHOW bdr.default_apply_delay;
bdr.default_apply_delay
----------------------------
2s
(1 )
  1. BDRの各ノード状態確認

BDRの各ノード間のレプリケーションが正常に動作していることを「bdr.bdr_nodes」を利用して確認します。

(node1にて実施)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;

-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                                      <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                                      <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest    <-- node1から作成されていることを確認
  1. 検証用テーブルとデータの作成

pgbebchを利用して、検証時に利用するテーブルとデータを作成します。

(node1にて実施)
$ pgbench -i -s 10 bdrtest
$ psql -h node1 -d bdrtest

=# INSERT INTO pgbench_accounts (aid, bid, abalance) VALUES (1000001, 1, 0);
7.5.4.4.1. PRIMARY KEYまたはUNIQUE制約
  1. INSERT vs INSERT

■ 競合概要

最も一般的な競合として、2つの異なるノードのINSERTが同じPRIMARY KEYの値(または、単一のUNIQE制約の値)を持つデータを挿入するケースを検証しました。

■ 検証結果

タイムスタンプに従い、最後にデータが作成された方が保持されました。

■ 競合発生手順

各ノードからそれぞれINSERTを実行し、INSERT vs INSERT の競合を発生させます。

(node1のデータベースへSQLを投入)
=# INSERT INTO pgbench_accounts (aid, bid, abalance) VALUES (1000002, 2, 0);

(node2のデータベースへSQLを投入)
=# INSERT INTO pgbench_accounts (aid, bid, abalance) VALUES (1000002, 3, 0);

競合ログを「bdr.bdr_conflict_history」を利用して確認し、競合の発生を確認します。

※ 競合履歴テーブルでは競合ログが出力されている側のサーバをローカル、反対側をリモートとして表示します。 よって、それぞれのサーバで実行されたトランザクションをローカルトランザクション、リモートトランザクションとして説明します。

(node1またはnode2の競合履歴テーブルに競合ログが出力)
=# \x
=# SELECT * FROM bdr.bdr_conflict_history ORDER BY conflict_id DESC LIMIT 1;

-[ RECORD 1 ]------------+---------------------------------------------------
conflict_id              | 48
local_node_sysid         | 6371561413984311673
local_conflict_xid       | 3944663
local_conflict_lsn       | 3/765949B8
local_conflict_time      | 2017-01-25 10:36:03.787698+09
object_schema            | public
object_name              | pgbench_accounts
remote_node_sysid        | 6371553825031594764
remote_txid              | 3960113
remote_commit_time       | 2017-01-25 10:32:48.772254+09
remote_commit_lsn        | 2/CA1AD950
conflict_type            | insert_insert
conflict_resolution      | last_update_wins_keep_local
local_tuple              | {"aid":1000002,"bid":3,"abalance":0,"filler":null}
remote_tuple             | {"aid":1000002,"bid":2,"abalance":0,"filler":null}
local_tuple_xmin         | 3944662
local_tuple_origin_sysid |
error_message            |
error_sqlstate           |
error_querystring        |
error_cursorpos          |
error_detail             |
error_hint               |
error_context            |
error_columnname         |
error_typename           |
error_constraintname     |
error_filename           |
error_lineno             |
error_funcname           |

それぞれのテーブルの状態を確認し、競合発生時の処理を確認します。

※ 後にデータが作成されたローカルトランザクションが保持されています。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
  aid   | bid | abalance | filler
--------+-----+----------+--------
1000001 |   1 |        0 |
1000002 |   3 |        0 |
(2 行)

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
  aid   | bid | abalance | filler
--------+-----+----------+--------
1000001 |   1 |        0 |
1000002 |   3 |        0 |
(2 行)

※ last_update_wins_keep_local : 最新のタイムスタンプであるローカル側の更新が適用
  1. INSERT vs UPDATE

■ 競合概要

一つのノードでINSERTしたデータともう片方のノードでUPDATEしたデータが同じPRIMARY KEYの値を持つケースを検証しました。

■ 検証結果

INSERT/UPDATEの競合が発生した場合、競合を解消するためオペレータ側でのデータ操作が必要となり、注意が必要です。

■ 競合発生手順

下記テーブルを初期状態とします。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
  aid   | bid | abalance | filler
--------+-----+----------+--------
1000001 |   1 |        0 |
(1 )

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
  aid   | bid | abalance | filler
--------+-----+----------+--------
1000001 |   1 |        0 |
(1 )

各ノードからそれぞれINSERT/UPDATEを実行し、INSERT vs UPDATE の競合を発生させます。

(node1のデータベースへSQLを投入)
=# INSERT INTO pgbench_accounts (aid, bid, abalance) VALUES (1000002, 4, 0);

(node2のデータベースへSQLを投入)
=# UPDATE pgbench_accounts SET aid = 1000002 WHERE aid = 1000001;

競合ログを「bdr.bdr_conflict_history」を利用して確認し、競合の発生を確認します。

(node1またはnode2の競合履歴テーブルに競合ログが出力)
=# \x
=# SELECT * FROM bdr.bdr_conflict_history ORDER BY conflict_id DESC LIMIT 1;

-[ RECORD 1 ]------------+---------------------------------------------------
conflict_id              | 49
local_node_sysid         | 6371561413984311673
local_conflict_xid       | 3944715
local_conflict_lsn       | 3/81295CF8
local_conflict_time      | 2017-01-25 10:36:46.780822+09
object_schema            | public
object_name              | pgbench_accounts
remote_node_sysid        | 6371553825031594764
remote_txid              | 3960133
remote_commit_time       | 2017-01-25 10:33:31.640126+09
remote_commit_lsn        | 2/D1CE6F00
conflict_type            | insert_insert
conflict_resolution      | last_update_wins_keep_local
local_tuple              | {"aid":1000002,"bid":1,"abalance":0,"filler":null}
remote_tuple             | {"aid":1000002,"bid":4,"abalance":0,"filler":null}
local_tuple_xmin         | 3944714
local_tuple_origin_sysid |
error_message            |
error_sqlstate           |
error_querystring        |
error_cursorpos          |
error_detail             |
error_hint               |
error_context            |
error_columnname         |
error_typename           |
error_constraintname     |
error_filename           |
error_lineno             |
error_funcname           |

それぞれのテーブルの状態を確認し、競合発生時の処理を確認します。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
 1000002 |   4 |        0 |
(2 )

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000002 |   1 |        0 |
(1 )

PRIMARY KEYのUPDATEで発生した競合でリモート側では一意制約違反のために適用プロセスがデータ反映できず、 ローカル側では論理レプリケーション中に接続が切断され、データの整合性がとれなくなる状態が確認されました。

(node1のデータベースログ)
LOG:  starting background worker process "bdr (6371553825031594764,1,43905,)->bdr (6371561413984311673,2,"
ERROR:  duplicate key value violates unique constraint "pgbench_accounts_pkey"
DETAIL:  Key (aid)=(1000002) already exists.
CONTEXT:  apply UPDATE from remote relation public.pgbench_accounts in commit 1/2F279190, xid 3941360 commited at 2017-01-10 11:00:22.736985+09 (action #2) from node (6371561413984311673,2,43865)
LOG:  worker process: bdr (6371553825031594764,1,43905,)->bdr (6371561413984311673,2, (PID 21287) exited with exit code 1

(node2のデータベースログ)
bdr (6371553825031594764,1,43905,):receive LOG:  starting logical decoding for slot "bdr_43865_6371553825031594764_1_43905__"
bdr (6371553825031594764,1,43905,):receive DETAIL:  streaming transactions committing after 1/2F279190, reading WAL from 1/2F279068
bdr (6371553825031594764,1,43905,):receive LOG:  logical decoding found consistent point at 1/2F279068
bdr (6371553825031594764,1,43905,):receive DETAIL:  There are no running transactions.
bdr (6371553825031594764,1,43905,):receive LOG:  could not receive data from client: Connection reset by peer
bdr (6371553825031594764,1,43905,):receive LOG:  unexpected EOF on standby connection

※ 競合するタプルをローカル側から手動で削除するか、新しいリモートタプルと競合しなくなるようにUPDATEする必要がある

上記状態では、ローカル側からのレプリケーションが実施されない状態が続くため、解消するために、ローカル側のデータをUPDATEします。

(node1のデータベースへSQLを投入)
=# UPDATE pgbench_accounts SET aid = 1000003 WHERE aid = 1000002;
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
 1000003 |   4 |        0 |
(2 行)

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000002 |   1 |        0 |
 (1 行)

※ 競合は解消され、次のデータからレプリケーションが始まるがデータの整合性で問題あり
  1. UPDATE vs DELETE

■ 競合概要

一つのノードでUPDATEしたデータともう片方のノードでDELETEしたデータが同じPRIMARY KEYの値を持つケースを検証しました。

■ 検証結果

UPDATE/DELETEが競合した場合、UPDATEが破棄されました。

■ 競合発生手順

下記テーブルを初期状態とします。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
(1 )

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
(1 )

各ノードからそれぞれUPDATE/DELETEを実行し、UPDATE vs DELETE の競合を発生させます。

(node1のデータベースにSQLを投入)
=# UPDATE pgbench_accounts SET bid = 4 WHERE aid = 1000001;

(node2のデータベースにSQLを投入)
=# DELETE FROM pgbench_accounts WHERE aid = 1000001;

競合ログを「bdr.bdr_conflict_history」を利用して確認し、競合の発生を確認します。

(node1またはnode2の競合履歴テーブルに競合ログが出力)
=# \x
=# SELECT * FROM bdr.bdr_conflict_history ORDER BY conflict_id DESC LIMIT 1;

-[ RECORD 1 ]------------+---------------------------------------------------
conflict_id              | 51
local_node_sysid         | 6371561413984311673
local_conflict_xid       | 0
local_conflict_lsn       | 3/8BF7BA38
local_conflict_time      | 2017-01-25 10:37:45.561678+09
object_schema            | public
object_name              | pgbench_accounts
remote_node_sysid        | 6371553825031594764
remote_txid              | 3960157
remote_commit_time       | 2017-01-25 10:34:30.564936+09
remote_commit_lsn        | 2/D9831CC0
conflict_type            | update_delete
conflict_resolution      | skip_change
local_tuple              |
remote_tuple             | {"aid":1000001,"bid":4,"abalance":0,"filler":null}
local_tuple_xmin         |
local_tuple_origin_sysid |
error_message            |
error_sqlstate           |
error_querystring        |
error_cursorpos          |
error_detail             |
error_hint               |
error_context            |
error_columnname         |
error_typename           |
error_constraintname     |
error_filename           |
error_lineno             |
error_funcname           |

それぞれのテーブルの状態を確認し、競合発生時の処理を確認します。

※ DELETE後のUPDATEが破棄されています。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
 aid | bid | abalance | filler
-----+-----+----------+--------
(0 行)

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
 aid | bid | abalance | filler
-----+-----+----------+--------
(0 行)

※ skip_change : 変更を無視し、破棄された
  1. INSERT vs DELETE

■ 競合概要

一つのノードでINSERTしたデータがもう片方のノードでDELETEされたデータと同じPRIMARY KEYの値を持つケースを検証しました。

■ 検証結果

DELETEの処理が破棄されました。競合発生の状態についてはシステムカタログから確認することができませんでした。

■ 競合発生手順

下記テーブルを初期状態とします。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
(1 )

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
(1 )

各ノードからそれぞれINSERT/DELETEを実行し、INSERT vs DELETE の競合を発生させます。

(node1のデータベースにSQLを投入)
=# INSERT INTO pgbench_accounts (aid, bid, abalance) VALUES (1000002, 5, 0);

(node2のデータベースにSQLを投入)
=# DELETE FROM pgbench_accounts WHERE aid = 1000002;

競合ログを「bdr.bdr_conflict_history」を利用して確認し、競合の発生を確認します。

※ 競合は確認出来ませんでした。

(node1またはnode2の競合履歴テーブルに競合ログが出力されない)
=# \x
=# SELECT * FROM bdr.bdr_conflict_history ORDER BY conflict_id DESC LIMIT 1;

それぞれのテーブルの状態を確認します。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
 1000002 |   5 |        0 |
(2 )

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
 1000002 |   5 |        0 |
(2 )
  1. DELETE vs DELETE

■ 競合概要

2つの異なるノードで実行されたDELETEが同じPRIMARY KEYの値を持つデータを削除するケースを検証しました。

■ 検証結果

DELETE/DELETEが競合した場合、片方のDELETEが無視され処理を完了しました。

■ 競合発生手順

下記テーブルを初期状態とします。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
(1 )

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
   aid   | bid | abalance | filler
---------+-----+----------+--------
 1000001 |   1 |        0 |
(1 )

各ノードからそれぞれDELETEを実行し、DELETE vs DELETE の競合を発生させます。

(node1のデータベースにSQLを投入)
=# DELETE FROM pgbench_accounts WHERE aid = 1000001;

(node2のデータベースにSQLを投入)
=# DELETE FROM pgbench_accounts WHERE aid = 1000001;

競合ログを「bdr.bdr_conflict_history」を利用して確認し、競合の発生を確認します。

(node1またはnode2の競合履歴テーブルに競合ログが出力)
=# \x
=# SELECT * FROM bdr.bdr_conflict_history ORDER BY conflict_id DESC LIMIT 1;

-[ RECORD 1 ]------------+---------------------------------------------------------
conflict_id              | 52
local_node_sysid         | 6371561413984311673
local_conflict_xid       | 0
local_conflict_lsn       | 3/A193CA88
local_conflict_time      | 2017-01-25 10:39:07.794699+09
object_schema            | public
object_name              | pgbench_accounts
remote_node_sysid        | 6371553825031594764
remote_txid              | 3960197
remote_commit_time       | 2017-01-25 10:35:52.78243+09
remote_commit_lsn        | 2/E8E9A388
conflict_type            | delete_delete
conflict_resolution      | skip_change
local_tuple              |
remote_tuple             | {"aid":1000001,"bid":null,"abalance":null,"filler":null}
local_tuple_xmin         |
local_tuple_origin_sysid |
error_message            |
error_sqlstate           |
error_querystring        |
error_cursorpos          |
error_detail             |
error_hint               |
error_context            |
error_columnname         |
error_typename           |
error_constraintname     |
error_filename           |
error_lineno             |
error_funcname           |

それぞれのテーブルの状態を確認し、競合発生時の処理を確認します。

※ ローカル側のノードからのDELETEが無視されています。

(node1のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
 aid | bid | abalance | filler
-----+-----+----------+--------
(0 行)

(node2のデータベースを確認)
=# SELECT * FROM  pgbench_accounts WHERE aid >= 1000001;
 aid | bid | abalance | filler
-----+-----+----------+--------
(0 行)

※ skip_change : 変更を無視し、破棄された
7.5.4.4.2. 外部キー制約
  1. 外部キー制約の競合

■ 競合概要

1つのノードで外部キー制約が定義されたテーブルに対してデータを挿入し、上記データ挿入がもう片方のノードに反映される前に、 もう片方のノードにおいて外部キーの参照先であるテーブルのデータを削除することで、外部キー制約が定義されたテーブルへのデータ挿入と、外部キー参照先テーブルに対するデータ削除を競合させます。

■ 検証結果

外部キー制約に違反する処理が実施された場合でもBDRではエラーにならず、データが外部キー制約違反の状態になりました。

■ 競合発生手順

下記の親子関係を持つテーブルを作成します。

(node1のデータベースに接続)
=# CREATE TABLE parent(id integer primary key);
=# CREATE TABLE child(id integer primary key, parent_id integer not null references parent(id));
=# INSERT INTO parent(id) VALUES (1), (2);
=# INSERT INTO child(id, parent_id) VALUES (11, 1), (12, 2);

各ノードからそれぞれINSERT/DELETEを実行し、外部キー制約の競合を発生させます。

※ node1からのINSERTはnode2では、子テーブルで参照されている親が存在せず、 node2のDELETEはnode1で参照している親データを削除しようとしているので、それぞれ適用出来なくなります。

(node1のデータベースにSQLを投入)
=# INSERT INTO child(id, parent_id) VALUES (21, 2);

(node2のデータベースにSQLを投入)
=# DELETE FROM child WHERE parent_id = 2;
=# DELETE FROM parent WHERE id = 2;

それぞれのテーブルの状態を確認し、競合発生時の処理を確認します。

(node1のデータベースを確認)
=# SELECT * FROM child;
id | parent_id
----+-----------
 11 |         1
 21 |         2
(2 )

=# SELECT * FROM parent;
 id
----
1
(1 )
(node2のデータベースを確認)
=# SELECT * FROM child;
id | parent_id
----+-----------
 11 |         1
 21 |         2
(2 行)

=# SELECT * FROM parent;
 id
----
1
(1 行)

※ 外部キー制約違反のデータがchildテーブルに存在する。
7.5.4.4.3. 排他制約
  1. 排他制約の競合

■ 競合概要

排他な関係である2つのデータを異なるノードから同時にテーブルにINSERTし、ノード間のデータに競合が発生するケースを検証します。

■ 検証結果

一つのノードでINSERTされたデータをもう片方のノードに反映させる際に排他制約違反が発生し、ノード間の接続が切断されます。 競合を解消するためにはオペレータでのデータ操作が必要となるため注意が必要です。

■ 競合発生手順

下記の排他制約を持つテーブルを作成します。

(node1のデータベースにSQLを投入)
=# CREATE TABLE sample (aid integer primary key, range daterange, price integer);
=# ALTER TABLE sample ADD EXCLUDE USING gist (price WITH =, range WITH &&);

各ノードからそれぞれINSERTを実行し、排他制約の競合を発生させます。

※ [2012-04-18 ~ 2012-04/20]の期間が競合しているために排他制約違反を起こしています。

(node1のデータベースにSQLを投入)
=# INSERT INTO sample VALUES(1, '[2012-04-16, 2012-04-20]', 10000);

(node2のデータベースにSQLを投入)
=# INSERT INTO sample VALUES(2, '[2012-04-18, 2012-04-23]', 10000);
(node1またはnode2の競合履歴テーブルに競合ログが出力されない)
=# \x
=# SELECT * FROM bdr.bdr_conflict_history ORDER BY conflict_id DESC LIMIT 1;

それぞれのテーブルの状態を確認し、競合発生時の処理を確認します。

※ 他のノードで実行されたデータ更新を反映する際に、制約違反が発生し、BDRによるノード間の接続が切断されます。

(node1のデータベースを確認)
=# SELECT * FROM sample;
 aid |          range          | price
-----+-------------------------+-------
   1 | [2012-04-16,2012-04-21) | 10000
(1 )

(node2のデータベースを確認)
=# SELECT * FROM sample;
 aid |          range          | price
-----+-------------------------+-------
   2 | [2012-04-18,2012-04-24) | 10000
(1 )
(node1のデータベースログ)
bdr (6371561413984311673,2,42879,):receive LOG:  starting logical decoding for slot "bdr_42892_6371561413984311673_2_42879__"
bdr (6371561413984311673,2,42879,):receive DETAIL:  streaming transactions committing after 1/15D41640, reading WAL from 1/15D413B0
bdr (6371561413984311673,2,42879,):receive LOG:  logical decoding found consistent point at 1/15D413B0
bdr (6371561413984311673,2,42879,):receive DETAIL:  There are no running transactions.
bdr (6371561413984311673,2,42879,):receive LOG:  unexpected EOF on standby connection
LOG:  starting background worker process "bdr (6371553825031594764,1,42892,)->bdr (6371561413984311673,2,"
ERROR:  conflicting key value violates exclusion constraint "sample_price_range_excl"
DETAIL:  Key (price, range)=(10000, [2012-04-18,2012-04-24)) conflicts with existing key (price, range)=(10000, [2012-04-16,2012-04-21)).
CONTEXT:  apply INSERT from remote relation public.sample in commit 1/194FBC20, xid 3940666 commited at 2017-01-06 16:12:28.275376+09 (action #2) from node (6371561413984311673,2,42879)
LOG:  worker process: bdr (6371553825031594764,1,42892,)->bdr (6371561413984311673,2, (PID 27843) exited with exit code 1

(node2のデータベースログ)
LOG:  starting background worker process "bdr (6371561413984311673,2,42879,)->bdr (6371553825031594764,1,"
ERROR:  conflicting key value violates exclusion constraint "sample_price_range_excl"
DETAIL:  Key (price, range)=(10000, [2012-04-16,2012-04-21)) conflicts with existing key (price, range)=(10000, [2012-04-18,2012-04-24)).
CONTEXT:  apply INSERT from remote relation public.sample in commit 1/15D41640, xid 3957701 commited at 2017-01-06 16:09:10.299493+09 (action #2) from node (6371553825031594764,1,42892)
LOG:  worker process: bdr (6371561413984311673,2,42879,)->bdr (6371553825031594764,1, (PID 29311) exited with exit code 1
bdr (6371553825031594764,1,42892,):receive LOG:  starting logical decoding for slot "bdr_42879_6371553825031594764_1_42892__"
bdr (6371553825031594764,1,42892,):receive DETAIL:  streaming transactions committing after 1/194FB9D0, reading WAL from 1/194FB700
bdr (6371553825031594764,1,42892,):receive LOG:  logical decoding found consistent point at 1/194FB700
bdr (6371553825031594764,1,42892,):receive DETAIL:  There are no running transactions.
bdr (6371553825031594764,1,42892,):receive LOG:  unexpected EOF on standby connection

※ 各ノードで制約違反が発生し、BDRによる接続が切断されてしまう。解消のためリモートタプルが競合するローカルタプルを削除または変更する

制約違反の状態を解消するために、両側のデータをUPDATEします。

(node1のデータベースにSQLを投入)
=# UPDATE sample SET range = '[2012-04-11,2012-04-16]' WHERE aid = 1;
(node2のデータベースにSQLを投入)
=# UPDATE sample SET range = '[2012-04-23,2012-04-29]' WHERE aid = 2;
(node1のデータベースを確認)
=# SELECT * FROM sample;
 aid |          range          | price
-----+-------------------------+-------
   1 | [2012-04-11,2012-04-17) | 10000
   2 | [2012-04-23,2012-04-30) | 10000
(2 )

(node2のデータベースを確認)
=# SELECT * FROM sample;
 aid |          range          | price
-----+-------------------------+-------
   2 | [2012-04-23,2012-04-30) | 10000
   1 | [2012-04-11,2012-04-17) | 10000
(2 )
7.5.4.4.4. グローバルなデータ
  1. グローバルなデータの競合

■ 競合概要

ロール(グローバルデータ)の情報がノード間で異なる状態で、他のノードに存在しないロールを利用した場合に発生する競合のケースを検証しました。

■ 検証結果

レプリケーション先のノードに同名のロールが存在しない場合、エラーになります。 エラーを解消するには、オペレータ側での操作が必要となるため注意が必要です。

■ 競合発生手順

新規に作成したロールでテーブルを作成します。

(node1のデータベースにSQLを投入)
=# CREATE ROLE testuser SUPERUSER LOGIN;
=# \c testuser
=# CREATE TABLE test01 (id integer primary key);
=# \d
         リレーションの一覧
 スキーマ |  名前  |        |  所有者
----------+--------+----------+----------
 public   | test01 | テーブル | testuser
(1 )
(node2のデータベースにSQLを投入)
=# \d
リレーションがありません。

※ ロールなどはレプリケーション対象とならないので、そのロールで作成されたテーブルもレプリケーション対象とならない

データベースログを確認し、ERRORが継続して出力されていれば競合が発生しています。

(node2のデータベースログ)
LOG:  starting background worker process "bdr (6371561413984311673,2,42879,)->bdr (6371553825031594764,1,"
ERROR:  role "testuser" does not exist
CONTEXT:  during DDL replay of ddl statement: CREATE  TABLE  public.test01 (id pg_catalog.int4   , CONSTRAINT test01_pkey PRIMARY KEY (id) )  WITH (oids=OFF)
    apply QUEUED_DDL in commit 1/15DD2278, xid 3958054 commited at 2017-01-06 17:37:58.608995+09 (action #2) from node (6371553825031594764,1,42892)
LOG:  worker process: bdr (6371561413984311673,2,42879,)->bdr (6371553825031594764,1, (PID 30136) exited with exit code 1

※ 権限は関係なく、同名のロールが作成されれば、競合は解決する

レプリケーションを再開させるためにロールなどのグローバルデータを作成します。

(node2のデータベースにSQLを投入)
=# CREATE ROLE testuser LOGIN;
=# \d
       リレーションの一覧
 スキーマ |  名前  |        |  所有者
----------+--------+----------+----------
 public   | test01 | テーブル | testuser
(1 )
7.5.4.4.5. ロックの競合とデッドロックの中断
  1. ロックの競合とデッドロックの中断

■ 競合概要

一つのノードで取得したロックともう片方のノードで取得したロックが、デッドロック状態となるケースを検証しました。

■ 検証結果

異なるノードで実行されたトランザクションでデッドロック状態が発生した場合、ロックが解除されるまでロック待ちが発生します。 PostgreSQLにおいてデッドロックが発生した場合、デッドロックを引き起こすトランザクションが自動でロールバックされますが、 異なるノード間で発生したデッドロックについては検出できないため、ロック待ち状態が続きます。

■ 競合発生手順

下記テーブルを初期状態とします。

(node1のデータベースにSQLを投入)
=# CREATE TABLE sample (aid integer primary key, range daterange, price integer);
=# ALTER TABLE sample ADD EXCLUDE USING gist (price WITH =, range WITH &&);
=# INSERT INTO sample VALUES(1, '[2012-04-16, 2012-04-20]', 10000);
=# INSERT INTO sample VALUES(2, '[2012-04-21, 2012-04-30]', 12000);
(node1のデータベースを確認)
=# SELECT * FROM sample;
 aid |          range          | price
-----+-------------------------+-------
   1 | [2012-04-16,2012-04-21) | 10000
   2 | [2012-04-21,2012-05-01) | 12000
(2 )

(node2のデータベースを確認)
=# SELECT * FROM sample;
 aid |          range          | price
-----+-------------------------+-------
   1 | [2012-04-16,2012-04-21) | 10000
   2 | [2012-04-21,2012-05-01) | 12000
(2 )

1つのノードからテーブルをロックし、もう片方のノードからはUPDATEを実行し、ロック待ちの影響を受ける事を確認します。 ロックが解除されるまで、BDR適用プロセスはロック待ちが発生します。

(node1のデータベースにSQLを投入)
=# BEGIN;
=# LOCK TABLE sample IN ACCESS EXCLUSIVE MODE;
=# SELECT pg_sleep(10);
=# END;

(node2のデータベースにSQLを投入)
=# UPDATE sample SET price = 14000 WHERE aid = 2;
(ロック待ちなので競合ログが出力されない)
=# \x
=# SELECT * FROM bdr.bdr_conflict_history ORDER BY conflict_id DESC LIMIT 1;
7.5.4.4.6. その他

自動的には解決出来ないデータの相違が発生した場合は、以下設定を使用して手動で調整する必要があります。

※ レプリケーション環境を破壊することが可能であるため使用する際には注意が必要です。

項番 パラメータ 説明
1 bdr.do_not_replicate(boolean)
このパラメータセットを持つトランザクションで行われた変更は、
他ノードへのレプリケーションのためにキューに格納されない。
=# CREATE TABLE test (id int primary key, name text);
=# \q
(node1に対してのみレコード追加を実行)
$ export PGOPTIONS='-c bdr.do_not_replicate=on'
$ psql -h node1 bdrtest
=# INSERT INTO test values(1, 'test');
=# SELECT * from test;
 id | name
----+------
  1 | test
(1 行)

(node2には反映されていないことを確認)
=# SELECT * from test;
 id | name
----+------
(0 行)
項番 パラメータ 説明
2 bdr.skip_ddl_replication(boolean)
DDLによる変更のレプリケーションをスキップする。
一部のノードのみにDDLを実行したい場合に有効。
スーパーユーザのみが設定可能。
(node1にのみテーブルを作成)
=# BEGIN;
=# SET LOCAL bdr.skip_ddl_replication = true;
=# CREATE TABLE skip (id int primary key, name text);
=# END;
=# \d
       リレーションの一覧
 スキーマ |  名前  |    型    |  所有者
----------+--------+----------+----------
 public   | sample | テーブル | postgres
 public   | skip   | テーブル | postgres
 public   | test   | テーブル | postgres
(3 行)

(node2には反映されていないことを確認)
=# \d
       リレーションの一覧
 スキーマ |  名前  |    型    |  所有者
----------+--------+----------+----------
 public   | sample | テーブル | postgres
 public   | test   | テーブル | postgres
(2 行)
項番 パラメータ 説明
3 bdr.permit_unsafe_ddl_commands(boolean)
安全にレプリケーションできないスキーマの変更を許可する。
スーパーユーザのみが設定可能。
以下のようなデフォルトでは実行が許可されていないクエリを実行可能。
* CREATE TABLE AS
* ALTER TABLE ... ADD COLUMN ... DEFAULT
* CREATE MATERIALIZED VIEW
* REFRESH MATERIALIZED VIEW
(デフォルトではエラーとなる)
=# CREATE TABLE test2 AS SELECT * from test;
ERROR:  CREATE TABLE AS is not supported when bdr is active

(エラーが出ないことを確認)
=# BEGIN;
=# SET LOCAL bdr.permit_unsafe_ddl_commands = true;
=# CREATE TABLE test2 AS SELECT * from test;
=# END;

7.5.4.5. 検証結果

一部の競合パターンにて、意図しない動作が発生するため、現段階では競合が発生しないパターンで利用すべきと考えます。

7.5.5. ノード障害と復旧

7.5.5.1. 検証の目的

複数ノードで構成されるクラスタ環境内の1ノードに障害が発生した場合でも、他ノードで継続利用可能か否かを確認します。 また、障害が発生したノードをクラスタ環境に復旧させる手順を確認します。

7.5.5.2. 検証内容

本検証では2台で構成されたBDRクラスタに対して、下記を実施しました。

  1. ノード障害
  2. ノード復旧
ノード障害と復旧

7.5.5.3. 検証環境

こちらの環境 で検証を実施しました。

7.5.5.4. 検証手順

7.5.5.4.1. ノード障害
  1. BDRの各ノード状態確認

BDRの各ノード間のレプリケーションが正常に動作していることを「bdr.bdr_nodes」テーブルを用いて確認します。

(node1にて実施)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                                                                  <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                                                                  <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest                                <-- node1から作成されていることを確認
  1. 検証用テーブルとデータの作成

pgbebchを利用して、検証時に利用するテーブルとデータを作成します。

(node1にて実施)
$ pgbench -i -s 10 bdrtest
  1. トランザクション実行

pgbenchを利用して、ノード1にトランザクションを継続的に実行します。 ノード2に障害が発生した場合に、ノード1に対して実行したトランザクションにエラーが発生するか否かを確認します。

$ pgbench -h node1 -c 10 -t 100000 bdrtest
starting vacuum...end.
  1. ノード障害

ノード2で動作するPostgreSQLを停止させます。

(node2にて実施)
$ pg_ctl stop -m i
waiting for server to shut down..... done
server stopped
  1. ノード1のログ確認

ノード2障害時に出力されるログメッセージを確認します。 下記メッセージは出力されますが、pgbenchのトランザクションは継続して実行可能です。

$ tail -f [PostgreSQLのログファイル]
LOG:  starting background worker process "bdr (6367633348875313343,1,34478,)->bdr (6369931070716042622,2,"
ERROR:  establish BDR: could not connect to server: Connection refused
Is the server running on host "node2" (192.168.1.3) and accepting
              TCP/IP connections on port 5432?
  1. BDRの各ノード状態確認

ノード2障害後、ノード状態が変化するか否かを「bdr.bdr_nodes」テーブルを用いて確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)の状態のまま
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest
※ 状態変化は確認できませんでした。
  1. ノードの切り離し

障害が発生したノード2を切り離しするため、「bdr.bdr_part_by_node_names」関数を実行します。

(node1のデータベースに接続)
=# SELECT bdr.bdr_part_by_node_names(ARRAY['node2']);
bdr_part_by_node_names
------------------------

(1 row)
  1. ノードの切り離し結果確認

ノードの切り離し結果を「bdr.bdr_nodes」テーブルを用いて確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn , node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | k                  <-- k(削除)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest
  1. システムカタログの残データ削除

本手順は推奨される手順ではありませんが、削除状態のノードが「bdr.bdr_nodes」テーブルに残っているとノード復旧が実行できないため暫定的な対処です。 BDRのIssuesで「bdr.bdr_connections」のデータも削除する事が提案されていたので、こちらも暫定的な手順ですが実行します。 詳細は下記をご参照下さい。

(node1のデータベースに接続)
=# DELETE FROM bdr.bdr_connections USING bdr.bdr_nodes WHERE node_status = 'k' AND"
(node_sysid, node_timeline, node_dboid) = (conn_sysid, conn_timeline, conn_dboid);
=# DELETE FROM bdr.bdr_nodes where node_status = 'k';
  1. トランザクション状態実行状態確認
手順1で実行したpgbenchにエラーが発生してないことを確認します。本検証ではエラーは発生しませんでした。
7.5.5.4.2. ノード復旧

BDRノードの復旧方法(ノード追加)する場合、既存ノードのデータベースと復旧させるノードのデータベースのスキーマおよびデータを同期させる必要があります。 ノード間のデータコピーには、論理コピーと物理コピーの2つの手法があります。

表 7.8 コピー取得方法
項番 コピー取得 説明 備考
1 bdr.bdr_group_join 関数実行 ユーザが指定したノード内データベースのスキーマとデータダンプを取得 pg_dumpコマンドに相当
2 bdr_init_copyコマンド ユーザが指定したノード上の全てのデータベースのコピーを取得 pg_basebackupコマンドに相当

■ 論理コピーによる復旧

  1. トランザクション実行

pgbenchを利用して、ノード1にトランザクションを継続的に実行します。 ノード2に復旧する際に、ノード1に対して実行したトランザクションの停止が必要か否かを確認します。

$ pgbench -h node1 -c 10 -t 100000 bdrtest
starting vacuum...end.
  1. ノード2のPostgreSQL起動

ノード2上で動作するPostgreSQLを起動させます。

$ pg_ctl start
  1. BDRの無効化

ノード2のデータベースからBDRを削除し、BDR拡張機能を削除します。

(node2のデータベースに接続)
=# SELECT bdr.remove_bdr_from_local_node(true);
WARNING:  forcing deletion of possibly active BDR node
NOTICE:  removing BDR from node
NOTICE:  BDR removed from this node. You can now DROP EXTENSION bdr and,
if this is the last BDR node on this PostgreSQL instance, remove bdr from shared_preload_libraries.
 remove_bdr_from_local_node
----------------------------

=# DROP EXTENSION bdr;
DROP EXTENSION
  1. データベース削除

BDRで利用したデータベースを削除します。

(node2のデータベースに接続)
=# DROP DATABASE bdrtest ;                ※ 接続が残っており、削除できない場合はPostgreSQLを再起動
DROP DATABASE
  1. データベースの再作成

BDRで利用するデータべースを再度作成します。

(node2のデータベースに接続)
=# CREATE DATABASE bdrtest;
  1. BDR有効化

無効化したBDRを再度有効化します。

(node2のデータベースに接続)
=# CREATE EXTENSION btree_gist;
CREATE EXTENSION
=# CREATE EXTENSION bdr;
CREATE EXTENSION
  1. ノードの追加

ノードを追加(復旧)させるため、「bdr.bdr_group_join」関数を実行します。

(node2のデータベースに接続)
=# SELECT bdr.bdr_group_join(
   local_node_name := 'node2',
   node_external_dsn := 'host=node2 port=5432 dbname=bdrtest',
   join_using_dsn := 'host=node1 port=5432 dbname=bdrtest'
   );
bdr_group_join
----------------

(1 )
  1. ノード追加の確認待ち

ノードが追加されたことを確認するため、「bdr.bdr_node_join_wait_for_ready」関数を実行します。

(node2のデータベースに接続)
=# SELECT bdr.bdr_node_join_wait_for_ready();
※ トランザクションが実行中の場合、上記関数の結果が戻りません。

node1のPostgreSQLログファイルに下記メッセージが出力された後、復旧の処理が開始されません。

(node1のログメッセージ抜粋)
LOG:  logical decoding found initial starting point at 0/BB399BF0
DETAIL:  10 transactions need to finish.

pgbenchコマンドで実行中のトランザクションを停止すると、下記ログメッセージが出力され、復旧処理が開始されます。

(node1のログメッセージ抜粋)
LOG:  logical decoding found consistent point at 0/B2895648
DETAIL:  There are no running transactions.
LOG:  exported logical decoding snapshot: "00046FE0-1" with 0 transaction IDs
  1. BDRの各ノード状態確認

ノード2が追加されたことを「bdr.bdr_nodes」テーブルの情報から確認します。

(node1のデータベースに接続)
=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest                  <-- node1から作成されていることを確認

■ 物理コピーによる復旧

  1. トランザクション実行

pgbenchを利用して、ノード1にトランザクションを継続的に実行します。 ノード2に復旧する際に、ノード1に対して実行したトランザクションの停止が必要か否かを確認します。

$ pgbench -h node1 -c 10 -t 100000 bdrtest
starting vacuum...end.
  1. ノード2のPostgreSQL停止確認

ノード2上で動作するPostgreSQLが停止していることを確認します。

(node2にて実施)
$ pg_ctl status
pg_ctl: no server running
  1. 物理コピーの取得

ノード2上で「bdr_init_copy」コマンドを実行し、node1上のコピーを取得します。

(node2にて実施)
$ rm -rf $PGDATA/*
$ bdr_init_copy -D $PGDATA -n node2 -h node1 -p 5432 -d bdrtest
  --local-host=node2 --local-port=5432 --local-dbname=bdrtest
  bdr_init_copy: starting ...
  Getting remote server identification ...
  Detected 1 BDR database(s) on remote server
  Updating BDR configuration on the remote node:
  bdrtest: creating replication slot ...
  bdrtest: creating node entry for local node ...
  Creating base backup of the remote node...
  50357/50357 kB (100%), 1/1 tablespace
  Creating restore point on remote node ...
  Bringing local node to the restore point ...
  トランザクションログをリセットします。
  Initializing BDR on the local node:
  bdrtest: adding the database to BDR cluster ...
  All done

node1のPostgreSQLログファイルに下記メッセージが出力された後、復旧の処理が開始されません。

(node1のログメッセージ抜粋)
LOG:  logical decoding found initial starting point at 0/BB399BF0
DETAIL:  10 transactions need to finish.

pgbenchコマンドで実行中のトランザクションを停止すると、下記ログメッセージが出力され、 復旧処理が開始されます。

(node1のログメッセージ抜粋
LOG:  logical decoding found consistent point at 0/B2895648
DETAIL:  There are no running transactions.
STATEMENT:  SELECT pg_create_logical_replication_slot('bdr_25434_6369931070716042622_2_25434__', 'bdr');
  1. BDRの各ノード状態確認

ノード2が追加されたことを「bdr.bdr_nodes」テーブルの情報から確認します。

(node1のデータベースに接続)
bdrtest=# SELECT node_name, node_local_dsn, node_status, node_init_from_dsn FROM bdr.bdr_nodes;
-[ RECORD 1 ]------+------------------------------------
node_name          | node1
node_local_dsn     | host=node1 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn |
-[ RECORD 2 ]------+------------------------------------
node_name          | node2
node_local_dsn     | host=node2 port=5432 dbname=bdrtest
node_status        | r                  <-- r(正常)であることを確認
node_init_from_dsn | host=node1 port=5432 dbname=bdrtest                  <-- node1から作成されていることを確認

7.5.5.5. 検証結果

ノード障害が発生した際にも、他のノードではトランザクションを継続実行することが可能でした。 障害ノードを復旧させるためにはデータ操作が禁止されているシステムカタログのデータ削除が必要でした。

7.5.5.6. 備考

ノード障害後および復旧中にDDLを実行すると下記のエラーが出力されます。

=# CREATE TABLE test2(id int);
ERROR:  No peer nodes or peer node count unknown, cannot acquire global lock
HINT:  BDR is probably still starting up, wait a while

7.6. BDR性能検証

7.6.1. 更新性能検証結果

7.6.1.1. 検証目的

本試験はBDRの本来のユースケースである、 高レイテンシ環境との双方向レプリケーション環境における更新クエリのレスポンス改善とそれに伴う性能改善を確認したものです。

7.6.1.1.1. 検証内容

今回は 2014年度検証報告書 (可用性編) に記載された東京-シンガポール間の回線情報(応答速度76.95ms、帯域幅0.16Gbits/s)をもとに、 以下のような環境を構築しpgbenchを実行しました。

試験概要図

通常のストリーミングレプリケーションであれば、 更新クエリを実行したい場合はシンガポールのマスタノードに更新要求を実施する必要がありますが、 BDRでは最寄りのノードに対して更新要求を実施することが可能です。

7.6.1.1.2. 検証環境

こちらの環境 で検証を実施しました。

7.6.1.1.3. 検証手順
  1. 環境構築

pgbenchを実行する準備を行います。(BDR, SR環境両方)

# pgbench -i test -U postgres -s 10
NOTICE:  table "pgbench_history" does not exist, skipping
NOTICE:  table "pgbench_tellers" does not exist, skipping
NOTICE:  table "pgbench_accounts" does not exist, skipping
NOTICE:  table "pgbench_branches" does not exist, skipping
creating tables...
100000 of 100000 tuples (100%) done (elapsed 0.25 s, remaining 0.00 s).
vacuum...
set primary keys...
done.

node1, node2ともにテーブルが作成されていることを確認します。

# psql -U postgres test -c "\d"
                リレーションの一覧
 スキーマ |       名前       |        |  所有者
----------+------------------+----------+----------
 public   | pgbench_accounts | テーブル | postgres
 public   | pgbench_branches | テーブル | postgres
 public   | pgbench_history  | テーブル | postgres
 public   | pgbench_tellers  | テーブル | postgres
(4 )

# /usr/pgsql-9.4/bin/psql -U postgres -h node2 test -c "\d"
                リレーションの一覧
 スキーマ |       名前       |        |  所有者
----------+------------------+----------+----------
 public   | pgbench_accounts | テーブル | postgres
 public   | pgbench_branches | テーブル | postgres
 public   | pgbench_history  | テーブル | postgres
 public   | pgbench_tellers  | テーブル | postgres
(4 )

帯域制限及び遅延設定を行います。

(clientはnode2向け通信のみ制限)
tc qdisc add dev eth0 root handle 1:0 htb
tc class add dev eth0 parent 1:0 classid 1:10 htb rate 160Mbit
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip dst 192.168.0.12/32 flowid 1:10
tc qdisc add dev eth0 parent 1:10 handle 10:1 netem delay 38ms 1ms

(node1はnode2向け通信のみ制限)
tc qdisc add dev enp0s25 root handle 1:0 htb
tc class add dev enp0s25 parent 1:0 classid 1:10 htb rate 160Mbit
tc filter add dev enp0s25 protocol ip parent 1:0 prio 1 u32 match ip dst 192.168.0.12/32 flowid 1:10
tc qdisc add dev enp0s25 parent 1:10 handle 10:1 netem delay 38ms 1ms

(node2は両ノード向け通信とも制限)
tc qdisc add dev enp0s25 root handle 1:0 htb
tc class add dev enp0s25 parent 1:0 classid 1:0 htb rate 160Mbit
tc qdisc add dev enp0s25 parent 1:0 handle 10:1 netem delay 38ms 1ms

帯域制限の結果以下のような環境になります。

表 7.9 帯域制限後のネットワーク情報
Client/Server client node1 node2
client
遅延:0.5 ms
帯域幅:934 Mbits/s
遅延:76ms
帯域幅:160 Mbits/s
node1
遅延:0.5 ms
帯域幅:934 Mbits/s
遅延:76ms
帯域幅:160 Mbits/s
node2
遅延:76ms
帯域幅:160 Mbits/s
遅延:76ms
帯域幅:160 Mbits/s
  1. レスポンスタイム確認

更新クエリの応答速度を確認します。

# time psql -h node1 -x -c "UPDATE pgbench_branches SET bbalance = bbalance + 100 WHERE bid = 1" test
UPDATE 1

real    0m0.025s
user    0m0.001s
sys     0m0.001s

# time psql -h node2 -x -c "UPDATE pgbench_branches SET bbalance = bbalance + 100 WHERE bid = 1" test
UPDATE 1

real    0m0.259s
user    0m0.001s
sys     0m0.000s
(ネットワーク遅延に伴いレスポンスが低下していることを確認)
  1. SR環境の性能試験

非同期SR構成のマスターに対してpgbenchを実施します。

# pgbench -U postgres -p 5433 -h node2 -s 10 -c 10 test -T 180
scale option ignored, using count from pgbench_branches table (10)
starting vacuum...end.
transaction type: TPC-B (sort of)
scaling factor: 10
query mode: simple
number of clients: 10
number of threads: 1
duration: 180 s
number of transactions actually processed: 3078
latency average: 584.795 ms
tps = 17.042442 (including connections establishing)
tps = 17.057455 (excluding connections establishing)
  1. BDR環境の性能試験

競合が発生しているか確認するために、「bdr.bdr_conflict_history」を参照し競合履歴数を確認します。

# psql -h node1 -x -c "SELECT count(*) FROM bdr.bdr_conflict_history;" test
-[ RECORD 1 ]
count | 52265

# psql -h node2 -x -c "SELECT count(*) FROM bdr.bdr_conflict_history;" test
-[ RECORD 1 ]-
count | 132935

BDR環境のノードにpgbenchを実施します。

# pgbench -U postgres -h node1 -s 10 -c 10 test -T 180
scale option ignored, using count from pgbench_branches table (10)
starting vacuum...end.
transaction type: TPC-B (sort of)
scaling factor: 10
query mode: simple
number of clients: 10
number of threads: 1
duration: 180 s
number of transactions actually processed: 16677
latency average: 107.933 ms
tps = 92.598796 (including connections establishing)
tps = 92.602290 (excluding connections establishing)

競合が発生していないことを確認します。

# psql -h node1 -x -c "SELECT count(*) FROM bdr.bdr_conflict_history;" test
-[ RECORD 1 ]
count | 52265

# psql -h node2 -x -c "SELECT count(*) FROM bdr.bdr_conflict_history;" test
-[ RECORD 1 ]-
count | 132935
7.6.1.1.4. 検証結果

本検証では以下を確認することが出来ました。

  • レスポンスタイムの低減

    低レイテンシのサーバに更新クエリを実行可能であるため、レスポンスタイムを低減することが出来ました。

  • TPSの向上

    レスポンスタイムが低減されたため、TPSの向上が見られました。
    "17.057455" -> "92.602290"(約5.4倍)

ただし、本検証では片系のみに変更を実施し競合が発生しないようにした試験だったため、このような結果になったものと思われます。

本検証で使用しているpgbench(TCP-Bライク)のような、現在の値に対して加算していくような処理の場合、結果整合で競合を解決することができないため、 絶対に競合が発生しない構成が必要です。

例)更新するテーブルを拠点ごとに分ける等

7.7. まとめ

7.7.1. BDR検証まとめ

本検証では、BDRの機能や特徴および主なユースケースを机上調査を実施した上で、BDRの動作検証および性能検証を実施しました。 本検証で実施したBDRの動作検証の結果は下表の通りです。

表 7.10 結果評価基準
記号 意味
問題なし。
利用時に問題になるケースがある。
× 対応していない。事実上使えない。
表 7.11 BDR検証結果
項番 検証概要 結果 補足
1 ノード追加/削除 ノード削除はオンラインで実行可能。ノード追加時にトランザクションの停止が必要。 また、削除したノードを追加する場合にはシステムカタログの操作が必要。
2 グローバルシーケンス シーケンスの競合を防ぐことが可能。ただし、マニュアルに記載された制限事項については確認が必要。
3 選択的レプリケーション BDRを利用して任意のテーブルのデータ集約等を実現可能
4 更新処理競合時の動作 更新が競合するパターンで意図しない動作が発生し、競合解消のため手動での操作が必要。
5 ノード障害と復旧 ノード復旧時にはトランザクションの停止が必要。 また、障害が発生したノードを復旧させる際にシステムカタログの操作が必要。

本検証で利用したバージョン(1.0.2)では更新が競合するパターンで意図しない動作が発生するため、BDRを適用する場合、競合が発生しないようなアプリケーション設計やテーブル設計が必要になります。

例) 更新するテーブルを拠点ごとに分ける等

ノード障害時の運用においても、一般的でないシステムカタログの操作を必要とするといった今後の改善が必要と思われる結果が確認されました。

また、BDRの選択的レプリケーションを用いることで、PostgreSQLのストリーミング・レプリケーションでは実現できない、テーブル単位のレプリケーションが実現可能なことを確認できました。 上記機能を用いて、システム間のデータ連携(データ集約等)を柔軟に実現できると考えております。

性能検証の結果より、ユースケースで想定している「遠距離拠点間で双方向に更新する」場合に、レスポンスタイムの低減と処理向上につながることが確認できました。

机上で調査した通り、BDRを適用することで「遠距離拠点間のトランザクション性能改善」や選択的レプリケーション機能を利用した「柔軟なデータ連携」を実現できると考えます。

7.8. 参考文献

[BDR]Postgres-BDR ドキュメント http://bdr-project.org/docs/stable/

7.8.1. 関数一覧

表 7.12 関数一覧
項番 関数 参照ドキュメント
1 bdr.bdr_group_create functions-node-mgmt.html
2 bdr.bdr_group_join functions-node-mgmt.html
3 bdr.bdr_part_by_node_names functions-node-mgmt.html
4 bdr.remove_bdr_from_local_node functions-node-mgmt.html
5 bdr.bdr_node_join_wait_for_ready functions-node-mgmt.html
6 bdr.table_set_replication_sets functions-replication-sets.html
7 bdr.table_get_replication_sets functions-replication-sets.html
8 bdr.connection_get_replication_sets functions-replication-sets.html

7.8.2. システムカタログ一覧

表 7.13 システムカタログ一覧
項番 システムカタログ 参照ドキュメント
1 bdr.bdr_conflict_history catalog-bdr-conflict-history.html
2 bdr.bdr_nodes catalog-bdr-nodes.html
3 bdr.bdr_connections catalog-bdr-connections.html

8. まとめ

本文書ではPostgreSQLにおけるレプリケーション技術として標準機能として組み込まれているストリーミングレプリケーションと ロジカルレプリケーションについて取り上げました。さらに、マルチマスタ構成に対応したBi-Directional Replication(BDR)についても取り上げました。

ストリーミングレプリケーションは、2010年9月にリリースされたPostgreSQL9.0で標準機能として組み込まれ、徐々に実運用での利用される機会が増えるとともに、実運用で必要となる機能改善がその後のバージョンアップで継続的に行われています。

本文書ではストリーミングレプリケーション構成を構築する基本的な手順を確認した上で、レプリケーションスロット、遅延レプリケーション、WAL圧縮といった新しい機能に対しても、設定から実際の動作を確認するまでの具体的な手順を整理することができました。障害発生時の運用についてはPostgreSQL9.5から提供されるようになったpg_rewindコマンドを使うことで、これまで必要とされていたフルバックアップ取得を不要とする運用が可能になっていることを確認できました。さらに、複数台のスレーブが存在するあるいはカスケードレプリケーションといったより複雑な構成で障害が発生した場合の運用手順についても確認しました。

ロジカルレプリケーションは、2017年10月にリリースされたPostgreSQL10.0で標準機能として組み込まれました。ストリーミングレプリケーションと類似したアーキテクチャを持ちながら、テーブル単位や更新種別単位で柔軟なレプリケーション形態を取ったり、複製先を更新することができる等の特徴を有しています。

本文書ではロジカルレプリケーション構成を構築する基本的な手順、同期レプリケーション、複数Subscriptionへのレプリケーション、カスケード構成といった応用的な使い方、監視方法や障害発生時の挙動、レプリケーション開始後のテーブル追加、定義変更の手順について検証しました。検証結果より、現状のロジカルレプリケーションは柔軟にテーブル単位、更新種別単位で複製できることによりレプリケーションの活用範囲が広がった一方で、更新競合を解消する機能が貧弱なことやレプリケーションされないSQL、オブジェクトが多数あるといった制約事項もあるため、現状では更新競合が発生しないよう複製先を参照用途に限定して利用するのが現実的と考えます。

BDRは、現状のストリーミングレプリケーションでは不可能なマルチマスタ構成に対応することから、シングルマスタ構成に起因する課題解消が期待されます。今回の検証では、BDRの特徴、ユースケース、メカニズムを類似機能、製品と机上比較した上で、更新処理競合時の動作、ノード障害時の動作、更新性能についてそれぞれ検証しました。更新性能の検証結果より、ユースケースで想定している「遠距離拠点間で双方向に更新する」場合に、レスポンスタイムの低減とTPS向上につながることがわかりました。

一方で、現時点のバージョンでは更新が競合するパターンで意図しない動作が発生するため、現段階では競合が発生しないパターンで利用すべきと考えます。そこで、BDRの利用時は競合が発生しにくいテーブル設計にすることが望ましいです。また、ノード障害時の運用においても、一般的でないシステムカタログの操作を必要とするといった今後の改善が必要と思われる結果が確認されました。

これらの結果から、BDRについてはユースケースと実際の利用用途が合致しているかを見極めた上で利用するかどうかを判断する必要があるといえます。

今回の検証結果がストリーミングレプリケーション構成を実際に利用している方々の運用改善につながること、またロジカルレプリケーションやBDRを利用すべきかどうかを判断する際の参考情報として活用いただけることを期待しています。

9. 著者

(企業・団体名順)

所属企業・団体名 部署名 氏名
第1.0版
(2016年度WG3)
株式会社アシスト データベース技術本部 竹内 尚也
株式会社アシスト データベース技術本部 柘植 丈彦
株式会社オージス総研 プラットフォームサービス本部 IT基盤技術部 大西 斉
TIS株式会社 IT基盤技術本部 IT基盤技術推進部 中西 剛紀
日本電信電話株式会社 オープンソースソフトウェアセンタ 坂田 哲夫
株式会社富士通ソーシアルサイエンスラボラトリ プラットフォームインテグレーション本部 第四システム部 小山田 政紀
株式会社富士通ソーシアルサイエンスラボラトリ プラットフォームインテグレーション本部 第四システム部 高橋 勝平
株式会社富士通ソーシアルサイエンスラボラトリ プラットフォームインテグレーション本部 第四システム部 香田 紗希
第2.0版
(2017年度WG3)
株式会社アシスト サービス事業部 サポートセンター 神谷 美恵子
株式会社アシスト データベース技術本部 技術統括部 竹内 尚也
株式会社アシスト データベース技術本部 技術統括部 柘植 丈彦
株式会社オージス総研 プラットフォームサービス本部 IT基盤技術部 大西 斉
TIS株式会社 IT基盤技術本部 中西 剛紀
株式会社富士通ソーシアルサイエンスラボラトリ ソリューション開発センター ソリューションビジネス部 小山田 政紀
株式会社富士通ソーシアルサイエンスラボラトリ 第二システム事業本部 第三システム部 香田 紗希