CentOS 7 で DRBD/Pacemaker/Corosync で High Availability NFS

やってみました。

## Vagrant

sv01/sv02 が NFS をマウントするクライアントで、nfs01/nfs02 が NFS サーバです。

```ruby
Vagrant.configure(2) do |config|
  config.vm.box = "bento/centos-7.2"
  config.ssh.insert_key = false
  config.vm.define "sv01" do |config|
    config.vm.hostname = "sv01"
    config.vm.network :private_network, ip: "192.168.33.11", virtualbox__intnet: "ha-nfs"
  end
  config.vm.define "sv02" do |config|
    config.vm.hostname = "sv02"
    config.vm.network :private_network, ip: "192.168.33.12", virtualbox__intnet: "ha-nfs"
  end
  config.vm.define "nfs01" do |config|
    config.vm.hostname = "nfs01"
    config.vm.network :private_network, ip: "192.168.33.21", virtualbox__intnet: "ha-nfs"
  end
  config.vm.define "nfs02" do |config|
    config.vm.hostname = "nfs02"
    config.vm.network :private_network, ip: "192.168.33.22", virtualbox__intnet: "ha-nfs"
  end
  config.vm.provider :virtualbox do |v|
    v.linked_clone = true
  end
end
```

`vagrant up` の後、名前で解決できるように hosts を作成しておきます。

```sh
sudo tee /etc/hosts <<EOS
127.0.0.1     localhost localhost.localdomain localhost4 localhost4.localdomain4
::1           localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.33.11 sv01
192.168.33.12 sv02
192.168.33.21 nfs01
192.168.33.22 nfs02
192.168.33.20 nfsvip
EOS
```

nfsvip は Floating IP で付ける予定の NFS サーバのアクティブ側の仮想アドレスです。

## パッケージのインストール

必要なパッケージを yum でインストールします。DRBD は ELrepo からインストールします。

```sh
# [nfs01/nfs02]
sudo yum -y install http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
sudo yum -y install kmod-drbd84 pacemaker corosync resource-agents pcs nfs-utils
```

## DRBD

DRBD の設定を行います。Vagrant 環境での検証なので簡単化のためにループバックデバイスを使います。

```sh
# [nfs01/nfs02]
sudo fallocate -l 1G /var/opt/drbddisk
sudo losetup /dev/loop0 /var/opt/drbddisk

sudo tee /etc/drbd.d/nfs.res <<EOS
resource nfs
{
    protocol C;
    device /dev/drbd0;
    meta-disk internal;

    on nfs01 {
        address 192.168.33.21:7791;
        disk /dev/loop0;
    }

    on nfs02 {
        address 192.168.33.22:7791;
        disk /dev/loop0;
    }
}
EOS
```

メタデータを作成して開始します。

```sh
# [nfs01/nfs02]
sudo drbdadm create-md nfs
sudo systemctl start drbd.service
sudo systemctl status drbd.service
```

開始直後なので Inconsistent になっています。

```sh
# [nfs01/nfs02]
cat /proc/drbd
# (...snip...)
# 0: cs:Connected ro:Secondary/Secondary ds:Inconsistent/Inconsistent C r-----
#    ns:0 nr:0 dw:0 dr:0 al:8 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:1048508
```

nfs01 をプライマリにします。

```sh
# [nfs01]
sudo drbdadm primary nfs --force
```

`--force` したので同期も開始します。

```sh
# [nfs01]
cat /proc/drbd
# (...snip...)
# 0: cs:SyncSource ro:Primary/Secondary ds:UpToDate/Inconsistent C r-----
#    ns:1720 nr:0 dw:0 dr:2632 al:8 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:1046788
#        [>....................] sync'ed:  0.4% (1046788/1048508)K
#        finish: 0:08:43 speed: 1,720 (1,720) K/sec
```

DRBD のブロックデバイスを xfs でフォーマットします。

```sh
# [nfs01]
sudo mkfs.xfs /dev/drbd/by-res/nfs
```

マウントポイントを作成してマウントします。

```sh
# [nfs01]
sudo mkdir -p /drbd/nfs
sudo mount /dev/drbd/by-res/nfs /drbd/nfs
```

必要なディレクトリを作成します。
それぞれ exportfs するディレクトリと `/var/lib/nfs` に `mount --bind` するディレクトリです（`ocf:heartbeat:nfsserver` が勝手にやります）。

```sh
# [nfs01]
sudo mkdir -p /drbd/nfs/nfsroot
sudo mkdir -p /drbd/nfs/var-lib-nfs
```

アンマウントしてセカンダリに戻します。

```sh
# [nfs01]
sudo umount /drbd/nfs
sudo drbdadm secondary nfs
```

次は nfs02 で作業します。もうそろそろ同期も終わっているでしょう。

```sh
# [nfs02]
cat /proc/drbd
# (...snip...)
# 0: cs:Connected ro:Secondary/Secondary ds:UpToDate/UpToDate C r-----
#    ns:728889 nr:0 dw:1054333 dr:725379 al:12 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:f oos:0
```

DRBD リソースをプライマリにします。

```sh
# [nfs02]
sudo drbdadm primary nfs
```

マウントポイントを作成してマウントします。

```sh
# [nfs02]
sudo mkdir -p /drbd/nfs
sudo mount /dev/drbd/by-res/nfs /drbd/nfs
```

nfs01 で作成したディレクトリが見えていることを確認します。

```sh
# [nfs02]
ls /drbd/nfs
# nfsroot  var-lib-nfs
```

アンマウントしてセカンダリに戻ります。

```sh
# [nfs02]
sudo umount /drbd/nfs
sudo drbdadm secondary nfs
```

DRBD の作業は終わりなので停止しておきます（DRBD は Pacemaker から開始される）。

```sh
# [nfs01/nfs02]
sudo systemctl stop drbd.service
sudo systemctl status drbd.service
```

## Corosync

Corosync の設定ファイルを作成します。下記のような感じで作成します。２ノードなので quorum で `two_node: 1` が必要です。

```sh
# [nfs01/nfs02]
sudo tee /etc/corosync/corosync.conf <<EOS
totem {
    version: 2
    crypto_cipher: none
    crypto_hash: none
    token: 4000
    rrp_mode: active
    transport: udpu

    interface {
        ringnumber: 0
        bindnetaddr: 192.168.33.0
        mcastaddr: 239.255.1.1
        mcastport: 5405
    }
}

logging {
    timestamp: on
    debug: off
    to_stderr: no
    to_syslog: no
    to_logfile: yes
    logfile: /var/log/cluster/corosync.log
}

nodelist {
    node {
        ring0_addr: 192.168.33.21
        nodeid: 1
    }
    node {
        ring0_addr: 192.168.33.22
        nodeid: 2
    }
}

quorum {
    provider: corosync_votequorum
    two_node: 1
}
EOS
```

開始します。pacemaker を開始すると corosync も開始するので pacemaker だけで良いです。

```sh
# [nfs01/nfs02]
sudo systemctl start pacemaker.service
sudo systemctl status pacemaker.service
```

両方のノードがオンラインになっていることを確認します。

```sh
# [nfs01]
sudo crm_mon -1
# (...snip...)
#
# Online: [ nfs01 nfs02 ]
#
# (...snip...)
```

## Pacemaker

Pacemaker のリソース定義を行います。

まずはプロパティやリソースデフォルトを設定します。

```sh
# [nfs01]
sudo pcs property set stonith-enabled="false"
sudo pcs property set no-quorum-policy="ignore"
sudo pcs property set start-failure-is-fatal="false"
sudo pcs resource defaults migration-threshold="5"
sudo pcs resource defaults resource-stickiness="INFINITY"
sudo pcs resource defaults failure-timeout="3600s"
```

CIB をファイルに出力します。

```sh
# [nfs01]
sudo pcs cluster cib output.cib
```

出力した CIB ファイルを対象にリソースを作成していきます。

まずは DRBD のリソース。これは Master/Slave リソースです。

```sh
# [nfs01]
sudo pcs -f output.cib resource create drbd ocf:linbit:drbd \
  drbd_resource="nfs" drbdconf="/etc/drbd.conf" \
    op start timeout="240s" on-fail="restart" \
    op stop  timeout="100s" on-fail="block" \
    op monitor interval="20s" timeout="20s" start-delay="1m" on-fail="restart" \
      --master meta notify="true" 
```

DRBD デバイスのマウント、仮想アドレスの付与、NFS サーバ、exportfs のリソースを作成します。これらは通常のリソースです。

```sh
# [nfs01]
sudo pcs -f output.cib resource create fs ocf:heartbeat:Filesystem \
  device="/dev/drbd/by-res/nfs" directory="/drbd/nfs" fstype="xfs" \
    op start timeout="60s" on-fail="restart" \
    op stop  timeout="60s" on-fail="block" \
    op monitor interval="10s" timeout="60s" on-fail="restart"

sudo pcs -f output.cib resource create vip ocf:heartbeat:IPaddr2 \
  ip="192.168.33.20" cidr_netmask="24" nic="enp0s8" \
    op start timeout="20s" on-fail="restart" \
    op stop  timeout="20s" on-fail="block" \
    op monitor interval="10s" timeout="20s" on-fail="restart"

sudo pcs -f output.cib resource create nfsserver ocf:heartbeat:nfsserver \
  nfs_shared_infodir="/drbd/nfs/var-lib-nfs" \
    op start timeout="20s" \
    op stop  timeout="20s" \
    op monitor interval="10s" timeout="10s" on-fail="restart" start-delay="20s"

sudo pcs -f output.cib resource create exportfs ocf:heartbeat:exportfs \
  clientspec="192.168.33.0/24" options="rw,no_root_squash" directory="/drbd/nfs/nfsroot" fsid="root"
```

グループにします。

```sh
# [nfs01]
sudo pcs -f output.cib resource group add group-nfs fs vip nfsserver exportfs
```

drbd リソースと↑のグループとの制約を作成します。↑のグループは drbd のマスターと同じノードでなければならないし、drbd のプロモートが先に行われる必要があります。

```sh
# [nfs01]
sudo pcs -f output.cib constraint colocation add master drbd-master with group-nfs INFINITY
sudo pcs -f output.cib constraint order promote drbd-master then start group-nfs
```

CIB ファイルの変更を適用します。

```sh
# [nfs01]
sudo pcs cluster cib-push output.cib
```

ステータスを表示させてそれっぽくなっていれば OK です。

```sh
# [nfs01]
sudo pcs status
# (...snip...)
#
# Online: [ nfs01 nfs02 ]
# 
# Full list of resources:
# 
#  Master/Slave Set: drbd-master [drbd]
#      Masters: [ nfs01 ]
#      Slaves: [ nfs02 ]
#  Resource Group: group-nfs
#      fs (ocf::heartbeat:Filesystem):    Started nfs01
#      vip        (ocf::heartbeat:IPaddr2):       Started nfs01
#      nfsserver  (ocf::heartbeat:nfsserver):     Started nfs01
#      exportfs   (ocf::heartbeat:exportfs):      Started nfs01
# 
# (...snip...)
```

## クライアントからマウント

sv01 からマウントして適当にファイルを書いてみます。

```sh
# [sv01]
sudo mount -t nfs nfsvip:/drbd/nfs/nfsroot /mnt -o rw,rsize=8192,wsize=8192,soft,intr,timeo=20,retrans=3
uname -n | sudo tee /mnt/$(uname -n)
```

sv02 からもマウントして適当にファイルを書いてみます。

```sh
# [sv02]
sudo mount -t nfs nfsvip:/drbd/nfs/nfsroot /mnt -o rw,rsize=8192,wsize=8192,soft,intr,timeo=20,retrans=3
uname -n | sudo tee /mnt/$(uname -n)
```

相互に読めることを確認します。

```sh
# [sv01]
cat /mnt/sv02

# [sv02]
cat /mnt/sv01
```

## 障害発生

NFS のノードで障害が発生したときの動作を確認します。

sv01 でマウント先のファイルに連続して書き込みまくります。

```sh
# [sv01]
sudo touch /mnt/date.txt
sudo chown $USER: /mnt/date.txt
while sleep 0.1; do date 1>&2; date > /mnt/date.txt; done
```

おもむろにアクティブ側のノードを強制終了させます。

```sh
# [nfs01]
sudo halt -f
```

20秒ぐらい書き込みが止まった後、I/O error になって書き込みが再開しました。

```sh
# [sv01]
# (...snip...)
# Tue Jun  7 01:47:23 UTC 2016
# Tue Jun  7 01:47:23 UTC 2016
# -bash: /mnt/date.txt: Input/output error
# Tue Jun  7 01:47:40 UTC 2016
# Tue Jun  7 01:47:40 UTC 2016
# (...snip...)
```

実際の時間や動作は Pacemaker/Corosync の設定や NFS のマウントオプションによって変わります。

なお、DRBD のディスクにループバックデバイスを使用しているのと、pacemaker の自動起動を設定していないので、ノードの再起動後は手動でループバックデバイスの作成と pacemaker の起動を行う必要があります。

```sh
# [nfs01]
sudo losetup /dev/loop0 /var/opt/drbddisk
sudo systemctl start pacemaker
```

## ノード起動時の monitor で NFS Server が開始してしまう問題

NFS のノードを再起動して pacemaker を起動したときにですが、起動したノードの側で下記のように NFS server が起動した旨のログが記録されました。

```
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Status: rpcbind
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Status: nfs-mountd
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Starting NFS server ...
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Start: rpcbind i: 10
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Start: v3locking: 0
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Start: nfs-mountd i: 10
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Start: nfs-idmapd i: 10
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Start: rpc-statd i: 10
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: executing sm-notify
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: NFS server started
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Status: nfs-idmapd
nfsserver(nfsserver)[3486]: 2016/06/07_01:51:31 INFO: Status: rpc-statd
```

この後直ぐに停止されています。

```
nfsserver(nfsserver)[3782]: 2016/06/07_01:51:32 INFO: Stopping NFS server ...
nfsserver(nfsserver)[3782]: 2016/06/07_01:51:32 INFO: Stop: threads
nfsserver(nfsserver)[3782]: 2016/06/07_01:51:32 INFO: Stop: rpc-statd
nfsserver(nfsserver)[3782]: 2016/06/07_01:51:32 INFO: Stop: nfs-idmapd
nfsserver(nfsserver)[3782]: 2016/06/07_01:51:32 INFO: Stop: nfs-mountd
nfsserver(nfsserver)[3782]: 2016/06/07_01:51:32 INFO: Stop: rpcbind
nfsserver(nfsserver)[3782]: 2016/06/07_01:51:32 INFO: NFS server stopped
```

アクティブ側（DC側でもある）では下記のように nfsserver が両方のノードで開始している旨のログが記録されていました。

```
Jun 07 01:51:32 [19992] nfs02    pengine:    error: native_create_actions:  Resource nfsserver (ocf::nfsserver) is active on 2 nodes attempting recovery
Jun 07 01:51:32 [19992] nfs02    pengine:  warning: native_create_actions:  See http://clusterlabs.org/wiki/FAQ#Resource_is_Too_Active for more information.
```

この後に両方のノードで NFS server が停止してアクティブ側でのみ再開しているのですが、このためクライアントからは一時的に書き込みが失敗しています。

```
Tue Jun  7 01:51:32 UTC 2016
Tue Jun  7 01:51:32 UTC 2016
-bash: /mnt/date.txt: Permission denied
Tue Jun  7 01:51:32 UTC 2016
-bash: /mnt/date.txt: Permission denied
Tue Jun  7 01:51:32 UTC 2016
Tue Jun  7 01:51:32 UTC 2016
Tue Jun  7 01:51:39 UTC 2016
Tue Jun  7 01:51:39 UTC 2016
```

nfsserver の ocf ファイル (`/usr/lib/ocf/resource.d/heartbeat/nfsserver`) を見た感じ、

```sh
nfsserver_monitor ()
{
        # Skip trying to start processes once before failing
        # when run from nfsserver_start ()
        if [ "$1" == "fromstart" ]; then
                ocf_log info "fromstart"
                fromstart=1
        else
                tries=1
        fi
:
                ocf_log info "Status: rpcbind"
                rpcinfo &> /dev/null
                rc=$?
                if [ "$rc" -ne "0" ]; then
                        if [ ! "$fromstart" ] && [ "$tries" -gt "0" ]; then
                                nfsserver_start frommonitor
                                rc=$?
                                let tries=$tries-1
                        fi
                        if [ "$rc" -ne "0" ]; then
                                ocf_exit_reason "rpcbind is not running"
                                return $OCF_NOT_RUNNING
                        fi
                fi
:
}
```

monitor でリソースの開始が行われてしまうようです・・・？

ocf は resource-agents パッケージに含まれていますが、アップストリームではこのような内容にはなっていませんでした。

CentOS 7 の resource-agents の SRPM を見てみたところ bz1304370-nfsserver-fix-systemd-status-detection.patch でこの変更が行われていましたが・・・

[RHBZ#1304370](https://bugzilla.redhat.com//show_bug.cgi?id=1304370)

非公開のようです。

monitor でリソースが開始されるのはなにかの間違いだとしか思えません。とりあえず ocf で `tries=0` にすれば monitor からリソースの開始は行われなくなるので、下記のようにパッチすることにしました。

```sh
sudo cp -a /usr/lib/ocf/resource.d/heartbeat/nfsserver{,.orig}
sudo sed 's/tries=1/tries=0/g' -i /usr/lib/ocf/resource.d/heartbeat/nfsserver
diff -u /usr/lib/ocf/resource.d/heartbeat/nfsserver{.orig,}
```

```diff
--- /usr/lib/ocf/resource.d/heartbeat/nfsserver.orig    2016-05-12 14:02:40.000000000 +0000
+++ /usr/lib/ocf/resource.d/heartbeat/nfsserver 2016-06-07 02:04:09.825775009 +0000
@@ -364,7 +364,7 @@
                ocf_log info "fromstart"
                fromstart=1
        else
-               tries=1
+               tries=0
        fi
 
        # systemd
```

## nfsserver はクローンリソースでも良い？

下記だと nfsserver は systemd のリソースで定義してクローンリソースにしていました。

https://www.suse.com/documentation/sle-ha-12/book_sleha_techguides/data/sec_ha_quick_nfs_resources.html#sec_ha_quick_nfs_resources_nfsserver

それもアリなのかな？、試してみました。まずは DRBD のマウントポイントを exportfs する構成で、この場合は exportfs もクローンリソースにできます。

```sh
sudo pcs resource delete exportfs
sudo pcs resource delete nfsserver
sudo pcs resource cleanup exportfs
sudo pcs resource cleanup nfsserver

sudo pcs resource create nfsserver systemd:nfs-server \
  op monitor interval="30s"

sudo pcs resource create exportfs ocf:heartbeat:exportfs \
  clientspec="192.168.33.0/24" options="rw,no_root_squash" directory="/drbd/nfs" fsid="root" \
  op monitor interval="30s"

sudo pcs resource group add group-nfsserver nfsserver exportfs
sudo pcs resource clone group-nfsserver

sudo pcs constraint colocation add group-nfs with group-nfsserver-clone INFINITY
sudo pcs constraint order start group-nfsserver-clone then start group-nfs
```

次に DRBD のマウントポイントのサブディレクトリを exportfs する構成で、この場合は exportfs は通常のリソースで fs とか vip とかと同じグループにします。

```sh
sudo pcs resource delete exportfs
sudo pcs resource delete nfsserver
sudo pcs resource cleanup exportfs
sudo pcs resource cleanup nfsserver

sudo pcs resource create nfsserver systemd:nfs-server \
  op monitor interval="30s" --clone

sudo pcs resource create exportfs ocf:heartbeat:exportfs \
  clientspec="192.168.33.0/24" options="rw,no_root_squash" directory="/drbd/nfs/nfsroot" fsid="root" \
  op monitor interval="30s"

# fs vip exportfs
sudo pcs resource group add group-nfs exportfs

sudo pcs constraint colocation add group-nfs with nfsserver-clone INFINITY
sudo pcs constraint order start nfsserver-clone then start group-nfs
```

見た感じ、どちらの方法でも問題無さそうでした。`/var/lib/nfs/` って同期しなくてもいいものなの？

`/var/lib/nfs/` を同期していないとフェイルオーバーしたときにクライアントでマウントしなおす必要があると思ってたんだけどそんなことも無さそう。tcp/udp とか nfs v3/v4 とかによって異なるのだろうか？