Proxmox の vzdump が監視を誤爆させていた — ZFS スナップショットと sanoid でバックアップを分ける

毎日03:00に走る vzdump が、04:00〜04:15 に I/O を飽和させ、監視スクレイプをタイムアウトさせていた。その結果、ノードが落ちていないのに「NodeDown」が誤発報する。本記事では、発生源である日次のフルダンプの自動実行を止め、世代管理を ZFS スナップショット(sanoid)へ移した経緯を書く。判断の根拠になった実測値と、導入・ロールバックの手順、そして残した課題を書く。

目次

vzdump が起こしていた誤検知の正体

症状は単純である。03:00に始まる vzdump が全ゲスト(約24台)を順次フル読みし、zstd で圧縮する。区間全体で I/O が高止まりし、04:11〜04:18 に監視のスクレイプがタイムアウトして、NodeDown が発報する。ノードは正常なのに、である。

実測したタスクログでは、03:00 開始に対して完了は 04:34 頃であった。90分以上にわたり I/O が高止まりする計算になる。バックアップ時間帯の外(11:11 実測)の /proc/pressure/io は some 4.09% / full 3.82% で、平常時は低負荷である。つまり負荷はバックアップの時間帯に集中して発生している。

ここで重要なのは、監視だけを直しても発生源の負荷は残るという点である。しきい値やタイムアウトを緩めれば誤報は消えるが、ゲストの I/O は毎日90分間圧迫され続ける。だから今回は、監視側ではなく発生源そのものを軽くする方向で設計した。

実測した保管の実体 — 659G・8世代・同一プール

まず、保管の実体を確認した。推測で設計しないためである。

  • du -sh /var/lib/vz/dump = 659G、世代は 2026_09_27〜2026_10_10 の 8 世代
  • zfs list では rpool が 1.04T USED / 1.60T AVAIL、そのうち rpool/ROOT/pve-1 が 746G(ダンプの保管先)、rpool/data が 255G(ゲスト実体)
  • proxmox-backup-client は pve01 / pve03 の両方に導入済みだが、PBS サーバ本体は未設置
  • pve03 は du -sh /var/lib/vz/dump が 41K(空)で、rpool も 118G USED / 123G AVAIL しかなく、退避先として容量不足
  • pve01 には sanoid / zfs-auto-snapshot とも未導入。apt 候補には sanoid 2.1.0-1.1 があった
  • 既存スナップショットは pvesr の __replicate_*(32件)と手動の pre-op17-upgrade のみ
  • pvesr は全 22 ジョブが State=OK / FailCount=0(15分毎・宛先 pve03)

ジョブ設定も確認した。/etc/pve/jobs.cfg にある vzdump ジョブは次の内容である。

vzdump: weekly-backup-ansible
        schedule 03:00
        all 1
        compress zstd
        exclude 105
        mode snapshot
        node pve01
        prune-backups keep-daily=7,keep-monthly=3,keep-weekly=4
        storage local

ジョブ名は weekly-backup-ansible だが、schedule 03:00 に曜日トークンがない。つまり実測では毎日動いていた。名前に引きずられて「週次で動いている」と思い込むと、負荷の見積もりを誤る。ここは実測で確定させるべき点である。

保管先が rpool/ROOT/pve-1、ゲスト実体が rpool/data と、両者とも同一の rpool 上にある。これは、プール障害で保管先とゲストを同時に失う構造を意味する。実は現行のフルバックアップも、この構造を変えていない。同じ障害ドメインの中に置いてある以上、プール障害に対する冗長性は元々持っていないのである。であればこそ、「フルダンプでなければならない」理由は薄い。保護水準を下げずに I/O と容量を削減できる余地がある、というのがこの設計の出発点になった。

バックアップの目的は2つある

設計を分ける前に、目的を分けておく。バックアップには、性格の異なる2つの目的がある。

  • 最新データの保持: 障害で失ったデータを、できるだけ新しい状態で、できるだけ速く戻すこと。指標は復旧時間(RTO)と、どの時点まで戻せるか(RPO)である。
  • 世代管理: 過去の複数の時点を残しておくこと。とくにランサムウェアは、接続されているバックアップまで暗号化してしまう。だから「感染する前の世代」を時系列で残し、そこへ戻せる必要がある。誤操作やファイルの改変からの復旧も、同じ目的に入る。

この2つは似ているようで、求める仕組みが違う。最新データの保持だけなら、常に直近の1世代があれば足りる。しかし世代管理まで求めるなら、過去へ遡れる複数の世代を、しかも勝手に書き換わらない形で残さなければならない。ZFS スナップショットが読み取り専用で不変な点は、このランサムウェア対策として効いてくる。

今回の見直しの出発点は、この2つを1つの仕組み(毎日の全量ダンプ)にまとめて担わせていた点にあった。全量ダンプは、最新データも世代も残せる。だがその代償として、毎回すべてのデータを読み出し、圧縮する。I/O と容量を大きく消費する。だから、目的ごとに道具を分ける方向へ設計を寄せた。

なぜ PBS を入れず、sanoid を選んだのか

真の増分バックアップと重複排除、そして別ホストへの退避まで考えるなら、PBS(Proxmox Backup Server)が本命である。しかし今回は見送った。理由は設置先の容量である。pve03 は空き 123G で不足し、pve02 は停止中である。PBS を置く場所が決まらない以上、導入してもバックアップの実体を置けない。

そこで、要件を「世代管理」に絞り、ZFS スナップショットで満たすことにした。世代管理が目的であれば、毎回フルダンプである必要はない。ZFS のスナップショットは CoW(コピーオンライト = Copy-on-Write)で、追加のフルコピーを伴わない。容量効率と実行時間の両面で、この要件に合っている。

判断の軸は、「今この瞬間に守りたいものは何か」である。別ホスト退避は理想だが、置き場所がない。それなら、まず同一プール内で世代を細かく残し、別ホスト退避は容量が確保できた段階で足す。順序を分けて考えることにした。

ZFS スナップショットと vzdump は何が違うのか

誤解しやすい点を整理しておく。

  • vzdump: ゲストを停止または freeze して整合性を取ったうえで、アーカイブを書き出す。別プールや別ホストへ退避できる。ただし全量読みと圧縮の I/O が重い。
  • ZFS スナップショット: その時点のファイルシステム状態を保持する。取得はほぼ一瞬で、I/O 負荷が小さい。ただし同一プール内にあり、crash-consistent(書き込み途中の状態を含む)である。

両者は代替ではなく補完の関係にある。世代の細かさはスナップショットで担保し、別プール・別ホストへの退避は距離を置いて残す。

ここで引っかかるのが整合性である。ZFS スナップショットは crash-consistent であり、vzdump のように「ゲストを freeze してから取る」わけではない。当初はこれを「LXC のファイルシステム整合性が vzdump の freeze と厳密に同等かは断定できない。推測と実測を分けておくべき点だ」と書いていた。だが、実機で確かめたところ、この見立ては半分当たっていて、半分は間違っていた。次節で実測を書く。

LXC の freeze と sanoid フック — 「推測の域」を実測で埋める

「LXC は vzdump のように freeze できないのではないか」という懸念は、実機で確かめると解消した。順に書く。

pct freeze は存在しない。だが freeze はできる

まず、Proxmox の CLI に pct freeze があるかを確認した。結論は無い。

# pct freeze 101
ERROR: unknown command 'pct freeze'

pct のサブコマンドに freeze / unfreeze は用意されていない(実測: pve-manager 8.4.21)。ここだけ見ると「LXC は freeze できない」と誤解してしまう。だが、実体は別のところにある。

# ls -l /usr/bin/lxc-freeze /usr/bin/lxc-unfreeze
-rwxr-xr-x 1 root root 44920 Nov 25  2025 /usr/bin/lxc-freeze
-rwxr-xr-x 1 root root 44920 Nov 25  2025 /usr/bin/lxc-unfreeze

# lxc-freeze --help
Usage: lxc-freeze --name=NAME
lxc-freeze freezes a container with the identifier NAME

lxc-freeze / lxc-unfreeze は LXC(実測: 6.0.0)の標準コマンドとして導入済みである。これは cgroup の freezer を操作するもので、--name= にコンテナ名を渡して停止・再開させる。つまり LXC の freeze は可能で、pct に薄いラッパーが無いだけだった。

sanoid には pre/post スナップショットフックがある

次に、sanoid 側に「スナップショットの前後で外部コマンドを挟む口」があるかを確認した。sanoid 2.1.0 の本体(/usr/sbin/sanoid)を読むと、pre_snapshot_script と post_snapshot_script が実装されている。設定に書けば、スナップショットの直前・直後に任意のスクリプトを実行できる。

推測で済ませず、実際に発火するかを一次実測した。daily が当日ぶん取得済みだと通常はスナップショットが増えないため、検証用の一時 config(hourly=1 とフックを指定)を作り、sanoid --take-snapshots を対象 dataset 1本に絞って実行した。結果は次のとおり。

executing pre_snapshot_script '/tmp/sanoid_hooktest/hook.sh' on dataset 'rpool/data/subvol-100-disk-1'
taking snapshot rpool/data/subvol-100-disk-1@autosnap_2026-10-11_16:17:36_hourly
executing post_snapshot_script '/tmp/sanoid_hooktest/hook.sh' on dataset 'rpool/data/subvol-100-disk-1'

フック側で受け取った環境変数は次のとおりだった。

SCRIPT=pre  TARGET=rpool/data/subvol-100-disk-1 SNAPNAME=autosnap_2026-10-11_16:17:36_hourly TYPES=hourly
SCRIPT=post TARGET=rpool/data/subvol-100-disk-1 SNAPNAME=rpool/data/subvol-100-disk-1@autosnap_2026-10-11_16:17:36_hourly TYPES=hourly

pre と post の両方が発火し、SANOID_TARGET(dataset)、SANOID_SNAPNAME(スナップショット名)、SANOID_TYPES(daily / hourly などの種別)、SANOID_SCRIPT(pre / post)が渡ることが確認できた。検証で生成したスナップショット1本は直後に zfs destroy し、一時ファイルも削除して元の状態へ戻している。

実測から見えた連携の形と、注意点

この2つの実測を重ねると、スナップショット型でも freeze 整合性に近づけられることが分かる。pre_snapshot_script で対象コンテナを lxc-freeze し、post_snapshot_script で lxc-unfreeze すれば、スナップショットの瞬間だけ書き込みを止められる。vzdump の mode snapshot(freeze して整合性を取る)に、仕組みとしては近い。

ただし、そのままでは足りない点も実測で見えた。

  • フックは dataset 単位で呼ばれる。対象は rpool/data/subvol-134-disk-0 のような名前で、コンテナ番号そのものではない。SANOID_TARGET から vmid を抽出し、コンテナ名へ引き直すマッピングが要る。
  • SANOID_SNAPNAME は pre ではスナップショット名のみ、post では dataset を含むフルパスである(実測で非対称を確認)。フック側で同じ前提を置かない方がよい。
  • 停止中のコンテナに freeze をかけると失敗する。pct status で running のものだけを対象にする、といった分岐が要る。
  • no_inconsistent_snapshot の扱いで、pre フック失敗時の挙動が変わる。freeze に失敗したときに「不整合でも取る」のか「取らない」のかを、あらかじめ決めておく必要がある。

つまり、連携は可能であり、口も用意されている。ただし本番 config に入れるには、vmid マッピングと失敗時の方針を決める必要がある。現行の sanoid.conf はフック未設定であり、ここは次段の改善として残した。本記事の主題である「I/O 飽和の解消」と「世代管理の確立」は、この連携を待たずに成立する。

sanoid の導入手順と保持ポリシー

導入は、再現性を要さない単発作業であるため Ansible ではなく run_command で実行してよい範囲と判断した。手順は次のとおり。

  1. apt-get install -y sanoid
  2. /etc/sanoid/sanoid.conf にポリシーを書く
  3. systemctl enable --now sanoid.timer
  4. zfs list -t snapshot | grep -c autosnap で世代生成を確認する

保持ポリシーは GFS(Grandfather-Father-Son)で、daily 7 / weekly 4 / monthly 3 とした。直近を細かく、過去を粗く残す形である。既存の8世代は prune せず、検証期間の安全網として残した。新しい仕組みが期待どおり動くことを確認するまでは、古い世代を消さない方針である。

フルダンプの自動実行を止める

スナップショットで世代管理を担保できるなら、フルバックアップを毎日回し続ける理由はない。設計の初期には「日次から週次へ落とし、負荷を1/7に減らす」案を置いていた。しかし最終的には、フルダンプの自動実行そのものを停止した。

実測では、weekly-backup-ansible は enabled 0 で自動実行が止まっている。日次・週次・月次の世代は sanoid のスナップショットが保持しており、世代管理はそちらで完結する。フルダンプは、大規模な変更の前など、必要な場面で手動取得すればよい。

週次で1本だけ残す案を採らなかったのは、中途半端に負荷と容量を残すより、役割をはっきり分けた方が運用が単純になるためである。可搬アーカイブ(別ホストへの移送用)はそれ自体が別の目的であり、置き場所の容量が確保できた段階で、PBS などの専用機構として用意する。

/etc/pve/jobs.cfg はクラスタ共有である。変更は全ノードへ波及するため、事前に退避を取得してから触る。退避があれば、1コマンドで元へ戻せる。

ロールバック(撤回)手順

可逆性を確保しておく。変更は2つあり、それぞれ戻し方を用意した。

対策 ロールバック
フルダンプ自動実行の停止 pvesh set /cluster/backup/weekly-backup-ansible --enabled 1 で再有効化(必要なら --schedule "03:00" で日次へ戻す)、または退避した jobs.cfg を戻す
sanoid 導入 systemctl disable --now sanoid.timer → apt-get remove -y sanoid → 生成した世代を zfs destroy

「戻せる」状態にしておけば、多少のずれは1コマンドで解消できる。だから今回は、承認を待たずに進められる範囲として設計した。

検証方法

効果は次の4点で確認する。

  1. zfs list -t snapshot | grep -c autosnap が 1 以上(世代が生成されている)
  2. 平常日に /proc/pressure/io が低く、vzdump 由来の I/O 飽和が再発しない
  3. AlertManager で NodeDown / NodeIowaitHigh(pve01)が 0 件のままである
  4. スナップショットからのリストア試験(テスト CT への復元)に成功する

特に4番は重要である。世代が残っていても、復元できなければ意味がない。復元まで通して初めて、バックアップとして成立する。

残した課題

正直に残しておく。

  • sanoid のスナップショットは crash-consistent である。ただし LXC 側の freeze(lxc-freeze / lxc-unfreeze)と sanoid の pre/post フックは実機で使えることを確認済み。本番 config への組み込みは vmid マッピングと失敗時方針の確定を待って次段とする(現行 config はフック未設定)
  • freeze を挟んだ場合でも、厳密な整合性の最終確認はリストア試験で行う
  • pve03 へのオフサイト退避(zfs send / syncoid)は、空き 123G のため現状では不可である
  • PBS は設置先と容量の決定後、将来の課題として残す
  • Ceph は HEALTH_WARN(OSD count 2 < osd_pool_default_size 3)のままである。これは別の課題として切り分けた

監視とバックアップは別の問いに答える

今回の学びは、監視とバックアップを混ぜないことである。監視は「今おかしいか」に答え、バックアップは「失ったときに戻せるか」に答える。問いが違う。片方の都合でもう片方を歪めると、どちらも成立しなくなる。

vzdump はバックアップの道具だが、その負荷が監視を誤らせていた。だから負荷を監視の側へ寄せるのではなく、バックアップの設計そのものを見直した。監視のしきい値を緩める解決を選ばなかったのは、それでは発生源が残るからである。

着手前チェックリスト

  • [ ] バックアップジョブが実際に有効か(enabled と schedule を実測)
  • [ ] 世代数と保管容量(du -sh)
  • [ ] 保管先とゲスト実体が同一プールか(zfs list)
  • [ ] 退避先ホストの空き容量
  • [ ] スナップショット機構の導入状況
  • [ ] ロールバック手段が1コマンドで用意できるか

まとめ — 発生源を軽くする

日次のフルダンプの自動実行を止め、世代管理は ZFS スナップショットへ移した。I/O 飽和は解消し、監視の誤報も収まる見込みである。監視のしきい値ではなく、バックアップの設計を変えたことが本質である。

バックアップは「たくさん取る」ことが目的ではない。戻せる範囲と頻度を、容量と負荷の制約の中で設計する。そして、目的が「最新データの保持」なのか「世代管理(ランサムウェア対策を含む)」なのかを分けておけば、道具の選び方も自ずと変わる。

整合性についても、最初から断定するのではなく、実機で確かめて「何ができて、何を決めれば使えるか」まで持っていく。pct freeze が無いことは freeze 不可を意味しないし、sanoid のフックは実在した。推測の域を実測で埋める。その積み重ねが、後から見直すときの判断を速くする。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

金重総合研究所の主席研究員。
子供の頃から研究者を目指し、ライフワークとして日々様々な研究をしています。
経営・マネジメント・金融・DXあたりが本職です。
私を採用したい人、私と一緒に働きたい人、一緒に知識を肥やしていきたい人はぜひお声がけ下さい。

目次