FreeBSDのHASTについて、および簡易ツール

　高可用性があるサーバを作る方法の１つとして、適当に検証していたものをまとめました。
　また、標準のツールでは不便だったので、フォローするツールを作成しています。


# 特徴など
* ストレージをBlock Deviceレベルで同期
	* 実際にファイルを扱うにはBlock Deviceに対してufsやZFSでフォーマット/マウントする
* PrimaryからSecondaryへの同期
	* 同時利用はできない
	* 基本的にはActive-Standbyとして利用
* 同期対象サーバの変更は可能
	* /etc/hast.confを修正し、service hastd reload で無停止で変更可能と思われる（※）
* サイズの拡張はできない
	* 同期デバイスを増やせるので、それで対応
	* ストレージを拡張する可能性があるのであれば、ZFSを使うのがよさそう
* FreeBSD 11.0では標準で利用可能
* マニュアルなど
	* https://www.freebsd.org/doc/handbook/disks-hast.html
	* http://www.jp.freebsd.org/cgi/mroff.cgi?sect=8&cmd=&lc=1&subdir=man&dir=jpman-11.0.2%2Fman&subdir=man&man=hastd
	* http://www.jp.freebsd.org/cgi/mroff.cgi?sect=5&cmd=&lc=1&subdir=man&dir=jpman-11.0.2%2Fman&subdir=man&man=hast.conf
	* http://www.jp.freebsd.org/cgi/mroff.cgi?sect=8&cmd=&lc=1&subdir=man&dir=jpman-11.0.2%2Fman&subdir=man&man=hastctl


# インストール・設定
* インストールは特に必要なし（FreeBSD 11.0）
* 設定ファイル
	* replicationはmemsyncがデフォルト、fullsyncが安全で遅い
	* execでイベント発生時にプログラムの実行が可能

```/etc/hast.conf
replication memsync
resource shared1 {
    local /dev/vtbd1p1
    on host1 {
        remote 192.168.0.102
    }
    on host2 {
        remote 192.168.0.101
    }
}
resource shared2 {
    local /var/hast/shared.img
    on host1 {
        remote 192.168.0.102
    }
    on host2 {
        remote 192.168.0.101
    }
}
```


* ブロックデバイスを利用する場合はそのまま指定する
	* パーティションは切っておいたほうがよいと思われる
		* サイズを一致させるため
		* ストレージのサイズが大きくなった時（仮想環境など）に後ろのブロックを利用するため

```console:
# gpart create -s gpt vtbd1
# gpart add -t freebsd-ufs -s 10G vtbd1
# gpart show vtbd1
```

* ファイルを使う場合は必要なサイズのファイルを作る
	* ※ただし、ファイルを使った場合に問題が生じるケースがある模様
		* HASTのターゲットファイルがufs上の時は問題が起こったが、ZFS上であれば問題なかった
		* HASTのターゲットファイルがufs上の時でも、HASTデバイスをZFSで扱っている時は問題なかった
		* /var/log/messages に「Unable to flush disk cache on activemap update: Inappropriate ioctl for device.」が残る（この時に問題があるかどうかは不明。今のところは問題なく同期できているようだった）

```console:
# mkdir /var/hast
# dd if=/dev/zero of=/var/hast/shared.img bs=10m count=1024
```

* ブロックデバイスにしろファイルにしろ、サイズが不一致の場合は同期できない
	* /var/log/messagesにエラーが残る
	* 逆にサイズさえ合っていればブロックデバイスとファイルとの同期もされる模様


# hastctl による操作
## 初期化
* create でリソースを初期化する
* 初めてアクセスする前に実行する
* 問題が生じて再初期化時にも利用

```console:
# hastctl create shared1
```

## 状態変更
* role primary 時に /dev/hast/{リソース名} にアクセス可能になる
* 同時に両方primaryにできる（排他制御などはしてない模様）

```console:
# hastctl role primary shared1
```

* role secondary にすることで、primaryと同期される
* hastd起動時はrole init なので、secondaryにする必要がある

```console:
# hastctl role secondary shared1
```

## 状態確認
* 同期の状態などを確認可能

```console:
# hastctl status
# hastctl list
```

* Split-brainになった場合
	* /var/log/messages にSplit-brainを検知した旨と対象のリソース名が残る（Primary Secondary共）
	* hast.conf の exec で通知を受け取ることも可能
	* セカンダリ側で下記を実行する：

```console:
# hastctrl role init {リソース名}
# hastctrl create {リソース名}
# hastctrl role secondary {リソース名}
```


# 状態について
* roleはinit, primary, secondaryの3種類
* role initの時は動作していない
* role primaryの時に /dev/hast/{リソース名} として参照可能になる
* role secondaryの時にprimaryからの同期データを受信している
* role primaryの排他制御などはしていないらしく、同時にprimaryにしてしまうことができる
	* これを回避する方法は特にない？（primaryに変えるときに注意するしかない？）
	* 切替え時には両方をsecondaryにしてから、片方をprimaryにする必要がある
		* 両方primaryにするとhastdが刺さった状態になる模様。プロセスをkillする必要がある。

```console:
# kill -KILL `cat /var/run/hastd.pid`
```

* hastctl statusでcompleteになっていても、同期は完了していない模様
	* hastctl listで表示されるdirtyが0になった時に完了している？
* HASTデバイスを利用するサービスはrc.confで有効にしないほうがよい
	* role primary ＆ mount後にアクセスする必要があるため
	* 上記でアクセス可能になったあとでservice {サービス名} onestart で起動
* 起動時はrole initのため、なんらかの方法でrole secondaryにする必要がありそう
	* secondaryは自動的に設定しても問題なさそうな気がする（※）
	* primaryにするのは初期やfailover時なので、手作業やスクリプトでprimaryにするのでよさげ
* CARPを使った事例ではcarpの状態変更時にdevdからの呼び出しでroleを変更してる模様
* pacemaker + corosyncでの対応もおそらく可能（※）


# ファイルシステムについて
* ufsよりZFSのほうがパフォーマンスがよさそう（※）
* HASTで同期している領域を拡張することはできない模様
	* 領域を増やしたい場合はhast.confに別途追加する形になりそう
	* ZFSでアクセスするようにしたほうがよいかもしれない
		* zpool replaceで大きいパーティションに変更可能
		* zpool addで領域を増やすことも可能
			* ただし、こういう運用をする場合は管理が煩雑になる可能性がある
			* 壊れやすく、importできなくなる可能性が高くなる


## ufsで利用する場合
* 初期化
	* -U はソフトアップデートを有効化
	* -j はジャーナリング有効化

```console:
# newfs -U -j /dev/hast/shared1
# mount -o noatime /dev/hast/shared1 /mnt/shared1
```

* ufsの場合はmountする前にfsckでチェックする

```console:
# hastctl role primary shared1
# fsck -y -t ufs /dev/hast/shared1
# mount -o noatime /dev/hast/shared1 /mnt/shared1
```

## ZFSで利用する場合
* 初期化

```console:
# zpool create -m none hast-zfs /dev/hast/shared2
# zfs create -o mountpoint=/mnt/shared2 hast-zfs/shared
```

* role prymaryから状態変更する時にzpool exportしておかないと問題が生じる
	* また、zpool exportしておかないとshutdownに失敗する時がある（どこかで刺さっている？）

```console:
# zpool export hast-zfs
# hastctl role secondary shared2
```

* zpool import時は正常にexportされていない可能性があるため（ダウン時のリカバリ等）、-fオプションを常に付けたほうがよいかもしれない

```console:
# hastctl role primary shared2
# zpool import -f -d /dev/hast/ hast-zfs
```

* 異常終了時はzpool exportされていないため、zpool statusを見るとstate:UNAVAILになっている
	* この時はzpool exportしておいたほうがよいかもしれない
	* exportしていないときにrole primaryになると自動的にONLINEになる（mountはされない）

## /etc/fstab のサンプル
```mount:/etc/fstab
/dev/hast/shared1   /mnt/shared1     ufs     noauto,rw,noatime       0       0
hast-zfs/zfs    /mnt/shared2     noauto,rw,nfsv4acls    0 0
```


# 簡易ツール
## dirty値チェック
- オプションなしの場合、dirty値があるリソースとそのdirty値が表示される
- オプション-aでdirty値がないリソース（とそのdirty値）も表示される
- オプション-qで何も表示しない
- オプション-hでdirtyのサイズを見やすくする
- リソース名を渡すと、そのリソースのみチェックする
- role primary 以外の時はアラートを出す
- 終了ステータスにdirty値があるリソースの数を返す

```sh:hastdirty.sh
#!/bin/sh

# check options
while getopts aqh opt
do
  case "$opt" in
    a) option_all=1 ;;
    q) option_quiet=1 ;;
    h) option_human=1 ;;
  esac
done

# target resources
shift $(($OPTIND - 1))
if [ "$*" ]; then
  resources="$*"
else
  resources=$(/sbin/hastctl dump | /usr/bin/awk '/^[[:space:]]*resource:[[:space:]]/ {print $2}')
fi

print_human()
{
  size=$1
  type=
  while [ "$size" -gt 1024 ]; do
    size=$(($size / 1024))
    case "$type" in
      "") type=K ;;
      K) type=M ;;
      M) type=G ;;
      G) type=T ;;
    esac
  done
  echo -n "$size$type"
}

# view dirty
dirtycount=0
for res in $resources; do
  roletype=$(/sbin/hastctl list $res | /usr/bin/awk '/^[[:space:]]*role:[[:space:]]/ {print $2}')
  if [ "$roletype" != "primary" ]; then
    [ ! "$option_quiet" ] && echo "resource $res role is not primary"
    continue
  fi

  dirty=$(/sbin/hastctl list $res | /usr/bin/awk '/^[[:space:]]*dirty:[[:space:]]/ {print $2}')
  [ "$dirty" -ne 0 ] && dirtycount=$(($dirtycount + 1))
  [ "$option_quiet" ] && continue

  if [ "$dirty" -ne 0 -o "$option_all" ]; then
    if [ "$option_human" ]; then
      echo "$res "$(print_human $dirty)
    else
      echo "$res $dirty"
    fi
  fi
done

exit $dirtycount

```

## Sprit-brain時の同期状態リセット
- リソース名を渡すと、そのリソースの同期状態をリセットする
- ただし、role primaryの時は何もしない
- role init の時はrole secondaryに変更しないため、そのままでは同期されない（role secondaryに変更する必要がある）

```sh:hastrecover.sh
#!/bin/sh

syslog_facility="user.notice"
syslog_tag="hast-recover"

logger="/usr/bin/logger -p $syslog_facility -t $syslog_tag"

resource=$1

if [ ! "$resource" ]; then
  echo "usage: "`basename $0`" <resourcename>"
  exit
fi

sleep 2

# check role secondaryj
roletype=$(/sbin/hastctl list $resource | /usr/bin/awk '/^[[:space:]]*role:[[:space:]]/ {print $2}' )
[ ! "$roletype" ] && exit
[ "$roletype" = "primary" ] && exit

# recover
$logger "HAST [$resource] recover role $roletype"
/sbin/hastctl role init $resource
/sbin/hastctl create $resource
/sbin/hastctl role $roletype $resource

```


## （やや安全な）roleの変更
- role primary に変更するときに注意点があるため、それをフォローするためのもの
- リソース名を指定しない時はall扱い

```sh:hastrole.sh
#!/bin/sh

maxwait=60

roletype=$1
shift 1

if [ ! "$roletype" ]; then
  echo "usage: "`basename $0`" <roletype> [name ...]"
  exit
fi

if [ "$*" ]; then
  hastdevs="$*"
else
  hastdevs=$(/sbin/hastctl dump | /usr/bin/awk '/^[[:space:]]*resource:[[:space:]]/ {print $2}')
fi

# check hastd enabled
if ! /bin/pgrep -q hastd; then
  $logger "hastd not running"
  exit
fi

change_role()
{
  _roletype=$1
  shift 1

  for hdev in $*; do
    /sbin/hastctl role $_roletype $hdev \
    	|| echo "Unable to change role to primary for resource: $hdev"
  done
}

# main
case "$roletype" in
  init|secondary)
    change_role $roletype $*
  ;;

  primary)
    # wait for not running secondary
    for hdev in $hastdevs; do
      for i in $(/usr/bin/jot $maxwait); do
        /bin/pgrep -fq "hastd: ${hdev} \(secondary\)" || break
        sleep 1
      done

      if /bin/pgrep -fq "hastd: ${hdev} \(secondary\)" ; then
        echo "FATAL: Secondary process for resource ${hdev} is still running after $maxwait seconds."
        exit 1
      fi
    done

    # change role primary
    change_role primary $resources

    for hdev in $hastdevs; do
      for i in $(/usr/bin/jot $maxwait); do
        [ -c /dev/hast/$hdev ] && break
        sleep 1
      done

      if [ ! -c /dev/hast/$hdev ]; then
        echo "GEOM provider /dev/hast/$hdev did not appear."
        exit 1
      fi
    done
  ;;

esac

```