增量 Checkpoint 原理:快照生成与变更追踪
增量 Checkpoint,说白了就是只记录变化的部分。你想想看,如果每次备份都把整个数据集复制一遍,那磁盘再大也扛不住。我在早期做存储系统时,就见过全量备份把生产环境 IO 打满的惨案。从那以后,我对增量技术就特别上心。
这一章我们重点聊三个东西:增量快照怎么生成的、变更追踪的两种主流技术(COW 和 ROW)、以及增量链和基线到底是什么关系。嗯,这些都是实战中绕不开的概念。
增量快照的生成机制
增量快照的核心思路很简单:只保存从上一次快照到当前时刻之间发生变化的数据块。但实现起来,有几个关键步骤必须走对。
- 建立基线:第一次做快照时,必须先生成一个全量快照,这就是后续所有增量的起点。我习惯叫它「锚点」。
- 追踪变更:系统持续监控数据块的写入操作,记录哪些块被修改了。
- 生成增量:到达快照时间点时,把变更列表里对应的数据块提取出来,打包成一个增量文件。
- 更新元数据:记录增量文件与基线的关联关系,包括时间戳、块偏移、校验和等信息。
关键点:增量快照不复制未修改的数据。这意味着每次增量的大小通常只有全量的 1%~10%,具体取决于工作负载的写入模式。
我在项目中遇到过一种情况:某个数据库的写入模式是「先写日志,再写数据文件」。结果日志文件的增量特别大,数据文件反而没什么变化。后来我们调整了快照策略,把日志和数据分开追踪,效果好了很多。
变更追踪技术:COW 与 ROW
变更追踪是增量快照的基石。目前主流的有两种:写时复制(Copy-On-Write,COW) 和 写时重定向(Redirect-On-Write,ROW)。这两者思路完全不同,适用场景也不一样。
| 特性 | COW(写时复制) | ROW(写时重定向) |
|---|---|---|
| 基本原理 | 写入前先把原数据块复制到快照区 | 写入时直接把新数据写到新位置 |
| 读性能 | 正常读,无额外开销 | 需要查映射表,略慢 |
| 写性能 | 第一次写入慢(复制开销),后续正常 | 每次写入都要更新映射,开销稳定 |
| 空间占用 | 快照区保存原始数据,空间占用稳定 | 新数据写在新位置,旧数据可能变碎片 |
| 典型场景 | 虚拟机快照、文件系统快照 | 分布式存储、对象存储 |
COW 的工作流程
COW 的逻辑很直观。当应用要写入一个数据块时,系统先检查这个块是否已经被快照保护。如果是,就把原始块复制到快照区,然后再执行写入。这样快照区保存的就是「快照时刻的数据」,而主存储区保存的是「最新数据」。
# 伪代码:COW 写入流程
def cow_write(block_id, new_data):
if block_id in snapshot_protected:
# 复制原始块到快照区
snapshot_data = read_original_block(block_id)
write_to_snapshot_area(block_id, snapshot_data)
# 标记该块已复制,避免重复复制
mark_as_copied(block_id)
# 执行实际写入
write_to_primary(block_id, new_data)
我的经验:COW 在第一次写入时会有明显的延迟,因为要读一次、写两次(一次到快照区,一次到主存储)。如果应用对写入延迟敏感,建议提前预热快照区,或者考虑用 ROW。
ROW 的工作流程
ROW 的思路更「懒」。它不复制原始数据,而是把新数据直接写到新的位置,然后更新一个映射表。映射表记录了「逻辑块号 → 物理位置」的对应关系。快照时刻的映射表被冻结,后续的写入只影响新的映射表。
# 伪代码:ROW 写入流程
def row_write(logical_block_id, new_data):
# 分配新的物理位置
new_physical_addr = allocate_new_block()
# 写入新数据
write_to_physical(new_physical_addr, new_data)
# 更新映射表(只更新当前活跃的映射表)
update_mapping(logical_block_id, new_physical_addr)
# 快照映射表保持不变,指向旧数据
注意:ROW 的映射表会随着时间增长变得越来越大。我曾经见过一个系统,运行半年后映射表占了几十 GB 内存。一定要设计好映射表的压缩和淘汰机制,否则内存会爆。
增量链与基线的概念
增量快照不是孤立存在的。它们之间通过「链」关联起来,而基线就是这条链的起点。
增量链
增量链是指一系列增量快照按时间顺序组成的链条。每个增量快照都依赖于前一个快照(无论是全量还是增量)。要恢复某个时间点的数据,必须从基线开始,沿着增量链依次应用每个增量。
举个例子:
- 基线(全量):T0 时刻的数据全集
- 增量 1:T0 → T1 的变化
- 增量 2:T1 → T2 的变化
- 增量 3:T2 → T3 的变化
要恢复 T3 时刻的数据,你需要:基线 + 增量1 + 增量2 + 增量3。链条越长,恢复时间越长,出错的概率也越大。
实战建议:我一般建议增量链的长度不要超过 7 个。超过这个数,就应该做一次新的全量快照,重新建立基线。这叫「基线重置」,能有效控制恢复时间。
基线的选择策略
基线不一定是全量快照。在某些场景下,我们可以用「差异基线」—— 只记录与上一个基线的差异。但这会增加恢复的复杂度。我个人更倾向于定期做全量基线,比如每周一次全量,每天一次增量。
下面这张图展示了增量链与基线的完整关系:
增量链的维护与风险
增量链虽然节省空间,但维护起来有不少坑。我总结了几条实战经验:
- 链不能太长:超过 10 个增量后,恢复时间会指数级增长。因为每个增量都要读取、校验、应用。
- 定期做全量基线:我习惯每周一次全量,每天一次增量。这样恢复时最多只需要应用 7 个增量。
- 增量文件要校验:每个增量文件都应该附带校验和。我曾经遇到过磁盘静默错误导致增量文件损坏,恢复时才发现数据不对。
- 考虑并行恢复:如果增量之间没有依赖关系(比如按文件切分),可以并行应用多个增量,大幅缩短恢复时间。
避坑指南:我曾经在一个项目中,增量链长达 30 个。结果恢复时发现中间某个增量文件损坏,导致整个恢复失败。后来我们强制要求:增量链长度不超过 7,并且每个增量都做 CRC 校验。这个教训花了我两周时间才搞定。
小结
增量 Checkpoint 的核心就是「只记变化」。COW 适合读多写少的场景,ROW 适合写密集的场景。增量链要控制长度,基线要定期重置。这些概念搞清楚了,后面讲增量恢复和全量恢复时,你就知道为什么恢复时间会随着增量链长度变化了。