3、核心原理:Redirect-on-Write (RoW) 机制详解

好,我们进入增量快照最核心的机制——重定向写入(Redirect-on-Write,RoW)。

说实话,很多人一开始会把 RoW 和 Copy-on-Write(CoW)搞混。我当年刚接触分布式存储时也踩过这个坑。简单来说:CoW 是把老数据复制一份再改,RoW 是直接把新数据写到别处,把指针指过去。

你想想看,做快照最怕什么?怕写放大。CoW 每次写都要先读老块、再写新块,延迟一下子就上去了。RoW 呢?它压根不碰老数据,直接写新位置。这就是它高效的根本原因。

3.1 RoW 的核心思想:写新不写旧

RoW 的哲学很简单——新数据写入新位置,旧数据原地不动。

我们来看一个典型的场景:

  • 你有一个 1TB 的卷,做了快照 S1
  • 快照完成后,应用修改了块 LBA 1000
  • CoW 的做法:先把 LBA 1000 的老数据拷贝到快照预留区,再覆盖写 LBA 1000
  • RoW 的做法:直接把新数据写到另一个空闲块,然后把卷的 LBA 1000 映射到新块

看到了吗?RoW 少了一次读老数据的操作。这在写密集场景下,性能优势非常明显。

关键区别:CoW 是「先备份再覆盖」,RoW 是「直接重定向」。RoW 的写路径更短,但元数据管理更复杂。

3.2 重定向写入的完整流程

我习惯把 RoW 的写入流程拆成 5 步。这样理解起来更清晰:

  1. 接收写请求:应用要写 LBA X,数据为 D_new
  2. 分配新块:从空闲池中分配一个物理块 P_new
  3. 写入新块:将 D_new 写入 P_new
  4. 更新映射表:将 LBA X 的物理地址从 P_old 改为 P_new
  5. 保留老块:P_old 标记为「快照保留」,不能被回收

嗯,这里要注意:第 5 步非常关键。如果快照还没删除就把老块回收了,数据就丢了。我在项目中遇到过因为回收逻辑写错导致快照损坏的事故,教训深刻。

我的建议:老块不要立即回收,而是先标记为「快照引用」。等所有依赖该块的快照都删除后,再真正释放。这个机制叫「引用计数」。

3.3 元数据管理的核心:映射表

RoW 的灵魂不在数据路径,而在元数据。说白了,就是那张「逻辑地址 → 物理地址」的映射表。

没有快照时,映射表是一对一的:LBA X → P_X。有了快照后,事情就复杂了——同一个 LBA 在不同快照时间点可能对应不同的物理块。

我画了一张图,帮你理解 RoW 下的映射关系:

RoW 重定向写入与映射关系 逻辑地址空间 LBA 0 LBA 1 LBA 2 ... LBA N 映射表 LBA 0 → P_5 LBA 1 → P_12 LBA 2 → P_8 ... LBA N → P_3 物理存储 P_3 (快照保留) P_5 (活跃) P_8 (活跃) P_12 (活跃) P_15 (空闲) RoW 写入后,映射表更新为新物理块地址,老物理块保留给快照使用

3.4 快照链与映射表版本化

实际系统中,快照往往不止一个。你可能每小时做一个快照,一天下来就有 24 个。这时候映射表怎么管?

我见过两种主流做法:

方案 原理 优点 缺点
全量映射表 每个快照维护一份完整的 LBA→PBA 映射 查询快,删除快照简单 空间开销大,N 个快照需要 N 倍元数据
增量映射表 只记录快照时间点后发生变化的 LBA 映射 元数据量小,适合大量快照 查询时需要回溯快照链,延迟略高

我个人更倾向于增量映射表 + 分层合并的策略。为什么?因为全量映射表在快照数量多时,内存根本扛不住。我曾经在一个 100TB 集群上试过,全量映射表吃了 32GB 内存,而增量方案只用了不到 4GB。

避坑指南:增量映射表虽然省内存,但快照链不能太长。我曾经见过一个客户做了 1000 个快照,结果读一个块要回溯 500 次映射,IO 延迟从 1ms 飙升到 50ms。解决方案是定期做「快照合并」——把多个增量快照合成一个全量快照。

3.5 RoW 的写放大分析

我们来算一笔账。假设每次写操作是 4KB:

  • CoW 写放大:读老块(4KB) + 写老块到快照区(4KB) + 写新数据(4KB) = 12KB 实际 IO
  • RoW 写放大:写新数据(4KB) + 更新映射表(通常小于 1KB) = 约 4KB 实际 IO

RoW 的写放大只有 CoW 的 1/3。但 RoW 也有代价——读性能可能下降。因为数据分散在不同物理位置,顺序读变成了随机读。不过,现代 SSD 的随机读性能已经很强,这个代价在很多场景下可以接受。

我的经验:在纯 SSD 集群上,RoW 的写性能优势非常突出。但在 HDD 集群上,随机读的代价会放大,这时候需要结合预读缓存来弥补。我建议根据存储介质来选择策略。

3.6 元数据持久化与一致性

映射表是 RoW 的命根子。如果映射表丢了,整个卷的数据就全完了。所以元数据的持久化是重中之重。

我常用的做法是:

  1. 写前日志(WAL):每次映射表更新前,先把操作记到日志里
  2. 双写映射表:内存中维护一份,定期刷到持久化存储
  3. 校验和:每个映射条目带 CRC32 校验,防止静默数据损坏

嗯,这里有个细节:WAL 本身也要考虑性能。我习惯把 WAL 放在独立的 NVMe 设备上,避免和数据路径争抢 IO。

小技巧:映射表可以按 LBA 范围分片。比如 0-1TB 的映射由节点 A 管理,1TB-2TB 由节点 B 管理。这样既提高了并发度,也降低了单点故障风险。

3.7 RoW 的典型应用场景

最后,我总结一下 RoW 最适合的场景:

  • 写密集型工作负载:比如数据库日志、视频监控写入
  • 大量快照场景:比如每小时快照,保留 30 天
  • SSD/NVMe 存储:随机读性能好,RoW 的读代价可控
  • 云原生环境:容器卷快照、虚拟机磁盘快照

RoW 不是银弹。如果你的场景是读多写少,而且快照数量很少,CoW 可能更简单。但如果你追求写性能、需要大量快照,RoW 是更优的选择。