はてなのアルバイトでmaster-slave構成のPostgreSQL(9.3.*)クラスタをアップグレードするための調査をすることになりました。
サービスの性質上できるだけ無停止でアップグレードする必要があったため、Slony-Iを使ったダウンタイムなし(もしくは最小)のアップグレードに向けて調査・実験した内容を紹介します。
背景
PostgreSQLでメジャーバージョンをアップグレードするにはdump/restoreかpg_upgradeのどちらかの方法で行います。
しかし、このどちらもデータベースの停止が必須です。さらに、メジャーバージョン間ではシステム内のデータ形式の互換性が保証されていないため、異なるメジャーバージョン間ではレプリケーションを行うことができません。
そのため、レプリケーションしているPostgreSQLを無停止(もしくは最短の停止時間)でアップグレードするためには、サードパーティー製のツールを導入する必要があります。
これにはSlony-I(Slony)を始めLondisteやBucardoがありますが、今回はメジャーバージョン間でのレプリケーションをサポートし、これらの中では高速に動作するSlonyを使った、レプリケーションクラスタのアップグレードについて調査・実験した内容をまとめます。
基本的にはSlonyのドキュメントにあるPgbenchのスキーマを利用したレプリケーション構築を元に、3台のPostgreSQLでレプリケーションを構築し、アップグレードのために旧マスタから新マスタにスイッチオーバするまでの内容となっています。
Slonyについて
SlonyはPostgreSQLで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にスイッチオーバすることを考えます。
- マスタノードとしてPostgreSQL9.3を用意する。Pgbenchを回して、テーブル定義・データを用意する
- スレーブノードとしてPostgreSQL9.4を2台用意する
- マスタノードからスレーブノードにテーブル定義を同期する
- マスタノードと2台のスレーブノードでレプリケーションを組み、データが同期されることを確認する
- スイッチオーバしてマスタが入れ替えてもレプリケーションが継続できていることを確認する
スイッチオーバまで確認できれば旧マスタをレプリから外し、アップグレードしてからレプリに戻すことで全体が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_
サンプルを修正して使っているので、コメント部分を見れば何をしているのか大体わかりますが、
注意点として、
- slony2.系ではstore node()の第3引数(event node)を指定しなければならない
- スイッチオーバしてレプリケーション元が変わる可能性がある場合は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のメジャーバージョン間でレプリケーション、スイッチオーバが可能だと確認できました。
今後の課題や、これから試すこととしてはざっくり以下のようなものがあります。
参考
本家
http://slony.info/
https://github.com/ssinger/slony1-engine
レプリケーションの設定
http://raghavt.blogspot.jp/2012/07/simple-slony-i-replication-setup.html
http://raghavt.blogspot.jp/2014/12/switchoverswitchback-in-slony-i-while.html
http://sixwish.jp/Technote/Slony/ver105-02/
レプリケーションの動き
http://seesaawiki.jp/w/harada_toshi/d/Slony-I%A4%CE%A5%EC%A5%D7%A5%EA%A5%B1%A1%BC%A5%B7%A5%E7%A5%F3%CA%FD%BC%B0?wiki_id=46665
http://d.hatena.ne.jp/ttshrk/20110414/1302760971
Let'sPostgres
slony, pgpoolを使ったレプリケーション。概要が充実
http://lets.postgresql.jp/documents/technical/pgpool/4/