ZFS

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

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

動機

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

前提

  • OpenZFS (FreeBSD 11.1-RELEASE-p9)

概要

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

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

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

お急ぎコース→意義

refreservationとは

zfsでは空き容量をプールに属する多くのデータセット(ファイルシステムやボリューム)で共有しており、何も設定しなければ全てのデータセットが平等に扱われて、使った者勝ちで空き容量を消費していく。そこで特定のデータセットを特別扱いする仕組みが予約とクオータで、クオータが「最大利用可能量」であるのに対し予約は「最小利用保証量」と考えればよい。

予約に関わる書き込み可能なプロパティが2種類ある。zfs(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ではボリューム(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のファイルで上書きすると、REFERWRITTENも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ほど書き込むと、REFERWRITTENが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を使う必要がある場合には、上記の挙動で空き領域を使い果たしたり、スナップショット作成に失敗したりしないよう、十分気を付けなければいけない。

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