PostgreSQL のデータ領域を DRBD で冗長化し、Keepalived の VRRP モードで HA 構成にしてみます。PostgreSQL-9.0 でストリーミング・レプリケーションやホットスタンバイの機能が加わったわけですが、非同期レプリケーションであることもあり、スレーブの破棄・追加は容易なのですが、マスターの破棄・追加がとても面倒くさいと感じたため、マスターは HA 化してしまおうと思います。
参考:
で、結論を先に言いますと、リソース管理の考え方のない Keepalived は、そのままだと、DB やファイルサーバには向いていないんではなかろうか、とも思います。いずれ HeartBeat もやってみます。
最終的には、以下のような構成で:

PostgreSQL 1 台目を、普通にセットアップ
面倒なので、今回は IPTABLES はオフにしておきます。
[root@node1 ~]# service iptables stop
ファイアウォールルールを適用中: [ OK ]
チェインポリシーを ACCEPT に設定中filter [ OK ]
iptables モジュールを取り外し中 [ OK ]
[root@node1 ~]# chkconfig iptables off
[root@node1 ~]#
PostgreSQL をインストールします。
[root@node1 ~]# yum install -y postgresql84 postgresql84-server
(中略)
Installed:
postgresql84.x86_64 0:8.4.4-1.el5_5.1
postgresql84-server.x86_64 0:8.4.4-1.el5_5.1
Dependency Installed:
postgresql84-libs.x86_64 0:8.4.4-1.el5_5.1
Complete!
[root@node1 ~]#
セットアップします。
[root@node1 ~]# mkdir -m 0700 /pgdata/ /pg_xlog/
[root@node1 ~]# chown postgres.postgres /pgdata/ /pg_xlog/
[root@node1 ~]# su - postgres -c "initdb --pgdata=/pgdata/ --xlogdir=/pg_xlog/ \
--encoding=UTF-8 --no-locale --username=admin --pwprompt --auth=md5"
データベースシステム内のファイルの所有者は"postgres"ユーザでした。
このユーザがサーバプロセスを所有しなければなりません。
データベースクラスタはロケールCで初期化されます。
デフォルトのテキスト検索設定はenglishに設定されました。
ディレクトリ/pgdataの権限を設定しています ... ok
ディレクトリ/pg_xlogの権限を設定しています ... ok
サブディレクトリを作成しています ... ok
デフォルトのmax_connectionsを選択しています ... 100
デフォルトの shared_buffers を選択しています ... 32MB
設定ファイルを作成しています ... ok
/pgdata/base/1にtemplate1データベースを作成しています ... ok
pg_authidを初期化しています ... ok
新しいスーパーユーザのパスワードを入力してください:<パスワード>
再入力してください:<パスワード>
パスワードを設定しています ... ok
依存関係を初期化しています ... ok
システムビューを作成しています ... ok
システムオブジェクトの定義をロードしています ... ok
変換を作成しています ... ok
ディレクトリを作成しています ... ok
組み込みオブジェクトに権限を設定しています ... ok
情報スキーマを作成しています ... ok
template1データベースをバキュームしています ... ok
template1からtemplate0へコピーしています ... ok
template1からpostgresへコピーしています ... ok
成功しました。以下を使用してデータベースサーバを起動することができます。
postgres -D /pgdata
または
pg_ctl -D /pgdata -l logfile start
[root@node1 ~]# cp /pgdata/postgresql.conf /pgdata/postgresql.conf.orig
[root@node1 ~]# vi /pgdata/postgresql.conf
[root@node1 ~]# diff -uNr /pgdata/postgresql.conf.orig /pgdata/postgresql.conf
--- /pgdata/postgresql.conf.orig
+++ /pgdata/postgresql.conf
@@ -56,7 +56,7 @@
# - Connection Settings -
-#listen_addresses = 'localhost' # what IP address(es) to listen on;
+listen_addresses = '*' # what IP address(es) to listen on;
# comma-separated list of addresses;
# defaults to 'localhost', '*' = all
# (change requires restart)
[root@node1 ~]# cp /pgdata/pg_hba.conf /pgdata/pg_hba.conf.orig
[root@node1 ~]# vi /pgdata/pg_hba.conf
[root@node1 ~]# diff -uNr /pgdata/pg_hba.conf.orig /pgdata/pg_hba.conf
--- /pgdata/pg_hba.conf.orig
+++ /pgdata/pg_hba.conf
@@ -72,3 +72,5 @@
host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5
+# IPv4 network connections:
+host all all 192.168.1.0/24 md5
[root@node1 ~]# su - postgres -c "pg_ctl -D /pgdata/ start"
サーバは起動中です。
[root@node1 ~]# psql -h node1.priv -U admin template1
ユーザ admin のパスワード:<パスワード>
psql (8.4.4)
"help" でヘルプを表示します.
template1=# \q
[root@node1 ~]# (crontab -u postgres -l; \
echo "@reboot /usr/bin/pg_ctl -D /pgdata/ start") | \
crontab -u postgres -
[root@node1 ~]# crontab -l -u postgres
@reboot /usr/bin/pg_ctl -D /pgdata/ start
[root@node1 ~]#
この状態で、しばらく運用していたという設定で、次へ行きます (もちろん実運用であれば、チューニングやら、何らかのバックアップが必要ですが、そこはパスで)。
2 台目を用意して、DRBD を設定
データやユーザも増えてきて、サービスを止められなくなってきたので、HA 化しようと思いたったとします。下記を買ってきたと想定します。
- 1 台目への追加パーツ
- DRBD 用の追加 NIC 1 枚 (eth1)
- DRBD でレプリケーションする、HDD 2 つ
- WAL 領域用 (sdb 10GB)
- データ領域用 (sdc 10GB)
- 2 台目のマシン 1 台
- NIC は 2 枚搭載 (通常用の eth0, DRBD 用の eth1)
- HDD
- システム用 (sda)
- WAL 領域用 (sdb 20GB)
- データ領域用 (sdc 20GB)
Ether カードは、DRBD のデータ転送用を追加します。VRRP はサービス側のネットワークにそのまま流します。専用化やボンディングも考えられます。
追加 HDD は、各ホストに 2 台ずつ用意しました。意図としましては、PostgreSQL のデータ領域と WAL の書き込み先を、別々に DRBD でレプリケートするためです。WAL 領域はシーケンシャル I/O が主体で、書き込みは同期で行なわれますので、ヘッドの動きを抑えつつ、DRBD のプロトコル C でレプリケートします (まあそれを言うのならば、DRBD のメタ領域は internal ではなく external にすべきですし、ファイルシステムも ext3 ではなく ext2 にすべきなのですが、まあいいや)。対して、とかく I/O バウンドなサービスでボトルネックとなるデータ領域は、ランダムアクセスが主体ですし、書き込みも非同期ですので、パフォーマンスのダウンを抑えるために、DRBD のプロトコル A で非同期レプリケーションを行ないます。フェイルオーバの際に、非同期 DRBD のデータ領域で取りこぼしがあったとしても、そこはデータベースのリカバリで、WAL から復元できるからです。
まずは、node1 で、下記のようなパーティションを作りました。
[root@node1 ~]# fdisk -l /dev/sdb
Disk /dev/sdb: 10.7 GB, 10737418240 bytes
255 heads, 63 sectors/track, 1305 cylinders
Units = シリンダ数 of 16065 * 512 = 8225280 bytes
デバイス Boot Start End Blocks Id System
/dev/sdb1 1 1305 10482381 83 Linux
[root@node1 ~]# fdisk -l /dev/sdc
Disk /dev/sdc: 10.7 GB, 10737418240 bytes
255 heads, 63 sectors/track, 1305 cylinders
Units = シリンダ数 of 16065 * 512 = 8225280 bytes
デバイス Boot Start End Blocks Id System
/dev/sdc1 1 1305 10482381 83 Linux
[root@node1 ~]#
DRBD をインストールします。
[root@node1 ~]# yum install -y kmod-drbd82 drbd82
(中略)
Installed:
drbd82.x86_64 0:8.2.6-1.el5.centos kmod-drbd82.x86_64 0:8.2.6-2
Complete!
[root@node1 ~]#
DRBD の設定ファイルを書きます。
[root@node1 ~]# cat > /etc/drbd.conf
resource res0 {
protocol C;
on node1.priv {
device /dev/drbd0;
disk /dev/sdb1;
address 192.168.2.1:7789;
meta-disk internal;
}
on node2.priv {
device /dev/drbd0;
disk /dev/sdb1;
address 192.168.2.2:7789;
meta-disk internal;
}
}
resource res1 {
protocol A;
on node1.priv {
device /dev/drbd1;
disk /dev/sdc1;
address 192.168.2.1:7790;
meta-disk internal;
}
on node2.priv {
device /dev/drbd1;
disk /dev/sdc1;
address 192.168.2.2:7790;
meta-disk internal;
}
}
[root@node1 ~]#
node1 で DRBD を起動します。
[root@node1 ~]# drbdadm create-md res0
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]
success
[root@node1 ~]# drbdadm create-md res1
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]
success
[root@node1 ~]# service drbd start
Starting DRBD resources: [ d(res0) d(res1) n(res0) n(res1) ].
..........
***************************************************************
DRBD's startup script waits for the peer node(s) to appear.
- In case this node was already a degraded cluster before the
reboot the timeout is 0 seconds. [degr-wfc-timeout]
- If the peer was available before the reboot the timeout will
expire after 0 seconds. [wfc-timeout]
(These values are for resource 'res0'; 0 sec -> wait forever)
To abort waiting enter 'yes' [ 14]:yes
[root@node1 ~]# drbdadm -- -o primary all
[root@node1 ~]# mkfs.ext3 /dev/drbd0
(中略)
[root@node1 ~]# mkfs.ext3 /dev/drbd1
(中略)
[root@node1 ~]#
面倒なので、node1 ←→ node2 へ、root ユーザによるパスワードなしの ssh アクセスが可能であるとさせてください。node1 のディスクと同じレイアウトのパーティションを node2 のディスクに作成します。
[root@node1 ~]# sfdisk -d /dev/sdb | ssh node2.priv "sfdisk /dev/sdb"
(中略)
新たな場面:
ユニット = 512 バイトのセクタ、0 から数えます
Device Boot Start End #sectors Id System
/dev/sdb1 63 20964824 20964762 83 Linux
/dev/sdb2 0 - 0 0 空
/dev/sdb3 0 - 0 0 空
/dev/sdb4 0 - 0 0 空
新たなパーティションの書き込みに成功
パーティションテーブルを再読み込み中...
(中略)
[root@node1 ~]# sfdisk -d /dev/sdc | ssh node2.priv "sfdisk /dev/sdc"
(中略)
新たな場面:
ユニット = 512 バイトのセクタ、0 から数えます
Device Boot Start End #sectors Id System
/dev/sdc1 63 20964824 20964762 83 Linux
/dev/sdc2 0 - 0 0 空
/dev/sdc3 0 - 0 0 空
/dev/sdc4 0 - 0 0 空
新たなパーティションの書き込みに成功
パーティションテーブルを再読み込み中...
(中略)
[root@node1 ~]#
node2 で DRBD をインストールし、起動します。
[root@node2 ~]# service iptables stop
ファイアウォールルールを適用中: [ OK ]
チェインポリシーを ACCEPT に設定中filter [ OK ]
iptables モジュールを取り外し中 [ OK ]
[root@node2 ~]# chkconfig iptables off
[root@node2 ~]# yum install -y kmod-drbd82 drbd82
(中略)
Installed:
drbd82.x86_64 0:8.2.6-1.el5.centos kmod-drbd82.x86_64 0:8.2.6-2
Complete!
[root@node2 ~]# scp node1.priv:/etc/drbd.conf /etc/drbd.conf
drbd.conf 100% 540 0.5KB/s 00:00
[root@node2 ~]# drbdadm create-md res0
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]
success
[root@node2 ~]# drbdadm create-md res1
(中略)
* If you wish to opt out entirely, simply enter 'no'.
* To continue, just press [RETURN]
success
[root@node2 ~]# service drbd start
Starting DRBD resources: [ d(res0) d(res1) n(res0) n(res1) ].
..........
***************************************************************
DRBD's startup script waits for the peer node(s) to appear.
- In case this node was already a degraded cluster before the
reboot the timeout is 0 seconds. [degr-wfc-timeout]
- If the peer was available before the reboot the timeout will
expire after 0 seconds. [wfc-timeout]
(These values are for resource 'res0'; 0 sec -> wait forever)
To abort waiting enter 'yes' [ 10]:
[root@node2 ~]# cat /proc/drbd
version: 8.2.6 (api:88/proto:86-88)
GIT-hash: 3e69822d3bb4920a8c1bfdf7d647169eba7d2eb4 build by
buildsvn@c5-x8664-build, 2008-10-03 11:30:17
0: cs:SyncTarget st:Secondary/Primary ds:Inconsistent/UpToDate C r---
ns:0 nr:6752 dw:6752 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 oos:10475272
[>....................] sync'ed: 0.2% (10229/10236)M
finish: 7:16:28 speed: 288 (320) K/sec
1: cs:SyncTarget st:Secondary/Primary ds:Inconsistent/UpToDate A r---
ns:0 nr:9952 dw:9952 dr:0 al:0 bm:0 lo:0 pe:0 ua:0 ap:0 oos:10472072
[>....................] sync'ed: 0.2% (10226/10236)M
finish: 7:16:20 speed: 256 (320) K/sec
[root@node2 ~]#
初期同期が終わりそうにないので寝ます。
データベースの領域を移動
朝までには終わっていました。
node1 で、データの場所を移動し DB を起動します。
[root@node1 ~]# mkdir /mnt/res0 /mnt/res1
[root@node1 ~]# mount /dev/drbd0 /mnt/res0
[root@node1 ~]# mount /dev/drbd1 /mnt/res1
[root@node1 ~]# su - postgres -c "pg_ctl -D /pgdata/ stop"
サーバ停止処理の完了を待っています....完了
サーバは停止しました
[root@node1 ~]# crontab -e -u postgres
[root@node1 ~]# crontab -l -u postgres
[root@node1 ~]# mv /pg_xlog/ /mnt/res0/
[root@node1 ~]# mv /pgdata/ /mnt/res1/
[root@node1 ~]# ln -sf /mnt/res0/pg_xlog /pg_xlog
[root@node1 ~]# ln -sf /mnt/res1/pgdata /pgdata
[root@node1 ~]# su - postgres -c "pg_ctl -D /pgdata/ start"
サーバは起動中です。
[root@node1 ~]#
node2 に PostgreSQL を入れ、ディレクトリの準備をします。
[root@node2 ~]# yum install -y postgresql84 postgresql84-server
(中略)
Installed:
postgresql84.x86_64 0:8.4.5-1.el5_5.1
postgresql84-server.x86_64 0:8.4.5-1.el5_5.1
Dependency Installed:
postgresql84-libs.x86_64 0:8.4.5-1.el5_5.1
Complete!
[root@node2 ~]# mkdir /mnt/res0 /mnt/res1
[root@node2 ~]# ln -sf /mnt/res0/pg_xlog /pg_xlog
[root@node2 ~]# /mnt/res1/pgdata /pgdata
[root@node2 ~]# ln -sf /mnt/res1/pgdata /pgdata
[root@node2 ~]#
マスター/バックアップになるためのスクリプトを node1, node2 ともに設置して、まずは手作業で、正しく動作することを確認しておきます。
[root@node1 ~]# cat > ~/pgsql_notify_master
#!/bin/sh
service drbd start
drbdadm primary all
mount /dev/drbd0 /mnt/res0/
mount /dev/drbd1 /mnt/res1/
su - postgres -c "/usr/bin/pg_ctl -D /pgdata/ start"
[root@node1 ~]# chmod 0755 ~/pgsql_notify_master
[root@node1 ~]# cat > ~/pgsql_notify_backup
#!/bin/sh
su - postgres -c "/usr/bin/pg_ctl -D /pgdata/ stop"
umount /mnt/res1/
umount /mnt/res0/
drbdadm secondary all
[root@node1 ~]# chmod 0755 ~/pgsql_notify_backup
Keepalived を VRRP モードで設定
node1, node2 で、CentOS の “testing” レポジトリから、Keepalived をインストールします。
[root@node1 ~]# cd /etc/yum.repos.d/
[root@node1 yum.repos.d]# wget \
http://dev.centos.org/centos/5/CentOS-Testing.repo
(中略)
[root@node1 yum.repos.d]# cd
[root@node1 ~]# yum --enablerepo=c5-testing install -y keepalived
(中略)
Installed:
keepalived.x86_64 0:1.1.15-0.el5.centos
Complete!
[root@node1 ~]# cp /etc/keepalived/keepalived.conf \
/etc/keepalived/keepalived.conf.orig
[root@node1 ~]# cat > /etc/keepalived/keepalived.conf
vrrp_instance pgsql {
garp_master_delay 5
virtual_router_id 200
advert_int 1
state BACKUP
priority 100
interface eth0
nopreempt
authentication {
auth_type PASS
auth_pass hogefuga
}
virtual_ipaddress {
192.168.1.33/24 dev eth0
}
notify_master "/root/pgsql_notify_master"
notify_backup "/root/pgsql_notify_backup"
notify_fault "/root/pgsql_notify_backup"
}
[root@node1 ~]# /root/pgsql_notify_backup
pg_ctl: PIDファイル"/pgdata/postmaster.pid"がありません
サーバが動作していますか?
umount: /mnt/res1/: マウントされていません
umount: /mnt/res0/: マウントされていません
[root@node1 ~]# service keepalived start
keepalived を起動中: [ OK ]
[root@node1 ~]# chkconfig keepalived on
[root@node1 ~]#
node2 も同様です。
これでとりあえず、HA 構成のできあがりです。
Keepalived の管理リソースについて
Keepalived が VRRP モードで管理するのはマスター←→バックアップの状態遷移だけであり、両ノード間で管理してくれる共有リソースは仮想 IP アドレスだけです。
そこで何が問題かというと、対向が起動していない状態で Keepalived を起動すれば、「マスターに遷移した」ということで Keepalived は “notify_master” を実行するのですが、停止の際 (keepalived を明示的に停止させたり、ランレベル移行やシステム終了を行なった際) には、”notify_backup” を実行しません (なぜなんだろう? “notify_terminate” ハンドラでもあればいいのに)。そうなると、VIP だけ対向へ移るのですが、Keepalived の管理外の共有リソース (今回で言うと、PostgreSQL のサービスとDRBD のプライマリであること) は、Keepalived のフェイルオーバと無関係に残ってしまい、マスターに移行しようとした対向は、DRBD プライマリになれません。当然、シャットダウン時の、各サービスの停止順序もバラバラです。
しかたがないので、keepalived サービスの stop の際にバックアップノード化をするようにします。
[root@node1 ~]# cp /etc/init.d/keepalived /etc/init.d/keepalived.orig
[root@node1 ~]# vi /etc/init.d/keepalived
[root@node1 ~]# diff -U 10 /etc/init.d/keepalived.orig /etc/init.d/keepalived
--- /etc/init.d/keepalived.orig 2010-10-11 17:38:22.000000000 +0900
+++ /etc/init.d/keepalived 2010-10-11 17:39:22.000000000 +0900
@@ -25,20 +25,21 @@
echo
[ $RETVAL -eq 0 ] && touch /var/lock/subsys/$prog
}
stop() {
echo -n $"Stopping $prog: "
killproc keepalived
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f /var/lock/subsys/$prog
+ test $RETVAL -eq 0 && /root/pgsql_notify_backup
}
reload() {
echo -n $"Reloading $prog: "
killproc keepalived -1
RETVAL=$?
echo
}
# See how we were called.
[root@node1 ~]#
ネット上では、daemontools を入れて、keepalived のフォアグラウンド起動とバックアップ化スクリプトを組み合わせる構成が多いようですが、DJB は distro 的/FHS 的にはメンドくさいんだよなぁ… (DJB に対して、それ以上の感情はありませんよ)。VRRP が本来はルータ用のプロトコルであることや、Keepalived が元々は、どうやら LVS と組み合わせてロードバランサとして利用するために作られたことを考えると、IP アドレス以外の共有リソースがある用途をあまり考慮していないようにも見うけられます。HeartBeat であれば、DRBD との連動機能もあるし、そもそも共有リソースの考え方があるので、DB やファイルサーバ、ストレージ等、共有リソースとその依存関係がある場合には、HeartBeat (や PaceMaker か、LifeKeeper でも良いですが) の方が良いのかも知らんです…。