tom__bo’s Blog

MySQL!! MySQL!! @tom__bo

PostgreSQLで異なるメジャーバージョン間のレプリケーション実験

はてなのアルバイトでmaster-slave構成のPostgreSQL(9.3.*)クラスタをアップグレードするための調査をすることになりました。
サービスの性質上できるだけ無停止でアップグレードする必要があったため、Slony-Iを使ったダウンタイムなし(もしくは最小)のアップグレードに向けて調査・実験した内容を紹介します。

背景

PostgreSQLでメジャーバージョンをアップグレードするにはdump/restoreかpg_upgradeのどちらかの方法で行います。
しかし、このどちらもデータベースの停止が必須です。さらに、メジャーバージョン間ではシステム内のデータ形式の互換性が保証されていないため、異なるメジャーバージョン間ではレプリケーションを行うことができません。
そのため、レプリケーションしているPostgreSQLを無停止(もしくは最短の停止時間)でアップグレードするためには、サードパーティー製のツールを導入する必要があります。
これにはSlony-I(Slony)を始めLondisteやBucardoがありますが、今回はメジャーバージョン間でのレプリケーションをサポートし、これらの中では高速に動作するSlonyを使った、レプリケーションクラスタのアップグレードについて調査・実験した内容をまとめます。

基本的にはSlonyのドキュメントにあるPgbenchのスキーマを利用したレプリケーション構築を元に、3台のPostgreSQLレプリケーションを構築し、アップグレードのために旧マスタから新マスタにスイッチオーバするまでの内容となっています。

Slonyについて

SlonyPostgreSQLでmaster-slave構成のレプリケーションを構築するためのツールです。
複数のスレーブに対してテーブル単位でレプリケーションをすることができ、カスケード構成を取ることも可能です。
また、Slonyはトリガーを使ってデータの変更をレプリケーションをするため、メジャーバージョン間でのレプリケーションが可能になっています。これを利用したアップグレード方法は他(ex1, ex2)などでも紹介されています。
僕が調べた範囲ではPostgres9.3以前のメジャーバージョンをアップグレードする方法は現状ではSlonyを使う方法がベストでした。
一方でSlonyを使うデメリットとは、

  • 2015年2月頃から開発がストップしている
  • 構築が非常に面倒(slonikコマンドを覚える必要がある)
  • DDLコマンドによる変更はslaveに同期できない(slonikコマンドを使うことでできるものもある)
  • Primary keyの無いテーブルは基本的に無停止で同期できない
  • ラージオブジェクト・ロールの変更は同期できない

があります。
特に開発がストップしているのは問題で、(公式githubも更新が止まっている)ドキュメントのミスもあまり修正されていないようです。
300ページ近い充実したドキュメントがあるにもかかわらず、3台のPostgreSQLレプリケーションを組むにもなかなか苦労しました。(これは僕の英語力のせい)

公式のページではPostgres9.3までのサポート報告までしかありませんが、Postgres9.5のドキュメントでもSlonyの紹介があること

It is also possible to use certain replication methods, such as Slony, to create a standby server with the updated version of PostgreSQL. This is possible because Slony supports replication between different major versions of PostgreSQL.

や、9.2->9.4でのレプリケーションの例が見つかったので、諦めずにSlonyをためしてみることにしました。

実験内容

9.3のレプリケーションクラスタを9.4にアップグレードします。
PostgreSQL標準のWALによるレプリケーションからの移行はひとまず考えず、以下の手順で9.3のマスタを9.4にスイッチオーバすることを考えます。

  1. マスタノードとしてPostgreSQL9.3を用意する。Pgbenchを回して、テーブル定義・データを用意する
  2. スレーブノードとしてPostgreSQL9.4を2台用意する
    • Slave1はマスタノードとスイッチオーバさせるノード
    • Slave2はスイッチオーバ後のレプリケーションの確認用ノード(本番も複数台のslaveノードがあるため)
  3. マスタノードからスレーブノードにテーブル定義を同期する
  4. マスタノードと2台のスレーブノードでレプリケーションを組み、データが同期されることを確認する
  5. スイッチオーバしてマスタが入れ替えてもレプリケーションが継続できていることを確認する

スイッチオーバまで確認できれば旧マスタをレプリから外し、アップグレードしてからレプリに戻すことで全体が9.4になるという流れです。

(※Slonyではレプリケーションを連結したカスケード構成を取れることから、master-slave構成のことをprovider-subscriberと表現していますが、ここではカスケード接続することはないので、わかりやすくmaster-slaveとします)


環境・準備

Mac上でvirtualboxを利用してDebian8を3台作って実験しました。

  • Master(Postgres9.3) 192.168.32.11
  • Slave1(Postgres9.4) 192.168.32.12
  • Slave2(Postgres9.4) 192.168.32.13

それぞれのPostgres, Slonyのインストールは省略します。
実験なので、アクセス制限をせず、ホスト名も設定していません。
.bashrc, pg_hba, postgresql.confを共通で以下の様に設定しました。

# .rcの設定
# slony
export CLUSTERNAME=slony_example
export MASTERDBNAME=pgbench
export SLAVEDBNAME=pgbench
export MASTERHOST=192.168.32.11
export SLAVEHOST_1=192.168.32.12
export SLAVEHOST_2=192.168.32.13
export REPLICATIONUSER=pgsql
export PGBENCHUSER=pgbench

#pg_hba.confに追記
host    all             all             0.0.0.0/0            trust

#postgresql.conf
listen_addresses = '*'
port = 5432

Slonyのドキュメントに沿ってユーザはpgbenchを回すpgbenchと、レプリを実行するpgsqlを作成する。

# それぞれのサーバで
createuser -SRD $PGBENCHUSER
createuser -SRD $REPLICATIONUSER


DBの作成 (pgbench用)

createdb -O $PGBENCHUSER -h $MASTERHOST $MASTERDBNAME
createdb -O $PGBENCHUSER -h $SLAVEHOST_1 $SLAVEDBNAME
createdb -O $PGBENCHUSER -h $SLAVEHOST_2 $SLAVEDBNAME


マスタにのみサンプルのデータを作成

pgbench -i -s 1 -U $PGBENCHUSER -h $MASTERHOST $MASTERDBNAME


pgbenchでできるpgbench_historyにはprimary keyがないので、追加

psql -U $PGBENCHUSER -h $MASTERHOST -d $MASTERDBNAME -c "begin; alter table pgbench_history add column id serial; update pgbench_history set id = nextval(’pgbench_history_id_seq’); alter table pgbench_history add primary key(id); commit;"

テーブル定義は予めコピーしておく必要があるので、dumpして入れる。

pg_dump -s -U $REPLICATIONUSER -h $MASTERHOST $MASTERDBNAME | psql -U $REPLICATIONUSER -h $SLAVEHOST_1 $SLAVEDBNAME
pg_dump -s -U $REPLICATIONUSER -h $MASTERHOST $MASTERDBNAME | psql -U $REPLICATIONUSER -h $SLAVEHOST_2 $SLAVEDBNAME

レプリケーションの作成

レプリケーションの設定・操作はSlonyに付属しているSlonikコマンドを使います。
Slonikコマンドは以降のように、変数を含めたシェルスクリプトとして実行する必要があり、わかりづらいですが頑張るしかありません。

ノードの設定。
サンプルを元にノード、slonyによるレプリケーションクラスタの設定をしていきます。
今回のクラスタ名は"slony_example"としました。

#!/bin/sh

CLUSTER=slony_example
DBNAME1=pgbench
DBNAME2=pgbench
DBNAME3=pgbench
HOST1=192.168.32.11
HOST2=192.168.32.12
HOST3=192.168.32.13
SLONY_USER=pgsql
PGBENCH_USER=pgbench

slonik <<_EOF_
    # ----
    # This defines which namespace the replication system uses
    # ----
    cluster name = $CLUSTER;

    # ----
    # Admin conninfo's are used by the slonik program to connect
    # to the node databases.  So these are the PQconnectdb arguments
    # that connect from the administrators workstation (where
    # slonik is executed).
    # ----
    node 1 admin conninfo = 'dbname=$DBNAME1 host=$HOST1 user=$SLONY_USER';
    node 2 admin conninfo = 'dbname=$DBNAME2 host=$HOST2 user=$SLONY_USER';
    node 3 admin conninfo = 'dbname=$DBNAME3 host=$HOST3 user=$SLONY_USER';

    # ----
    # Initialize the first node.  The id must be 1.
    # This creates the schema "_test1" containing all replication
    # system specific database objects.
    # ----
    init cluster ( id = 1, comment = 'Node 1' );

    # ----
    # The Slony replication system organizes tables in sets.  The
    # smallest unit another node can subscribe is a set.  Usually the
    # tables contained in one set would be all tables that have
    # relationships to each other.  The following commands create
    # one set containing all 4 pgbench tables.  The "master" or origin
    # of the set is node 1.
    # ----
    create set ( id = 1, origin = 1, comment = 'All pgbench tables' );
    set add table ( set id = 1, origin = 1,
        id = 1, fully qualified name = 'public.pgbench_accounts',
        comment = 'Table accounts' );
    set add table ( set id = 1, origin = 1,
        id = 2, fully qualified name = 'public.pgbench_branches',
        comment = 'Table branches' );
    set add table ( set id = 1, origin = 1,
        id = 3, fully qualified name = 'public.pgbench_tellers',
        comment = 'Table tellers' );
    set add table ( set id = 1, origin = 1,
        id = 4, fully qualified name = 'public.pgbench_history',
        comment = 'Table history' );
    echo 'Complete create set';

    # ----
    # Create the second node, tell the two nodes how to connect to
    # each other and that they should listen for events on each
    # other.  Note that these conninfo arguments are used by the
    # slon daemon on node 1 to connect to the database of node 2
    # and vice versa.  So if the replication system is supposed to
    # use a separate backbone network between the database servers,
    # this is the place to tell it.
    # ----
    store node ( id = 2, comment = 'Node 2' , event node=1);
    store path ( server = 1, client = 2,
        conninfo = 'dbname=$DBNAME1 host=$HOST1 user=$SLONY_USER');
    store path ( server = 2, client = 1,
        conninfo = 'dbname=$DBNAME2 host=$HOST2 user=$SLONY_USER');
    echo 'Complete store node 2';

    # ---
    # third node
    # ---
    echo '1: Prepare store node 3';
    store node ( id = 3, comment = 'Node 3' , event node=1);
    echo '2: Prepare store node 3';
    store path ( server = 1, client = 3,
        conninfo = 'dbname=$DBNAME1 host=$HOST1 user=$SLONY_USER');
    store path ( server = 3, client = 1,
        conninfo = 'dbname=$DBNAME3 host=$HOST3 user=$SLONY_USER');
    echo 'Complete store node 3';

    # ---
    # 2nd and 3rd node
    # ---
    store path ( server = 2, client = 3,
        conninfo = 'dbname=$DBNAME2 host=$HOST2 user=$SLONY_USER');
    store path ( server = 3, client = 2,
        conninfo = 'dbname=$DBNAME3 host=$HOST3 user=$SLONY_USER');
    echo 'Complete 2-3 path';

_EOF_

サンプルを修正して使っているので、コメント部分を見れば何をしているのか大体わかりますが、
注意点として、

  1. slony2.系ではstore node()の第3引数(event node)を指定しなければならない
  2. スイッチオーバしてレプリケーション元が変わる可能性がある場合はstore path()を予め設定しないといけない
    今回だとserver1からserver2にスイッチオーバしたときserver2とserver3間でレプリケーションが必要なので、2-3ノード間のstore pathの設定があらかじめ必要。

というのがあります。
この辺りはサンプルのコードが更新されていないこととドキュメント上にも散らばって書かれているので、なかなか厄介でした。


ノードの設定ができたので、レプリケーションの設定をします。
slonikのsubscribe set()を使い、providerとreceiverをmaster(id=1)->slave1(id=2), master(id=1)->slave2(id=3)と設定します。

#!/bin/sh

CLUSTER=slony_example
DBNAME1=pgbench
DBNAME2=pgbench
DBNAME3=pgbench
HOST1=192.168.32.11
HOST2=192.168.32.12
HOST3=192.168.32.13
SLONY_USER=pgsql
PGBENCH_USER=pgbench

slonik <<_EOF_
    # ----
    # This defines which namespace the replication system uses
    # ----
    cluster name = $CLUSTER;

    # ----
    # Admin conninfo's are used by the slonik program to connect
    # to the node databases.  So these are the PQconnectdb arguments
    # that connect from the administrators workstation (where
    # slonik is executed).
    # ----
    node 1 admin conninfo = 'dbname=$DBNAME1 host=$HOST1 user=$SLONY_USER';
    node 2 admin conninfo = 'dbname=$DBNAME2 host=$HOST2 user=$SLONY_USER';
    node 3 admin conninfo = 'dbname=$DBNAME3 host=$HOST3 user=$SLONY_USER';

    # ----
    # Node 2 subscribes set 1
    # ----
    subscribe set ( id = 1, provider = 1, receiver = 2, forward = yes);
_EOF_

これをslave1で実行し、最後のsubscribe setを

subscribe set ( id = 1, provider = 1, receiver = 3, forward = yes);

に変更したものをslave2で実行します。

1コマンド中にsubscribe setをまとめて実行しようとすると失敗します。
また、forward=yesにしておくことで、変更をslave側のノードでも保存してようになるので、switchover/failover, カスケード構成を取るなど、その後にマスターになる可能性のある場合は適宜設定します。


これで、ノードとノード間のレプリケーションの設定ができたので、slonプロセスを立てることで、レプリケーションを稼働させます。

それぞれのサーバでslonプロセスを起動することで、マスターのイベント(データの変更)を他のノードに送り、受け取ったノードでその変更を適用するようになります。

(master)$ slon $CLUSTERNAME "dbname=$MASTERDBNAME user=$REPLICATIONUSER host=$MASTERHOST"
(slave1)$ slon $CLUSTERNAME "dbname=$SLAVEDBNAME user=$REPLICATIONUSER host=$SLAVEHOST_1"
(slave2)$ slon $CLUSTERNAME "dbname=$SLAVEDBNAME user=$REPLICATIONUSER host=$SLAVEHOST_2"

これでマスターに対して変更を加えるとスレーブに同期されて、レプリケーションできていることを確認できるはず。
また、スレーブノードはデータの更新がロックされていることもわかる。

pgbench=# INSERT INTO pgbench_accounts values(100003, 1, 0);
ERROR:  Slony-I: Table pgbench_accounts is replicated and cannot be modified on a subscriber node - role=0

スイッチオーバ

master(旧マスタ)からslave1(新マスタ)へスイッチオーバする。

#!/bin/sh

CLUSTER=slony_example
DBNAME1=pgbench
DBNAME2=pgbench
DBNAME3=pgbench
HOST1=192.168.32.11
HOST2=192.168.32.12
HOST3=192.168.32.13
SLONY_USER=pgsql

slonik <<_EOF_
    cluster name = $CLUSTER;
    node 1 admin conninfo = 'dbname=$DBNAME1 host=$HOST1 user=$SLONY_USER';
    node 2 admin conninfo = 'dbname=$DBNAME2 host=$HOST2 user=$SLONY_USER';
    node 3 admin conninfo = 'dbname=$DBNAME3 host=$HOST3 user=$SLONY_USER';

    lock set (id = 1, origin = 1);
    sync (id = 1);
    wait for event (origin = 1, confirmed = 2, wait on = 2);
    wait for event (origin = 1, confirmed = 3, wait on = 3);
    move set (id = 1, old origin = 1, new origin = 2);
    echo 'moved the set';
    wait for event (origin = 1, confirmed = 2, wait on = 1);

_EOF_

基本的にはドキュメント通りにmove set()コマンドを使うことで、スイッチオーバできるはずですが、syncの前にwait for eventを挟んであげないとたいてい失敗するので注意が必要です。

僕がやった時はいろいろな箇所で詰まってドキュメントとにらめっこしながら苦戦した覚えがあるのですが、まとめてしまうとこの程度でした。
マスタ1台、スレーブ2台でのレプリケーション、スイッチオーバーができれば、スレーブが複数台でも大きくは変わらないはずです。
マスタでのデータの変更がレプリケートできているかはここでは書いていませんが、上記の手順でやれば特に問題がなかったのと、データの同期ができているかはサンプルスクリプトのcheck.shを利用すると簡単に確認できます。

今後の課題

Slony-Iを利用することで、pgbenchで作成したテーブルのデータでは9.3から9.4のメジャーバージョン間でレプリケーション、スイッチオーバが可能だと確認できました。
今後の課題や、これから試すこととしてはざっくり以下のようなものがあります。

  • 9.5へのアップグレード
    • upsert使いたいので、9.5まであげたい
    • 上の手順のままできてほしい
  • 標準のWALレプリケーションとの連携
    • WALからSlony, SlonykからWALに戻す必要がある
  • レプリケーションの速度
    • トリガベースのレプリケーションなので、遅い
    • 上記の例でもmasterに適当なデータをインサートすると反映に1秒くらいかかることもある
  • データ更新がある中でのスイッチオーバ
    • 今回は最初のデータ用意にpgbenchを使っただけなので、リアルタイムにデータを更新しつつレプリケーションを構築したり、スイッチオーバする