スナップショット整理スクリプトがレプリケーションを壊した話
友人宅へのオフサイトレプリケーションが、ある夜から突然フル転送に戻ってしまい、細い自宅アップロード回線で3日がかりになったことがあります。原因は、スナップショット整理スクリプトがincremental sendに必要な共通スナップショットを消してしまっていたことでした。フル/incremental sendの仕組み、bookmarkによる軽量な差分管理、resumable sendが実際にどう救ってくれるかを整理します。
友人宅のNASへzfs send | zfs recvで毎晩オフサイトレプリケーションを組んでいたことは、暗号化の記事で少し触れました。ある晩から、いつもは数分で終わるはずの差分転送が、いきなり数百GB規模のフル転送に戻ってしまいました。自宅のアップロード回線は決して太くないので、これだけで3日がかりの仕事になりました。
原因を追うと、別に組んでいた古いスナップショットの整理スクリプトが、レプリケーションに使っていた共通のスナップショットまで一緒に消していました。incremental sendは送信元と受信先の両方に同じスナップショットが残っていることが前提で、その土台を自分で壊していたわけです。
この記事では、フルsendとincremental sendの仕組み、この事故を防ぐbookmarkという軽量な仕組み、そして万が一転送が途中で切れたときに救ってくれるresumable sendを整理します。
フルsendとincremental sendの違い
zfs sendには大きく2つのモードがあります。
# フルsend: スナップショットの中身をまるごと転送
zfs send tank/data@snap1 | zfs recv backup/data
# incremental send: 2つのスナップショットの差分だけを転送
zfs send -i tank/data@snap1 tank/data@snap2 | zfs recv backup/data
incremental send(-i)は、送信元にある2つのスナップショット間の差分ブロックだけを送るため、データ量が少なければ数秒〜数分で終わります。ただし成立条件が1つあり、基準となる@snap1が受信先(backup/data)にも同じ内容で存在していなければなりません。この基準スナップショットのことを、レプリケーションの文脈では「共通スナップショット」と呼びます。
冒頭の事故は、この共通スナップショットを送信元側で消してしまったケースです。共通の基準点が消えた時点で、ZFSは差分を計算する術を失い、次のレプリケーションはフルsendとしてやり直すしかなくなります。
-iと-Iの違い: 中間スナップショットを残すかどうか
-iは指定した2点間の差分だけを送りますが、-I(大文字)はその間に存在するすべての中間スナップショットも合わせて転送し、受信先にも同じスナップショット群を再現します。
# @snap1〜@snap4の間に@snap2, @snap3があった場合
zfs send -I tank/data@snap1 tank/data@snap4 | zfs recv backup/data
# → 受信先にも@snap2, @snap3が個別に作られる
バックアップ先で細かい世代管理をしたい場合は-I、最新状態だけ同期できればよい場合は-iで十分です。うちの友人宅NASへのレプリケーションは、復元時に選べる世代を増やしたかったので-Iを使っています。
bookmarkで「消してもいい共通点」を作る
共通スナップショットを送信元にずっと残しておくのは、地味にディスクを圧迫します。特に日次バックアップを何ヶ月分も回していると、その分だけ古いスナップショットが積み上がっていきます。ここで使えるのがbookmarkです。
zfs bookmark tank/data@snap1 tank/data#snap1
bookmarkは、スナップショットの「その時点を指すポインタ」だけを軽量に保持する仕組みで、実データ(ブロック)は保持しません。作成後は元の@snap1を削除しても、#snap1を基準にしたincremental sendが可能です。
zfs send -i tank/data#snap1 tank/data@snap2 | zfs recv backup/data
冒頭の事故のあと、うちのスナップショット整理スクリプトは、古いスナップショットを消す前に必ずbookmarkを打ってから消す、という手順に直しました。実データを保持し続けるコストを払わずに、差分計算の基準点だけを守れるようになっています。ただし、bookmarkはあくまで送信元側の話です。受信先の対応するスナップショット自体を消してしまえば、そもそも受信先に差分を当てる土台がなくなるので、そちらは別途保護が必要です。
resumable send: 転送が途中で切れても最初からやり直さない
友人宅への回線は決して安定しているとは言えず、フルsendの3日間のうち、実は一度転送が途中で切れたことがあります。以前のZFSならここで最初からやり直しでしたが、今のOpenZFSにはresumable sendがあります。
転送が中断すると、受信先にはreceive_resume_tokenというプロパティが残ります。
zfs get receive_resume_token backup/data
このトークンを使えば、途中からの再開が可能です。
zfs send -t $(zfs get -Hp -o value receive_resume_token backup/data) | zfs recv backup/data
実際に使ってみて助かったのは、すでに転送済みのブロックを再送しなくていいという点です。細い回線でフルsendをやり直す苦痛を考えると、resumable sendがなければあの3日間はもっと長くなっていたはずです。
今のレプリケーション運用
- スナップショットを削除する前に、レプリケーションで使っている共通スナップショットかどうかを必ず確認する(またはbookmarkを打ってから消す)
- 世代管理が必要な用途は
-I、最新同期だけでよい用途は-iを使い分ける - 転送が切れたら、まず
receive_resume_tokenの有無を確認してから再送する。フルsendからやり直すのは最終手段 - 定期レプリケーションのジョブと、スナップショット整理のジョブは別々のスクリプトで運用しがちなので、片方が消してよいスナップショットを、もう片方が使っていないかを必ず確認する
暗号化の記事で書いたraw sendの話も、結局はこのincremental send/bookmarkの仕組みの上に乗っています。オフサイトレプリケーションを自動化するなら、「何を消していいスナップショットとして扱うか」を先に設計しておくことをおすすめします。うちはあの3日間のおかげで、それを痛いほど学びました。