Tomoya's Blog (ja)

Just a blog, nothing fancy

ZFS SnapshotをGCPのColdline Storageに上げてみた

この記事はCAMPHOR- Advent Calendar 2016第6日目の記事です。

ZFSにはsnapshotを送受信する機能があり、バックアップを取るのに使用できる。安価にオフサイトバックアップを取るべく、Google Compute PlatformのColdline Storageにそのスナップショットを保存してみた。

理想と現実…

ZFSを使用しているシステム間であるsnapshotを送るとき、
# zfs send zroot/dataset/to/backup@snap | ssh remote-host "zfs recv zroot/backup
とする。続けて差分のsnapshotを送るときは
# zfs send -i zroot/dataset/to/backup@snap zroot/dataset/to/backup@snap2 | ssh remote-host "zfs recv zroot/backup"
とすればよい。recv側に指定したdatasetが受信したsnapshotで置き換えられる。
上の2コマンドで送信したsnapshotをローカルに受信して復元するときは
# ssh remote-host "zfs send zroot/backup@snap2" | zfs recv zroot/dataset/to/restore
で済む。これらを自動化するスクリプトは存在するし、簡単に書くこともできる。
しかし、Coldline Storageにはファイル単位でデータをアップロードしなければならないので、snapshotを送る際のパイプの後半と、snapshotから復元するときのパイプの前半はファイルへの読み書きでなければならない。Coldline Storageにファイルに書きだすのは標準出力のリダイレクションで済む。問題は、zfs sendでファイルに書きだしたsnapshotの差分をどうやってzfs recvで読み込むかだ。以下では

  • zroot/dataset/to/backup@snap1
  • zroot/dataset/to/backup@snap2
  • zroot/dataset/to/backup@snap3

の3つのsnapshotがあるとする。

ダメな方針: snapshotの差分ファイルをすべてcatで結合してrecvする

zfs send zroot/dataset/to/backup@snap1 > snap1
zfs send -i zroot/dataset/to/backup@snap1 zroot/dataset/to/backup@snap2 > snap2
zfs send -i zroot/dataset/to/backup@snap2 zroot/dataset/to/backup@snap3 > snap3
とsnapshotを書き出し、snap1snap2snap3の3つのファイルを得たとき、
cat snap1 snap2 snap3 | zfs recv -F zroot/dataset/to/restore
で読み込みを試した。しかし、@snap1のみが復元され、@snap2@snap3は無視された。これではダメだ…

差分ファイルを作成した順にrecvする

仕方がないのでそれぞれのsnapshotファイルを順にrecvすることに。
cat snap1 | zfs recv zroot/dataset/to/restore
cat snap2 | zfs recv zroot/dataset/to/restore
cat snap3 | zfs recv zroot/dataset/to/restore
これだとちゃんと復元できるが、snapshotの数が増えると全て手でやるのが面倒になる。スクリプトを書いて自動化したい。

というわけで作ってみた

Coldline Storage (に限らずgsutilで読み書きできるストレージ全般) にsnapshotを保存するzfs2gcpというスクリプトを作ってみた。なんとPOSIXシェルで動く! ソースはこちら。スクリプトからgsutilを呼んでいるので、動作にはgsutilがパスに通っている必要がある。ZFS2GCP_BUCKET環境変数にCloud Storageのバケットのurlをセットしたうえで次のように使用する。

snapshotを取って送信するとき

# zfs2gcp backup zroot/dataset/to/backup
バックアップするdatasetを指定するだけで、勝手にローカルにsnapshotを作成してこのスクリプトで作成した直前のsnapshotとの差分をColdline Storageに保存してくれる。

バックアップされたsnapshotの一覧を表示するとき

# zfs2gcp list
バケット内のsnapshotファイルの一覧を表示。特定のdatasetに関して表示したいときは
# zfs2gcp list <dataset>
で出力を絞れる。
出力はzroot/dataset/to/backup@zfs2gcp_1480840292のようにsnapshot名がzfs2gcp_となっているが、これはUNIX時間でのsnapshotが取られた時刻である。

snapshotから復元するとき

# zfs2gcp restore zroot/backup@zfs2gcp_timestamp zroot/dataset/to/restore
復元したいsnapshotの名前をdataset@zfs2gcp_<timestamp>の形式で指定すると、指定したsnapshotのファイルとその読み込みに必要なすべてのsnapshotのファイルを、Coldline Storageから落として来て正しい順に復元先にrecvしてくれる。

今後の課題

  • エラー処理: ファイルが存在しないなどのエラー時に、綺麗に終了するようにする。
  • snapshotファイルの削除
  • 既存のsnapshotをアップロードするモードの追加 (現在は新規に専用のsnapshotを作成している)
  • ホストごとに一意的なidを振る: 現状では別のホストで同名のdatasetに対して同時刻にバックアップを実行すると、Coldline Storage側でファイル名が衝突する。zfs2gcp listの出力には別ホストのsnapshotも含めて一緒くたに表示される。
  • 暗号化: 開発されている途中であるZFSでの暗号化が完成すれば、スクリプトで実装する必要性はなくなるかもしれない。
  • snapshotファイルのチェックサム: ZFSのrecvは受信時にファイルの誤りを訂正してくれないから、こちらで何らかの手段で整合性を担保しなければならない。

…時間があるときにやろう。