Table of Contents

Snapshot 使用指南——流式快照 / 增量快照 / 段表恢复

给谁看:需要流式帧文件快照 / raft 日志压缩的组件开发者 回答什么:StreamSnapshot vs IncrementalSnapshot 怎么选 / 按字节截断 vs 版本号截断 / 段表 O(1) 恢复 / 阈值合并 本篇只讲机制——帧字节布局见源码(Structures/Snapshot/ 各 partial) 定位:Structures/ 6 子族之一,搭配件;与 Mirror 同族异坐标系(字节截断 vs 版本号截断)

0. 一句话总纲

Snapshot = 完整状态基线纯流式形态 + 按字节截断 + 增量形态(新段 append 不重写旧段)

  • StreamSnapshot = 流式帧文件([Header 14B][Data][Footer 28B] 多帧)
  • IncrementalSnapshot = 段=帧的增量快照:新段 append 不重写旧段 + 段表 O(1) 恢复 + 阈值合并

两者共用同一帧格式StreamFrameCodec,Magic=SNHD/SNFT)与 SnapshotBase 三水位/2PC 能力;区别在语义层——StreamSnapshot 的帧 = 数据帧,IncrementalSnapshot 的帧 = 段(带 N₀ 8B 前缀)。

1. 定位

  • 搭配件:与 Ring/Log 主结构搭配,承载"完整状态基线"。Ring 是数据真相源(record 流),Snapshot 是基线导出/恢复加速件——存在性=优化非正确性,无快照也能走全量重放。
  • 形态差异(StreamSnapshot vs IncrementalSnapshot):
    • StreamSnapshot:流式帧文件——单条快照 = N 帧(每帧 [Header][Data][Footer],CRC64 流式增量);适合一次性整体导出。
    • IncrementalSnapshot:段=帧的增量——每段 = 一帧([Header][N₀ 8B][条目流][Footer]);新段 append 不重写旧段,快照越频繁写省越多(基准 8 次快照 4.5× 字节差);段累积达阈值自动合并。
  • 与 Mirror 同族异坐标系:Mirror 按版本号截断(版本链 N=2 轮转回收),Snapshot 按字节截断(纯流式无版本链)。两者都承载"完整状态基线",但 Mirror 是版本链形态(覆写历史可读),Snapshot 是追加流形态(append 可回滚、Overwrite 不可回滚)。
  • ⚠️ 与 Ring 自带区间快照(OpenSnapshotReader/Writer)无引用关系——Ring 那是导出能力(区间字节流),本子族是独立文件形态(SnapshotBase 持有独立 StorageEngine)。同名为"快照"是历史巧合,不要混用
  • raft 日志压缩场景IncrementalSnapshot = 镜像快照设计稿第三部件(TierWAL 内部快照)——段 = raft apply 序号快照点,新段不重写旧段契合"快照越频繁写省越多"的 raft 日志压缩特征。

1.1 选型决策

场景特征 选型 理由
一次性整体导出(备份/迁移/冷存档) StreamSnapshot 单条快照 = N 帧流式追加,CRC 增量累积零内存压力;不需要段表/合并
raft 日志压缩(周期性快照点) IncrementalSnapshot 新段 append 不重写旧段契合 raft 周期快照;段表 O(1) 恢复定位;阈值合并控制段数
raft 快照安装(外部镜像注入) IncrementalSnapshot + ImportSegmentAsync 替换语义——段表替换为单段,旧段物理回收
整体快照外存 + 版本化历史可读 Mirrormirror.md 版本链 N=2 轮转——覆写历史可读;与 Snapshot 同族异坐标系
区间字节流导出(依附 Ring 生命周期) Ring 的 OpenSnapshotReader/Writer 不是本子族——Ring 导出能力,无独立 StorageEngine

关键判断点:要"周期性快照 + 越频繁越省写"→ IncrementalSnapshot;要"一次性整体流"→ StreamSnapshot;要"版本化历史"→ Mirror。

2. Settings 全参数

SnapshotSettings 继承 SettingsMainEngine / MetaPolicyKind / MetaOpaqueBytes 公共面),StreamSnapshotSettings / IncrementalSnapshotSettings 各自派生。两子类公共字段相同,差异在专属配置。

参数 来源 StreamSnapshot 默认 IncrementalSnapshot 默认
MainEngine Settings ("tc.snapshot", 256MB, enableSegmentation=true, preallocateFile=false) ("tc.snapshot.inc", 256MB, enableSegmentation=true, preallocateFile=false)
Name Settings "tc.snapshot" "tc.snapshot.inc"
PreallocateFile Settings false(按需增长,GB/TB 流式不预分配) false
DeleteOnClose Settings false(跨重启必须保留) false
MetaPolicyKind Settings Disabled(默认不装 meta——纯流式可走 Backward 扫描兜底) Disabled(增量快照强烈建议Managed——段表靠 opaque meta 落盘)
MetaOpaqueBytes Settings 0 须 ≥ 段表容量 = 8 + 段数上限 × 40(如 64 段 → ≥2576B)
SessionBufferSize SnapshotSettings 128KB(双 buffer 写会话/读会话单 buffer 大小) 128KB
CompactSegmentThreshold IncrementalSnapshotSettings 专属 8(段数达此值自触发 CompactSegments——低频全量重写)

便捷构造(无参 new StreamSnapshotSettings() / new IncrementalSnapshotSettings())即用默认值;要自定义注入 StorageEngineOptionsmeta 装配IncrementalSnapshot 段表 = opaque meta 载体,禁用 meta = 段表丢失 → 恢复只能 Backward 扫描兜底(O(N) 全盘),生产场景请开 Managed

3. 构造与生命周期

SnapshotBase 继承 LifecycleBase<SnapshotRecoveryHints>——三段式生命周期:newInitialize()(同步 void,启动后台恢复)→ WaitForReady()(等恢复就绪);异步调用方用 WaitForReadyAsync。无便捷工厂(与 Log 族同形态——封闭结构才有 CreateAsync)。

using TC.Tier.Core.IO;
using TC.Tier.Runtime.Storage;
using TC.Tier.Runtime.Structures.Snapshot;

// ① 文件系统(组合根;示例用 mem,真磁盘改 "local:///path")
using var fs = TierFs.New("memory:");

// ② StreamSnapshot:流式帧文件(meta 关闭,走 Backward 扫描兜底)
var streamSettings = new StreamSnapshotSettings();
using var stream = new StreamSnapshot(fs, streamSettings);
stream.Initialize();
stream.WaitForReady();

// ③ IncrementalSnapshot:增量快照(meta 开启——段表靠 opaque 落盘)
var incSettings = new IncrementalSnapshotSettings
{
    MetaPolicyKind = MetaPolicyKind.Managed,
    MetaOpaqueBytes = 4096,   // 容纳 ≥100 段表条目(8 + 100×40 = 4008B)
    CompactSegmentThreshold = 16,
};
using var inc = new IncrementalSnapshot(fs, incSettings);
inc.Initialize();
inc.WaitForReady();

// Dispose 幂等——Resources 链统一释放(主引擎/meta 引擎/MetaPolicy 全 owned)

Dispose 幂等:SnapshotBase 把主引擎、Managed 模式下的 meta 引擎、MetaPolicy 全部登记进 Resources(owned),统一释放顺序。Initialize 后台启动恢复(OnInitializeBegin 同步 init 主引擎 + meta 引擎,WaitForReadyAsync 等 join 完成),恢复算法经 CreateRecovery 工厂单点创建(注入实例则直接用)。

3.1 分步构造 + hints 注入(观察恢复/注入水位)

raft 节点重启已知 apply 序号时,可注入 SnapshotRecoveryHints 优先于扫盘:

// 已知写尾(如 raft 节点重启前持久化的 apply 水位)
var hints = new SnapshotRecoveryHints
{
    WriteAddress = knownWriteTail,
    PhysicalWriteAddress = knownPhysTail,   // 可选;缺省走 AlignUpToSector
};
using var inc = new IncrementalSnapshot(fs, incSettings);
inc.Initialize(hints);   // 透传给 IncrementalRecovery.OnRecoveryCoreAsync
inc.WaitForReady();
// 恢复后段表已加载、_nextSeq 已续接,可直接 ReadAllChunksAsync 或 AppendSegmentAsync

注入恢复算法实例(替换默认 IncrementalRecovery):

IRecovery<SnapshotRecoveryHints> customRecovery = new MySnapshotRecovery(...);
using var inc = new IncrementalSnapshot(fs, incSettings, recovery: customRecovery);
// 基类构造透传——CreateRecovery() 工厂被跳过(注入实例优先)

4. API 详表

4.1 StreamSnapshot

API 签名 行为
OpenWrite StreamFrameWriter OpenWrite() 物理起点续接开新帧写入器;flush 回调推进双水位(逻辑按 data.Length 非对齐、物理按对齐长度)
OpenWriteRange StreamFrameWriter OpenWriteRange(LogicalAddress start, LogicalAddress end) 指定逻辑区间开帧(物理续接、逻辑不推进——逻辑↔物理由 checkpoint 二分换算)
OpenRead StreamFrameReader OpenRead() _truncatedAddress 读到 _writeAddress(剩余可读 = 区间长度)
OpenReadRange StreamFrameReader OpenReadRange(LogicalAddress start, LogicalAddress end) 指定逻辑区间读
StreamFrameWriter
WriteAsync ValueTask WriteAsync(ReadOnlyMemory<byte> data, ct) 写一个 entry(首次自动加帧头;CRC64 增量累积;EntryCount+1)。快速路径同步完成
Write void Write(ReadOnlySpan<byte> data) 同步轨
CompleteAsync ValueTask CompleteAsync(ct) 写帧尾(FooterMagic + TotalLength + EntryCount + Crc64)+ flush + 重置(可开新帧)
Complete void Complete() 同步轨
DisposeAsync ValueTask DisposeAsync() Complete 的帧自动闭环(仅 using 也安全)
TotalDataLength long 当前帧已写 data 字节数
EntryCount long 当前帧 entry 条数
StreamFrameReader
ReadDataAsync ValueTask<int> ReadDataAsync(Memory<byte> dest, ct) 读 data(EOF 返回 0,自动解析校验 footer CRC64)。帧头 magic 非法抛 IOException
ReadAllChunksAsync IAsyncEnumerable<ReadOnlyMemory<byte>> ReadAllChunksAsync(ct) 64KB chunk 流式枚举
IsFooterValid bool footer 是否已解析且 CRC 通过(读至 data 末尾自动解析)
EntryCount / TotalLength / StoredChecksum long/long/ulong footer 字段(解析前为 0)

写流程OpenWriteWriteAsync × N → CompleteAsyncusing 自动 Dispose。多帧:CompleteAsyncResetFrame 内部复位,可直接继续 WriteAsync 开新帧。

// StreamFrameWriter 写 + Complete(多帧)
await using var writer = stream.OpenWrite();

// 帧一:写 N 个 entry
foreach (var entry in entriesChunk1)
    await writer.WriteAsync(entry, ct);
await writer.CompleteAsync(ct);   // 帧尾落盘 + flush + ResetFrame(EntryCount 清零)

// 帧二:紧接再写(首 WriteAsync 自动加新帧头)
await writer.WriteAsync(someEntry, ct);
await writer.CompleteAsync(ct);
// DisposeAsync 等价 CompleteAsync(若忘记 Complete 也安全闭环)

// 读回:校验 footer CRC64
await using var reader = stream.OpenRead();
var buf = new byte[64 * 1024];
int n;
while ((n = await reader.ReadDataAsync(buf, ct)) > 0)
    Consume(buf.AsSpan(0, n));
if (!reader.IsFooterValid) throw new InvalidDataException("帧 CRC 校验失败");

4.2 IncrementalSnapshot

API 签名 行为
AppendSegmentAsync ValueTask<long> AppendSegmentAsync(long n0, IAsyncEnumerable<ReadOnlyMemory<byte>> chunks, ct) 追加段[Header 14B][N₀ 8B][chunks][Footer 28B] 一帧 → 2PC 提交 → 段表注册(追加)+ meta 原子落盘。返回 n0。段数达阈值自触发合并。单写者——并发/重入抛 InvalidOperationException
ImportSegmentAsync ValueTask<long> ImportSegmentAsync(long n0, IAsyncEnumerable<ReadOnlyMemory<byte>> chunks, ct) 替换段(raft 快照安装语义):写完整镜像段 → 段表替换为单段(旧段物理回收)。chunk 流异常 = Abort 回滚——新段清除、旧快照完好
ReadAllChunksAsync IAsyncEnumerable<ReadOnlyMemory<byte>> ReadAllChunksAsync(ct) 顺序流式读全部段(跳过每段 N₀ 前缀)= 最新快照条目流。段级 CRC64 校验失败抛 InvalidDataException
ReadSegmentDataAsync IAsyncEnumerable<ReadOnlyMemory<byte>> ReadSegmentDataAsync(int index, ct) 段 i 的数据流(测试/诊断用;越界抛)
CompactSegmentsAsync ValueTask CompactSegmentsAsync(ct) 合并全部段为新基线段(读全段拼接 → 写新帧 → 截旧段 → 段表=单段 + meta 落盘)。低频——成本 = 一次全量写
ClearAsync ValueTask ClearAsync(ct) 清空全部段(截断全量 + 段表空 + meta 落盘)
SegmentCount int 段数(内存段表,含最新段)
LatestN0 long 最新段覆盖点 N₀(无段=0)
GetSegmentN0 long GetSegmentN0(int index) 段 i 的 N₀(单调递增;越界抛)

段格式[Header 14B][N₀ 8B][chunks...][Footer 28B]。N₀ = 段自描述覆盖点(raft apply 序号),独立读一段可知其覆盖点。段表(opaque meta 载体)= [count 4B][pad 4B][条目 40B × N],每条目 = LogicalStart 16B + PhysStart 16B + N₀ 8B

段表恢复 + 阈值合并示例:

// 追加段(raft 快照:n0 = apply 序号)
await inc.AppendSegmentAsync(applyIndex, ProduceChunksAsync(ct), ct);
// 段数 ≥ CompactSegmentThreshold(默认8) → 自触发 CompactSegmentsCoreAsync
//   合并 = 一次全量重写为新基线段(覆盖点 N₀ 不变)+ 旧段物理回收 + 段表=单段 + meta 落盘

// raft 快照安装(替换语义):导入完整镜像段,段表替换为单段
await inc.ImportSegmentAsync(applyIndex, ProduceFullImageAsync(ct), ct);

// 恢复后读最新快照(段表驱动逐段顺序拼接)
await foreach (var chunk in inc.ReadAllChunksAsync(ct))
    Consume(chunk);
// 段级帧 CRC64 失败抛 InvalidDataException

// 显式合并(不依赖阈值)
await inc.CompactSegmentsAsync(ct);

单写者契约AppendSegmentAsync / ImportSegmentAsync / CompactSegmentsAsync / ClearAsync 互斥(_writing 0/1 闸门);并发/重入抛 InvalidOperationException。raft 快照低频串行天然契合。多生产者场景须外部队列汇聚。

4.3 共有(SnapshotBase)

API 签名 行为
三水位
WriteAddress ref LogicalAddress 逻辑写尾(非对齐)
PhysicalWriteAddress ref LogicalAddress 物理写尾(扇区对齐,DIO 写基准)
TruncatedAddress ref LogicalAddress 逻辑截断点(头截断推进)
Size long 流大小 = WriteAddress - TruncatedAddress
Append / AppendAsync void Append(ReadOnlySpan<byte>) / ValueTask AppendAsync(ReadOnlyMemory<byte>, ct) append(可回滚——双水位推进;空数据 no-op)
Overwrite / OverwriteAsync void Overwrite(LogicalAddress, ReadOnlySpan<byte>) / ... 不可回滚——原位覆写破坏旧数据,2PC 不覆盖
截断(按字节)
TruncatePrefix void TruncatePrefix(LogicalAddress address) 头截断(业务调,回收已读头部)+ 推进 TruncatedAddress
TruncateSuffix void TruncateSuffix(LogicalAddress address) 尾截断(2PC Abort 内部)+ 写窗口作废
ReclaimRange void ReclaimRange(LogicalAddress? from, LogicalAddress? to) 区间 PunchHole
2PCITransactionParticipant
Prepare / PrepareAsync void Prepare(long seq) flush + meta.Commit(记 LastPreparedSeq + 水位)
ConfirmCommitted void ConfirmCommitted(long seq) CAS 推进 LastCommittedSeq + 推进 CommittedWriteAddress(Abort 回退点)
Abort / AbortAsync void Abort(long seq) TruncateSuffix(CommittedWriteAddress) 回滚 append 部分(Overwrite 不可回滚)
OnCommitted void OnCommitted(long seq, Action callback) 注册提交回调(已提交到更高 seq 立即同步触发)
meta opaque
SetOpaqueMeta void SetOpaqueMeta(ReadOnlySpan<byte> data) 登记外部 opaque(随下次水位提交原子落盘)。Disabled 抛 InvalidOperationException;超 MetaOpaqueBytes 抛 ArgumentException
ReadOpaqueMeta ReadOnlySpan<byte> ReadOpaqueMeta() 读最近已提交 opaque(无/禁用 = 空)
地址算术
AdvanceAddress LogicalAddress AdvanceAddress(LogicalAddress addr, long delta) 段感知地址推进/回退
Distance long Distance(LogicalAddress from, LogicalAddress to) 段感知两地址间字节距离
SectorSize int 设备扇区大小
DioAlignment int(protected) max(SectorSize, 4096)——保证 DIO 兼容

4.4 2PC 编排(跨结构原子性)

SnapshotBase 实现 ITransactionParticipant——可参与 Transactions/TransactionLog 协调器驱动的跨结构原子性(详见 session.md)。Snapshot 侧的 2PC 语义:

  • Prepare(seq)_engine.Flush() + 记 LastPreparedSeq + WriteMeta()(meta 落盘 prepared 水位——悬干状态)
  • ConfirmCommitted(seq):CAS 推进 LastCommittedSeq + 推进 CommittedWriteAddress(Abort 回退基准)+ WriteMeta() + FireTransactionCallbacks
  • Abort(seq)TruncateSuffix(CommittedWriteAddress) 回滚 append 部分(Overwrite 不可回滚)+ WriteMeta()
  • OnCommitted(seq, callback):注册回调(已提交到更高 seq 立即同步触发——链式触发)
// Snapshot 单独走 2PC(IncrementalSnapshot 内部即用——段写事务)
long seq = 1;
stream.Prepare(seq);              // flush + meta 记 prepared
stream.ConfirmCommitted(seq);     // CAS 推进 committed + CommittedWriteAddress 推进
// 后续 Abort(seq-1) 不影响;CommittedWriteAddress 是新的回退基准

// 异常路径
stream.Abort(seq);   // TruncateSuffix(CommittedWriteAddress)——append 部分回滚,Overwrite 不回滚

IncrementalSnapshot.AppendSegmentAsync 内部即走 2PCPrepare(seq)ConfirmCommitted(seq) → 段表注册 + meta 落盘;chunk 流异常 = Abort(seq)——尾截断回滚(新段物理清除,旧段完好)。这是会话模式一致性核心(详见 session.md)。

5. 自定义注入工厂

SnapshotBase 构造接受三个可选注入参数;子类(StreamSnapshot/IncrementalSnapshot)转发到基类构造。

注入点 类型 用途
recovery IRecovery<SnapshotRecoveryHints>? 恢复算法实例(默认走 CreateRecovery() 工厂——StreamSnapshot=DefaultSnapshotRecovery、IncrementalSnapshot=IncrementalRecovery)。注入实例则跳过工厂
metaPolicyFactory MetaPolicyFactory<SnapshotMetaHeader, SnapshotMetaPayload>? meta 策略工厂委托(MetaPolicyKind → IMetaPolicy);默认走 CreateMetaPolicyDefault——Managed/Transport/Disabled 三选一。自定义注入可替换 Managed 实现
metaTransport IMetaTransport? Transport 模式的传输实例(默认回落到内嵌 MetaHost——meta 块作为 IS_META 帧追加进快照流)
Contracts
ISnapshotCodec internal sealed StreamFrameCodec 流式帧 codec——Header/Footer 读写、magic 校验。两子类共用同一实例(构造期 new StreamFrameCodec() 注入基类)
ISnapshotWriteSession IDisposable, IAsyncDisposable 双 buffer flush 流水线写会话——WriteAsync/Write/WriteSmall/FlushIfFullAsync/FlushAsync/OnFlushed 事件
ISnapshotReadSession IAsyncDisposable 双 buffer 异步预读读会话——ReadAsync/LogicalStart/LogicalEnd/PhysicalEnd(物理/逻辑偏移分离)
SnapshotMetaHeader 12B struct meta 三段式 header(MagicValue/Version/Flags/PayloadLength/PaddingLength
SnapshotMetaPayload 80B struct meta payload(三水位 + CommittedWriteAddress + LastCommittedSeq/LastPreparedSeq
SnapshotRecoveryHints readonly struct 恢复提示(WriteAddress?/PhysicalWriteAddress?)——上层注入已知水位,优先于扫盘
SnapshotRecordFields readonly record struct 帧字段包(Header: Flags/PayloadLength/PaddingLength;Footer: TotalLength/EntryCount)
StreamFrameCodec internal sealed ISnapshotCodec 默认实现——两子类共用
ITransactionParticipant 接口 SnapshotBase 实现——2PC 协调器(Transactions/TransactionLog)驱动跨结构原子性,详见 session.md

6. 持久化机制

6.1 流式帧格式(StreamFrameCodec

两子类共用同一帧格式

[Header 14B][Data][Footer 28B][padding 对齐到扇区]
  • Header 14BStreamFrameHeader):MagicValue(4B,"SNHD") + Version(2B,major=1/minor=0) + Flags(2B) + PayloadLength(4B) + PaddingLength(2B)
  • Footer 28BStreamFrameFooter):Magic(4B,"SNFT") + TotalLength(8B) + EntryCount(8B) + Crc(8B)
  • CRC64:覆盖 Header + Data + Footer 前 20B(CRC 自身 8B 不参与)。流式增量累积(UnifiedCrc64,边写边算,不驻内存整块)——GB/TB 级快照可零内存压力流式写
  • FooterMagic:供 Backward 扫描定位帧尾(恢复取真尾——从尾向头扫 chunkSize=64KB 块找 FooterMagic 命中)
  • 对齐粒度:扇区(codec Alignment=512);StreamSnapshotSettings 默认段几何 256MB + 分段开启 + 不预分配(GB/TB 流式跨段扩容)

6.2 按字节截断 vs Mirror 版本号截断

维度 Snapshot(StreamSnapshot/IncrementalSnapshot) Mirror(WholeMirror/PagedMirror)
截断坐标系 字节LogicalAddress——头/尾字节地址) 版本号(版本链 N=2 轮转)
append 可回滚 TruncateSuffix(CommittedWriteAddress)——尾部字节回滚 ❌(版本链不可回滚,靠 N=2 轮转回收旧版本)
Overwrite 不可回滚 ✅ 独立方法名显形(覆写破坏旧数据,2PC 不覆盖) WritePage 等覆写语义
版本链 ❌ 无版本概念 ✅ N=2 轮转
epoch ❌ 无
同族定位 纯流式形态(基线导出) 版本链形态(基线镜像)

同族异坐标系:两者都承载"完整状态基线",但 Snapshot 是追加流(Append 可回滚、Overwrite 不可回滚、无版本链),Mirror 是版本链(N=2 轮转、覆写历史可读)。raft 日志压缩选哪个看场景:流式整体导出 → StreamSnapshot;版本化镜像 → Mirror;段级增量(新段不重写旧段) → IncrementalSnapshot。

6.3 IncrementalSnapshot 段累积机制

段1:[Header 14B][N0₁ 8B][条目流₁][Footer 28B]  ← append,不重写
段2:[Header 14B][N0₂ 8B][条目流₂][Footer 28B]  ← append
...
段N:[Header 14B][N0_N 8B][条目流_N][Footer 28B] ← append
[合并达阈值 → CompactSegments → 新基线段(N0_N 不变)+ 旧段物理回收]
  • 新段 append 不重写旧段:段写 = OpenWriteSession(physStart) 物理续接,不动旧段;段表注册在 2PC ConfirmCommitted 后(SetOpaqueMeta(SerializeSegments()) + WriteMeta()),opaque meta 搭水位线原子落盘(同一块同一 CRC)
  • 段表 = O(1) 恢复定位:恢复时 DeserializeSegments(ReadOpaqueMeta()) 一次拿到全部段起点(LogicalStart + PhysStart + N₀),免全盘扫描
  • 段表条目 40BLogicalStart 16B + PhysStart 16B + N₀ 8BPhysStart 必须持久化——逻辑↔物理有 flush padding 偏差,无法从逻辑推算)
  • 段级 CRC64:每段独立帧 CRC——崩溃 = 旧段完好、半写段恢复期被悬干裁决截掉
  • 阈值合并:段数 ≥ CompactSegmentThreshold(默认 8)自触发 CompactSegmentsCoreAsync——读全段拼接 → 写新基线帧 → 截旧段(TruncatePrefix 到新基线帧起点向下 4096 对齐)→ 段表=单段 + meta 落盘。低频——成本 = 一次全量写;raft 只需最新快照,段累积不无限增长

6.4 StreamFrameWriter checkpoint 机制(逻辑↔物理换算)

StreamSnapshot 内部维护帧 checkpoint 列表 _frameCheckpoints——每次 OpenWrite / OpenWriteRange 调用 RecordCheckpoint(logical, physical) 追加(按 logical 单调)。LogicalToPhysical(logical) 二分查找最后一个 logical <= 目标 的 checkpoint,再叠加偏移得到物理地址。

为何需要:DIO 写要求扇区对齐(每次 flush 块 = AlignUp(logicalBytes, SectorSize)),但逻辑字节流非对齐。多次 flush 后逻辑↔物理产生 padding 偏差累积,不能简单线性换算。checkpoint = 每个 OpenWrite 起点的双坐标系锚点,二分 + 偏移叠加 = O(log N) 换算。

OpenWriteRange(start, end) 的 flush 回调只推进物理尾(不推进逻辑尾)——逻辑区间由调用方拥有,物理续接。读侧 OpenRead / OpenReadRangeAlignDownAddress(LogicalToPhysical(start)) 定位物理预读起点。

7. 恢复协议

SnapshotBaseLifecycleBase<SnapshotRecoveryHints> 模板——InitializeCreateRecovery() 单点 → RecoveryBase<SnapshotRecoveryHints> 状态机驱动(状态机/闸门/进度全在模板)。

7.1 StreamSnapshot 恢复(DefaultSnapshotRecovery

三级回退(按优先级):

  1. hints(外部注入水位):SnapshotRecoveryHints.WriteAddress 非 null → 直接采用;PhysicalWriteAddress 缺省走 AlignUpToSector(hints.WriteAddress)
  2. meta O(1) 水位MetaPolicyKind != DisabledMetaPolicy.LoadAsync + ReadMetaPayload → 一次拿到 WriteAddress/PhysicalWriteAddress/TruncatedAddress/CommittedWriteAddress/LastCommittedSeq/LastPreparedSeq
  3. Backward 扫描兜底(Disabled):LocateLastFrameEnd()——从 CommittedTail 向头按 64KB 块扫,找 FooterMagic 命中 + Footer 完整可读 → 帧尾 = magic 位置 + FooterSize。前缀洞天然免疫(从尾向头扫,TruncatePrefix 段内打洞不影响)

悬干裁决(append 回滚):meta LastPreparedSeq > LastCommittedSeq = append 写崩溃(Prepare 后 Confirm 前)→ TruncateSuffix(CommittedWriteAddress) 回滚 append 部分(Overwrite 不可回滚,悬干不涉及)。

7.2 IncrementalSnapshot 恢复(IncrementalRecovery

三步核心

  1. 三水位恢复(同 StreamSnapshot 三级回退——hints → meta → Backward 扫描)
  2. 悬干裁决(段写崩溃):meta LastPreparedSeq > LastCommittedSeq = 段写 Prepare 后 Confirm 前崩溃 → TruncateSuffix(CommittedWriteAddress) 尾截断回滚(未提交段物理清除——失败即清理;已提交段表/数据完好)。无"自动认领未提交段"语义——未提交 = 失败 = 清理
  3. 段表 O(1) 加载DeserializeSegments(ReadOpaqueMeta()) 一次拿到全部段起点(仅已提交段;恢复免全盘扫描)+ _nextSeq = _lastCommittedSeq + 1(2PC 事务序号续接——段写事务单调)

恢复后 SegmentCount / LatestN0 可直接查询;调用方即可 ReadAllChunksAsync 拿最新快照条目流,或继续 AppendSegmentAsync 追加新段,或 ImportSegmentAsync 安装新快照。

7.3 SnapshotRecoveryHints 注入

// 上层(如 raft 节点)已知 apply 序号或外部水位时注入
var hints = new SnapshotRecoveryHints
{
    WriteAddress = knownWriteTail,           // 优先采用
    PhysicalWriteAddress = knownPhysTail,   // 可选;缺省走 AlignUpToSector
};
stream.Initialize(hints);   // LifecycleBase<>.Initialize(hints) 透传给恢复核心
stream.WaitForReady();

hints 优先级最高——适合 raft 节点重启时已知 apply 序号场景;缺省走 meta O(1);meta 禁用时走 Backward 扫描。

7.4 阈值合并与 raft 日志压缩

raft 日志压缩的核心需求:只需最新快照。IncrementalSnapshot 的段累积机制契合——

  • 每次 raft 快照 = 一段(N₀ = apply 序号)
  • 新段 append 不重写旧段(快照越频繁写省越多)
  • 段表 O(1) 恢复定位(免全盘扫描)
  • 段数达阈值自合并为新基线段(低频全量重写——成本可控)
  • ImportSegmentAsync 用于 raft 快照安装(替换语义——新镜像段替旧快照集)

raft 节点重启 → IncrementalRecovery 段表加载 → ReadAllChunksAsync 拿最新快照 → 从 LatestN0 续接 raft log replay。

7.5 恢复全景(端到端时序)

┌─ SnapshotBase.Initialize(hints) ─────────────────────────────┐
│  ① OnInitializeBegin: _engine.Initialize()(同步)          │
│     _metaEngine?.Initialize()(Managed 模式——并行启动)    │
│  ② CreateRecovery() → 默认/注入 IRecovery<SnapshotRecoveryHints>  │
│  ③ RecoveryBase 状态机驱动:                                  │
│     a. WaitForDependenciesAsync: join 主引擎 + meta 引擎就绪 │
│     b. OnRecoveryCoreAsync(hints): 三水位 + 悬干 + 段表     │
│  ④ WaitForReady 返回——水位已裁决、段表已加载、可读写        │
└──────────────────────────────────────────────────────────────┘

三水位恢复链(IncrementalSnapshot):
  hints.WriteAddress?  ─┬─►  采纳 + AlignUpToSector  (RaiseProgress 50)
  缺省 → meta.LoadAsync ─┼─►  SnapshotMetaPayload 推三水位 + CommittedWriteAddress
  缺省 → Backward 扫描 ─┴─►  LocateLastFrameEnd() 找 FooterMagic (RaiseProgress 50→90)

悬干裁决: meta.LastPreparedSeq > LastCommittedSeq
  ─► TruncateSuffix(CommittedWriteAddress)  (RaiseProgress 60)
     未提交段物理清除;已提交段表/数据完好

段表加载: DeserializeSegments(ReadOpaqueMeta()) → _segments
  _nextSeq = _lastCommittedSeq + 1  (RaiseProgress 90)

8. 反模式

# 反模式 后果 / 正解
1 混淆 Ring 区间快照与 StreamSnapshot/IncrementalSnapshot Ring 的 OpenSnapshotReader/Writer 是 Ring 导出能力(区间字节流,依附 Ring 生命周期),本子族是独立文件形态(自有 StorageEngine)。混用 = 生命周期错绑/资源泄漏。正解:独立选型,Ring 区间导出走 Ring API,文件快照走本子族
2 IncrementalSnapshot 段累积不合并 段数无限增长 → 恢复期段表解析/读段拼接 O(N) 线性退化;磁盘空间累积。正解:设 CompactSegmentThreshold(默认 8)或定期显式 CompactSegmentsAsync
3 跨实例组合 DeleteOnClose=true 跨重启组合必须 DeleteOnClose=false——否则真相源(Ring)重建时快照被清。SnapshotSettings 默认 false勿改 true
4 IncrementalSnapshot 关闭 meta(MetaPolicyKind=Disabled 段表靠 opaque meta 落盘,禁用 = 段表丢失 → 恢复走 Backward 扫描 O(N) 全盘 + 段表无法续接。正解:开 Managed + MetaOpaqueBytes ≥ 8 + 段数上限 × 40
5 OverwriteAppend Overwrite 不可回滚——覆写破坏旧数据,2PC 不覆盖,Abort 不能回滚。正解:要回滚走 Append/AppendAsync
6 多生产者并发 AppendSegmentAsync 单写者硬性要求(_writing 0/1 闸门)——并发抛 InvalidOperationException。正解:多生产者经外部队列汇聚到单写者;raft 快照低频串行天然契合
7 StreamFrameWriterCompleteAsync 也不 using 未完成帧无 Footer → 读侧 magic 校验失败/CRC 无法验。正解await using 自动闭环(DisposeAsync 等价 CompleteAsync)
8 MetaOpaqueBytes 容量不足(IncrementalSnapshot) 段表序列化超容量 → SetOpaqueMeta 抛 ArgumentException,段表无法落盘。正解:按 8 + 段数上限 × 40 估算,留余量(如 100 段 → ≥4008B → 设 4096B)
9 零拷贝 span 跨 await 消费 StreamFrameReader.ReadDataAsync ReadDataAsync 已是拷贝交付(填充 caller buffer),但若用 ReadAllChunksAsync 返回的 ReadOnlyMemory<byte> 跨 await 持有需注意——ArrayPool 租用块在枚举下一次迭代后归还。正解:跨 await 持有须拷贝出去

9. 想深入?指路

想懂什么 去哪
Snapshot 子族源码(帧布局/段表/恢复算法/会话双 buffer 流水线) src/TC.Tier.Runtime/Structures/Snapshot/(各 partial)
meta 统一协议(opaque 搭车水位线/块格式/Managed/Transport 三模式) meta.md
Session 协调协议(写/读/检查点三 op、悬挂裁决、故障模型——2PC 协调层) session.md
StorageEngine 使用(读写/水位/恢复/Compact/Allocate 写窗口) storage-engine.md
段表恢复细节(IncrementalSnapshot opaque 段表载体/序列化格式) segment-table.md
同族 Ring(数据真相源——OpenSnapshotReader/Writer 区间导出能力) ring.md
同族 Log(WAL——raft log replay 续接点) log.md
同族索引(HashIndex/BTreeIndex/SkipListIndex——派生数据) index.md
同族 Metadata(版本链单值元数据——跨重启单值状态) versioned-metadata.md
同族 Mirror(同族异坐标系——版本号截断的镜像形态) mirror.md
Lease 协议(raft 共识层 lease) lease-protocol.md
Structures 总览(6 子族选型/组合 KV/恢复统一模型) structures.md
raft 日志压缩(IncrementalSnapshot 镜像快照第三部件) src/TC.Tier.Products/Wal/(TierWal)

10. 端到端示例:raft 日志压缩场景

raft 节点周期性快照 + 重启恢复的典型编排:

using var fs = TierFs.New("local:///var/tier/raft-node-1");
var settings = new IncrementalSnapshotSettings
{
    MetaPolicyKind = MetaPolicyKind.Managed,   // 段表靠 opaque 落盘——必开
    MetaOpaqueBytes = 4096,                   // 容纳 ~100 段表条目
    CompactSegmentThreshold = 8,              // 8 段自合并
};
using var snap = new IncrementalSnapshot(fs, settings);
snap.Initialize();
snap.WaitForReady();

// ── 周期性快照(raft apply 回调里调) ──
async Task OnRaftApplySnapshot(long applyIndex, IAsyncEnumerable<ReadOnlyMemory<byte>> stateStream, CancellationToken ct)
{
    // 段 = raft apply 序号快照点;新段 append 不重写旧段
    await snap.AppendSegmentAsync(applyIndex, stateStream, ct);
    // 段数 ≥ 8 时自合并为新基线段(覆盖点不变),旧段物理回收
}

// ── raft 快照安装(外部镜像注入——如 Leader 同步快照) ──
async Task OnRaftInstallSnapshot(long applyIndex, IAsyncEnumerable<ReadOnlyMemory<byte>> fullImage, CancellationToken ct)
{
    // 替换语义:段表替换为单段,旧段物理回收
    await snap.ImportSegmentAsync(applyIndex, fullImage, ct);
}

// ── 重启恢复 ──
// Initialize() 内部 IncrementalRecovery:
//   1. 三水位恢复(hints → meta → Backward 扫描)
//   2. 悬干裁决(meta prepared > committed → 尾截断回滚)
//   3. 段表 O(1) 加载 + _nextSeq 续接
// 恢复后即可读最新快照 + raft log replay 续接
async Task<IAsyncEnumerable<ReadOnlyMemory<byte>>> LoadLatestSnapshot(CancellationToken ct)
{
    Console.WriteLine($"段数={snap.SegmentCount}, 最新N0={snap.LatestN0}");
    return snap.ReadAllChunksAsync(ct);   // 逐段顺序拼接 + CRC64 校验
}

关键点

  • MetaPolicyKind=Managed 必开——段表 opaque 落盘,恢复 O(1) 定位
  • CompactSegmentThreshold 调到实际段数上限内(默认 8 适合多数场景)
  • ImportSegmentAsync 用于 raft 快照安装(Leader → Follower 同步)
  • 段级 CRC64 + 悬干裁决 = 崩溃安全(未提交段物理清除,已提交段完好)
  • raft log replay 从 LatestN0 + 1 续接(apply 序号单调递增)