2. 快照技术原理:写时复制(COW)、重定向写(ROW)、快照链与依赖关系
聊到镜像回滚,快照技术是绕不开的核心。说白了,快照就是给数据拍一张“照片”,记录某个时刻的状态。但底层怎么实现的?我这些年踩过不少坑,今天把三种主流技术掰开揉碎了讲。
2.1 写时复制(Copy-On-Write,COW)
COW 是最经典的做法。它的核心思想是:快照创建时,不复制任何数据。只有原始数据要被修改时,才把旧数据复制到快照预留的空间里。
举个例子,你有个 100GB 的卷,拍了快照。之后只修改了 1GB 的数据,那快照就只占用 1GB 的空间。嗯,这里要注意:读性能在快照创建后几乎无影响,但写性能会下降,因为每次写操作都要先读旧数据、再写快照区。
- 创建快照:瞬间完成,仅记录元数据指针
- 首次写入:拦截写请求,将原位置数据复制到快照预留区
- 更新指针:将原数据块指针指向新写入的数据
- 后续写入:直接覆盖原位置,无需再复制
我在项目中遇到过一个问题:某个数据库系统频繁做小数据量更新,COW 导致写放大严重。原本 4KB 的写入,因为要复制旧数据,实际磁盘 I/O 变成了 8KB+。后来我建议改用 ROW 方案,才解决了这个瓶颈。
2.2 重定向写(Redirect-On-Write,ROW)
ROW 的思路完全不同。它不复制旧数据,而是把新数据写到新的位置,然后更新指针映射表。你想想看,这样是不是就避免了 COW 的写放大问题?
ROW 的工作流程是这样的:
- 创建快照:同样瞬间完成,记录当前数据块映射关系
- 写入新数据:直接写到空闲空间,更新映射表
- 读取数据:根据映射表,判断读快照版本还是当前版本
我个人习惯在容器镜像场景用 ROW。比如 Docker 的 overlay2 文件系统,底层就是 ROW 的思想。每次容器写入,都写到上层可写层,底层镜像纹丝不动。这样回滚时,直接丢弃上层即可,速度极快。
| 对比项 | COW | ROW |
|---|---|---|
| 写性能 | 首次写入慢(需复制) | 始终较快(直接写新位置) |
| 读性能 | 快照读需查链 | 快照读需查映射表 |
| 空间占用 | 仅保存旧数据 | 新旧数据共存 |
| 适用场景 | 读多写少 | 写频繁、容器场景 |
2.3 快照链与依赖关系
快照不是孤立存在的。你拍了一个快照,再拍第二个,它们之间就形成了链式依赖。这个依赖关系,说白了就是:子快照依赖父快照的数据完整性。
举个例子:
- 快照 A(父)→ 快照 B(子)→ 当前数据
- 如果删除快照 A,B 就变成孤儿,数据可能不完整
- 删除快照 B,A 不受影响
为什么会这样?因为 COW 模式下,快照 B 的某些数据块可能指向快照 A 的预留区。删了 A,那些数据块就找不到了。ROW 模式类似,映射表是链式查找的。
- 只能从链尾删除快照(最安全的做法)
- 删除中间快照,需要合并数据(COW 合并旧数据,ROW 合并映射表)
- 快照链越长,回滚速度越慢(因为要回溯的层数多)
我遇到过最惨的一次:某运维同学删了中间快照,没做合并检查,结果整个快照链崩溃,数据丢了 3 天的增量。嗯,从那以后,我强制团队所有快照操作必须走自动化脚本,禁止手动删除。
最后说一句,快照链的深度直接影响回滚时间。我建议生产环境快照链不超过 5 层,超过这个数,回滚时 I/O 压力会很大。如果你需要长期保留多个时间点,考虑用增量备份代替快照链。