快照技术基础:快照原理、全量快照与增量快照、写时复制与重定向写
各位同学,咱们今天聊聊快照。说实话,快照这个词在存储领域被用得太多了,但真正理解它底层原理的人,其实不多。
我记得刚入行那会儿,有个老前辈跟我说:「快照就是给数据拍张照片。」这个比喻很形象,但不够精确。你想想看,给一个正在运行的系统拍照片,数据还在不断写入,你怎么保证这张「照片」是完整的?
嗯,这就是快照技术要解决的核心问题——在数据持续变化的过程中,捕获一个时间点的一致性视图。
快照的本质:指针的冻结
说白了,快照不是把数据复制一遍。它冻结的是元数据,是数据块的指针关系。
我习惯用一个比喻来解释:想象你有一堆乐高积木搭成的城堡。快照不是重新搭一个一模一样的城堡,而是拍一张照片,记录下每块积木在哪个位置。之后你拆掉城堡重新搭飞船,那张照片依然能告诉你——「原来城堡长这样」。
在文件系统或存储卷里,数据被切成固定大小的块(通常是 4KB 或 64KB)。每个块有一个指针。快照就是把这些指针的「当前状态」保存下来。
核心要点:快照不拷贝数据本身,只拷贝数据块的指针映射表。真正的数据拷贝发生在后续写入时。
全量快照 vs 增量快照
这两个概念,我当年在项目里踩过坑,所以今天重点讲清楚。
全量快照,就是给整个数据卷拍一张完整的照片。每次做全量快照,都要记录所有数据块的指针状态。好处是恢复简单——拿过来直接用。坏处呢?
- 时间成本高:TB 级别的卷,光扫描元数据就要几分钟
- 空间成本高:每次全量快照都要保留一份完整的指针表
- 对 IO 有冲击:扫描过程中会锁住部分元数据
增量快照,只记录从上一次快照到当前时间点之间,发生变化的数据块指针。
举个例子。假设你的卷有 100 万个数据块。第一次全量快照记录了全部 100 万个指针。一小时后,只有 2000 个块被修改了。增量快照只记录这 2000 个块的指针变化。
| 对比维度 | 全量快照 | 增量快照 |
|---|---|---|
| 首次创建时间 | 与数据量成正比 | 与数据量成正比(首次需全量) |
| 后续创建时间 | 仍然与数据量成正比 | 只与变化量成正比 |
| 恢复速度 | 快,单次加载 | 慢,需按链回溯 |
| 空间占用 | 每次独立,空间大 | 共享基础数据,空间小 |
| 典型场景 | 初始备份、迁移 | 定期备份、CDP |
我的经验:生产环境中,我建议采用「全量+增量」混合策略。比如每周日做一次全量快照,周一到周六每小时做一次增量快照。这样恢复时,最多只需要加载一次全量加上最近几次增量,速度和空间都兼顾了。
写时复制(Copy-on-Write, CoW)
这是实现快照最经典的技术。我最早接触 CoW 是在 ZFS 文件系统上,后来在 Linux LVM 里也看到类似的实现。
CoW 的核心逻辑很简单:当快照创建之后,如果源卷上的某个数据块要被修改,先把原来的数据块复制到快照预留的空间里,然后再修改源卷。
流程是这样的:
- 创建快照:冻结当前指针表,标记所有数据块为「共享」状态
- 写入请求到达:应用程序要修改数据块 A
- 检查状态:发现数据块 A 被快照共享
- 复制原数据:把数据块 A 的原始内容拷贝到快照存储区
- 更新指针:快照的指针指向拷贝后的原始数据,源卷的指针指向新写入的数据
- 写入完成:应用程序继续
你可能会问:「每次写入都要先读后写,性能会不会很差?」
会,但只在第一次写入时。同一个数据块后续的修改,因为已经复制过了,直接覆盖就行。这就是所谓的「一次复制,多次写入」。
我曾经踩过的坑:CoW 快照在写密集场景下,会有明显的「写放大」效应。一次 4KB 的写入,可能触发一次 4KB 的读取(复制原数据)和两次 4KB 的写入(复制到快照区 + 写入源卷)。实际吞吐量可能下降 30%-50%。如果你的业务是 OLTP 类的高频写入,一定要给快照预留足够的 IOPS 预算。
重定向写(Redirect-on-Write, RoW)
CoW 有性能损耗,那有没有更好的办法?有,就是重定向写。
RoW 的思路完全不同:不修改原来的数据块,直接把新数据写到新的位置,然后更新指针映射。
还是刚才那个例子:
- 创建快照:冻结当前指针表
- 写入请求到达:应用程序要修改数据块 A
- 分配新块:在存储池里分配一个新的数据块 B
- 写入新数据:把新数据直接写到数据块 B
- 更新指针:源卷的指针从 A 改为指向 B,快照的指针仍然指向 A
- 写入完成
看到了吗?整个过程不需要读取原始数据,也没有复制操作。写入延迟比 CoW 低得多。
| 对比维度 | 写时复制(CoW) | 重定向写(RoW) |
|---|---|---|
| 写入流程 | 先读原数据,再写两处 | 直接写新位置,更新指针 |
| 写入延迟 | 较高(额外读操作) | 较低(无额外读操作) |
| 空间回收 | 简单,原位置可覆盖 | 复杂,需垃圾回收 |
| 碎片化 | 低 | 高(数据散落在不同位置) |
| 典型实现 | ZFS、LVM | WAFL、Btrfs |
我的建议:如果你的场景是读多写少(比如虚拟机模板、容器镜像),CoW 更合适,空间利用率高。如果是写多读少(比如日志系统、监控数据),RoW 的写入性能优势更明显。没有银弹,只有权衡。
快照链与空间回收
增量快照做多了,会形成一条快照链。每个快照依赖前一个快照的数据。恢复时,需要沿着链回溯,把所有的增量变化叠加起来。
这里有个问题:删除中间的快照怎么办?
比如你有快照 S1、S2、S3。S2 引用了 S1 的一些数据块,S3 又引用了 S2 的一些数据块。现在要删除 S2。
你不能直接删。因为 S3 可能还依赖 S2 里的数据块。正确的做法是:
- 找出 S2 独有的数据块(S1 和 S3 都不依赖的)
- 把 S3 依赖的 S2 数据块「合并」到 S3 或 S1 里
- 然后才能安全删除 S2
这个过程叫「快照合并」或「快照删除合并」。实现起来相当复杂,我见过不少存储系统在这个环节出 bug,导致数据丢失。
避坑指南:我曾经在生产环境里遇到过删除快照后,整个文件系统卡死的情况。原因是快照链太长(超过 256 层),合并算法复杂度飙升,把 CPU 吃满了。后来我定了个规矩:增量快照链深度不超过 64 层,超过就做一次全量快照来「截断」链条。
知识体系总览
下面这张图,把今天讲的核心概念串起来了。你可以把它当作本章的知识地图。