4. 增量快照与断点续传结合:增量传输的断点续传模型、一致性保障、数据校验机制

增量快照做完了,数据也压缩好了。但网络不是完美的,传输过程中随时可能断。我见过太多系统,增量数据传了一半,连接断了,然后从头再来——这在大数据量下简直是灾难。

所以这一章,我们聊聊怎么把断点续传和增量快照结合起来。说白了,就是让传输过程也能“增量”。

4.1 增量传输的断点续传模型

先想一个问题:断点续传的本质是什么?

是记录“我已经传到了哪里”。对于全量文件,我们记录字节偏移量。但对于增量快照,情况复杂得多——增量数据是逻辑分片的,不是连续的字节流。

我个人习惯把增量快照的断点续传模型分成三层:

  1. 分片层:把增量数据切成固定大小的逻辑块(比如 4MB 一块)
  2. 索引层:记录每个分片的元数据(分片ID、起始偏移、哈希值)
  3. 状态层:记录哪些分片已确认、哪些待传输

举个例子,假设一次增量快照产生了 200MB 的变更数据。我会把它切成 50 个 4MB 的分片。每个分片独立计算 SHA-256,然后按顺序传输。接收方每收到一个分片,就回传一个 ACK。发送方维护一个位图(bitmap),标记哪些分片已确认。

连接断了怎么办?很简单——发送方读取位图,找到第一个未确认的分片,从这个分片开始继续传。已经传完的分片不需要重传。

核心模型:增量快照断点续传 = 逻辑分片 + 位图确认 + 最小重传粒度

这里有个关键点:分片大小怎么定?我在项目中踩过坑。分片太小(比如 64KB),位图太大,管理开销高;分片太大(比如 256MB),断点后重传的代价高。我一般建议 4MB~16MB,具体看网络带宽和延迟。

增量快照断点续传模型 发送方(源端) 分片队列 [0..49] 位图 bitmap[50] 已确认:分片 0..23 待传输:分片 24..49 传输 网络 接收 接收方(目标端) 接收缓冲区 SHA-256 校验 ACK / NAK 回传 断点恢复:从分片 24 开始重传 💡 关键设计点 • 分片大小:4MB~16MB,平衡管理开销与重传代价 • 位图持久化:每收到 ACK 立即刷盘,防止进程崩溃丢失进度

4.2 一致性保障

断点续传最怕什么?怕数据不一致。你想想看,增量快照本身就是在某个时间点的一致性快照基础上做的。如果传输过程中,源端又发生了新的变更,那接收方拼出来的数据可能是个“四不像”。

我遇到过这样一个案例:某系统在做增量传输时,源端同时在进行数据 compaction。结果接收方收到的增量数据,一部分是 compaction 前的,一部分是 compaction 后的——数据直接乱套了。

怎么解决?核心思路是快照隔离。

4.2.1 快照版本号

每次增量快照生成时,分配一个全局唯一的版本号(比如递增的 LSN 或时间戳)。这个版本号贯穿整个传输过程:

  • 发送方:在增量快照的元数据中记录版本号
  • 传输过程:每个分片都携带这个版本号
  • 接收方:校验所有分片的版本号是否一致

如果传输中断后恢复,发送方会检查当前增量快照的版本号是否仍然有效。如果源端已经产生了新的快照(版本号变了),那就放弃旧的断点,重新开始。

注意:版本号变更不一定要重新全量传输。如果增量快照是 append-only 的(比如 WAL 日志),可以只传输新增的部分。但如果是原地更新的(比如 LSM-Tree 的 compaction),旧快照可能已经失效,必须重新生成。

4.2.2 事务边界对齐

增量快照的断点续传,必须保证事务边界完整。你不能让接收方拿到半个事务。

我个人的做法是:在分片时,以事务为最小不可分割单元。一个事务的变更数据,必须完整地放在同一个分片里。如果分片大小到了阈值,但当前事务还没结束,那就等事务提交后再切分片。

场景 问题 解决方案
事务跨分片 断点后只恢复部分事务,数据不完整 分片以事务边界对齐,禁止跨分片事务
源端并发写入 增量快照包含未提交事务 只捕获已提交事务,使用 MVCC 快照
分片乱序到达 接收方按分片 ID 乱序写入,违反事务顺序 接收方缓存乱序分片,按事务顺序回放

4.3 数据校验机制

传输过程中,数据可能损坏。网络丢包、磁盘静默错误、内存 bit flip——这些我都遇到过。所以校验不是可选项,是必选项。

我一般用三层校验:

4.3.1 分片级校验

每个分片传输完成后,接收方计算 SHA-256,与发送方携带的哈希值对比。不一致就发 NAK,要求重传这个分片。

这里有个细节:哈希值怎么传?我习惯把哈希值放在分片的元数据头部,和分片数据一起传输。这样接收方收到后可以立即校验,不需要额外的通信开销。

// 分片数据结构
struct Chunk {
    uint64_t chunk_id;        // 分片ID
    uint64_t version;         // 快照版本号
    uint64_t offset;          // 在增量快照中的偏移
    uint32_t data_len;        // 数据长度
    uint8_t  data[data_len];  // 实际数据
    uint8_t  sha256[32];      // 数据的 SHA-256 哈希
};

// 校验逻辑
bool verify_chunk(const Chunk* chunk) {
    uint8_t computed[32];
    sha256(chunk->data, chunk->data_len, computed);
    return memcmp(computed, chunk->sha256, 32) == 0;
}

4.3.2 分片链式校验

分片级校验只能保证单个分片的数据完整性。但增量快照是有序的——分片之间有依赖关系。比如分片 5 的变更可能依赖于分片 4 的变更结果。

我建议在分片元数据中加入前一个分片的哈希值,形成哈希链。这样接收方不仅能校验单个分片,还能校验分片之间的顺序和完整性。

小技巧:哈希链的最后一个分片的哈希值,可以作为整个增量快照的“指纹”。接收方在全部接收完成后,可以用这个指纹做一次全局校验,确保没有遗漏任何分片。

4.3.3 端到端校验

分片校验和链式校验都做完了,是不是就万无一失了?不一定。我遇到过一种情况:所有分片校验都通过了,但接收方应用增量数据后,发现数据总量对不上。

原因是增量快照生成时,源端记录了总数据量和总行数。接收方在应用完所有分片后,应该对比这些总量指标。如果对不上,说明增量快照本身可能有问题(比如生成过程中遗漏了某些变更)。

端到端校验的典型做法:

  1. 发送方在增量快照元数据中记录:总数据量、总行数、总事务数
  2. 接收方在应用完成后,统计本地数据的变化量
  3. 对比两个值,不一致则标记为失败,触发全量重传

经验之谈:端到端校验是最后一道防线。虽然开销大(需要全量统计),但能发现分片校验和链式校验都发现不了的问题。我建议在关键业务场景中开启,非关键场景可以只做分片校验。

4.4 避坑指南

最后分享几个我踩过的坑:

  • 位图持久化不及时:我曾经遇到过发送方进程崩溃,位图没来得及刷盘。重启后位图丢失,所有分片重传。解决方案是每收到一个 ACK 就 fsync 位图文件。
  • 分片大小固定导致尾部浪费:最后一个分片可能只有几 KB,但元数据开销一样大。我后来改用动态分片——前 N-1 个分片固定大小,最后一个分片自适应。
  • 校验算法选择不当:CRC32 速度快但碰撞概率高,不适合关键数据。SHA-256 安全但计算开销大。我一般折中:分片校验用 CRC32C(硬件加速),端到端校验用 SHA-256。
  • 忽略接收方的写入顺序:接收方如果乱序写入,可能导致数据不一致。我要求接收方按分片 ID 顺序写入,乱序分片先缓存。

嗯,这一章的内容就这些。增量快照的断点续传,说白了就是三个东西:分片、位图、校验。把这三件事做好,传输过程基本不会出大问题。