FreeBSDのCARPについて、及びHAST制御スクリプト

# 特徴など
- FreeBSDで標準で利用可能（おそらく10.x以降）
- VIPの切替えを利用してActive/Standbyを構成する
	- クラウド上で利用する場合は注意が必要になる可能性が高い
- carpのステータスが変わったのをdevdで検知し、制御ツールを起動する
- マニュアルなど
	- https://www.freebsd.org/doc/en/books/handbook/carp.html
	- http://www.jp.freebsd.org/cgi/mroff.cgi?subdir=man&lc=1&cmd=&man=carp&dir=jpman-11.0.2%2Fman&sect=0
	- http://www.jp.freebsd.org/cgi/mroff.cgi?subdir=man&lc=1&cmd=&man=devd&dir=jpman-11.0.2%2Fman&sect=0
	- http://www.jp.freebsd.org/cgi/mroff.cgi?sect=5&cmd=&lc=1&subdir=man&dir=jpman-11.0.2%2Fman&subdir=man&man=devd.conf


# インストール・設定
- インストールは特に必要なし（FreeBSD 11.0、おそらく10.xも必要なし）
- /boot/loader.conf 
  - carpモジュールをロード

```console:
# sysrc -f /boot/loader.conf carp_load="YES"
```

- /etc/rc.conf
    - ネットワーク設定にCARPで使うvipの設定を追加
        - vhid {id}	-- バーチャルホストID。CARPで冗長化するIPアドレスごとの識別子
        - advskew {n}	-- 死活監視への応答遅延(n/256秒)
            - この値をいくつにするのが適切かは未検証
        - pass {phrase}		-- パスフレーズ。冗長化するIF間で合わせる
        - IPアドレス/マスク	-- ネットマスクは必ず32にする

```console:host1
# tee -a /etc/rc.conf <<EOF
ifconfig_vtnet0="inet 192.168.0.101 netmask 255.255.255.0"
ifconfig_vtnet0_alias0="inet vhid 1 pass {パスフレーズ} alias 192.168.0.100/32"
EOF
````
```console:host2
# tee -a /etc/rc.conf <<EOF
ifconfig_vtnet0="inet 192.168.0.102 netmask 255.255.255.0"
ifconfig_vtnet0_alias0="inet vhid 1 advskew 50 pass {パスフレーズ} alias 192.168.0.100/32"
EOF
```

- /usr/local/etc/devd/carp.conf
    - devdの設定
        - ※CARPの状態変更を全て受け取る場合
        - system	-- システム名。"CARP"
        - subsystem	-- サブシステム名。CARPの場合は {vhid}@{if名}
        - type	-- 通知の種別。CARPの場合は"INIT", "MASTER", "BACKUP"のいずれか
        - action	-- 実行するコマンド
            - 後述の制御スクリプトを利用している

```conf:/usr/local/etc/devd/carp.conf
notify 0 {
        match "system"          "CARP";
        match "subsystem"       "[0-9]+@[0-9a-z]+";
        match "type"            "(INIT|MASTER|BACKUP)";
        action "/path/to/carphast.sh $type $subsystem";
};
```

- /etc/rc.local
    - devdが起動する前に制御が必要な場合

```console:
# tee -a /etc/rc.local <<EOF
/path/to/carphast.sh AUTO 1@vtnet0
EOF
```

# 状態・操作について
- 状態は INIT, MASTER, BACKUP の3種類
	- 基本的にはMASTER/BACKUPを切り替える
- 実行したサーバの状態をBACKUP（Standby）に変更する
	- 反対側サーバのstateがMASTER（Active）に変わる

```console:
# ifconfig vtnet0 vhid 1 state backup
```

- 実行したサーバの状態をMASTER（Active）に変更する
	- 反対側サーバのstateがBACKUP（Standby）に変わる
	- advskew値が小さいほうでのみ可能（同じなら両方可能）

```console:
# ifconfig vtnet0 vhid 1 state master
```

- 実行したサーバの状態をINITにする
	- インターフェイスを停止した時にINITになる模様
	- 反対側サーバのstateがMASTER（Active）に変わる

```console:
# ifconfig vtnet0 down
```

- shutdownやrebootした時は残っているほうがMASTERになる
- 起動時は先にbootしたほうがMASTERになる
- 中途半端な死活状態になった時にどうなるかは未確認
	- pingが通っていれば生きている扱い？


# 状態変更時・起動時の処理について
- HAProxyのようなネットワークのみで完結するものであれば特に処理は必要ないと思われる
- devdが状態変更時に制御スクリプトを呼び出す
	- サーバ起動時はdevdが開始されてないため、/etc/rc.local で制御スクリプトを呼び出す
- MySQLをfailoverする場合はおおよそ下記のようになると思われる
    - CARPがtype:INITになった時：
        - ※この時は何もしない？
    - CARPがtype:BACKUPになった時：
        - `service mysql-server stop`
        - ファイルシステムのアンマウント（ufsとzfsで違う）
        - `hastctl role seconday all`
    - CARPがtype:MASTERになった時：
        - `hastctl role primary all`
        - ファイルシステムのマウント（ufsとzfsで違う）
        - `service mysql-server start`

# CARPを利用したHAST制御スクリプト
- CARPの状態を利用してHASTを管理する
    - 第1引数にCARPの状態を渡す
    - 第1引数がAUTOの時はCARPのstateから判断する
    - 第2引数にvhidとIF名が渡されるが、現在は未使用
        - vhidが複数ある場合には制御したほうがよさそう
- 制御スクリプトと同じディレクトリに carphast.conf があればその設定を読み込む
    - services は起動したいサービス名。複数可
    - zpools は importしたいZFSのプール名。複数可
    - resources は 利用するHASTのリソース名。複数可。allの時は全て利用する
- /dev/hast/* に現れるデバイスは /etc/fstab の設定に応じてmountする
    - ZFSはimportしたあとに/etc/fstabの内容でmountしている

```sh:carphast.conf
services="mysql-server"
zpools="hast-zfs"
resources="all"
#resources="shared1 shared2"
```
```sh:carphast.sh
#!/bin/sh

conffile=${0%.sh}".conf"
if [ -f $conffile ]; then
. $conffile
fi

resources=${resources:-all}

action=$1
vhid=${2%@*}
ifname=${2#*@}

syslog_facility="user.notice"
syslog_tag="carp-hast"
maxwait=60
delay=3

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


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


# 
case "$action" in
  MASTER|BACKUP|INIT)
    $logger "State Changed. I/F: $ifname VHID: $vhid state: $action"
  ;;

  AUTO)
    action=$(/sbin/ifconfig $ifname | /usr/bin/awk '/[[:space:]]*carp:[[:space:]]+([A-Z]+)[[:space:]]vhid[[:space:]]'"$vhid"'[[:space:]]?/ {print $2; exit}' )
    if [ "$action" ]; then
      $logger "State Changed. I/F: $ifname VHID: $vhid state: $action"
    else
      die "carp state not found"
    fi
  ;;

  *)
    die "$action is not yet implemented"
  ;;

esac


reverse_list()
{
  _revlist=
  for _revfile in $*; do
    _revlist="$_revfile $_revlist"
  done
  echo $_revlist
}

die()
{
  $logger "FATAL: "$*
  exit 1
}


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


stop_services()
{
  for service in $( reverse_list $* ); do
    if /usr/sbin/service ${service} onestatus | /usr/bin/grep -q "running as" ; then
      /usr/sbin/service ${service} onestop \
      	|| $logger "Unable to stop service: ${service}."
    fi
  done
}

change_role()
{
  roletype=$1
  shift 1

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

# main
case "$action" in
  BACKUP|INIT)
    # stop services
    stop_services $services

    # unmount zfs
    for pool in $zpools; do
      if /sbin/zpool status | /usr/bin/grep -q "pool: $pool" ; then
        /sbin/zpool export -f $pool \
        	|| $logger "Unable to export zpool: ${pool}."
      fi
    done

    # unmount ufs
    for mdev in $(/sbin/mount -p | /usr/bin/awk '/^\/dev\/hast\// {print $1}'); do
      for hdev in $hastdevs; do
        if [ "$mdev" = "/dev/hast/$hdev" ]; then
          /sbin/umount -f $mdev \
          	|| $logger "Unable to unmount: ${mdev}."
        fi
      done
    done

    # change role
    if [ "$action" = "BACKUP" ]; then
      roletype="secondary"
    else
      roletype="init"
    fi
    change_role $roletype $resources

    $logger "Change role $roletype completed."
  ;;

  MASTER)
    # stop services
    stop_services $services

    # 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
        die "Secondary process for resource ${hdev} is still running after $maxwait seconds."
      fi
    done

    # change role primary
    change_role primary $resources
    sleep $delay

    # wait for the /dev/hast/* devices to appear
    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
        die "GEOM provider /dev/hast/$hdev did not appear."
      fi
    done

    # mount zfs
    if [ "$zpools" ]; then
      for pool in $zpools; do
        /sbin/zpool status | /usr/bin/grep -q -E "[[:space:]]*$pool[[:space:]]+ONLINE[[:space:]]" && break;

        /sbin/zpool import -d /dev/hast/ -f -N $pool \
          || die "Unable to import zpool: ${pool}."
      done

      /sbin/zpool scrub $zpools

      for pool in $zpools; do
        for mdev in $(/usr/bin/awk '/^'"$pool"'\// {print $1}' /etc/fstab); do
          $logger "mount $mdev"
          /sbin/mount -p | /usr/bin/grep -q -e "^${mdev}[[:space:]]" && break;

          /sbin/mount ${mdev} \
          	|| die "Unable to mount: ${mdev}."
        done
      done
    fi

    # mount ufs
    for mdev in $(/usr/bin/awk '/^\/dev\/hast\// {print $1}' /etc/fstab); do
      for hdev in $hastdevs; do
        if [ "$mdev" = "/dev/hast/$hdev" ]; then
          $logger "mount $mdev"
          /sbin/mount -p | /usr/bin/grep -q -e "^${mdev}[[:space:]]" && break;

          /sbin/fsck -y -t ufs ${mdev} \
          	|| die "Failed to fsck: ${mdev}."

          /sbin/mount ${mdev} \
          	|| die "Unable to mount: ${mdev}."
        fi
      done
    done

    # start services
    for service in ${services}; do
      /usr/sbin/service ${service} onestart \
      	|| $logger "Failed to start service: ${service}."
    done

    $logger "Change role primary completed."
  ;;

esac

```