2013年8月18日日曜日

zfs send/recvについての考察

MacZFSのalpha版、試しているのですがちゃんと動きません。具体的にはFinderからZFSボリュームにフォルダをコピーするとクラッシュ。カーネルパニックすらなく突然再起動。
自分のビルド/導入方法が悪いのかとも思ったが少し古め(20130730)のビルド済み版でもクラッシュ。実機でもクリーンインストールしたVMでもダメだったので環境依存では無さそうだが・・・

本当にバグのようならレポートしないといけないですね。しかしMLも複数あるしgithubやircもあるしで、いったいどこに報告すればいいのやら・・・


さて、表題の話です。
ZFSはsnapshotを利用した高度なバックアップが実現できることが魅力ですが、これらsnapshotの受け渡しに利用されるzfs send/recvの挙動は中々謎が多いです(個人的には)。
公式のドキュメントを読めばいいはずなのですが、素人には少々難解で正直何を言っているか掴みきれない面があります。何かと直訳っぽいのが難解さの主要因ですが、かといって本家英語版ドキュメントを読んでも自分の頭では理解しきれなかったりします。

というわけで後々のために、今回は自分に必要な機能について検証してみたいと思います。
環境はZEVO・・・ではなくZFS on Linux 0.6.1です。
「後々のために」というのは、今書いているZFS向けの自動バックアップスクリプトのことで、そこでzfs send/recvの利用を想定しているためです。
とりあえず「ZFS TimeMachine」という安直かつ既出の名前で呼んでいるのですが、その名の通りAppleのTimeMachineに着想を得て(要はパクって)書いてます。

似たようなスクリプトは数え切れないほど既出なのですが、
・@yesterdayのような相対名ではなく@2012-0304のような日付で管理できる
・ローカルスナップショットだけでなくsend/recvを利用して別プール・別マシンへ転送できる
・ローカルにはバックアップを保持せず容量を食わない(Apple風)
・スナップショットの取得間隔(毎時・毎日・毎週・毎月・毎年)を細かく設定できる
・設定に応じて毎時→毎日のようにスナップショットを適宜リネームできる(Apple風)
・子孫ファイルシステムのバックアップ有無も指定できる
・24時間稼働でないマシンでも可能な限り取得漏れが起こらない
・24時間稼働でないマシンでも削除漏れやリネーム漏れが起こらない
を全て満たすスクリプトを見つけられなかったので自分で書くことにしました。

見つけられなかったというところがミソで、要は同系統のスクリプトが多すぎて機能を把握しきれないのと、どれが決定版と呼べる代物か見当もつかなかったわけです。
別に決定版ではなくマイナーなモノでも用途に合致してればなんの問題もないわけですが、そこは日本人気質(?)。

そうして自己満足による車輪の再発明をすることで類似品の氾濫に自ら加担するわけですが、自分書きにしない要素はとことん適当なため、ruby1.9以降が無いと動かない・設定可能項目が多すぎてiniっぽいファイルを書かないと動かない(スクリプトの引数じゃダメ)・そもそもスクリプトが1ファイルに収まってない(無理矢理やればできるけどやらない)という酷いものに。
なのでほぼ完成してはいるものの、zfs send/recvの仕様をちゃんと決めても公開できるものにはならないかもしれない。


大幅にズレましたがzfs send/recvの話に戻ります。
上記のスクリプトを踏まえると
・子孫ファイルシステムにも対応させる
・ローカルには最新スナップショットだけ残して最新の差分だけsendする
この2点をどう両立させられるかを確かめる必要があります。

というわけで以下のようなデータセットを用意します。
pool
NAME
r0pool
r0pool/src
r0pool/src/child
pool/srcを今回のバックアップ対象として、
# zfs snapshot -r pool/src@00
# zfs send -R pool/src@00 | zfs recv pool/tgt
# zfs list -o name -t snapshot
NAME
pool/src@00
pool/src/child@00
pool/tgt@00
pool/tgt/child@00
このようにしてバックアップ先(受信側)のpool/tgtを作成します。ここまでが下準備です。
zfs snapshotの引数に-rを付けることで子孫ファイルシステムであるpool/src/childまで含めてスナップショット@00を取得できます。
また、zfs send -Rとすることで子孫ファイルシステムのスナップショットまでまとめて送信できます。このときpool/tgtが自動的に作成されます(事前に作る必要無し)。

ちなみに、zfs send -Rとzfs send -rには細かい違いがあるのですが、-rの方は本家SolarisのZFSにしか存在しないオプションらしく、それ以外のOSSな実装では利用できません。
ZFS on Linuxではusageには出てきますが実際は使えません。
ZEVOはもちろんのこと、OpenIndianaの最新版(151a8)でも利用不可だったため今後Solaris以外で使えるようになることは無いんじゃないかと思います。
というわけで、-rオプションは使わない(Solarisを利用しての検証もしない)ことにします。

さて、pool/srcとpool/tgtの用意ができたので、まず
# zfs snapshot -r pool/src@01
# zfs list -o name -t snapshot
NAME
pool/src@00
pool/src@01
pool/src/child@00
pool/src/child@01
pool/tgt@00
pool/tgt/child@00
として最新スナップショット@01を取得します。差分送信のために必要な@00は残しておきます。
差分送信のためのzfs sendのオプションは-iなので、@00と@01の差分送信を
# zfs send -R -i @00 pool/src@01 | zfs recv pool/tgt
# zfs list -o name -t snapshot
NAME
pool/src@00
pool/src@01
pool/src/child@00
pool/src/child@01
pool/tgt@00
pool/tgt@01
pool/tgt/child@00
pool/tgt/child@01
このように行います。無事送信できました。pool/tgtだけが過去のスナップショットを保持していればいいので
# zfs destroy -r pool/src@00
# zfs rename -r pool/tgt@00 @00a
# zfs rename -r pool/tgt@01 @01a
# zfs list -o name -t snapshot
NAME
pool/src@01
pool/src/child@01
pool/tgt@00a
pool/tgt@01a
pool/tgt/child@00a
pool/tgt/child@01a
pool/srcの@00は削除します。また、スクリプトでの@2012-0304-05 -> @2012-0304のようなリネームを想定し、pool/tgtのスナップショットを@00a、@01aのようにリネームしました。
(実際はリネーム後のスナップショットが最新の差分生成元スナップショットにならないような設計のつもりですが、実験のため適当にリネームしています)

次にpool/srcの方で@02を取得し、先ほどと同様にsend/recvします。
# zfs snapshot -r pool/src@02
# zfs send -R -i @01 pool/src@02 | zfs recv pool/tgt
# zfs list -o name -t snapshot
NAME
pool/src@01
pool/src@02
pool/src/child@01
pool/src/child@02
pool/tgt@00a
pool/tgt@01
pool/tgt@02
pool/tgt/child@00a
pool/tgt/child@01
pool/tgt/child@02
送信はできましたがpool/tgtの@01aが@01に戻ってしまっています。これは-Rオプションの仕様で、子孫ファイルシステムを含む複数のファイルシステムにまたがるスナップショットを転送する関係上、受信側のスナップショット名とのコンフリクトを避けるために名前の保持が必要だからではないかと(勝手に)思っています。子孫スナップショットを扱わない(-Rを利用しない)場合には起きない問題です。

また、@00aがそのままなのはpool/src側に該当するスナップショットが存在しないため名前が渡されなかったからです(多分)。
恐らく自分にとっては問題無い挙動ではあるものの、できれば差分生成元のスナップショット名も維持して欲しい気分にはなります。

しかし、問題はスナップショット名の維持だけではありません。zfs send/recvは、受信側ファイルシステム(pool/tgt)において最新スナップショットからの変更が無いことを前提に動作しているので、送信先ファイルシステムになんらかの書き込みがあった場合は問題が発生します。
# zfs destroy -r pool/src@01
# zfs rename -r pool/tgt@01 @01a
# zfs rename -r pool/tgt@02 @02a
# zfs snapshot -r pool/src@03
# zfs list -o name -t snapshot
NAME
pool/src@02
pool/src@03
pool/src/child@02
pool/src/child@03
pool/tgt@00a
pool/tgt@01a
pool/tgt@02a
pool/tgt/child@00a
pool/tgt/child@01a
pool/tgt/child@02a
先ほどと同様のこのような状態から
# touch /pool/tgt/hoge
# zfs send -R -i @02 pool/src@03 | zfs recv pool/tgt
cannot receive incremental stream: destination pool/tgt has been modified
since most recent snapshot
pool/tgtにアクセスした後では送信に失敗します。最悪の場合、atime=onのファイルシステムでは書き込みどころかファイルリードだけでも失敗する恐れがあります。

受信側ファイルシステムをマウントしなければ良いだけの話ではあるものの、誤ってマウントしてしまう可能性は除外できないし、手動実行ならまだしも定期実行のスクリプトではできるだけ排除したい問題です。

そういった問題への対処方法としてzfs recvには-Fオプションが用意されています。
-Fオプションによって最新スナップショットへの強制ロールバックが行われるため上記の問題は解決することができます。動作としては以下のようになります。
# zfs send -R -i @02 pool/src@03 | zfs recv -F pool/tgt
# zfs list -o name -t snapshot
NAME
pool/src@02
pool/src@03
pool/src/child@02
pool/tgt@02
pool/tgt@03
pool/tgt/child@02
pool/tgt/child@03
・・・あれ?
pool/tgt側の過去スナップショット(@00a、@01a)が消えています。名前が戻るだけならまだしもこれはマズイです。
どうやらsend -Rとrecv -Fの組み合わせだと、受信側は受け取ったスナップショットストリームに厳密に合わせたスナップショット構成に変更してしまうようです。
こちらの問題(?)も同様に-Rオプション無しではrecv -Fとしていても発生しません。send -Rによって生成される複雑なスナップショットストリームを扱っていることでこのような挙動になるのでしょうか。

何はともあれ、これでは「送信元には差分生成元になる最新スナップショットだけ残し、バックアップ先に差分を蓄積していく」という仕様が実現できません。
これを解決するためには
・pool/tgtが確実にマウントされないようにするor確実にread-onlyでマウントする(できるのか?)
・send/recvの度に別途rollbackを実行する
・send -Rを利用せず、複数回のsend/recvによって子孫スナップショットを送信する
この3つが考えられそうです。

1つ目に関しては乗り気にはなれない。不便さが増しそう(な気がする)。
2つ目は確実そうですが、子孫スナップショットまで一括のrollbackができない(-rは子孫スナップショットではなく、過去スナップショットを一括で扱う場合のオプション)ため、手間は3つ目と変わらなさそうです。なので3つ目がいいかな?

というわけで、zfs send/recvについて別に深くもない検証でした。
zfs send/recv周りは最新のSolarisでしか使えないオプションが複数あるくらい複雑かつ発展途上な感じなので、スクリプトにせよ手動にせよ使う場合には自分の環境と用途に合わせて色々試してみる必要がありそうです。

0 件のコメント:

コメントを投稿