3. 状态快照与增量保存:全量快照、差异快照、压缩与合并

在Crash边界恢复体系中,状态快照是持久化保存系统或应用关键内存状态的核心手段。全量快照、差异快照、压缩与合并三层技术共同构成了高效、可迁移的恢复基线。本章聚焦其原理、实现流程、配置方法及工程调优要点。

3.1 快照类型与适用场景

快照类型原理空间效率恢复速度适用场景
全量快照完整复制所有页面/堆区域低(占用最大)最快(无需依赖基快照)初始 checkpoint、周期基线、跨版本迁移
差异快照仅记录自上次全量快照后变更的页面/块中等中等(需加载基快照 + 差异)连续多次 checkpoint、日志精简
压缩快照对全量或差异快照进行压缩(zstd/lz4)高稍慢(需解压)存储带宽受限、长期归档
合并快照将多个差异快照与基快照合并为新的全量快照高(合并后空间释放)快(恢复时只需单个全量)定期整理、减少恢复依赖链

3.2 全量快照实现流程

  1. 挂起执行流:在安全点(如中断处理完成、任务切换前)冻结目标进程/线程。
  2. 内存区域枚举:遍历所有已映射的虚拟内存区域(VMA),记录地址、大小、权限。
  3. 逐页拷贝:按页(通常4KB)读取物理/虚拟内存,写入快照文件头部元数据 + 页面数据。
  4. 元数据持久化:写入快照版本号、时间戳、寄存器上下文(可选)、校验和。
  5. 恢复执行:写完成后解锁执行流。

以下是一个简化的全量快照写入流程(C 伪代码):

void full_snapshot(const char *path, task_t *task) {
    snapshot_file_t *sf = snapshot_create(path);
    // 1. 写入快照头部
    sf->header.magic   = SNAP_MAGIC;
    sf->header.version = SNAP_VERSION;
    sf->header.timestamp = get_timestamp();
    sf->header.checksum = 0; // 临时占位
    // 2. 遍历进程内存映射
    vma_list_t *vma = task->vma_list;
    while (vma) {
        sf_region_t reg = { .start = vma->start, .size = vma->size };
        snapshot_write_region(sf, ®);
        // 3. 逐页拷贝数据
        for (addr_t a = vma->start; a < vma->start + vma->size; a += PAGE_SIZE) {
            uint8_t page[PAGE_SIZE];
            copy_page(task, a, page); // 从源地址复制
            snapshot_write_page(sf, a, page);
        }
        vma = vma->next;
    }
    // 4. 计算并写入全局校验和
    sf->header.checksum = crc32(sf->buffer, sf->offset);
    snapshot_close(sf);
}

3.3 差异快照实现

差异快照依赖写时跟踪(Write Tracking)。常用方法有:

  • 硬件 dirty bit:利用 CPU 页表脏位,在写入时由 MMU 置位,快照线程扫描并清零。
  • 软件 shadow page:在内存管理器中维护一份写保护副本,写前触发缺页中断并记录。

差异快照生成步骤:

  1. 在上一轮全量快照完成后,开启写跟踪(如设置页表脏位监控)。
  2. 等待一段时间或收到 checkpoint 触发。
  3. 挂起执行流,遍历所有监控页面,生成“脏页位图”(dirty bitmap)。
  4. 只将位图中标记为脏的页面复制到差异快照文件。
  5. 清除脏位,恢复执行。

性能关键指标:差异快照的大小取决于脏页率(R_dirty)和快照间隔(Tinterval)。典型嵌入式场景中,R_dirty < 5% 时差异快照比全量快照节省 95% 以上存储。

3.4 压缩与合并策略

压缩:对快照数据(全量或差异)施加流式压缩算法。推荐的压缩配置:

算法压缩等级解压速度压缩率(典型)适用场景
LZ41-9(常用4)~500 MB/s2:1 ~ 3:1嵌入式、低延时恢复
Zstd1-19(常用3)~300 MB/s3:1 ~ 5:1存储优先、服务器端
Snappy固定~800 MB/s1.5:1 ~ 2.5:1极高吞吐、实时快照

合并:当积累 n 个差异快照后,可将“全量快照 + 后序所有差异快照”合并成一个新的全量快照,删除旧差异链。合并流程:

  • 加载基快照到内存。
  • 按时间顺序应用每个差异快照中的脏页更新。
  • 将最终状态写为新的全量快照。
  • 原子替换快照文件(先写临时文件,再 rename)。

3.5 配置与命令示例(基于 SimEnv-Checkpoint 框架)

YAML 配置快照策略:

snapshot:
  mode: hybrid                  # full | diff | hybrid
  baseline_interval: 3600       # 全量快照间隔(秒)
  diff_interval: 60             # 差异快照间隔(秒)
  compression: zstd
  compression_level: 3
  merge_policy:
    max_diff_count: 10          # 差异快照数达到10后自动合并
    merge_window: "00:00"       # 在每天0点合并(可选)
  output_dir: /var/checkpoints/

命令行触发快照:

checkpointctl --pid 1234 --action snapshot --type diff
checkpointctl --pid 1234 --action merge --target /tmp/merged.snap
checkpointctl --pid 1234 --action list

API 调用示例(Python 绑定):

import crlib

snap = crlib.Checkpoint(pid=1234)
snap.create(type="diff", compress=True, level=3)
snap.merge(base="/path/base.snap", diffs=["diff1.snap", "diff2.snap"], output="merged.snap")

3.6 常见错误与优化

⚠ 常见错误 1:差异快照被部分写入后宕机
如果差异快照在写入期间系统崩溃,快照文件可能不完整,恢复时校验和不匹配。解决方案:写前先写一个临时文件,写完后原子 rename;或使用事务性写入 (write + fsync + rename)。
⚠ 常见错误 2:脏页跟踪未及时清零导致重复记录
若差异快照生成后未清除脏位,下一次差异快照会包含已有数据,造成空间膨胀和恢复逻辑错误。务必在快照生成后原子性地清除脏位(配合内存屏障)。
💡 性能优化要点
  • 多线程拷贝:全量快照时使用多个工作线程并行复制不同区域,可提升 2-4 倍吞吐。
  • 异步压缩:将快照数据通过管道送入独立的压缩线程,避免写操作阻塞内存访问。
  • 增量合并阈值动态调整:根据近期脏页率变化自动调整 diff_interval,可在负载低时增加快照频率,高负载时自动降级。
快照类型与合并流程 初始状态 全量快照 V1 差异快照 D2 差异快照 D3 差异快照 D4 合并新全量
快照 V2 合并策略 V1 + D2 + D3 + D4 → 新全量 V2 释放旧差异链空间