Slony-I 2.2.7 のドキュメントからの情報です。
概要の確認とチュートリアルを試してみます。
Slony-I とは
Slony-I は PostgreSQL のレプリケーションシステムで、複数レプリカの作成、カスケードレプリケーション、プロモーションをサポートしており、次の特徴を持つ
- 異なるメジャーバージョン間でもレプリケーション可能
- 異なるハードウェアアーキテクチャ、OSの間でもレプリケーション可能
- 一部のテーブルだけをレプリケーションすることが可能
- あるテーブルをレプリカ(A)に、別のテーブルをレプリカ(B)にレプリケーションといったことが可能
- テーブル毎にレプリカ元のデータベースサーバーが異なっていてもレプリケーション可能
PostgreSQL は version 10 から logical replication に対応し、今後のバージョンアップにはこれが使えますが、旧バージョンから 10 に上げるには当然ながら使えません(9.4 から logical decoding が使えるようになっているので追加の何かで logical replication できるような気もします)。ということで旧バージョンからの更新を短いダウンタイムで行うための手段として Slony-I は候補となります。
System Requirements
- PostgreSQL 8.3 以降 (8.3.x, 8.4.x , 9.0.x, 9.1.x, 9.2.x, 9.3.x,9.4.x, 9.5.x での動作が確認されている)。これより前のバージョンでは Slony-I 1.2.x を使う必要がある。2.2.6 の release note に Support for PG10 とあるので 10.x にも対応しているはず
以降は推奨
- NTP などで時刻を同期すること、UTC や GMT といった安定したタイムゾーンを使うこと(夏時間のないタイムゾーン、PostgreSQL が認識できるタイムゾーンが好ましい)
- 信頼性の高いネットワーク(WAN 越しにレプリケーションする場合、それぞれの slon プロセスはそれぞれのローカルネットワーク内で実行するべし)
- データベースのエンコーディングは揃えるべし
Slony-I Concepts
セットアップするためには次の概念を理解する必要がある。
- Cluster
- Node
- Replication Set
- Origin, Providers and Subscribers
- slon daemons
- slonik configuration processor
ロシア語の意味も理解しておくと良い。
- slon は象 🐘
- slony は象の複数形 🐘🐘🐘
- slonik は小さな象
Cluster
Cluster はレプリケーションを組む PostgreSQL インスタンスの集合で、各 Slonik スクリプトで次のように定義する。
cluster name = something;
Cluster 名が something
だった場合、それぞれのデータベースに _something
という schema が作成される。
Node
レプリケーションを構成するうちのひとつひとつのデータベースを Node と呼び、各 Slonik スクリプトの冒頭で次のように定義される。
NODE 1 ADMIN CONNINFO = 'dbname=testdb host=server1 user=slony';
この ADMIN CONNINFO は libpq の PQconnectdb() 関数に渡される
Replication Set
Node 間でレプリケーションされるテーブルとシーケンスのセット。
Origin, Providers and Subscribers
各 Replication Set には Origin node があり、それはアプリケーションによるレコードの変更が唯一許されている場所であり、Master provider とも呼ばれる。Replication Set の他の node は Subscriber となります。ただし、Slony-I はカスケードレプリケーションをサポートしているため、Subscriber が別の Replication set の Origin である可能性もあります。
slon daemons
Cluster 内の各 Node ではレプリケーションイベントを処理する slon プロセスが稼働している。C 言語で書かれており、処理する主な2つのイベントは次の通り。
- Configuration events
Slonik スクリプトが実行された場合に発生し、クラスタ構成の変更が送られる - SYNC events
レプリケーションされたテーブルへの変更が SYNC にまとめられて Subscriber に送られ、適用される
slonik configuration processor
Slonik コマンドは小さな言語となっているスクリプトを実行してクラスタの設定変更イベントを送る。このイベントには Node の追加や削除、通信 path の変更、Subscriber の追加、削除が含まれる。
Current Limitations
Slony-I は次の変更を自動でレプリケートしない。
- Large objects (BLOBS)
- DDL
- Uses and Roles
Slony-I は trigger によって変更を捉えているため、これらの変更は捉えることができませんが、SLONIK EXECUTE SCRIPT を使うことで DDL を各 Node で実行可能です。
Tutorial
Replicating Your First Database
pgbench を使ってレプリケーション設定を試します。PostgreSQL 9.3 から PostgreSQL 9.6 に同期させてみます。CentOS 7 のサーバー2台 (pg1, pg2) に PGDG リポジトリから PostgreSQL と Slony-I をインストールします。簡略化のために pg_hba.conf
の認証設定は trust
で。
pg1
sudo yum -y install yum install https://download.postgresql.org/pub/repos/yum/9.3/redhat/rhel-7-x86_64/pgdg-centos93-9.3-3.noarch.rpm
sudo yum -y install perl
sudo yum -y install postgresql93 postgresql93-contrib slony1-93
sudo /usr/pgsql-9.3/bin/postgresql93-setup
sudo /usr/pgsql-9.3/bin/postgresql93-setup initdb
sudoedit /var/lib/pgsql/9.3/data/pg_hba.conf
sudo systemctl enable postgresql-9.3
sudo systemctl start postgresql-9.3
pg2
sudo yum -y install https://download.postgresql.org/pub/repos/yum/9.6/redhat/rhel-7-x86_64/pgdg-centos96-9.6-3.noarch.rpm
sudo yum -y install perl
sudo yum -y install postgresql96 postgresql96-contrib slony1-96
sudo /usr/pgsql-9.6/bin/postgresql96-setup initdb
sudoedit /var/lib/pgsql/9.6/data/pg_hba.conf
sudo systemctl enable postgresql-9.6
sudo systemctl start postgresql-9.6
今後の手順で使う環境変数を次のように設定します。
export CLUSTERNAME=slony_example
export MASTERDBNAME=pgbench
export SLAVEDBNAME=pgbench
export MASTERHOST=pg1
export SLAVEHOST=pg2
export REPLICATIONUSER=postgres
export PGBENCHUSER=pgbench
Creating the pgbench User
pgbench 用ユーザーの作成。
sudo -iu postgres createuser -SRD $PGBENCHUSER
Preparing the Databases
pgbench 用のデータベースを作成し、1度 pgbench を実行してテーブルを作成します。
sudo -iu postgres createdb -O $PGBENCHUSER $MASTERDBNAME
/usr/pgsql-9.3/bin/pgbench -i -s 1 -U $PGBENCHUSER $MASTERDBNAME
pgbench=# \d+
List of relations
Schema | Name | Type | Owner | Size | Description
--------+------------------+-------+---------+---------+-------------
public | pgbench_accounts | table | pgbench | 13 MB |
public | pgbench_branches | table | pgbench | 40 kB |
public | pgbench_history | table | pgbench | 0 bytes |
public | pgbench_tellers | table | pgbench | 40 kB |
(4 rows)
pgbench=# \d pgbench_accounts
Table "public.pgbench_accounts"
Column | Type | Modifiers
----------+---------------+-----------
aid | integer | not null
bid | integer |
abalance | integer |
filler | character(84) |
Indexes:
"pgbench_accounts_pkey" PRIMARY KEY, btree (aid)
pgbench=# \d pgbench_branches
Table "public.pgbench_branches"
Column | Type | Modifiers
----------+---------------+-----------
bid | integer | not null
bbalance | integer |
filler | character(88) |
Indexes:
"pgbench_branches_pkey" PRIMARY KEY, btree (bid)
pgbench=# \d pgbench_tellers
Table "public.pgbench_tellers"
Column | Type | Modifiers
----------+---------------+-----------
tid | integer | not null
bid | integer |
tbalance | integer |
filler | character(84) |
Indexes:
"pgbench_tellers_pkey" PRIMARY KEY, btree (tid)
pgbench=#
pgbench_history
テーブルには PRIMARY KEY が存在しないが、Slony は PRIMARY KEY またはそれ相当の INDEX を必要とするため PRIMARY KEY を追加する。
pgbench=# \d pgbench_history
Table "public.pgbench_history"
Column | Type | Modifiers
--------+-----------------------------+-----------
tid | integer |
bid | integer |
aid | integer |
delta | integer |
mtime | timestamp without time zone |
filler | character(22) |
pgbench=#
psql -U $PGBENCHUSER -d $MASTERDBNAME -c "ALTER TABLE pgbench_history ADD COLUMN id serial"
psql -U $PGBENCHUSER -d $MASTERDBNAME -c "ALTER TABLE pgbench_history ADD PRIMARY KEY(id)"
Slony は PL/PGSQL を使うので createlang で作成する。
sudo -iu postgres createlang plpgsql $MASTERDBNAME
pg2 に DB を作成する。
sudo -iu postgres createdb -O $PGBENCHUSER $SLAVEDBNAME
pg1 から pg2 へスキーマをコピーする。
pg_dump -s -U $REPLICATIONUSER -h $MASTERHOST $MASTERDBNAME \
| psql -U $REPLICATIONUSER -h $SLAVEHOST $SLAVEDBNAME
Configuring the Database For Replication
設定テーブル、ストアド・プロシージャ、トリガーの作成と設定はすべて slonik コマンドを使って行います。
Using slonik Command Directly
slonik コマンドへの入力を自分で用意する方法です。簡略化およびミスを減らすために後述の別のツールを使う方法もあります。
sudo /usr/pgsql-9.3/bin/slonik <<_EOF_
#--
# レプリケーションシステムのネームスペースを定義する
# この例では slony_example とする
#--
cluster name = $CLUSTERNAME;
#--
# admin conninfo で slonik が DB にログインするための情報を定義する
# 構文は C-API の PQconnectdb に渡すもの
# --
node 1 admin conninfo = 'dbname=$MASTERDBNAME host=$MASTERHOST user=$REPLICATIONUSER';
node 2 admin conninfo = 'dbname=$SLAVEDBNAME host=$SLAVEHOST user=$REPLICATIONUSER';
#--
# 最初のノードを初期化する
# これは _$CLUSTERNAME スキーマを作成する
#--
init cluster ( id=1, comment = 'Master Node');
#--
# Slony-I はレプリケーションテーブルを set として定義する
# 次のコマンドは 4 つの pgbench 用テーブルを set (id = 1) としている
# master (origin) は 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='accounts table');
set add table (set id=1, origin=1, id=2, fully qualified name = 'public.pgbench_branches', comment='branches table');
set add table (set id=1, origin=1, id=3, fully qualified name = 'public.pgbench_tellers', comment='tellers table');
set add table (set id=1, origin=1, id=4, fully qualified name = 'public.pgbench_history', comment='history table');
#--
# 2 番目の node (id = 2) を定義して node 間で接続するための情報 (path) を定義する
#--
store node (id=2, comment = 'Slave node', event node=1);
store path (server = 1, client = 2, conninfo='dbname=$MASTERDBNAME host=$MASTERHOST user=$REPLICATIONUSER');
store path (server = 2, client = 1, conninfo='dbname=$SLAVEDBNAME host=$SLAVEHOST user=$REPLICATIONUSER');
_EOF_
これを実行すると pg1, pg2 の pgbench データベースの _slony_example
スキーマに沢山の slony 用テーブルが作成され、レプリケーション対象テーブルに trigger
が作成されています。
pgbench=# set search_path to public,_slony_example;
SET
pgbench=# \d
List of relations
Schema | Name | Type | Owner
----------------+----------------------------+----------+----------
_slony_example | sl_action_seq | sequence | postgres
_slony_example | sl_apply_stats | table | postgres
_slony_example | sl_archive_counter | table | postgres
_slony_example | sl_components | table | postgres
_slony_example | sl_config_lock | table | postgres
_slony_example | sl_confirm | table | postgres
_slony_example | sl_event | table | postgres
_slony_example | sl_event_lock | table | postgres
_slony_example | sl_event_seq | sequence | postgres
_slony_example | sl_failover_targets | view | postgres
_slony_example | sl_listen | table | postgres
_slony_example | sl_local_node_id | sequence | postgres
_slony_example | sl_log_1 | table | postgres
_slony_example | sl_log_2 | table | postgres
_slony_example | sl_log_script | table | postgres
_slony_example | sl_log_status | sequence | postgres
_slony_example | sl_node | table | postgres
_slony_example | sl_nodelock | table | postgres
_slony_example | sl_nodelock_nl_conncnt_seq | sequence | postgres
_slony_example | sl_path | table | postgres
_slony_example | sl_registry | table | postgres
_slony_example | sl_seqlastvalue | view | postgres
_slony_example | sl_seqlog | table | postgres
_slony_example | sl_sequence | table | postgres
_slony_example | sl_set | table | postgres
_slony_example | sl_setsync | table | postgres
_slony_example | sl_status | view | postgres
_slony_example | sl_subscribe | table | postgres
_slony_example | sl_table | table | postgres
public | pgbench_accounts | table | pgbench
public | pgbench_branches | table | pgbench
public | pgbench_history | table | pgbench
public | pgbench_history_id_seq | sequence | pgbench
public | pgbench_tellers | table | pgbench
(34 rows)
pgbench=# \d pgbench_accounts
Table "public.pgbench_accounts"
Column | Type | Modifiers
----------+---------------+-----------
aid | integer | not null
bid | integer |
abalance | integer |
filler | character(84) |
Indexes:
"pgbench_accounts_pkey" PRIMARY KEY, btree (aid)
Triggers:
_slony_example_logtrigger AFTER INSERT OR DELETE OR UPDATE ON pgbench_accounts FOR EACH ROW EXECUTE PROCEDURE _slony_example.logtrigger('_slony_example', '1', 'k')
_slony_example_truncatetrigger BEFORE TRUNCATE ON pgbench_accounts FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.log_truncate('1')
Disabled triggers:
_slony_example_denyaccess BEFORE INSERT OR DELETE OR UPDATE ON pgbench_accounts FOR EACH ROW EXECUTE PROCEDURE _slony_example.denyaccess('_slony_example')
_slony_example_truncatedeny BEFORE TRUNCATE ON pgbench_accounts FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.deny_truncate()
pgbench=# \d pgbench_branches
Table "public.pgbench_branches"
Column | Type | Modifiers
----------+---------------+-----------
bid | integer | not null
bbalance | integer |
filler | character(88) |
Indexes:
"pgbench_branches_pkey" PRIMARY KEY, btree (bid)
Triggers:
_slony_example_logtrigger AFTER INSERT OR DELETE OR UPDATE ON pgbench_branches FOR EACH ROW EXECUTE PROCEDURE _slony_example.logtrigger('_slony_example', '2', 'k')
_slony_example_truncatetrigger BEFORE TRUNCATE ON pgbench_branches FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.log_truncate('2')
Disabled triggers:
_slony_example_denyaccess BEFORE INSERT OR DELETE OR UPDATE ON pgbench_branches FOR EACH ROW EXECUTE PROCEDURE _slony_example.denyaccess('_slony_example')
_slony_example_truncatedeny BEFORE TRUNCATE ON pgbench_branches FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.deny_truncate()
pgbench=# \d pgbench_history
Table "public.pgbench_history"
Column | Type | Modifiers
--------+-----------------------------+--------------------------------------------------------------
tid | integer |
bid | integer |
aid | integer |
delta | integer |
mtime | timestamp without time zone |
filler | character(22) |
id | integer | not null default nextval('pgbench_history_id_seq'::regclass)
Indexes:
"pgbench_history_pkey" PRIMARY KEY, btree (id)
Triggers:
_slony_example_logtrigger AFTER INSERT OR DELETE OR UPDATE ON pgbench_history FOR EACH ROW EXECUTE PROCEDURE _slony_example.logtrigger('_slony_example', '4', 'vvvvvvk')
_slony_example_truncatetrigger BEFORE TRUNCATE ON pgbench_history FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.log_truncate('4')
Disabled triggers:
_slony_example_denyaccess BEFORE INSERT OR DELETE OR UPDATE ON pgbench_history FOR EACH ROW EXECUTE PROCEDURE _slony_example.denyaccess('_slony_example')
_slony_example_truncatedeny BEFORE TRUNCATE ON pgbench_history FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.deny_truncate()
pgbench=# \d pgbench_tellers
Table "public.pgbench_tellers"
Column | Type | Modifiers
----------+---------------+-----------
tid | integer | not null
bid | integer |
tbalance | integer |
filler | character(84) |
Indexes:
"pgbench_tellers_pkey" PRIMARY KEY, btree (tid)
Triggers:
_slony_example_logtrigger AFTER INSERT OR DELETE OR UPDATE ON pgbench_tellers FOR EACH ROW EXECUTE PROCEDURE _slony_example.logtrigger('_slony_example', '3', 'k')
_slony_example_truncatetrigger BEFORE TRUNCATE ON pgbench_tellers FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.log_truncate('3')
Disabled triggers:
_slony_example_denyaccess BEFORE INSERT OR DELETE OR UPDATE ON pgbench_tellers FOR EACH ROW EXECUTE PROCEDURE _slony_example.denyaccess('_slony_example')
_slony_example_truncatedeny BEFORE TRUNCATE ON pgbench_tellers FOR EACH STATEMENT EXECUTE PROCEDURE _slony_example.deny_truncate()
pgbench=#
pg1 (origin) で slon を起動。
/usr/pgsql-9.3/bin/slon $CLUSTERNAME "dbname=$MASTERDBNAME user=$REPLICATIONUSER host=$MASTERHOST"
pg2 (subscriber) で slon を起動。
/usr/pgsql-9.6/bin/slon $CLUSTERNAME "dbname=$SLAVEDBNAME user=$REPLICATIONUSER host=$SLAVEHOST"
pg2 で pg_stat_activity
を見ると次のようなプロセスからの接続がありました。
slon.local_cleanup
slon.local_listen
slon.local_monitor
slon.local_sync
slon.node_2_listen
slon.origin_2_provider_2
slon.remoteWorkerThread_1
slon プロセスがなにやら SYNC してそうな出力をしますが、ここまでではまだ同期が始まっていません。
ここで再度 pgbench を実行してみます。今度は -T 300
で5分間実行されるようにしています。これの実行中に次の subscribe 設定をすることで、更新中の同期開始を試みます。
/usr/pgsql-9.3/bin/pgbench -s 1 -c 5 -T 300 -U $PGBENCHUSER -h $MASTERHOST $MASTERDBNAME
次のようにして slonik で subscribe 指示を出します。
sudo /usr/pgsql-9.3/bin/slonik <<_EOF_
# ----
# This defines which namespace the replication system uses
# ----
cluster name = $CLUSTERNAME;
# ----
# 各 node への接続情報
# ----
node 1 admin conninfo = 'dbname=$MASTERDBNAME host=$MASTERHOST user=$REPLICATIONUSER';
node 2 admin conninfo = 'dbname=$SLAVEDBNAME host=$SLAVEHOST user=$REPLICATIONUSER';
# ----
# Node 2 subscribes set 1
# Subscriber が cascade や failover で provider になるのであれば forward は yes とします
# ----
subscribe set ( id = 1, provider = 1, receiver = 2, forward = no);
_EOF_
これによって origin テーブルのレコードをコピーし、完了後、コピー開始時からの変更を反映していきます。
Using the altperl Scripts
cluster name
や admin conninfo
など slonik のへの入力を毎度生成するのは大変なので /etc/slony1-93/slon_tools.conf
の設定を元に生成してくれるツールがあります。 次のようにして pipe で slonik に渡すことで初期化や起動、Subscribe などの指示を出せます。
# Initialize cluster:
$ slonik_init_cluster | slonik
# Start slon (here 1 and 2 are node numbers)
$ slon_start 1
$ slon_start 2
# Create Sets (here 1 is a set number)
$ slonik_create_set 1 | slonik
# subscribe set to second node (1= set ID, 2= node ID)
$ slonik_subscribe_set 1 2 | slonik
テーブル / レコードの比較
正しく同期できているか、次のスクリプトで確認できます。order by
つきでそれぞれのデータベースからレコードを取得して diff で比較しています。
#!/bin/sh
echo -n "**** comparing sample1 ... "
psql -U $REPLICATIONUSER -h $MASTERHOST $MASTERDBNAME >dump.tmp.1.$$ <<_EOF_
select 'accounts:'::text, aid, bid, abalance, filler
from pgbench_accounts order by aid;
select 'branches:'::text, bid, bbalance, filler
from pgbench_branches order by bid;
select 'tellers:'::text, tid, bid, tbalance, filler
from pgbench_tellers order by tid;
select 'history:'::text, tid, bid, aid, delta, mtime, filler, id
from pgbench_history order by id;
_EOF_
psql -U $REPLICATIONUSER -h $SLAVEHOST $SLAVEDBNAME >dump.tmp.2.$$ <<_EOF_
select 'accounts:'::text, aid, bid, abalance, filler
from pgbench_accounts order by aid;
select 'branches:'::text, bid, bbalance, filler
from pgbench_branches order by bid;
select 'tellers:'::text, tid, bid, tbalance, filler
from pgbench_tellers order by tid;
select 'history:'::text, tid, bid, aid, delta, mtime, filler, id
from pgbench_history order by id;
_EOF_
if diff dump.tmp.1.$$ dump.tmp.2.$$ >$CLUSTERNAME.diff ; then
echo "success - databases are equal."
rm dump.tmp.?.$$
rm $CLUSTERNAME.diff
else
echo "FAILED - see $CLUSTERNAME.diff for database differences"
fi
Conclusion
リアルワールドでは pgbench の用な単純な構成ではないため、実際にこれを使ってデータベースのアップグレードを行うにはより詳しく調査する必要がありますが、なんとなく概要がわかりました。