Mail Serverなどに供給するFilesystemは安定性と冗長性が重要である。 しかし、一般にFilesystemの冗長化は非常に難しく、なかなかFreeに利用できる良い実装はない。
Linuxを利用する場合は、DRBDやlsyncd/rsyncを用いた同期が可能である。
しかし、FreeBSDを利用する場合、そのために用いる手法としては、実質HASTしかない。また、このHASTはNetBSDやOpenBSDには存在しない。
今時、MailServerを構築する際には、Lock問題が発生しないように、Maildirを利用しHome Directoryにファイル単位で受信したメールを保存するのが一般的である。この時、SMTP Serverは冗長性を確保したいので、Home DirectoryはNFSでshareしたいという要望がある。
以上より、FreeBSD 10.1を用いてHASTでHome Directoryを冗長化することにした。
当初 NAS4Free で行こうと思ったのだが、以下の理由で FreeBSD + HAST を用いてNFS Server を組むことにした。
仮想基盤の Storage が NAS4Free で RAID-Z2 であることと、HAST で筐体間冗長を構成することを考慮し、NFS Server 単体では冗長構成は構築しない。もし、冗長構成を取るなら
する手も考えられるが、今回は容量も50G程度なので、UFS(FFS)を利用する。
なお、当然のことだが、HASTの冗長は「片側を書き換えたら即逆側も書き換わる」冗長なので、いわゆるBackupではない。冗長である。なので、当然Backupは定期的に取るに越したことはない。NAS4Free側でsnapshotを取るのも悪くないが、やはりちゃんとbackupを取るべきであろう。
なお、UFSでのsnapshotの取得については、ここを参照のこと
今回は、以下の構成で構築する
諸元
OS | FreeBSD 10.1 | |
vCPU | 1 | |
Memory | 1024MB | |
HDD | Boot | 20GB |
HDD | Export | 50GB |
NIC | 2 |
とりあえず、普通にInstall
ada0 | GPT | 備考 | |
---|---|---|---|
ada0p1 | Freebsd-boot | 512KB | FreeBSD Boot code上の制限 |
ada0p2 | Freebsd-ufs | 19GB | |
ada0p3 | Freebsd-swap | 1GB |
proc /proc procfs rw 0 0
freebsd-update fetch
freebsd-update install
portsnap fetch
portsnap update
pkg install xe-guest-utilities
echo 'xenguest_enable=“YES”' » /etc/rc.conf.local
ここまで出来たら、shutdownして、VMをCopyし、2台目の設定を実施する。
HASTが動作するためには、GEOM_GATE(geom_gate.ko)がkernelにlinkされている必要がある。 通常は、hastdが起動されるタイミングでLKMとして読み込まれる(kernelにlinkされる)が、staticにkernelにlinkするならkernelを再構築する。その際 OPTIONS GEOM_GATE を追加すること
HASTの設定は、/etc/hast.confに記載する。
# # HAST: Highly Available STorage configuration. # # General configuration listen tcp4://0.0.0.0:8457 replication fullsync compression lzf timeout 10 on nfs001 { listen tcp4://10.1.101.3:8457 } on nfs002 { listen tcp4://10.1.101.4:8457 } # node configuration resource home { local /dev/ada1 on nfs001 { remote tcp4://10.1.101.4:8457 } on nfs002 { remote tcp4://10.1.101.3:8457 } }
hastctl create home
を実行service hastd onestart
を実行hastctl role primary home
hastctl role secondary home
hastctl status home
hastctl list -d home
を実行すると、詳細な情報が出力される。この時dirtyが0であることを確認するnewfs -O2 -U -j /dev/hast/home
mkdir /home
mount /dev/hast/home /home
echo 'hastd_enable=“YES”' » /etc/rc.conf.local
HASTの制御コマンドは以下
hastctl status [resource]
: HAST resource [resource] の状態を見るhastctl status all
: HASTに登録されている全ての[resource]の状態を見るhastctl role primary [resource]
[resource]をPrimaryにするhastctl role secondary [resource]
[resource]をSecondaryにするhastctl list [resource]
: [resource]の詳細情報を表示するhastで共有しているDiskの状態がおかしくなったので、以下を実施する
とりあえず、これが一番手っ取り早い。
split-brain状態になっていることを検出し、勝手にhastctl createするようにする方策を考えなければならない
ここまでで、HAST deviceは作成できた。
ここからは、CARPとdevdを利用して、Failoverの設定を行う
echo 'carp_load=“YES”' » /boot/loader.conf
を実行kldload carp
を実行するdevide CARP
を追加するifconfig_xn1_alias0=“vhid 2 advskew 10 pass UltraSecret alias xxx.xxx.xxx.xxx/32”
ifconfig_xn1_alias0=“vhid 2 advskew 10 pass UltraSecret alias xxx.xxx.xxx.xxx/32”
これでCARPの設定は終了。
CARPのI/FをBACKUPに切り替えるには、ifconfig xn1 vhid 2 state backup
でOK
ifconfig state backup で CARP の state を変化させた場合、相手側の I/F の state は変化しない。
従って、host-1 でifconfig xn1 vhid 2 state backup を実行した場合、host-2 で ifconfig xn1 vhid 2 state master を実行すること
VHIDは、KeepalivedやVRRPなどのVHIDと衝突しないように設定する必要があるので、注意
FreeBSDのCARPはVRRPと異なり、自分がBACKUPの時でも共有IPアドレスが自身に割り当てられているものとして扱われる。つまり、host-1がCARP MASTER、host-2がCARP BACKUPであって、共有IP AddressがAddr(s)であるとすると、Addr(s)への通信に返事を返すのはhost-1のみであるが、host-2のInterface Address TableにもAddr(s)が載る。
前項のCARPまで設定が終了すれば、HASTで同期しているFilesystemを公開する準備が整っている。 この時利用できるProtocolとしては、SAMBAやNFSがある。iSCSIは動作するか微妙。
そこで、Interfaceの状態が変化した場合にhastのroleを変化させる設定を行う。 この設定は、devdを利用する。
まず、/etc/dev.confの末尾に、以下を追記する。
notify 100 { match "system" "CARP"; match "subsystem" "[0-9]+@[0-9a-z]+"; match "type" "(MASTER|BACKUP|INIT)"; action "/usr/local/sbin/carp-hast-switch $subsystem $type"; };
その後、service devd restart
を実行する
次に、/usr/local/sbin/carp-hast-switchを作成する。
#! /bin/sh # # carp-hast-switch: shell script for change hast role when carp # status is changed. # # Original script by Freddie Cash <fjwcash@gmail.com> # Modified by Michael W. Lucas <mwlucas@BlackHelicopters.org> # and Viktor Petersson <vpetersson@wireload.net> # and HEO SeonMeyong <seirios@seirios.org> # Last modified 2015/11/10 HEO SeonMeyong <seirios@seirios.org> # ***WARNINGS*** # Need net.inet.carp.preempt=1 and same of advskew on carp. # Currently HAST device must formatted by UFS # ZFS code is impremented but not checked. # This script is assumed to match the ZFS pool name and HAST resource name ############################################################################## # Setting Variables and parse Arguments. #DEBUG=1 SYSLOG_FACILITY="user.notice" SYSLOG_TAG="carp-hast" IF=${1%@*} VHID=${1#*@} ACTION=$2 # Work around for boot time. devd execute this script before start hastd. [ ! `/bin/pgrep hastd` ] && exit case "${ACTION}" in MASTER|BACKUP|INIT) /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "State Changed. I/F: ${IF} VHID: ${VHID} state: ${ACTION}" ;; *) /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: ${ACTION} is not yet implemented" exit 1 ;; esac ############################################################################## # Get resources. HASTDEV=`/sbin/hastctl dump all | /usr/bin/grep resource | /usr/bin/sed -e 's/^.*:\ *//'` [ "x"$DEBUG != "x" ] && echo "HASTDEV = ${HASTDEV}" [ -z "${HASTDEV}" ] && exit 0 # no hast device. # get all carp interfaces ifs=`/sbin/ifconfig -l` for i in ${ifs}; do no_of_carp=`/sbin/ifconfig $i | /usr/bin/grep -c carp` [ "x"$DEBUG != "x" ] && echo "Interface $i has ${no_of_carp} CARP configuration" [ ${no_of_carp} != "0" ] && carps="${carps} $i" done [ "x"$DEBUG != "x" ] && echo "CARP I/F = ${carps}" [ -z "${carps}" ] && exit 0 # no carp I/F. PREEMPTION=`/sbin/sysctl net.inet.carp.preempt | /usr/bin/awk '{print $2}'` [ "x"$DEBUG != "x" ] && echo "CARP preemption = ${PREEMPTION}" [ ${PREEMPTION} != "1" ] && exit 0 # No carp preemption. May cause failure. ############################################################################## # Main. case "${ACTION}" in "MASTER") # make sure all carp is master. if [ -n "${carps}" ]; then for if in ${carps}; do vhid=`/sbin/ifconfig ${if} | /usr/bin/grep carp | /usr/bin/awk '{print $3 " " $4}'` /sbin/ifconfig ${if} ${vhid} state master done fi for disk in ${HASTDEV}; do # If there is secondary worker process, it means that remote primary process is # still running. We have to wait for it to terminate. for i in `jot 30`; do /bin/pgrep -f "hastd: ${disk} \(secondary\)" >/dev/null 2>&1 || break sleep 1 done if pgrep -f "hastd: ${disk} \(secondary\)" >/dev/null 2>&1; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: Secondary process for resource ${disk} is still running after 30 seconds." exit 1 fi /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "Role for HAST resources ${disk} switched to primary." /sbin/hastctl role primary ${disk} if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: Unable to change role to primary for resource ${disk}." exit 1 fi done # Wait for the /dev/hast/* devices to appear for disk in ${HASTDEV}; do for loop in $( jot 120 ); do [ -c "/dev/hast/${disk}" ] && break sleep 0.5 done if [ ! -c "/dev/hast/${disk}" ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: GEOM provider /dev/hast/${disk} did not appear." exit 1 fi FSFMT=`file -bs /dev/hast/${disk}` if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: /dev/hast/${disk} cannot define FS format." exit 1 fi FSFMT=`echo ${FSFMT} | /usr/bin/awk '{print $1 " " $2}'` case ${FSFMT} in "Unix Fast") /sbin/fsck -y -t ufs /dev/hast/${disk} >/dev/null 2>&1 if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: UFS fsck /dev/hast/${disk} failed." exit 1 fi /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "fsck /dev/hast/${disk} finished." e_code=`/sbin/mount /dev/hast/${disk} /hast/${disk} 2>&1` if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: UFS mount for resource ${disk} failed: ${e_code}." exit 1 fi /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "UFS /dev/hast/${disk} is mounted." ;; *) # If not UFS, Assume that filesystem is ZFS. e_code=`/sbin/zpool import -f ${disk} 2>&1` if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: ZFS import for resource ${disk} failed: ${e_code}." exit 1 fi /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "ZFS ${disk} is imported." ;; esac done # NFS Service ( run nfsd and mountd. ) /usr/sbin/service rpcbind restart /usr/sbin/service statd restart /usr/sbin/service lockd restart /usr/sbin/service nfsd restart /usr/sbin/service mountd restart /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "NFS started." # iSCSI service # /etc/rc.d/iscsi_target start ;; "BACKUP"|"INIT") # make sure all carp is backup if [ -n "${carps}" ]; then for if in ${carps}; do vhid=`/sbin/ifconfig ${if} | /usr/bin/grep carp | /usr/bin/awk '{print $3 " " $4}'` /sbin/ifconfig ${if} ${vhid} state backup done fi # stop iSCSI service # /etc/rc.d/iscsi_target forcestop # NFS Service ( stop nfsd and mountd. ) /usr/sbin/service mountd forcestop /usr/sbin/service nfsd forcestop /usr/sbin/service lockd forcestop /usr/sbin/service statd forcestop /usr/sbin/service rpcbind forcestop /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "NFS stoped." # Switch roles for the HAST resources for disk in ${HASTDEV}; do sleep 1 # First of all, set hast status to secondary. # Do not touch device before hast status become secondary. # This work protects to become split-brain status. /sbin/hastctl role secondary ${disk} 2>&1 if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: Unable to switch role to secondary for resource ${disk}." exit 1 fi /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "Role switched to secondary for resource ${disk}." # Unmount UFS for i in `/sbin/mount | /usr/bin/awk '{print $1 ":" substr($4,2)}' | /usr/bin/grep hast`; do if [ ${i%:*} = "/dev/hast/${disk}" ]; then if [ ${i#*:} = "ufs," ]; then e_code=`/sbin/umount /hast/${disk} 2>&1` if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: UFS unmount of resource ${disk} failed: ${e_code}" exit 1 fi /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "UFS /dev/hast/${disk} is unmounted" fi fi done # Export ZFS zpool list | egrep -q "^${disk} " if [ $? -eq 0 ]; then # Force export ZFS pool. e_code=`/sbin/zpool export -f ${disk} 2>&1` if [ $? -ne 0 ]; then /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "FATAL: ZFS export of resource ${disk} failed: ${e_code}." exit 1 fi /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "ZFS ${disk} is exported." fi done ;; *) /usr/bin/logger -p ${SYSLOG_FACILITY} -t ${SYSLOG_TAG} \ "Unknown CARP state. ${STATE}" ;; esac exit 0 ############################################################################## # Local Variables: # coding: utf-8 # mode: sh # sh-basic-offset: 4 # sh-indentation: 4 # End:
このファイルは「shell script」なので、chmod 750 carp-hast-switch
とchown root:wheel carp-hast-switch
を実行しておくこと。
iscsi target関連の設定はコメントアウトしている。(2015/04/14)
FilesystemをmountするDirectoryが/hastにhardcodeされている。(2015/04/14)
ZFSのcodeは組み込んであるが試験をしていない。また、zfsのpool名がhastのresource名と同じであることを仮定している
最後に、/hastを作成する。mkdir /hast
これで、hastがprimaryの時には、/hast/homeが作成される。
NFSサーバーの設定を行う。
rpc_lockd_enable="YES" rpc_statd_enable="YES" rpcbind_enable="YES" portmap_enable="YES" mountd_enable="YES" mountd_flags="-r" nfs_server_enable="YES" nfs_client_enable="YES"
/hast/home -network xxx.xxx.xxx.xxx/xx -maproot=root
今回はHome Directoryをshareすることを前提としているので、-maproot=root とか -maproot=0:0を記載しているが、root権限のファイルがNFSでshareされること自体は十分に検討する必要があるので注意すること
ここまで設定すれば NFS Server が Filesystem を export できるようになっている。 そこで以下のコマンドを実行する
# kill -HUP `cat /var/run/mountd.pid` # showmount -e /hast/home xxx.xxx.xxx.xxx/xx #
NFS Client側では、/etc/fstabに必要な設定を投入すれば、FilesystemをNFSでmountできる。
host-carp:/hast/home /home nfs rw,noinet6,tcp,soft,noatime,nfsv3,bg,wsize=32768,rsize=32768 0 0 #host-carp:/hast/home /home nfs rw,noinet6,tcp,hard,intr,noatime,nfsv3,bg,wsize=32768,rsize=32768 0 0
/usrなどの「必須filesystem」をmountする場合には、hard,intrを、/homeのような「システムに必須ではない」ファイルシステムはsoftを指定すると良い。
hard,intr : NFSサーバーが落ちている時にファイルシステムにアクセスすると、アクセスができるまで停止する。signalを送ることで割り込み(intr)がかかりエラーが帰る
soft : NFSサーバーが落ちている時にファイルシステムにアクセスすると即時エラーが帰る
読み込み・書き込みの最大転送サイズを変更する場合には、wsize=32768,rsize=32768
をつける。
非同期書き込みで良い場合にはasyncをつける
本節の問題は、carp-hast-switchを書き換えることで、とりあえず解決したはず。 以下は、過去の記録として残すのみで、考慮する必要は(現時点では)ないはず
サービスでHASTを利用する場合、事前に考えておくべきことが多い。 特に起動時の問題が大きい。定常状態まで行けば、あとはHASTdがSplit-Brainになった場合の対処だけとも言える。
起動時のhastの問題は、
ことである。むしろ、勝手に判断されるよりはましであるとも言える。
上記の問題を考えると、HASTdを起動するタイミングが問題になる。
本記事中に掲載した “carp-hast-switch” はCARPのstateの変化をdevd経由で取得しHASTdのroleを設定している。この時、NFSの挙動も制御しているため、定常状態では十分なのだが、OSの起動時には問題が発生する。
FreeBSDの起動順序では、
となっている。
この起動順序では、以下の問題が発生する
この問題は、要するに、「hastdの制御」と「hastdを用いたファイルシステムのexport」を混同しているから発生する。従って、この問題に対処するには、結局以下の方策のいずれかを採用するしかない。
上記案のいずれを採用するか決める前に、方針の決定と、問題点を抽出する。
方針は
ものとする
現時点で把握している問題をいかに挙げる。
以上から、実装としては、
- carp-hast-nfs-boot を作成し、/usr/local/etcに設置
* /etc/rc.confにcarp-hast-nfs-boot_enable=“YES”を記述
- /etc/rc.localに直に記載する
の2案が考えられるが、現時点では、後者のrc.localによる実装を採用する。 (2015/11/22)
結論としては、carp-hast-switchで
ように実装を修正することで、問題を解決した。
この手法で問題が解決できる理由は、CARPのステータスが「MASTER」になった際に、必ずcarp-hast-switchが呼ばれるため、最終的に正しく設定がなされることが期待出来るからである。
# Obsolete. Do not use this.(2015/11/22) #----- HAST initiate (Only boot time needed) if checkyesno hastd_enable; then echo "hast initializing" for if in `/sbin/ifconfig -l`; do if [ "0" != `/sbin/ifconfig ${if}|/usr/bin/grep -c carp` ]; then if [ -n "${if}" ]; then state=`/sbin/ifconfig ${if}|/usr/bin/grep carp|/usr/bin/awk '{print $2 " " $4}'` [ -x /usr/local/sbin/carp-hast-switch ] && \ /usr/local/sbin/carp-hast-switch ${state##* }@${if} ${state%% *} break else echo -n " no carp I/F" fi fi done unset if state echo " ... done" fi
FreeBSD 10.1 の段階では、以下の挙動が確認されている。
この状況において、
という流れが発生したとする。
この時、HASTは、Dirtyが小さい状況であっても「Split-Brain状態になった」と認識し、同期が外れた状況となってしまう。
状況 | host-1 | host-2 | |||||
---|---|---|---|---|---|---|---|
HAST | localcnt | remotecnt | HAST | localcnt | remotecnt | ||
0 | 初期状態 | primary | 0 | 0 | secondary | 0 | 0 |
1 | host-1 切断 | primary | 0 | 0 | secondary | 0 | 0 |
2 | host-1 unmount | primary | 1 | 0 | secondary | 0 | 0 |
3 | host-2 primary | secondary | 1 | 0 | primary | 0 | 0 |
4 | host-2でfsck実行 | secondary | 1 | 0 | primary | 1 | 0 |
5 | host-2 mount | secondary | 1 | 0 | primary | 1 | 0 |
6 | host-1 復帰 | secondary | 1 | 0 | primary | 1 | 0 |
7 | split-brain | secondary | 1 | 0 | primary | 1 | 0 |
8 | hastctl create home | secondary | 0 | 0 | primary | 1 | 0 |
9 | 最後 | secondary | 0 | 0 | primary | 1 | 0 |
これを見ると、host-1でroleがprimaryの状況のままでunmountしたことが、Degrade、ひいてはSplit-brainの引き金になっているように見える。当初のcarp-hast-switchは、このような実装であったため、ちょっとした試験で頻繁にSplit-Brainとなってしまっていた。なぜumountでsplit-brainが発生するかは不明だが、MemoryとDiskのSyncに起因すると推測している。
この問題は、要するにsecondaryになる前にumountしているのが問題なのだと考えられることから、シナリオを変更して、以下のようにする。
状況 | host-1 | host-2 | |||||
---|---|---|---|---|---|---|---|
HAST | localcnt | remotecnt | HAST | localcnt | remotecnt | ||
0 | 初期状態 | primary | 0 | 0 | secondary | 0 | 0 |
1 | host-1 切断 | primary | 0 | 0 | secondary | 0 | 0 |
2 | host-2 primary | secondary | 1 | 0 | primary | 0 | 0 |
3 | host-1 unmount | secondary | 0 | 0 | primary | 0 | 0 |
4 | host-2でfsck実行 | secondary | 0 | 0 | primary | 1 | 0 |
5 | host-2 mount | secondary | 0 | 0 | primary | 1 | 0 |
6 | host-1 復帰 | secondary | 0 | 0 | primary | 0 | 0 |
7 | 最後 | secondary | 0 | 0 | primary | 1 | 0 |
要するに、当初シナリオの2と3をひっくり返した。その結果、Split-Brainが発生しなくなった。
というわけで、carp-hast-switchを書き換えて問題は解決した。(現在掲示しているcarp-hast-switchはこの問題を修正済みのものである)