快照技术基础:快照原理、全量快照与增量快照、写时复制与重定向写

各位同学,咱们今天聊聊快照。说实话,快照这个词在存储领域被用得太多了,但真正理解它底层原理的人,其实不多。

我记得刚入行那会儿,有个老前辈跟我说:「快照就是给数据拍张照片。」这个比喻很形象,但不够精确。你想想看,给一个正在运行的系统拍照片,数据还在不断写入,你怎么保证这张「照片」是完整的?

嗯,这就是快照技术要解决的核心问题——在数据持续变化的过程中,捕获一个时间点的一致性视图。

快照的本质:指针的冻结

说白了,快照不是把数据复制一遍。它冻结的是元数据,是数据块的指针关系。

我习惯用一个比喻来解释:想象你有一堆乐高积木搭成的城堡。快照不是重新搭一个一模一样的城堡,而是拍一张照片,记录下每块积木在哪个位置。之后你拆掉城堡重新搭飞船,那张照片依然能告诉你——「原来城堡长这样」。

在文件系统或存储卷里,数据被切成固定大小的块(通常是 4KB 或 64KB)。每个块有一个指针。快照就是把这些指针的「当前状态」保存下来。

核心要点:快照不拷贝数据本身,只拷贝数据块的指针映射表。真正的数据拷贝发生在后续写入时。

全量快照 vs 增量快照

这两个概念,我当年在项目里踩过坑,所以今天重点讲清楚。

全量快照,就是给整个数据卷拍一张完整的照片。每次做全量快照,都要记录所有数据块的指针状态。好处是恢复简单——拿过来直接用。坏处呢?

  • 时间成本高:TB 级别的卷,光扫描元数据就要几分钟
  • 空间成本高:每次全量快照都要保留一份完整的指针表
  • 对 IO 有冲击:扫描过程中会锁住部分元数据

增量快照,只记录从上一次快照到当前时间点之间,发生变化的数据块指针。

举个例子。假设你的卷有 100 万个数据块。第一次全量快照记录了全部 100 万个指针。一小时后,只有 2000 个块被修改了。增量快照只记录这 2000 个块的指针变化。

对比维度 全量快照 增量快照
首次创建时间 与数据量成正比 与数据量成正比(首次需全量)
后续创建时间 仍然与数据量成正比 只与变化量成正比
恢复速度 快,单次加载 慢,需按链回溯
空间占用 每次独立,空间大 共享基础数据,空间小
典型场景 初始备份、迁移 定期备份、CDP

我的经验:生产环境中,我建议采用「全量+增量」混合策略。比如每周日做一次全量快照,周一到周六每小时做一次增量快照。这样恢复时,最多只需要加载一次全量加上最近几次增量,速度和空间都兼顾了。

写时复制(Copy-on-Write, CoW)

这是实现快照最经典的技术。我最早接触 CoW 是在 ZFS 文件系统上,后来在 Linux LVM 里也看到类似的实现。

CoW 的核心逻辑很简单:当快照创建之后,如果源卷上的某个数据块要被修改,先把原来的数据块复制到快照预留的空间里,然后再修改源卷。

流程是这样的:

  1. 创建快照:冻结当前指针表,标记所有数据块为「共享」状态
  2. 写入请求到达:应用程序要修改数据块 A
  3. 检查状态:发现数据块 A 被快照共享
  4. 复制原数据:把数据块 A 的原始内容拷贝到快照存储区
  5. 更新指针:快照的指针指向拷贝后的原始数据,源卷的指针指向新写入的数据
  6. 写入完成:应用程序继续

你可能会问:「每次写入都要先读后写,性能会不会很差?」

会,但只在第一次写入时。同一个数据块后续的修改,因为已经复制过了,直接覆盖就行。这就是所谓的「一次复制,多次写入」。

我曾经踩过的坑:CoW 快照在写密集场景下,会有明显的「写放大」效应。一次 4KB 的写入,可能触发一次 4KB 的读取(复制原数据)和两次 4KB 的写入(复制到快照区 + 写入源卷)。实际吞吐量可能下降 30%-50%。如果你的业务是 OLTP 类的高频写入,一定要给快照预留足够的 IOPS 预算。

重定向写(Redirect-on-Write, RoW)

CoW 有性能损耗,那有没有更好的办法?有,就是重定向写。

RoW 的思路完全不同:不修改原来的数据块,直接把新数据写到新的位置,然后更新指针映射。

还是刚才那个例子:

  1. 创建快照:冻结当前指针表
  2. 写入请求到达:应用程序要修改数据块 A
  3. 分配新块:在存储池里分配一个新的数据块 B
  4. 写入新数据:把新数据直接写到数据块 B
  5. 更新指针:源卷的指针从 A 改为指向 B,快照的指针仍然指向 A
  6. 写入完成

看到了吗?整个过程不需要读取原始数据,也没有复制操作。写入延迟比 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 层,超过就做一次全量快照来「截断」链条。

知识体系总览

下面这张图,把今天讲的核心概念串起来了。你可以把它当作本章的知识地图。

快照技术知识体系 快照(Snapshot) 全量快照 增量快照 快照链 独立指针表 恢复快 空间占用大 仅记录变化 空间效率高 恢复需回溯 链式依赖 合并删除 深度限制 写时复制(CoW) 重定向写(RoW) 核心权衡:全量 vs 增量 → 空间与速度的取舍;CoW vs RoW → 写入性能与碎片化的权衡