自製 PostgreSQL 2-node HA

前言

現在的雲端很發達, 幾個按鍵就可以產出一台機器, 然後也可以配合廠商的服務來做 HA。不過問題來了, 如果今天就是一個小的自行架設的機房, 兩台電腦要自己搞 HA 那要怎麼辦呢? 自行上網研究了一陣子, 得到的答案就是用 heartbeat 之類的東西。在研究的過程中, 有想過 HAProxy 或是 DNS round robin 之類的 solution, 但這都無法解決一台當掉另外一台備援要起來變成 primary 的問題。於是在看了 How to setup HAProxy with failover? 這篇文章講到 Stack Overflow 就是用 heartbeat 來做 failover 的機制。

不過這整件事情都很困難, 原因是如果搜尋 heartbeat 找到的資料似乎都已經很舊了, Linux-HA 更指出我們應該要去 Cluster Labs 才對。然後在上面出現了更多工具與名詞, 實在是容易混淆。所以這篇文章就是一個簡單的 Tutorial 一步一步在 Ubuntu Server 14.04 LTS 上架設一個 2-node failover 的 PostgreSQL HA。

如果你想要有更多資源的話, Ubuntu 的環境底下, Cluster Labs 的文件要看 Pacemaker 1.1 for CMAN or Corosync 1.x: Clusters from Scratch。然而官方的文件有非常多的錯誤, 因此還是會踩到一大堆雷。Ubuntu 自己也有一些筆記, ClusterStack/Natty, 而 PostgreSQL 的則是 High Availability with PostgreSQL and Pacemaker 這篇文章。基本上就是使用到 corosync 與 pacemaker 這兩個套件做 HA / Failover, 用 DRBD 來做網路磁碟同步, 因此上網搜尋也可以找這幾個關鍵字。

下面的 Tutorial 其實就是根據 High Availability with PostgreSQL and Pacemaker 這篇為基礎的。

基本原理

我們會有兩台電腦, 或說兩個 node, node A 與 node B。他們分別都吃不一樣的 IP, 如下段顯示 192.168.1.181 與 182。然而他們會再共同使用一個虛擬 IP 192.168.1.180。這個虛擬 IP 會綁定在這個叢集 (cluster) 的 Master node。假設 Master 掛掉了, 那麼備援的那一個 node (nodeB) 就會把這個 IP 吃下來。然後該啟動的服務就會在 nodeB 上啟動。

因此, 我們寫的程式連線到 PostgreSQL (或其他服務) 的時候都只要指定 192.168.1.180, node A 或 B 對我們來說就可以把他當作是一體, 不管誰掛掉了 192.168.1.180 就都還是活著。

前置設定

下面的 tutorial 會依照這邊的設定來設置:

網路設定
node A 192.168.1.181
node B 192.168.1.182
Virtual IP 192.168.1.180
磁區的架構

Physical Disk -> DRBD -> LVM -> xfs, mount on /shared/pgdata

在 node1 與 node2 底下, 硬碟各自切一塊 20G 做為 DRBD 的磁碟, 然後在 DRBD 上層再使用 LVM, 切割一個 2G 的 xfs 作為 PostgreSQL 的 pgdata 目錄。

PostgreSQL 的資料目錄

/shared/pgdata

說明

由於下面的篇幅, 幾乎所有會用到的指令都需要以 root 權限執行, 因此指令的部分, 都是以 root 的角色執行的, 省去了 sudo 的部分, 而提示字元也都是 # (代表 root), 如

apt-get install drbd8-utils

安裝與設定

下面是安裝與設定的部分, 我們會先安裝 OS, 接著安裝 DRBD 與設定, 然後是 PostgreSQL, 最後才是 Corosync + Pacemaker 的 cluster 安裝與設定。

安裝 Ubuntu

[Both node A & B]
兩台電腦都安裝 Ubuntu Server 14.04 LTS。在分割磁碟的時候有一個比較要注意的點, 如果 /boot 這個磁區在 LVM 裡面的話, 開機會出現一個錯誤, 其實跳過就沒事了, 但每次開機都會看到。所以依照 Ubuntu 自動分割的方式, 把 /boot 獨自切開來放在 LVM 外面。

以下是我自己切的方式:

Partition Size Usage / Filesystem Mount on
/dev/sda1 550M EFIBoot (boot flag on)
/dev/sda2 1G ext2 /boot
/dev/sda3 100G LVM
/dev/sda3 20G Do not use (這個就是給 DRBD 用的磁區)

LVM 裡面

Name Size Usage / Filesystem Mount on
swap 16G swap
root 84G ext4 /

灌完之後把 dhcp 關掉變成 static IP, 用 root 編輯 /etc/network/interfaces, 兩個 node IP 不同, 下方 address 填入正確的 IP。

iface p4p1 inet static
    address 192.168.1.181
    netmask 255.255.255.0
    gateway 192.168.1.1
    network 192.168.1.0
    dns-nameservers 8.8.8.8

接著重新啟動 interface

ifdown p4p1 && ifup p4p1

最後更新一下全部的套件

apt-get update
apt-get upgrade -y

LVM 設定

[Both node A & B]
由於 /dev/sda4 會是由 DRBD 控管, 因此我們要叫 LVM 不要去理會這個磁區。DRBD 控管的 /dev/sda4 會另外的出現在 /dev/drbd0, 因此我們要告訴 LVM 這個 device 是 LVM。另外, 由於 failover 的關係, 有可能這個 device 會一下子出現一下子又消失, 因此我們要叫 LVM 把 cache 給關掉。所以用 root 權限修改 /etc/lvm/lvm.conf, 找到 filter 與 write_cache_state 兩行, 改成下方的樣子:

# filter = ["a/.*/"]
# filter = ["r|/dev/sda4|", "a|/dev/drbd|", "a/.*/"]
filter = ["a|/dev/sda3|", "a|/dev/drbd|", "r/.*/"]
write_cache_state = 0

上面 lvm.conf 的 filter 設定, Ubuntu 灌好之後是第一行 filter = ["a/.*/"], 後來看說明我把他改成第二行。但是卻在每一次重開機之後打開 pacemaker 都會有 fail 的狀況。後來才發現第二行的設定雖然一開始把 sda4 reject 掉, 但還是會在 /dev/disk/by-id/... 之類的出現 (這個會被 a/.*/ match 到)。因此就要改成第三行, accept /dev/sda3 (原本的 LVM) 與 drbd, 然後 reject 剩下的全部。

最後, 由於 /dev/sda4 有可能已經被之前的設定給記住了, 因此我們要下個指令重新產生 kernel 的 device map

update-initramfs -u

DRBD 安裝與設定

[Both node A & B]
首先先安裝 drbd

apt-get install drbd8-utils

接著我們要來建造一個 resource 檔案, 我們把這個 resource 稱作為 shared, 使用 root 權限編輯 /etc/drbd.d/shared.res

resource shared {
    device /dev/drbd0;
    disk /dev/sda4;
    syncer {
      rate 150M;
      verify-alg md5;
  }
  on nodea {
      address 192.168.1.181:7788;
      meta-disk internal;
  }
  on nodeb {
      address 192.168.1.182:7788;
      meta-disk internal;
  }
}

由於我們會讓 corosync & pacemaker 接管 DRBD 的 service, 所以我們讓他開機起來的時候不要自動開啟這個服務 (這個步驟兩個 node 都要做)

update-rc.d drbd disable

接著我們要讓 DRBD 把我們剛剛設定好的 resource 建立起來 (這個步驟兩個 node 都要做)

如果出現了以下的錯誤

# drbdadm create-md shared
Writing meta data...
md_offset 19999485952
al_offset 19999453184
bm_offset 19998838784

Found LVM2 physical volume signature
    19530752 kB data area apparently used
    19530116 kB left usable by current configuration

Device size would be truncated, which
would corrupt data and result in
'access beyond end of device' errors.
You need to either
   * use external meta data (recommended)
   * shrink that filesystem first
   * zero out the device (destroy the filesystem)
Operation refused.

Command 'drbdmeta 0 v08 /dev/sda4 internal create-md' 
terminated with exit code 40

這代表磁區有殘留的東西, 要先把磁區清掉, 用下列的方式可以清除:

dd if=/dev/zero of=/dev/sda4 bs=1M count=128

然後再執行一次 create-md 的指令。

然後啟動這個 resource (這個步驟兩個 node 都要做)

drbdadm up shared

以上步驟都做好之後, 我們就可以把 drbd 這個服務叫起來 (這個步驟兩個 node 都要做)

service drbd start

[node A only]
在兩個 node 都把他叫起來過後, 就可以再進行一些初始化的動作了, 首先, 讓 node A 變成 primary

drbdadm primary --force shared

這個指令下了之後就會強迫把 node B 同步得跟 node A 一樣, 所以可以看 /proc/drbd 這個檔案觀察他的狀態

# root@nodea:~# cat /proc/drbd
version: 8.4.3 (api:1/proto:86-101)
srcversion: 69A5E1D3708F09A9D055736 
 0: cs:SyncSource ro:Primary/Secondary ds:UpToDate/Inconsistent C r-----
    ns:7262208 nr:0 dw:0 dr:7262936 al:0 bm:443 lo:0 pe:2 ua:0 ap:0 ep:1 wo:f oos:41566728
        [=>..................] sync'ed: 14.9% (40592/47680)Mfinish: 0:17:33 speed: 39,424 (39,032) K/sec

等到他 ds:UpToDate/UpToDate 的時候就是同步完成的時候, 可以繼續進行下一個動作。由於要做很久, 所以當初這個磁區才切這麼小的

建構 DRBD 下的 LVM

[node A only]
現在我們的 /dev/drbd0 已經 ready 了, 我們開始著手把檔案系統建置好。下面步驟只在 node A 執行, 我們在 /dev/drbd0 建立一個 LVM, 並且開一個新的 Volume Group 叫做 sharedvg, 然後開一個新的大小為 2G 的 Logical Volume 叫做 db

pvcreate /dev/drbd0
vgcreate sharedvg /dev/drbd0
lvcreate -L 2G -n db sharedvg

[Both node A & B]
我們把未來要掛載的目錄在兩台機器都先建好, 並且安裝好 xfs 所需要的套件

mkdir -p -m 0700 /shared/pgdata
apt-get install xfsprogs

[node A only]
我們在 node A 上面把剛剛建好的 /dev/sharedvg/db 格式化成 xfs, 並且把他掛載上來

mkfs.xfs -d agcount=8 /dev/sharedvg/db
mount -t xfs -o noatime,nodiratime,attr2 /dev/sharedvg/db /shared/pgdata

測試 DRBD

我們現在就可以作簡單的 DRBD 的測試。目前資料已經掛載在 node A 了, 因此隨便建立一個檔案在 /shared/pgdata 底下

echo "blablabla" >> /shared/pgdata/test

接著我們在 node A 把磁區卸載, 告知 LVM 解除 sharedvg 這個 Volume Group, 並且讓 shared 這個資源在 DRBD 變成 secondary

umount /shared/pgdata
vgchange -a n sharedvg
drbdadm secondary shared

我們在 node B 把磁區載入

drbdadm primary shared
vgchange -a y sharedvg
mount -t xfs -o noatime,nodiratime,attr2 /dev/sharedvg/db /shared/pgdata
cat /shared/pgdata/test

最後一行執行完應該就可以看到我們剛剛建立的 test 檔案, 內容應該為 blablabla。看到就代表兩個磁區已經會自動同步啦! 為了下面的步驟, 我們再用同樣的方式把磁碟從 node B 卸載並且在 node A 上載入, 並且把檔案刪掉恢復原狀

[on node B]

umount /shared/pgdata
vgchange -a n sharedvg
drbdadm secondary shared

[on node A]

drbdadm primary shared
vgchange -a y sharedvg
mount -t xfs -o noatime,nodiratime,attr2 /dev/sharedvg/db /shared/pgdata
rm /shared/pgdata/test

安裝 PostgreSQL

[Both node A & B]

apt-get install postgresql-9.3 postgresql-contrib-9.3

安裝好之後, Ubuntu 會自動建立一個 postgresql cluster 叫做 main, 但他所屬的位置不是我們要的, 因此我們把原本的 main 刪掉再自行建立一個新的在我們想要的位置 “/shared/pgdata

service postgresql stop
pg_dropcluster 9.3 main
pg_createcluster -d /shared/pgdata -s /var/run/postgresql 9.3 main

以上這個步驟在兩台機器都做, 但是 node B 的資料其實不會用到, 因為屆時會用 DRBD 同步, 因此直接把 node B 上的資料砍掉

[on node B]

rm -rf /shared/pgdata/*

最後, 如同 DRBD 一樣, 我們的 PostgreSQL 也會由 pacemaker 控制, 因此我們要取消開機時自動開啟 postgresql

[Both node A & B]

update-rc.d postgresql disable

安裝 Corosync 與 Pacemaker

由於接下來 Corosync 跟 Pacemaker 就會掌管 DRBD 以及 PostgreSQL, 所以我們要先把他們全部卸載下來。

[Both node A & B]
先把 DB 停下來

service postgresql stop

接著再把 DRBD 停掉

umount /shared/pgdata
vgchange -a n sharedvg
service drbd stop

[Both node A & B]
先安裝套件

apt-get install corosync pacemaker

接著設定 /etc/corosync/corosync.conf, 找到 bindnetaddr 設定成正確的 IP

bindnetaddr: 192.168.1.181

開機時啟動 corosync, 把 /etc/default/corosync 裡頭設定為 yes

START=yes

接著啟動 corosync 以及 pacemaker

service corosync start
service pacemaker start

此時你可以用 crm 這個工具來看目前 cluster 的狀態

# crm status
Last updated: Thu Aug  6 16:08:11 2015
Last change: Thu Aug  6 16:08:07 2015 via crmd on nodeb
Stack: corosync
Current DC: nodea (1084752310) - partition with quorum
Version: 1.1.10-42f2063
2 Nodes configured
0 Resources configured

Online: [ nodea nodeb ]

你也可以看目前的設定

# crm configure show
node $id="1084752309" nodea
node $id="1084752310" nodeb
property $id="cib-bootstrap-options" \
        dc-version="1.1.10-42f2063" \
        cluster-infrastructure="corosync"

最後的設定

[on node A]

以下的設定都只要在其中一個 node 設定就可以了, corosync 跟 pacemaker 會自動同步設定。

首先, 我們的架構是一個簡單的 2-node failover, 所以我們把 STONITH 跟 quorum 關掉。STONITH 是 Shoot The Other Node In The Head, 就是當有一個 node 的東西死掉的時候, 把他整個都殺死, 例如重新開機或關機之類的。而在 2-node 的 cluster 裡頭, 必須要把 no-quorum-policy 設成 ignore (請參考 Pacemaker 手冊)。

crm configure property stonith-enabled="false"
crm configure property no-quorum-policy="ignore"

接著, 我們來設定 DRBD。把 drbd “shared” 這個 resource 加到 pacemaker 裡頭, 並且指定他有 multi-state (ms)。下方的設定指出最多只能有一個 master (master-max), 每個 node 最多只能有一個 master (master-node-max), 總共可以有兩個 clone (clone-max), 每個 node 最多一個 clone (clone-node-max)。

crm configure primitive drbd_shared ocf:linbit:drbd params drbd_resource="shared" op start interval="0" timeout="240" op stop interval="0" timeout="120"

crm configure ms ms_drbd_shared drbd_shared meta master-max="1" master-node-max="1" clone-max="2" clone-node-max="1" notify="true"

然後是我們的檔案系統。如果想要知道他實際上會做什麼, 他的程式擺在 /usr/lib/ocf/resource.d/heartbeat/LVM

crm configure primitive shared_lvm ocf:heartbeat:LVM params volgrpname="sharedvg" op start interval="0" timeout="30" op stop interval="0" timeout="30"

crm configure primitive shared_fs ocf:heartbeat:Filesystem params device="/dev/sharedvg/db" directory="/shared/pgdata" options="noatime,nodiratime" fstype="xfs" op start interval="0" timeout="60" op stop interval="0" timeout="120"

然後是 PostgreSQL

crm configure primitive pg_lsb lsb:postgresql op monitor interval="30" timeout="60" op start interval="0" timeout="60" op stop interval="0" timeout="120"

然後是我們的 Virtual IP

crm configure primitive pg_vip ocf:heartbeat:IPaddr2 params ip="192.168.1.180" iflabel="pgvip" op monitor interval="5"

最後, 把他們全部及合在一起。我們把 shared_lvm, shared_fs, pg_lsb 和 pg_vip 合併起來變成一個叫做 HAServer 的群組。這個群組要執行在哪一個 node 上, 取決於 DRBD 在哪一個 node 上是 Master。而最後執行有個順序, 就是要等到 DRBD promote (成 master) 後, HAServer 才執行。

crm configure group HAServer shared_lvm shared_fs pg_lsb pg_vip

crm configure colocation col_drbd_shared inf: HAServer ms_drbd_shared:Master

crm configure order ord_pg inf: ms_drbd_shared:promote HAServer:start

這樣, 就大功告成了!

Failover 測試

先看看目前的狀態

root@nodea:~# crm status
Last updated: Tue Aug 18 14:44:41 2015
Last change: Tue Aug 18 14:43:20 2015 via crm_attribute on nodea
Stack: corosync
Current DC: nodea (1084752310) - partition with quorum
Version: 1.1.10-42f2063
2 Nodes configured
6 Resources configured

Online: [ nodea nodeb ]

 Master/Slave Set: ms_drbd_shared [drbd_shared]
     Masters: [ nodea ]
     Slaves: [ nodeb ]
 Resource Group: HAServer
     shared_lvm (ocf::heartbeat:LVM):   Started nodea 
     shared_fs  (ocf::heartbeat:Filesystem):    Started nodea 
     pg_lsb     (lsb:postgresql):       Started nodea 
     pg_vip     (ocf::heartbeat:IPaddr2):       Started nodea
root@nodea:~#

我們可以看到目前四個 resources (shared_lvm, shared_fs, pg_lsb, pg_vip) 都是在 nodea. 我們簡單的把 nodea 停下來看看

crm node standby nodea

接著再看一下狀態

root@nodeb:~# crm status
Last updated: Tue Aug 18 14:47:13 2015
Last change: Tue Aug 18 14:46:24 2015 via crm_attribute on nodea
Stack: corosync
Current DC: nodea (1084752309) - partition with quorum
Version: 1.1.10-42f2063
2 Nodes configured
6 Resources configured

Node nodea (1084752309): standby
Online: [ nodeb ]

 Master/Slave Set: ms_drbd_shared [drbd_shared]
     Masters: [ nodeb ]
     Stopped: [ nodea ]
 Resource Group: HAServer
     shared_lvm (ocf::heartbeat:LVM):   Started nodeb 
     shared_fs  (ocf::heartbeat:Filesystem):    Started nodeb 
     pg_lsb     (lsb:postgresql):       Started nodeb 
     pg_vip     (ocf::heartbeat:IPaddr2):       Started nodeb

此時所有的節點就已經都順利的改到 nodeb 上去了。接著就可以再做其他更進接的測試如拔網路線, 或是增加 SQL 的東西看看同步是否成功了!

當然做其他測試之前別忘記把狀態回覆, 也就是把剛剛弄成 standby 的 nodea 回復成 online

crm node online nodea

結語

個人覺得這整個東西真的很難設定與了解。也許是我的資質駑鈍, 重灌了不下十幾二十遍, 一直碰到奇怪的問題, 例如 DRBD 跑不起來因為 LVM vgchange 之後有時間差? 或者是 device busy 之類的問題, 還有一開始一直 split-brain… 也許是因為這整件事情我都太不熟悉了, LVM 的 Physical Volume, Volume Group, Logical Volume 什麼的, 都很不熟悉。

當我剛開始在研究測試 HA 設定時, 我並不是參照 PostgreSQL 這個文件設定的, 而是多方參考 Ubuntu Wiki 與 Pacemaker 的文件。當時是系統的 LVM 多切一個 Logical Volume 給 DRBD 作 block device, 上面是 ext3。可是這樣的設定不知道為什麼, 拔掉網路線或者是 reboot, 回來的時候就會造成 DRBD split-brian。然而如果是 service pacemaker restart 之類的就不會有這個問題。當時是改了 DRBD 的設定 (/etc/drbd.d/global_common.conf) 讓他 auto-recover:

common {
    handlers {
        fence-peer "/usr/lib/drbd/crm-fence-peer.sh";
        after-resync-target "/usr/lib/drbd/crm-unfence-peer.sh";
    }
    disk {
        fencing resource-only;
    }
 
    net {
        after-sb-0pri discard-zero-changes;
        after-sb-1pri discard-secondary;
        after-sb-2pri disconnect;
    }
}

後來我整個重新根據 High Availability with PostgreSQL and Pacemaker 這篇文件重新設定, 就沒有 split-brain 這個問題了。

Pacemaker 還有很多很複雜的設定方式, 目前也只是大概了解一點點而已, 如果要做更複雜的設定如 active/active 或是多於兩個節點的設定, 就要再繼續做更多研究了。

Reference