2. 快照技术原理:写时复制(COW)、重定向写(ROW)、快照链与依赖关系

聊到镜像回滚,快照技术是绕不开的核心。说白了,快照就是给数据拍一张“照片”,记录某个时刻的状态。但底层怎么实现的?我这些年踩过不少坑,今天把三种主流技术掰开揉碎了讲。

2.1 写时复制(Copy-On-Write,COW)

COW 是最经典的做法。它的核心思想是:快照创建时,不复制任何数据。只有原始数据要被修改时,才把旧数据复制到快照预留的空间里。

举个例子,你有个 100GB 的卷,拍了快照。之后只修改了 1GB 的数据,那快照就只占用 1GB 的空间。嗯,这里要注意:读性能在快照创建后几乎无影响,但写性能会下降,因为每次写操作都要先读旧数据、再写快照区。

关键流程:
  1. 创建快照:瞬间完成,仅记录元数据指针
  2. 首次写入:拦截写请求,将原位置数据复制到快照预留区
  3. 更新指针:将原数据块指针指向新写入的数据
  4. 后续写入:直接覆盖原位置,无需再复制

我在项目中遇到过一个问题:某个数据库系统频繁做小数据量更新,COW 导致写放大严重。原本 4KB 的写入,因为要复制旧数据,实际磁盘 I/O 变成了 8KB+。后来我建议改用 ROW 方案,才解决了这个瓶颈。

避坑指南:我曾经在 VMware 环境中发现,COW 快照链超过 32 层后,性能会断崖式下跌。建议生产环境快照链控制在 3-5 层以内。

2.2 重定向写(Redirect-On-Write,ROW)

ROW 的思路完全不同。它不复制旧数据,而是把新数据写到新的位置,然后更新指针映射表。你想想看,这样是不是就避免了 COW 的写放大问题?

ROW 的工作流程是这样的:

  • 创建快照:同样瞬间完成,记录当前数据块映射关系
  • 写入新数据:直接写到空闲空间,更新映射表
  • 读取数据:根据映射表,判断读快照版本还是当前版本

我个人习惯在容器镜像场景用 ROW。比如 Docker 的 overlay2 文件系统,底层就是 ROW 的思想。每次容器写入,都写到上层可写层,底层镜像纹丝不动。这样回滚时,直接丢弃上层即可,速度极快。

对比项 COW ROW
写性能 首次写入慢(需复制) 始终较快(直接写新位置)
读性能 快照读需查链 快照读需查映射表
空间占用 仅保存旧数据 新旧数据共存
适用场景 读多写少 写频繁、容器场景
我的经验:ROW 虽然写性能好,但长期运行会产生大量碎片。我建议配合定期整理(defrag)机制,或者设置快照自动合并策略。

2.3 快照链与依赖关系

快照不是孤立存在的。你拍了一个快照,再拍第二个,它们之间就形成了链式依赖。这个依赖关系,说白了就是:子快照依赖父快照的数据完整性。

举个例子:

  • 快照 A(父)→ 快照 B(子)→ 当前数据
  • 如果删除快照 A,B 就变成孤儿,数据可能不完整
  • 删除快照 B,A 不受影响

为什么会这样?因为 COW 模式下,快照 B 的某些数据块可能指向快照 A 的预留区。删了 A,那些数据块就找不到了。ROW 模式类似,映射表是链式查找的。

依赖关系核心规则:
  1. 只能从链尾删除快照(最安全的做法)
  2. 删除中间快照,需要合并数据(COW 合并旧数据,ROW 合并映射表)
  3. 快照链越长,回滚速度越慢(因为要回溯的层数多)

我遇到过最惨的一次:某运维同学删了中间快照,没做合并检查,结果整个快照链崩溃,数据丢了 3 天的增量。嗯,从那以后,我强制团队所有快照操作必须走自动化脚本,禁止手动删除。

快照链与依赖关系示意图 原始数据卷 快照 A(父) 快照 B(子) 当前数据 依赖关系:当前数据 → 快照B → 快照A 删除规则 ✅ 安全删除:快照B(子)→ 不影响快照A ❌ 危险删除:快照A(父)→ 快照B 和当前数据可能损坏 💡 必须从链尾开始删除,或先合并数据再删中间节点
重要提醒:快照不是备份!快照依赖原始数据卷,如果原始卷损坏,所有快照都失效。我见过有人把快照当备份用,结果磁盘物理故障,快照链全废。记住:快照用于快速回滚,备份用于灾难恢复,两者不能互相替代。

最后说一句,快照链的深度直接影响回滚时间。我建议生产环境快照链不超过 5 层,超过这个数,回滚时 I/O 压力会很大。如果你需要长期保留多个时间点,考虑用增量备份代替快照链。