ZFS的dataset快照技术

一、层级归属:ZFS 属于宿主内核层,粒度是 dataset

  • 快照作用范围是一个 dataset——不是整个系统,也不是单个文件。粒度只到 dataset。
  • 容器:宿主 ZFS 直接管得到(同内核、同文件系统)→ 快照对它等于文件级、同 TXG 的原子时点,保真度最高。
  • 虚拟系统:只有当虚拟磁盘以文件或 zvol 形式存在时才管得到,管到的是块;对客户机等于「拔电源那一刻」——崩溃一致,不是应用一致。
  • 嵌套(客户机内再跑 ZFS / 容器):宿主快照对客户机内部是黑盒,客户机的快照对宿主不可见。纪律:一个数据集只让一层管快照。
  • ZFS 不提供跨 dataset 的原子快照。zfs snapshot -r 只是把同一批名字一次建出来,不保证所有 dataset 落在同一个 TXG。要跨 dataset 一致,只能靠上层冻结写入者(fsfreeze / 数据库锁 / 短暂停写)。

二、dataset 边界 = 策略边界 = 回退粒度

快照频率、保留、压缩、是否送备份,全部按 dataset 声明,不做全池一刀切——全池快照等于把最热的数据也拖进代价里。

dataset放什么快照保留属性与备注
config/核心配置、证书、compose / quadlet 文件每 15 分钟4h / 7d / 8wcompression=zstd;小文件多:带宽轻、元数据重
data/<业务>/业务数据(DB、文档、代码仓)每 15~60 分钟24h / 30d / 12m每业务一个;核心库额外 zfs send 出去
share/导出给虚拟系统的只读面低频或不开可选只读导出;与运行面物理分开
runtime/容器可写层、虚拟磁盘、索引不快照—回滚语义模糊,且重跑即得
tmp/ cache/临时与缓存不快照—atime=off、compression=off
assets/iso / 镜像不快照—recordsize=1M

声明方式用属性 + 策略工具,不写 cron:

zfs set com.sun:auto-snapshot=false dataset/runtime     # zfs-auto-snapshot 认这个属性

sanoid.conf:
  [dataset/data]
    hourly=24  daily=30  monthly=12
    autosnap=yes      # 打快照:保持高频(便宜,护 RPO)
    autoprune=no      # 删过期快照:挪到业务低谷窗口单独跑
  [dataset/runtime]
    autosnap=no

设计含义:想能单独回退的东西必须单独成 dataset。不要把「经常要单独回退的小件」和「不该动的大件」混在一个 dataset 里。

三、五种文件操作的 IO 成本

操作带宽元数据说明
保存新文件写一份轻最省;压缩可减;sync 写会双写(ZIL + 池)
修改旧文件读改写 × 放大中~重成本 ∝ recordsize;随机小写必须调小 recordsize
复制为另一个文件读 + 写全量轻OpenZFS 2.2+ 支持块克隆(reflink),同池大文件近零拷贝
删除文件无中~重(海量小文件)空间异步回收;被快照持有的块不释放
移动 / 改名(同 dataset 内)无极轻纯元数据操作,瞬间完成
移动 / 改名(跨 dataset)读 + 写全量重rename 返回 EXDEV,退化成复制 + 删除,旧块还占着空间

四个放大器:sync 设置(fsync 双写 → 配 SLOG)、recordsize(随机小写的致命参数)、压缩(省池 IO、花 CPU)、快照保留造成的空闲碎片(间接变慢)。

关键推论:真正制造压力的是三类——随机小改写、sync 同步写、海量元数据操作(小文件频繁建删、跨 dataset 拷贝)。数据库正好命中前两类,所以它必须独立成 dataset 并把 recordsize 调到 8K~16K。

四、快照的成本模型(反直觉)

  • 打快照本身几乎免费:元数据级操作,不拷数据。副作用是会强制当前 TXG 立即落盘(为拿到一致时点),写高峰时会造成一次延迟抖动。
  • 真正的成本是空间租金:≈ 被保护数据的变动率 × 保留时间。数据不动就不收费——这是它与镜像 / 实时复制那类「常态付费」机制的根本区别。
  • 精度修正:ZFS 里没有「改写前先拷贝旧块」这一步(COW 本来就永远写新块)。有快照与无快照,写路径成本几乎一致;差别只是旧块不被释放 → 空间持续增长、free space 变碎 → 长期分配效率下降。
  • 两种定时副作用才是撞车大头:① prune(删过期快照)= 异步释放一大批块,IO 风暴;② zfs send 增量 = 遍历差异块并读取,与业务读争 IO。频率越高,这两件事越频繁。
  • 正解:snapshot 保持高频;prune 与 send 挪到业务低谷窗口单独跑;不要在整点批量打快照(错峰 5~10 分钟)。

五、回退粒度:rollback 是 dataset 级,单文件恢复走 .zfs

操作粒度后果用途
zfs rollbackdataset 级,全有或全无销毁更新的快照;该 dataset 之后产生的所有数据消失只用于整块业务回退(升级搞坏、迁移写坏)
.zfs/snapshot 拷贝文件级dataset 停在 HEAD,其余文件不动日常修复:误删、误改单个文件
zfs list -t snapshot -o name,creation dataset/data
cp -p /dataset/data/.zfs/snapshot/20261002-0300/app/db.conf  /dataset/data/app/db.conf
rsync -a /dataset/data/.zfs/snapshot/20261002-0300/app/conf.d/  /dataset/data/app/conf.d/

zfs diff dataset/data@20261002-0300 dataset/data        # 找出到底改了哪些文件
zfs clone dataset/data@20261002-0300 dataset/scratch/restore   # 取证用:不扰生产,用完销毁
  • 快照只读 → 从里面取数据天然安全;读的是旧块,不触发数据搬移,很便宜。
  • snapdir=hidden(默认)时 .zfs 不出现在 ls 里但路径可用;设成 visible 只是方便浏览(有视觉噪音与误删尝试,但删不掉)。
  • 铁律:数据库不能单文件回退(表空间 / redo / 字典互相引用)→ 只能整集回退 + redo / WAL 重放;或在克隆体里取完整数据目录做恢复后再导回。

六、一致性口径(按层)

  • 容器:宿主的原子快照 = 文件级一致(同一 TXG)。
  • 数据库:把该 DB 的全部文件放进同一个 dataset(数据 + redo / WAL + 配置)→ 原子快照等价于拔电源,而 PG 的 WAL 重放 / InnoDB 的 redo 恢复本就为拔电源而设计 → 无需额外 FLUSH,快照即合法备份基元。反之,redo 与数据分属两个 dataset 时快照不在同一 TXG,恢复会失败(最易踩)。
  • 虚拟系统:宿主裸快照 = 崩溃一致;要应用一致用 virsh snapshot-create-as --quiesce(走 guest agent 做 fsfreeze)。宿主与客户机不要两层都管。
  • 跨 dataset 回滚没有原子性 → 统一批次命名 + 回滚前停掉全部写入者 + 按批次整组回滚。回滚本身是元数据操作(秒级)。

七、备份分层:RPO / RTO 与失效模式

机制RPORTO常态代价能救什么
15 分钟快照15 min秒级≈零性能 + 空间租金误删、误改、跑错脚本、逻辑坏数据
zfs send 异地1h~1d分钟~小时带宽 + 遍历 IO池损坏、盘掉、单机丢失
冷备(离线介质)天~周小时~天人工 + 介质全灭、勒索、跨地域
  • 快照不是备份:同池快照随池一起死。它防的是「手滑」,而手滑概率远高于灾难级 0.01%——用灾难概率衡量快照,会把它的价值算低一个数量级。
  • 该不该叠加看失效模式是否重叠,不是看「每个都要付费」。三者失效模式几乎不重叠 → 值得叠加。
  • 常态付费的是镜像 vdev、实时异地复制、加密那一档;快照是按需付费(空间租金)。
  • 定频率原则:按「能容忍丢多少分钟的工作」定,不按性能余量(余量几乎不吃)。空间估算 = 日改动量 × 保留天数 × 1.2~1.5(碎片余量);只有高改动的 DB dataset 需要认真算。

八、机制目录的处理(.zfs 与备份软件的隐藏目录)

目录里面放什么处理策略
.zfs/snapshot(ZFS)历史数据本体(只读,零额外空间)备份 / 同步必须显式排除
_gsdata_(GoodSync)状态账本(指纹、同步历史、删除标记)同步任务里不能排除;也不能当业务数据备份或交付
  • 不排除 .zfs 的后果很隐蔽:rsync -a / cp -a / tar 会把整棵快照当普通目录拷走,备份体积与递归规模失控。
  • 算空间不要用 du:用 zfs list -t snapshot -o name,used dataset/data 与 zfs list -o name,used,usedbysnapshots,usedbydataset dataset/data。
  • 凡「机制目录」,在做备份 / 导出 / 交付清单时必须显式决定排除还是保留,不能靠工具默认行为。

九、与本架构(NixOS + 虚拟化)交界的四个坑

  • qcow2 压在 ZFS 上 = 双重 COW:虚拟磁盘改用 zvol(volblocksize 取 16K 与 qcow2 cluster 对齐)或 raw 镜像;chattr +C 是 Btrfs 专属,ZFS 上没有对应物。
  • 活跃数据目录不跨虚拟化边界共享:9p 的锁是客户端本地语义(基本一定放行)、virtiofs 可能转发但不保证 → 两个实例会同时写同一份数据(是必然损坏,不是可能损坏)。导出面只读、与运行面物理分开。
  • rootless 容器写出的文件在宿主上落高位 UID(subuid 起始 100000+),影响属主一致性、备份与恢复的可读性;用 --userns=keep-id、卷后缀 :U,或规范 subUidRanges。
  • 挂载依赖必须显式声明:systemd 单元要写 unitConfig.RequiresMountsFor = "/dataset/...",否则挂载点未就绪时会先在根文件系统上建目录并写数据(事后被挂载盖住,看起来像「数据丢了」)。