Slony-Iによるレプリケーションとオンラインリカバリこの記事は技術評論社『WEB+DB PRESS』の 『WEB+DB PRESS Vol.48』2008年12月刊 に掲載された原稿の草稿を、許可を得て公開しているものです。
また、日本PostgreSQLユーザ会の仕組み分科会で発表したWALとPITRの資料とpgpool-IIのオンラインリカバリに関する資料がありますので、こちらも参照してください。
1.1 はじめに本章では、Slony-Iによるレプリケーションシステムの構成方法と、オンラインリカバリを含むいくつかの運用テクニックについて説明します。1.2 Slony-IとはSlony-IはPostgreSQL専用の非同期型シングルマスタ・マルチスレーブのレプリケーションソフトです。2003年頃からJan Wieck氏によって開発が始められ、2004年6月にバージョン1.0がリリースされました。 以降、現在まで順調に開発が進んでおり安定版の最新バージョンは1.2.15です。 開発版は2.0rc3がリリースされています。
Slony-IのレプリケーションはMySQLのそれと同等の機能を提供しています。しかし、後に述べるようにSlony-Iはスレーブやマスターをオンラインリカバリ可能(サービスを停止することなくリカバリできる)です。これはバイナリログという過去の操作履歴をすべて保存しないとオンラインリカバリが困難なMySQLに対して大きなアドバンテージです(脚注1)。
ここでSlony-Iの動作機構を説明します(図1)。 Slony-Iはslonというデーモンプロセスと、 PostgreSQLに格納された各種テーブルやトリガ関数から成ります。 具体的な例を使ってレプリケーションの仕組みを見てみましょう。
マスター側のPostgreSQLにテーブルtblのUPDATE文が届いたとします。 そのとき、マスター側のPostgreSQLからスレーブ側へデータの更新が伝わる様子を説明します。
(1) 事前に登録されたトリガ関数が、更新情報をデーモンプロセスslonに伝える
トリガとは、指定したテーブルが更新(INSERT、UPDATE、DELETE)された場合に起動する仕組みで、
そこで実行される関数をトリガ関数といいます。
Slony-Iのレプリケーションは非同期なので、
マスター側のデーモンプロセスslonが更新情報を受け取ってから、
スレーブ側のPostgreSQLの更新が終了するまで、
数100ミリ秒かそれ以上の時間的遅れが生じます。
しかし、通常はほとんど問題になるような遅れはありません。
1.3 今回のシステム構成Slony-I独特の用語の説明を兼ねて、今回のシステム構成について解説します(図2)。
今回はセットが1つですが、
例えばセットset1はテーブルtbl_1をノード1からノード2にレプリケート、
セット2はテーブルtbl_2をノード1からノード3にレプリケートするなど、細かな設定が可能です。
1.4 インストールこれからSlony-Iのインストールを行いますが、いくつか下準備が必要です。全サーバに"postgres"というユーザを登録してください。 インストール作業はユーザpostgresで行います。 予め、ユーザpostgresの環境変数PATHにPostgreSQLとSlony-Iのバイナリパス"/usr/local/pgsql/bin"と"/usr/local/slony1/bin"を設定しておいてください。 export PATH=$PATH:/usr/local/pgsql/bin:/usr/local/slony1 なお、以降の説明における、ターミナルで作業する場合のプロンプト表示は次の形式とします。 ユーザ名 @ サーバ名>例えば、ユーザpostgresがサーバpostgres0で作業する場合は postgres@postgres0>とします。 特に、2台のPostgreSQLサーバpostgres0とpostgres1で共通の作業を行う場合は postgres@>と表記します。 1.4.1 PostgreSQLのインストールはじめにPostgreSQLをインストールします。PostgreSQLを入手します。最新版は8.3.4です。 http://www.postgresql.org/ftp/source/v8.3.4/ PostgreSQLの最新版をダウンロードしたら、適当なディレクトリで展開し、 configureコマンドとmakeコマンドを実行します。 root@> mkdir /usr/local/pgsql root@> chown postgres:postgres /usr/local/pgsql root@> su postgres postgres@> tar xvfz postgresql-8.3.4.tar.gz postgres@> cd postgresql-8.3.4 postgres@> ./configure postgres@> make && make install デフォルトのインストールディレクトリは/usr/local/pgsql/です。 1.4.2 設定インストールが終了したら、PostgreSQLを稼働させる2台のサーバ:postgres0とpostgres1で以下の設定を行います。1.4.2.1 データベースの初期化データベースの初期化を行います。以下のコマンドを実行してください。postgres@> initdb -D /usr/local/pgsql/data これでディレクトリ/usr/local/pgsql/data以下にデータベースが作成されます。 このディレクトリを「ベースディレクトリ」、ベースディレクトリ以下のデータを「データベースクラスタ」とよびます。 1.4.2.2 アーカイブログディレクトリの作成アーカイブログを保存するディレクトリを作成します。 ここでは、ベースディレクトリにarchive_logというディレクトリを作成します。postgres@> mkdir /usr/local/pgsql/data/archive_log 1.4.2.3 設定ファイル以上の準備が終わったら、2つの設定ファイルpostgresql.confとpg_hba.confの編集を行います。 これらはベースディレクトリ(今回はディレクトリ"/usr/local/pgsql/data")にあります。1つ目のファイルはposgtresql.confです。 数多くのパラメータがありますが、今回は次の3つのパラメータを設定してください。 listen_addresses = '*' archive_mode = on archive_command = 'cp %p /usr/local/pgsql/data/archive_log/%f' パラメータarchive_modeとarchive_commandはオンラインリカバリのために必要です。 同じく行頭の"#"を消去して、それぞれ値を設定してください。 パラメータarchive_commandの設定値にあるディレクトリ"/usr/local/pgsql/data/archive_log/"は 上で作成したアーカイブログの保存ディレクトリを記述してください。
2つ目のファイルはアクセス制御ファイルpg_hba.confです。
ここでは、ネットワーク192.168.1.0からのアクセスはすべて許可する設定をします。
host all all 192.168.1.0/24 trust |
postgres@> pg_ctl -D /usr/local/pgsql/data start
postgres@> createdb slonydb
http://www.slony.info/
http://www.slony.info/downloads/1.2/source/
root@> mkdir /usr/local/slony1 root@> chown postgres:postgres /usr/local/slony1 root@> su postgres postgres@> tar xvfj slony1-1.2.15.tar.bz2 cd slony1-1.2.15 ./configure --prefix=/usr/local/slony1 \ > --with-perltools=/usr/local/slony1/bin \ > --with-pgconfigdir=/usr/local/pgsql/bin postgres@> make && make install
postgres@postgres0> createdb slonydb postgres@postgres0> createlang plpgsql slonydb
図3 テーブルの作成
postgres@postgres0> psql slonydb -q slonydb=# CREATE TABLE tbl_pkey (id int primary key, data int); NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "tbl_pkey_pkey" for table "tbl_pkey" CREATE TABLE slonydb=# CREATE TABLE tbl_uniq (id int NOT NULL, data int, UNIQUE(id)); NOTICE: CREATE TABLE / UNIQUE will create implicit index "tbl_uniq_id_key" for table "tbl_uniq" CREATE TABLE slonydb=# CREATE TABLE tbl (id int, data int); CREATE TABLE
マスター側のPostgreSQLの準備が済んだので、データベーススキーマをスレーブ側にコピーします。
postgres@postgres0> pg_dump -s -C slonydb | ssh 192.168.1.101 psql
postgres@postgres0> psql slonydb -q slonydb=# INSERT INTO tbl VALUES (1,1); INSERT 0 1 slonydb=# SELECT * FROM tbl; id | data ----+------ 1 | 1 (1 row)
postgres@postgres0> cd /usr/local/slony1/etc postgres@postgres0> cp slon_tools.conf-sample slon_tools.conf
リスト1 slon_tools.confの抜粋
1:if ($ENV{"SLONYNODES"}) {
2: require $ENV{"SLONYNODES"};
3:} else {
4: $CLUSTER_NAME = 'slony_test';
5:
6: $LOGDIR = '/usr/local/slony1/log';
7:
8: $MASTERNODE = 1;
9:
10: add_node(node => 1,
11: host => '192.168.1.100',
12: dbname => 'slonydb',
13: port => 5432,
14: user => 'postgres',
15: password => '');
16:
17: add_node(node => 2,
18: host => '192.168.1.101',
19: dbname => 'slonydb',
20: port => 5432,
21: user => 'postgres',
22: password => '');
23:}
24:
25:$SLONY_SETS = {
26: "set1" => {
27: "set_id" => 1,
28: "origin" => 1,
29:
30: "table_id" => 1,
31: "sequence_id" => 1,
32:
33: "pkeyedtables" => ['tbl_pkey', ],
34: "keyedtables" => {'tbl_uniq' => 'tbl_uniq_id_key',},
35: "serialtables" => ["tbl"],
36: },
37:};
38:
39:# Please do not add or change anything below this point.
40:1;
作成したslon_tools.confファイルはスレーブ側にも保存しておきます。
postgres@postgres0> scp slon_tools.conf 192.168.1.101:/usr/local/slony1/etc/
postgres@postgres0> cd /usr/local/slony1/bin postgres@postgres0> slonik_init_cluster | slonik
postgres@postgres0> slonik_init_cluster # INIT CLUSTER cluster name = slony_test; node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432'; node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432'; init cluster (id = 1, comment = 'Node 1 - slonydb@192.168.1.100'); # STORE NODE store node (id = 2, event node = 1, comment = 'Node 2 - slonydb@192.168.1.101'); echo 'Set up replication nodes'; # STORE PATH echo 'Next: configure paths for each node/origin'; store path (server = 1, client = 2, conninfo = 'host=192.168.1.100 dbname=slonydb user=postgres port=5432'); store path (server = 2, client = 1, conninfo = 'host=192.168.1.101 dbname=slonydb user=postgres port=5432'); echo 'Replication nodes prepared'; echo 'Please start a slon replication daemon for each node';
http://www.slony.info/documentation/commandreference.htmlにあるので、興味のある方は覗いてみてください。
ところで、ノードやパスの情報はどこに記録されるのでしょうか。
答えは各PostgreSQLサーバのデータベースにです。
各サーバ、つまり各ノードにslony用のスキーマが作成され、
そのスキーマに作られるslony管理用のテーブルに記録されます。
スキーマ名は"_クラスタ名"で、例えば今回はスキーマ名が"slony_test"なので
管理用のスキーマは"_slony_test"となります。
postgres@postgres0> slonik_create_set set1 | slonik
cluster name = slony_test;
node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432';
node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432';
# TABLE ADD KEY
echo ' Adding unique key to table public.tbl...';
table add key (
node id = 1,
full qualified name='public.tbl'
);
# CREATE SET
try {
create set (id = 1, origin = 1, comment = 'Set 1 for slony_test');
} on error {
echo 'Could not create subscription set 1 for slony_test!';
exit -1;
}
# SET ADD TABLE
echo 'Subscription set 1 created';
echo 'Adding tables to the subscription set';
set add table (set id = 1, origin = 1, id = 1,
full qualified name = 'public.tbl', key=serial,
comment = 'Table public.tbl without primary key');
echo 'Add unkeyed table public.tbl';
set add table (set id = 1, origin = 1, id = 2,
full qualified name = 'public.tbl_pkey',
comment = 'Table public.tbl_pkey with primary key');
echo 'Add primary keyed table public.tbl_pkey';
set add table (set id = 1, origin = 1, id = 3,
full qualified name = 'public.tbl_uniq', key='tbl_uniq_id_key',
comment = 'Table public.tbl_uniq with candidate primary key tbl_uniq_id_key');
echo 'Add candidate primary keyed table public.tbl_uniq';
# SET ADD SEQUENCE
echo 'Adding sequences to the subscription set';
echo 'All tables added';
postgres@postgres0> slonik_subscribe_set set1 2 | slonik
cluster name = slony_test;
node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432';
node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432';
try {
subscribe set (id = 1, provider = 1, receiver = 2, forward = yes);
}
on error {
exit 1;
}
echo 'Subscribed nodes to set 1';
postgres@postgres0> slon_start 1 postgres@postgres0> ssh 192.168.1.101 postgres@postgres1> cd /usr/local/slony1/bin postgres@postgres1> slon_start 2
図5 マスター側での動作確認
postgres@postgres0> psql slonydb -q slonydb=# INSERT INTO tbl VALUES (2,2); INSERT 0 1 slonydb=# SELECT * FROM tbl; id | data | _Slony-I_slony_test_rowID ----+------+--------------------------- 1 | 1 | 1000000000000001 2 | 2 | 1000000000000002 (2 rows)
図6 スレーブ側での動作確認
postgres@postgres1> psql slonydb -q slonydb=# SELECT * FROM tbl; id | data | _Slony-I_slony_test_rowID ----+------+--------------------------- 1 | 1 | 1000000000000001 2 | 2 | 1000000000000002 (2 rows) slonydb=# UPDATE tbl SET data = 100 WHERE id = 1; ERROR: Slony-I: Table tbl is replicated and cannot be modified on a subscriber node
postgres@postgres0> slonik_move_set set1 1 2 | slonik
cluster name = slony_test; node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432'; node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432'; echo 'Locking down set 1 on node 1'; lock set (id = 1, origin = 1); echo 'Locked down - moving it'; move set (id = 1, old origin = 1, new origin = 2); echo 'Replication set 1 moved from node 1 to 2. Remember to'; echo 'update your configuration file, if necessary, to note the new location'; echo 'for the set.';
postgres@postgres1> psql slonydb -q slonydb=# UPDATE tbl SET data = 100 WHERE id = 1; UPDATE 1
postgres@postgres0> pg_ctl -D /usr/local/pgsql/data -m immediate stop
postgres@postgres1> cd /usr/local/slony1/bin postgres@postgres1> slonik_failover 1 2 | slonik
cluster name = slony_test;
node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432';
node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432';
try {
failover (id = 1, backup node = 2);
} on error {
echo 'Failure to fail node 1 over to 2';
exit 1;
}
echo 'Replication sets originating on 1 failed over to 2';
postgres@postgres1> slon_kill 1
postgres@postgres1> pg_ctl -D /usr/local/pgsql/data -m immediate stop postgres@postgres1> slon_kill 2
postgres@postgres0> slonik_drop_node 2 | slonik
cluster name = slony_test;
node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432';
node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432';
try {
drop node (id = 2, event node = 1);
} on error {
echo 'Failed to drop node 2 from cluster';
exit 1;
}
echo 'dropped node 2 cluster';
postgres@postgres1> pg_ctl -D /usr/local/pgsql/data start postgres@postgres1> dropdb slonydb postgres@postgres1> createdb slonydb postgres@postgres1> createlang plpgsql slonydb postgres@postgres1> psql slonydb -q slonydb=# CREATE TABLE tbl_pkey (id int primary key, data int); slonydb=# CREATE TABLE tbl_uniq (id int NOT NULL, data int, UNIQUE(id)); slonydb=# CREATE TABLE tbl (id int, data int);
postgres@postgres0> slonik_store_node 2 | slonik postgres@postgres0> slonik_subscribe_set set1 2 | slonik
cluster name = slony_test; node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432'; node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432'; # STORE NODE store node (id = 2, event node = 1, comment = 'Node 2 - slonydb@192.168.1.101'); echo 'Set up replication nodes'; # STORE PATH echo 'Next: configure paths for each node/origin'; store path (server = 1, client = 2, conninfo = 'host=192.168.1.100 dbname=slonydb user=postgres port=5432'); store path (server = 2, client = 1, conninfo = 'host=192.168.1.101 dbname=slonydb user=postgres port=5432'); echo 'Replication nodes prepared'; echo 'Please start a slon replication daemon for each node';
postgres@postgres1> slon_start 2
postgres@postgres0> pg_ctl -D /usr/local/pgsql/data -m immediate stop postgres@postgres0> slon_kill 1
postgres@postgres1> cd /usr/local/slony1/bin postgres@postgres1> slonik_failover 1 2 | slonik
postgres@postgres1> slonik_drop_node 1 | sed s/"event node = 1"/"event node = 2"/ | slonik
cluster name = slony_test;
node 1 admin conninfo='host=192.168.1.100 dbname=slonydb user=postgres port=5432';
node 2 admin conninfo='host=192.168.1.101 dbname=slonydb user=postgres port=5432';
try {
drop node (id = 1, event node = 2);
} on error {
echo 'Failed to drop node 1 from cluster';
exit 1;
}
echo 'dropped node 1 cluster';
postgres@postgres0> pg_ctl -D /usr/local/pgsql/data start postgres@postgres0> dropdb slonydb postgres@postgres0> createdb slonydb postgres@postgres0> createlang plpgsql slonydb postgres@postgres0> psql slonydb -q slonydb=# CREATE TABLE tbl_pkey (id int primary key, data int); slonydb=# CREATE TABLE tbl_uniq (id int NOT NULL, data int, UNIQUE(id)); slonydb=# CREATE TABLE tbl (id int, data int);
postgres@postgres1> slonik_store_node 1 | sed s/"event node = 1"/"event node = 2"/ | slonik postgres@postgres1> slonik_subscribe_set set1 1 | slonik
postgres@postgres0> slon_start 1
postgres@postgres0> slonik_move_set set1 2 1 | slonik