複数あるZFS pool間での起動切り替え

#おことわり
自己満足のために技術的に可能かどうかを検証したものであり、本記事のような構成が実用的意義を持つかどうかは考慮していません。

#動機
SSDは高速で嬉しいけれど、どうしても心情的に信頼しきれない。システムはSSDにインストールするとして、大事なデータはHDDでミラリングしておき、さらにSSDが壊れたときのためHDDからも起動できるようにしておきたい。[^1]

[^1]: この構成、実は1度だけ役に立ちました。あるとき突然SSDが認識されなくなり、勝手にHDDから再起動してサービスは継続したことがあります。後日のメンテでSSDを触ってから再起動したらSSDから起動したのですが、さて…。

#構成
* システム: DELL Precision 5820 Tower
* SSD: Intel SSDPEMKF256B8 (NVMe 256MB) [^2]
* HDD: Seagate BarraCuda ST1000DM010 (SATA 1TB) x2
* OS: FreeBSD 12.2-RELEASE-p1[^3]

SSDは単独でZFS pool (zroot)とし、HDDは2台でmirroring pool (tank)とする。tankには自前のデータを格納するほか、zrootに構築したシステムを逐次tankにバックアップしておき、SSDにトラブルが起きた場合にはtankだけで起動し可用性を確保するものとする。

[^2]: おそらくDELL用専用品番で、Intel Pro 7600p/760p/E 6100p シリーズと同世代の64層3DTLC（2018年Q1）。デフォルトではIntel Volume Management Deviceに繋がっていてFreeBSD 12では認識されない。BIOSでIntel VMD Technologyを無効化することで認識可能になる。

[^3]: FreeBSD 11以前でも/etc/rc.d/zfsbeを持ち込めばうまく行くのかもしれない。

#インストール
1. まずSSD (nvd0)にインストールする。Guided Root-on-ZFSで`GPT(BIOS+UEFI)`を選択。
2. 再起動し、mirroring poolを構成することだけを目的として、もう1回インストールする。ada0とada1を使い、Guided Root-on-ZFSで`GPT(BIOS+UEFI)`を選択、プール名を`tank`とし、素のswapパーティションを32GB確保する。
3. SSDから起動するようにUEFIを設定して起動

```shell-session
# gpart show
=>       40  500118112  nvd0  GPT  (238G)
         40     409600     1  efi  (200M)
     409640       1024     2  freebsd-boot  (512K)
     410664        984        - free -  (492K)
     411648    4194304     3  freebsd-swap  (2.0G)
    4605952  495511552     4  freebsd-zfs  (236G)
  500117504        648        - free -  (324K)

=>        40  1953525088  ada0  GPT  (932G)
          40      409600     1  efi  (200M)
      409640        1024     2  freebsd-boot  (512K)
      410664         984        - free -  (492K)
      411648    67108864     3  freebsd-swap  (32G)
    67520512  1886003200     4  freebsd-zfs  (899G)
  1953523712        1416        - free -  (708K)

=>        40  1953525088  ada1  GPT  (932G)
          40      409600     1  efi  (200M)
      409640        1024     2  freebsd-boot  (512K)
      410664         984        - free -  (492K)
      411648    67108864     3  freebsd-swap  (32G)
    67520512  1886003200     4  freebsd-zfs  (899G)
  1953523712        1416        - free -  (708K)
```

うっかりルートレベルのデータセットを使うと後々厄介なのでマウントポイントを消しておく。

```shell-session
# zfs set mountpoint=none zroot
# rmdir /zroot
```

tankをインポートして中身を消し、さらにマウントポイントを無効化する。

```shell-session
# zpool import -f tank
# zfs destroy -r tank
# zfs set canmount=off tank
```

以下のように2つのZFS poolが構成されている。

```shell-session
>zpool status
  pool: tank
 state: ONLINE
  scan: none requested
config:

        NAME        STATE     READ WRITE CKSUM
        tank        ONLINE       0     0     0
          mirror-0  ONLINE       0     0     0
            ada0p4  ONLINE       0     0     0
            ada1p4  ONLINE       0     0     0

errors: No known data errors

  pool: zroot
 state: ONLINE
  scan: none requested
config:

        NAME        STATE     READ WRITE CKSUM
        zroot       ONLINE       0     0     0
          nvd0p4    ONLINE       0     0     0

errors: No known data errors
```

swapの設定を/etc/fstabに書いておく。

```shell-session
# sed -i '' -e '/nvd0/{h;s/nvd0/ada0/;H;s/ada0/ada1/;H;g;}' /etc/fstab
# cat /etc/fstab
# Device                Mountpoint      FStype  Options         Dump    Pass#
/dev/nvd0p3             none    swap    sw              0       0
/dev/ada0p3             none    swap    sw              0       0
/dev/ada1p3             none    swap    sw              0       0
# swapon -a
```

あとは好きなようにシステムをセットアップする。動機で述べたように zroot はあくまでシステムだけに用い、自前のデータは tank プールを使うようにしておく。

#起動切り替え

tank上のバックアップは日常的にはマウントされては困るため```canmount=noauto```としておくが、tankから起動した場合は自動的にマウントされることが必要。FreeBSD 12ではルートファイルシステムの子は```canmount=noauto```でも自動的にマウントするようになっている(cf. [/etc/rc.d/zfsbe](https://github.com/freebsd/freebsd/blob/stable/12/libexec/rc/rc.d/zfsbe))[^4]ので、これを利用できるようにバックアップを取れば良い。

[^4]: もともと[ZFS Boot Environment](https://freebsd.seirios.org/doku.php?id=os:boot_environment)用の仕掛け。

バックアップの手順は、
1. zrootのスナップショットを作成
2. ルートファイルシステム(zroot/ROOT/default)をtank/altrootとしてバックアップ
3. その他zroot上のファイルシステムをtank/altroot/*としてバックアップ
4. tank/altroot以下で`canmount=on`のものを`canmount=noauto`にしておく。
5. tankの`bootfs`プロパティを設定する。

```shell-session
# zfs snapshot -r zroot@replicate
# zfs send -Rc zroot/ROOT/default@replicate |zfs receive -uv tank/altroot
# for fs in `zfs list -H -o name -r zroot |sed 1d |cut -d/ -f 2 |uniq |grep -v ROOT`; do zfs send -Rc zroot/${fs}@replicate |zfs receive -uvd tank/altroot; done
# zfs list -H -o name,canmount -r tank/altroot |grep 'on$' |cut -f 1 |xargs -n1 zfs set canmount=noauto
# zpool set bootfs=tank/altroot tank
# zfs list -o name,canmount,mountpoint,mounted -r tank/altroot
NAME                    CANMOUNT  MOUNTPOINT  MOUNTED
tank/altroot              noauto  /                no
tank/altroot/tmp          noauto  /tmp             no
tank/altroot/usr             off  /usr             no
tank/altroot/usr/home     noauto  /usr/home        no
tank/altroot/usr/ports    noauto  /usr/ports       no
tank/altroot/usr/src      noauto  /usr/src         no
tank/altroot/var             off  /var             no
tank/altroot/var/audit    noauto  /var/audit       no
tank/altroot/var/crash    noauto  /var/crash       no
tank/altroot/var/log      noauto  /var/log         no
tank/altroot/var/mail     noauto  /var/mail        no
tank/altroot/var/tmp      noauto  /var/tmp         no
```

この状態でSSDから起動する限りはtank/altroot以下はマウントされないが、SSDを抜いてHDDから起動するとtank/altroot以下がマウントされる。[^5]

[^5]: SSDが有効なままHDDから起動すると、ルートファイルシステム以外のtank/altroot以下とzroot以下のファイルシステムが二重にマウントされる。

以下のように差分バックアップができるので、適切なタイミングで実施されるように設定すればよい。

```shell-session
# zfs rename -r zroot@replicate @previous
# zfs rename -r tank/altroot@replicate @previous
# zfs snapshot -r zroot@replicate
# zfs send -Rc -i previous zroot/ROOT/default@replicate |zfs receive -uv tank/altroot
# for fs in `zfs list -H -o name -r zroot |sed 1d |cut -d/ -f 2 |uniq |grep -v ROOT`; do zfs send -Rc -i previous zroot/${fs}@replicate |zfs receive -uvd tank/altroot; done
# zfs destroy -r zroot@previous
# zfs destroy -r tank/altroot@previous
```