zfsのrefreservationプロパティの挙動を理解する

お急ぎの方は「[概要](#概要)」→「意義」→「まとめ」と読めばそれで済みます。

#動機
ZFSでデータセットの使用量制御に用いるrefreservationプロパティを調べていたら、自分の直感に反する挙動を示したので整理する。ていうか、ドキュメントをなんとかして欲しい。

#前提
* OpenZFS (FreeBSD 11.1-RELEASE-p9)

#概要
refreservationプロパティは子孫を含まないそのデータセット自身の使用量を予約する。この「データセット自身の使用量」は`REFER`のことだと思いがちだが、実際には`WRITTEN`に相当するようである。`WRITTEN`は最新のスナップショット以降に書き込まれ参照可能なデータの量であり、スナップショットが存在しない場合のみ`REFER`に等しい。つまりrefreservationを設定することは、最新のスナップショット以降に上書きまたは新規に書き込み保持できるデータ量を保証していることになる。そのため、

1. スナップショットを作成するたび予約量が丸々回復する
2. 前回スナップショットからの差分に相当する空き領域がないとスナップショットが作成できない

という一見直感に反する挙動を示す。これはボリュームの動作を保証するための挙動だと考えれば理解しやすい。

お急ぎコース→[意義](#意義)

#refreservationとは
zfsでは空き容量をプールに属する多くのデータセット（ファイルシステムやボリューム）で共有しており、何も設定しなければ全てのデータセットが平等に扱われて、使った者勝ちで空き容量を消費していく。そこで特定のデータセットを特別扱いする仕組みが[予約とクオータ](https://docs.oracle.com/cd/E19253-01/819-6260/gazvb/index.html "Oracle Solaris ZFS 管理ガイド")で、クオータが「最大利用可能量」であるのに対し予約は「最小利用保証量」と考えればよい。

予約に関わる書き込み可能なプロパティが2種類ある。[zfs(8)][]の記述を要約すると、

[zfs(8)]:https://www.freebsd.org/cgi/man.cgi?query=zfs&sektion=8

1. reservation  
そのデータセット自身と全ての子孫の合計使用量、つまり`USED`の最小保証量。合計使用量が設定に満たない場合でも、そのデータセットが設定された量を使用しているかのように振る舞う。予約されている分量は親データセットの使用量に計上され、親データセットの予約やクオータの計算に組み込まれる。
2. refreservation  
子孫を含まないそのデータセット自身の使用量（**`REFER`のことと思いきや、実際には`WRITTEN`に相当**）の最小保証量。使用量が設定に満たない場合でも、そのデータセットが設定された量を使用しているかのように振る舞う。予約されている分量は親データセットの使用量に計上され、親データセットの予約やクオータの計算に組み込まれる。refreservationが設定されている場合、the current number of "referenced" bytes（**`REFER`のことと思いきや、実際には`WRITTEN`に相当**）と同容量の空き領域がデータセットの外側にないとスナップショットを作成できない。

カッコ内の注釈は[zfs(8)][]などに明記されておらず、私が挙動から推定したものである。スナップショットは子孫データセットになるので、スナップショットが確保したデータはrefreservationが保証する使用量の計算に含まれない。スナップショットに確保されていない、データセット固有のデータ量は`WRITTEN`に示されている。これに対し、`REFER`はデータセットから参照できるデータ量を示し、スナップショットが確保していてまだデータセットから削除されていないデータを含んでいる。

つまり使用量の計算上は、スナップショットを作成した瞬間に、それまで含まれていたデータが子孫データセットへ追い出され、refreservationの保証するデータ使用量が丸々回復するように見えるということである。その時に回復した保証量の分だけ保証の枠外の空き容量を減らす必要があるので、十分空き容量がないとスナップショットを作成できないということが起きる。ZFSのスナップショットは作成時に領域をほとんど消費しないという触れ込みであるのにも関わらずだ。これはかなり直感に反する挙動であり、少なくとも私は最初困惑した。なぜこのような挙動になっているのだろうか。

#意義
refreservationはzpool version 9で導入された。reservationで予約した領域がスナップショットやクローンを取ることで消費されるのが嬉しくない場合がある、というのは一応理解できる。特にreservationだと困る例として、[Nex7's Blog](http://nex7.blogspot.jp/2013/03/reservation-ref-reservation-explanation.html "Reservation & Ref Reservation - An Explanation (Attempt)")ではボリューム(zvol)を挙げている。

[ボリューム](https://docs.oracle.com/cd/E19253-01/819-6260/gaypf/index.html "Oracle Solaris ZFS 管理ガイド")はブロックデバイスであるかのように振る舞うデータセットである。ZFSのデータセットは需要に応じて空き領域を消費するため特に固定のサイズはないが、ボリュームの場合は[volsizeプロパティ](https://docs.oracle.com/cd/E19253-01/819-6260/gcfhk/index.html "Oracle Solaris ZFS 管理ガイド")で決まるサイズが存在する。通常はサイズが1Gのブロックデバイスには1Gのデータを書き込めるのが当たり前の前提であり、400M書き込んだ時点で書き込めなくなるようなことは想定されていない。そこでデフォルトではボリュームに予約が設定され、1Gのボリュームに1Gのデータを書き込めるように保証している[^1]。refreservationが導入される前はreservationで予約していたが、この場合ボリュームのスナップショットを作成すると予約領域がスナップショットに消費されてしまい、1G予約しても1G書き込めない事態が発生する可能性がある。

[^1]: ボリュームのサイズとは無関係に書き込みができなくなる事態が起きることを許容できる、あるいは別の手段で予防できるのであれば、予約を設定せずにボリュームを作成することができる。sparse volumeまたはthin provisioningと呼ばれる手法で、その場合は需要に応じてプールの空き領域を消費していき、空き領域がなくなった時点で（ボリュームのサイズとは無関係に）書き込みエラーが発生する。

ではどうしたら、常に1Gのボリュームの全域に書き込める状態を保証できるだろうか。答えは簡単で、スナップショットを作成するたびに1Gの領域を予約し直せばよい。これがrefreservationで予約されたデータセットが示す、「直感に反する」挙動の正体である。

お急ぎコース→[まとめ](#まとめ)

#実際の挙動
このスナップショットに関わるrefreservationの挙動はなかなか直感的に理解することが難しいので、ファイルをvdevにしたプールを使って挙動を確認していこう。

```shell-session
# truncate -s 1G /tmp/testpool
# zpool create -O atime=off test /tmp/testpool
# zpool list test
NAME   SIZE  ALLOC   FREE  EXPANDSZ   FRAG    CAP  DEDUP  HEALTH  ALTROOT
test  1008M    93K  1008M         -     0%     0%  1.00x  ONLINE  -
# zfs list -o space,refer,written -t all -r test
NAME  AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test   880M  78.5K         0     23K              0      55.5K    23K      23K
```

なお`zfs list`で表示されているデータ量関連のプロパティの意味は以下の通りである。

| プロパティ | 内訳 | 意味 |
|:----------|:----|-----|
| `AVAIL`   |     | そのデータセットが利用可能な空き容量。親データセットの`AVAIL`と自身の`USEDREFRESERV`の合計。 |
| `USED`    |     | 子孫データセットも含めた合計使用量。以下4つの合計。 |
|           | `USEDSNAP` | 自身は参照しないがスナップショットが確保しているデータ量 |
|           | `USEDDS` | 自身が参照するデータ量（`REFER`と同じ?） |
|           | `USEDREFRESERV` | refreservationが保証している残り容量 |
|           | `USEDCHILD` | 子孫ファイルシステムの合計使用量＋αが参照するデータ量（子孫の`USED`とは厳密には一致しない） |
| `REFER`   |     | 自身が参照しているデータ量。スナップショットが確保している分を含む。 |
| `WRITTEN` |     | 自身が参照しているデータのうち、スナップショットが確保していないデータ量。 |



##ファイルシステムの場合
refreservation=400Mを指定したファイルシステムを作成する。保証量が`USEDREFRESERV`に計上され、ファイルシステムはほぼ空だが`USED`が400Mとなっている。親ファイルシステムの`AVAIL`はその分減って480Mであるが、自身の`AVAIL`は880Mのままである。仮に親ファイルシステムで480Mを使い切っても、自身には400Mの空きが保証されているということである。

```shell-session
# zfs create -o refreservation=400M test/test
# zfs list -o space,refer,written -t all -r test
NAME       AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test        480M   400M         0     23K              0       400M    23K      23K
test/test   880M   400M         0     23K           400M          0    23K      23K
```

100Mのファイルを作成すると`REFER`が100Mとなる。ここで`WRITTEN`も100Mとなり、`USEDREFRESERV`はその分減って300Mになる。自身の`AVAIL`も同じだけ減って780Mとなる。`USED`は変化せず、親ファイルシステムには影響しない。

```shell-session
# dd if=/dev/random of=/test/test/random bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes transferred in 2.219539 secs (47242957 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME       AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test        480M   400M         0     23K              0       400M    23K      23K
test/test   780M   400M         0    100M           300M          0   100M     100M
```

100Mのファイルを450Mのファイルで上書きすると、`REFER`も`WRITTEN`も450Mとなる。保証枠を使い果たしたので`USEDREFRESERV`は0に、`USED`が450Mになる。親ファイルシステムにも影響が及び、`AVAIL`は親子ともに430Mとなる。

```shell-session
# dd if=/dev/random of=/test/test/random bs=1M count=450
450+0 records in
450+0 records out
471859200 bytes transferred in 10.956761 secs (43065572 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME       AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test        430M   450M         0     23K              0       450M    23K      23K
test/test   430M   450M         0    450M              0          0   450M     450M
```

ここでスナップショットを作成する。スナップショットは空き領域を消費しないはずだが、親ファイルシステムの`AVAIL`はたったの29Mになった。test/testの`REFER`は450Mのまま変わらないが、スナップショットとして確保されたデータが外へ出て`WRITTEN`が0になっている。その分`USEDREFRESERV`が回復して400Mとなり、今あるファイルと合わせて`USED`は850Mになった。これが親ファイルシステムの空き領域が少なくなった理由である。スナップショット作成によって予約領域が丸々回復するというのは、一見直感に反する挙動であるが、後でボリュームについて見るように意図されたものである。

```shell-session
# zfs snapshot test/test@test
# zfs list -o space,refer,written -t all -r test
NAME            AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test            29.0M   851M         0     23K              0       851M    23K      23K
test/test        429M   850M         0    450M           400M          0   450M        0
test/test@test      -      0         -       -              -          -   450M     450M
```

ここで別に50Mのファイルを作成する。`WRITTEN`が50Mとなり、その分`USEDREFRESERV`が減って350Mに。`REFER`は2ファイル合わせて500Mとなっている。`USED`には影響がなく、したがって親ファイルシステムにも影響はない。親ファイルシステムの空き領域より大きなファイルを作成することができている。

```shell-session
# dd if=/dev/random of=/test/test/random2 bs=1M count=50
50+0 records in
50+0 records out
52428800 bytes transferred in 1.041906 secs (50320095 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME            AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test            29.5M   850M         0     23K              0       850M    23K      23K
test/test        379M   850M       13K    500M           350M          0   500M    50.0M
test/test@test      -    13K         -       -              -          -   450M     450M
```

スナップショットを破棄する。スナップショットで確保していた450Mが戻ってくるため、保証枠が使い果たされ`USEDREFRESERV`は0に。その結果`USED`が500Mになって、親ファイルシステムの`AVAIL`が379Mに回復する。

```shell-session
# zfs destroy test/test@test
# zfs list -o space,refer,written -t all -r test
NAME       AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test        379M   501M         0     23K              0       501M    23K      23K
test/test   379M   500M         0    500M              0          0   500M     500M
```

ここで再びスナップショットを作成しようとしても、作成できない。スナップショットの作成は空き容量を消費しないはずなのにout of spaceと言われてしまう。これは500Mのデータを自身の外で確保したいのだが、`AVAIL`は379Mしかないので実行できないということ。

```shell-session
# zfs snapshot test/test@test
cannot create snapshot 'test/test@test': out of space
```

###reservationの場合
ちなみにreservationの場合は子孫データセットを含めて管理するので、スナップショットにまつわる困惑する挙動は起こらない。ただし、予約した領域は`USED`に計上されないという大きな違いがある。親ファイルシステムの`USERCHILD`に計上されているのだが、これが何に由来するのかは解りにくい。

```shell-session
# zpool create -O atime=off test /tmp/testpool
# zfs list -o space,refer,written -t all -r test
NAME  AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test   880M  78.5K         0     23K              0      55.5K    23K      23K
# zfs create -o reservation=400M test/test
# zfs list -o space,refer,written -t all -r test
NAME       AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test        480M   400M         0     23K              0       400M    23K      23K
test/test   880M    23K         0     23K              0          0    23K      23K
```

予約したサイズ内であれば親ファイルシステムに影響なく利用できるし、サイズを超えれば空き領域が減るのはrefreservationのときと同じ。大きく違うのは、親ファイルシステムの空き領域と関係なくsnapshotの作成が可能であること。

```shell-session
# dd if=/dev/random of=/test/test/random bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes transferred in 2.134436 secs (49126618 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME       AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test        480M   400M         0     23K              0       400M    23K      23K
test/test   780M   100M         0    100M              0          0   100M     100M
# dd if=/dev/random of=/test/test/random bs=1M count=500
500+0 records in
500+0 records out
524288000 bytes transferred in 10.376187 secs (50528002 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME       AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test        380M   500M         0     23K              0       500M    23K      23K
test/test   380M   500M         0    500M              0          0   500M     500M
# zfs snapshot test/test@test
# zfs list -o space,refer,written -t all -r test
NAME            AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test             379M   501M         0     23K              0       501M    23K      23K
test/test        379M   500M         0    500M              0          0   500M        0
test/test@test      -      0         -       -              -          -   500M     500M
```

##ボリュームの場合
400Mのボリュームを作成するとrefreservationによって400M＋αが予約されている。このボリュームはほぼ未使用だが予約領域が`USEDREFRESERV`に計上され、`USED`が414Mとなっている。

```shell-session
# zpool create -O atime=off test /tmp/testpool
# zfs list -o space,refer,written -t all -r test
NAME  AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test   880M   124K         0     23K              0       100K    23K      23K
# zfs create -V 400M test/volume
# zfs list -o name,used,avail,refer,volsize,refreserv -t volume -r test
NAME          USED  AVAIL  REFER  VOLSIZE  REFRESERV
test/volume   414M   880M    12K     400M       414M
# zfs list -o space,refer,written -t all -r test
NAME         AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test          465M   415M         0     23K              0       415M    23K      23K
test/volume   880M   414M         0     12K           414M          0    12K      12K
```

ここでボリュームに100Mほど書き込むと、`REFER`と`WRITTEN`が101Mとなり、`USEDREFRESERV`はその分減って313Mになる。

```shell-session
# dd if=/dev/random of=/dev/zvol/test/volume bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes transferred in 2.397016 secs (43745055 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME         AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test          465M   415M         0     23K              0       415M    23K      23K
test/volume   779M   414M         0    101M           313M          0   101M     101M
```

スナップショットを作成すると、ファイルシステムのときと同様に予約領域が丸々回復して414Mとなり、その分親ファイルシステムの空き領域が減ってしまう。くり返しになるが、これは意図された挙動である。

```shell-session
# zfs snapshot test/volume@test
# zfs list -o space,refer,written -t all -r test
NAME              AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test               364M   516M         0     23K              0       516M    23K      23K
test/volume        779M   516M         0    101M           414M          0   101M        0
test/volume@test      -      0         -       -              -          -   101M     101M
```

ボリュームの残り300Mを書き込んでみる。`WRITTEN`が303Mとなり、その分`USEDREFRESERV`が減って111Mとなった。もうボリュームには空きがないのに、まだ111Mも予約していることになる。しかしこれは無駄なのではなく、必要があって予約されているのである。

```shell-session
# dd if=/dev/random of=/dev/zvol/test/volume bs=1M seek=100 count=300
300+0 records in
300+0 records out
314572800 bytes transferred in 7.246102 secs (43412695 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME              AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test               364M   516M         0     23K              0       516M    23K      23K
test/volume        476M   516M       56K    404M           111M          0   404M     303M
test/volume@test      -    56K         -       -              -          -   101M     101M
```

今度はボリュームの先頭100M分を書き換える。もともと書き込まれていたデータはスナップショットに確保されており、スナップショットの`USED`が101Mになっている。ボリュームの`USED`は516Mとなっているが、その内訳はスナップショット作成前に書き込まれていた100M分が`USEDSNAP`に、その後書き込んだ300M分と書き換えた先頭100M分が`USEDDS`に計上されており、予約領域`USEDREFRESERV`をほぼ使い果たしている。

```shell-session
# dd if=/dev/random of=/dev/zvol/test/volume bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes transferred in 2.648745 secs (39587648 bytes/sec)
# zfs list -o space,refer,written -t all -r test
NAME              AVAIL   USED  USEDSNAP  USEDDS  USEDREFRESERV  USEDCHILD  REFER  WRITTEN
test               364M   516M         0     23K              0       516M    23K      23K
test/volume        375M   516M      101M    404M          10.5M          0   404M     404M
test/volume@test      -   101M         -       -              -          -   101M     101M
```

このように、通常ならばスナップショット作成時には空き領域の消費はなく、そのあと上書きされた時に上書きされた分だけ空き容量が消費されるところを、refreservationを使うことでスナップショット作成時に今後上書きされる可能性のある量をまとめて空き容量から予約してしまうということをやっている。ボリュームで「今後上書きされる可能性のある量」とは、すなわちボリュームのサイズ（と管理用の＋α）に他ならないわけである。

#まとめ
refreservationの挙動は一見すると直感に反するように思える。とくに空き領域が足りずスナップショットが作成できないことがあることには驚く。しかしこれは、スナップショットを作成するたびに、必要な予約量を確保し直していると考えれば当然のことである。これはボリュームを利用する際に、常にボリュームの全域に書き込める状態を保証するために設計された挙動だと考えられる。

以上を踏まえて、reservationとrefreservationの使い分けについて考える。

* refreservationはボリュームの動作を保証するための仕掛けである[^1]。スナップショット作成時に空き領域を消費することに十分留意する。reservationではボリュームの動作を保証できず、それならば予約を設定しなくても同じことである。
* 通常のファイルシステムで最低使用量を保証したい場合はreservationを使う方が混乱が少ない。どうしてもrefreservationを使う必要がある場合には、上記の挙動で空き領域を使い果たしたり、スナップショット作成に失敗したりしないよう、十分気を付けなければいけない。