お急ぎの方は「概要」→「意義」→「まとめ」と読めばそれで済みます。
動機
ZFSでデータセットの使用量制御に用いるrefreservationプロパティを調べていたら、自分の直感に反する挙動を示したので整理する。ていうか、ドキュメントをなんとかして欲しい。
前提
- OpenZFS (FreeBSD 11.1-RELEASE-p9)
概要
refreservationプロパティは子孫を含まないそのデータセット自身の使用量を予約する。この「データセット自身の使用量」はREFER
のことだと思いがちだが、実際にはWRITTEN
に相当するようである。WRITTEN
は最新のスナップショット以降に書き込まれ参照可能なデータの量であり、スナップショットが存在しない場合のみREFER
に等しい。つまりrefreservationを設定することは、最新のスナップショット以降に上書きまたは新規に書き込み保持できるデータ量を保証していることになる。そのため、
- スナップショットを作成するたび予約量が丸々回復する
- 前回スナップショットからの差分に相当する空き領域がないとスナップショットが作成できない
という一見直感に反する挙動を示す。これはボリュームの動作を保証するための挙動だと考えれば理解しやすい。
お急ぎコース→意義
refreservationとは
zfsでは空き容量をプールに属する多くのデータセット(ファイルシステムやボリューム)で共有しており、何も設定しなければ全てのデータセットが平等に扱われて、使った者勝ちで空き容量を消費していく。そこで特定のデータセットを特別扱いする仕組みが予約とクオータで、クオータが「最大利用可能量」であるのに対し予約は「最小利用保証量」と考えればよい。
予約に関わる書き込み可能なプロパティが2種類ある。zfs(8)の記述を要約すると、
- reservation
そのデータセット自身と全ての子孫の合計使用量、つまりUSED
の最小保証量。合計使用量が設定に満たない場合でも、そのデータセットが設定された量を使用しているかのように振る舞う。予約されている分量は親データセットの使用量に計上され、親データセットの予約やクオータの計算に組み込まれる。 - 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ではボリューム(zvol)を挙げている。
ボリュームはブロックデバイスであるかのように振る舞うデータセットである。ZFSのデータセットは需要に応じて空き領域を消費するため特に固定のサイズはないが、ボリュームの場合はvolsizeプロパティで決まるサイズが存在する。通常はサイズが1Gのブロックデバイスには1Gのデータを書き込めるのが当たり前の前提であり、400M書き込んだ時点で書き込めなくなるようなことは想定されていない。そこでデフォルトではボリュームに予約が設定され、1Gのボリュームに1Gのデータを書き込めるように保証している1。refreservationが導入される前はreservationで予約していたが、この場合ボリュームのスナップショットを作成すると予約領域がスナップショットに消費されてしまい、1G予約しても1G書き込めない事態が発生する可能性がある。
ではどうしたら、常に1Gのボリュームの全域に書き込める状態を保証できるだろうか。答えは簡単で、スナップショットを作成するたびに1Gの領域を予約し直せばよい。これがrefreservationで予約されたデータセットが示す、「直感に反する」挙動の正体である。
お急ぎコース→まとめ
実際の挙動
このスナップショットに関わるrefreservationの挙動はなかなか直感的に理解することが難しいので、ファイルをvdevにしたプールを使って挙動を確認していこう。
# 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の空きが保証されているということである。
# 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
は変化せず、親ファイルシステムには影響しない。
# 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となる。
# 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になった。これが親ファイルシステムの空き領域が少なくなった理由である。スナップショット作成によって予約領域が丸々回復するというのは、一見直感に反する挙動であるが、後でボリュームについて見るように意図されたものである。
# 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
には影響がなく、したがって親ファイルシステムにも影響はない。親ファイルシステムの空き領域より大きなファイルを作成することができている。
# 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に回復する。
# 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しかないので実行できないということ。
# zfs snapshot test/test@test
cannot create snapshot 'test/test@test': out of space
reservationの場合
ちなみにreservationの場合は子孫データセットを含めて管理するので、スナップショットにまつわる困惑する挙動は起こらない。ただし、予約した領域はUSED
に計上されないという大きな違いがある。親ファイルシステムのUSERCHILD
に計上されているのだが、これが何に由来するのかは解りにくい。
# 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の作成が可能であること。
# 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となっている。
# 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になる。
# 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となり、その分親ファイルシステムの空き領域が減ってしまう。くり返しになるが、これは意図された挙動である。
# 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も予約していることになる。しかしこれは無駄なのではなく、必要があって予約されているのである。
# 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
をほぼ使い果たしている。
# 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を使う必要がある場合には、上記の挙動で空き領域を使い果たしたり、スナップショット作成に失敗したりしないよう、十分気を付けなければいけない。