TierWAL 使用指南
TierWAL = 协议中立 WAL 产品(raft/p2p 的存储中线):底层 EntryLog 封装 + 自定 opaque 容器 布局。全部持久化语义做齐、内容零知识——entry 与元数据均不解析(Opaque 槽), raft/p2p 是纯消费方,直接接线。核心面:批/单条追加 × 显式提交(组提交三维度自动触发) × 随机起点重放 × 头/尾截断 × 元数据 Opaque 槽 × 镜像快照(导出/导入)。 泛型产品类
TierWal,经TierWalBuilder装配(StartAsync 一步到位 = 构建 + 恢复 + 就绪)。
快速上手
using TC.Tier.Core.IO;
using TC.Tier.Products.Wal;
var fs = TierFs.New("memory:"); // 或 TierFs.New("local:///path/to/volume")
await using var wal = await TierWalOptions.Default
.WithWalName("raft-log")
.Builder(fs)
.StartAsync(); // 构建 + 恢复 + WaitForReady 一体
// 追加一批 entry(每条 = 一帧 payload——帧格式知识归调用方)
var entries = new List<ReadOnlyMemory<byte>> { new byte[] { 1, 2 }, new byte[] { 3 } };
var r = await wal.AppendBatchAsync(entries, default);
// r.StartIndex = 本批起始 index;r.Count = 条数
await wal.CommitAsync(default); // 显式提交(一次 fsync = 一批持久化)
Console.WriteLine(wal.PersistedIndex); // ≥ r.StartIndex + r.Count - 1
// 从任意 index 顺序重放
await foreach (var e in wal.ReadFromAsync(r.StartIndex, default))
Console.WriteLine($"{e.Index}: {e.Data.Length}B");
组件入口也可直接 new TierWalBuilder(fs, options)(Options.Builder(fs) 是糖)。
恢复 hints 通常无需提供(default 让底层自恢复);进程外已知水位时注入 WalRecoveryHints
(TailAddress/BeginAddress/CommittedOffset——地址直透 EntryLog)。
心智模型(三条铁律)
- index = 起点 + 顺序计数(设计 §8.7):raft 日志连续无空洞——帧内零 index,恢复按 「快照起点/重放起点 + 扫帧计数」推导;段 anchor 表随 opaque 原子落盘(恢复 O(1) 读全表)。
- 双水位分离:
AllocatedIndex= 内存分配尾(含攒批窗口,未持久);PersistedIndex= 本地 fsync 水位(last persisted index,CommitAsync 后推进——自动提交 只提前落盘不推进)。raft 语义注意:PersistedIndex 不是集群 commitIndex(多数派确认 归协议层按复制进度计算),是应答/推进 matchIndex 的本地依据。 - 分配与持久化分离:Append 只写入不 fsync;持久化时机 = 组提交策略自动触发,或调用方
显式
CommitAsync——raft 攒批 → CommitAsync → 应答(一次 fsync = 一批持久化)。
追加与提交
| 路径 | 形态 | 适用 |
|---|---|---|
AppendBatchAsync(entries) |
批 → WalAppendResult(StartIndex, Count) |
raft 攒批主路径 |
AppendSingleAsync(entry) |
单条 → [index, 1] |
稀疏写;三维度全 0 配置 = 每写即提交 |
BeginAppendBatch() 租约 |
ref struct,逐条 Append/AppendAsync 返回 index |
适配器甜区:scratch 逐条帧化直写(集合形态的 N+1 次分配 → ~0) |
// 批追加租约:using 包裹,绝不跨 await 持有(持 WAL 写锁——ref struct 编译器强制不逃逸)
using (var lease = wal.BeginAppendBatch())
{
foreach (var frame in frames)
lease.Append(frame); // 页满 = 底层让渡零阻塞;双页在途才背压
// lease.StartIndex = 批起点;lease.Count = 已追加条数
}
持久化触发(TierWalOptions 组提交三维度,任一满足即提交):
| 维度 | 参数 | 默认 | 说明 |
|---|---|---|---|
| 时间 | CommitInterval |
10ms | 距上次提交;-1ms 禁用 |
| 数据量 | MaxUnflushedBytes |
64KB | 未提交字节 |
| 条数 | MaxUnflushedCount |
1000 | 未提交条数 |
- 单条提交形态 = 三维度全 0(每次 Append 即触发提交);显式
CommitAsync与自动提交并存。 WaitForPersistedAsync(index):阻塞等到 index 已持久化(raft 推进 commitIndex 的依据);IsPersisted(index)非阻塞查询。
重放(随机起点顺序读)
// 异步轨:冷启动/追赶主路径
await foreach (var e in wal.ReadFromAsync(SnapshotIndex + 1, ct)) { ... }
// 同步轨:专用 worker 线程(apply 管道)——零异步排队,页切片零拷贝
foreach (var e in wal.ReadFromSync(startIndex)) { ... } // e.Data 仅枚举存活期有效,跨枚举持有须拷贝
// 批读租约:复制读热路径零分配(页切片视图流)
using (var lease = wal.ReadBatchLeaseSync(startIndex, maxCount, ct))
await foreach (var e in lease.Entries) { ... } // Dispose 后视图失效(页归还池)
定位 = O(1):任意起点 = 段表二分 + 段内扫帧计数(与起点无关、与总量无关,~5-11ms 量级, 见性能契约②)。重放吞吐 ~3M 条/s(64B entry,mem/local 同量级)。
截断(冲突修正 / 日志压缩)
await wal.TruncateSuffixAsync(newTailIndex, ct); // 截尾 [newTail+1, ∞)——raft 冲突日志修正
await wal.TruncatePrefixAsync(newHeadIndex, ct); // 戴头 [0, newHead)——快照压缩回收,与写完全并行
元数据 Opaque 槽(term/vote/config)
await wal.WriteMetaAsync(opaqueBytes, ct); // 原子替换:搭 EntryLog 水位线落盘 + CRC 校验兜底
ReadOnlyMemory<byte> meta = wal.ReadMeta(); // 未写 = Empty
- 内容零知识:blob 由协议层定义(如 term/vote/集群配置序列化);TierWAL 只做原子替换。
- 容器布局:
[TailIndex 8B][HeadIndex 8B][段表条目数 4B][pad 4B][段 anchor 表 N×24B][raft 元数据预留区]——段表与协议元数据共用容器(MetaOpaqueBytes,默认 16KB,配置多大都可以); 段表容量 = (总容量 − 24 − 预留区) / 24,预留区 = 剩余(配置表达)。 - meta 策略:默认
Managed(独立 .meta 引擎、恒单段、单槽覆盖原子语义、O(1) 恢复); 外部/远程托管走WithMetaPolicyKind(Transport)+WithMetaTransport(...)构建期一行注入。
镜像快照(日志压缩 / 冷节点追赶)
long n0 = await wal.SnapshotAsync(ct); // N₀ = 调用时 PersistedIndex;一体:镜像生成 → 本地存储 → 增量压缩截断
Console.WriteLine($"{wal.SnapshotHeadIndex}..{wal.SnapshotIndex}"); // 快照覆盖的 index 区间
// 冷启动:本地快照自动载入;重建状态机消费帧流(与导出格式同构)
await foreach (var frame in wal.ReadSnapshotEntriesAsync(ct)) { ... }
- 快照内容 = 主数据 [Head..N₀] 的原始条目帧流(WAL 自己生成,非 raft 状态机镜像);
快照后自动
TruncatePrefixAsync(N₀+1)压缩已镜像段(先快照后截断——被截区有覆盖)。 - 本地存储 = IncrementalSnapshot 段增量(每次只写增量段;raft 冷启动必须本地载入, 低频合并阈值内建)。
- 跨节点传输 = 注入
WithSnapshotPersistence(IAsyncTransferPersistence)(网络/对象存储/ 备份介质),随后ExportSnapshotAsync(Header(N₀) + 帧流 + Footer CRC)/ImportSnapshotAsync(读入校验 → 替换本地快照;导入后 SnapshotIndex = N₀,应用快照内容后向 leader 汇报 N₀)。 未注入传输面 = 单机形态:快照本地化开箱即用,Export/Import 抛InvalidOperationException。
装配与介质选型
TierWalBuilder 注入面(不注入回落默认;Meta 校验兜底不可绕过):
| 注入 | 用途 |
|---|---|
WithMetaPolicyFactory / WithMetaTransport |
自定义 meta 策略 / 元数据托管版本链(Transport 模式) |
WithCommitPolicy |
替换组提交策略(默认 = Options 三维度阈值) |
WithSnapshotPersistence |
快照跨节点传输面(Export/Import 前提) |
WithLogger |
日志器 |
持久化质量地板(DurabilityValidation,默认 true——StartAsync 零 IO fail-fast):
| 介质 | 判定 |
|---|---|
| Network(network://) | 拒绝——远端对象存储无 fsync 语义、延迟无界,契约① 无保障 |
| Virtual(.tier) | 须挂载 CarrierWriteThrough(载体写穿档——四 IO 模式全达标;基线档 p99.9 ≥150ms 不达标) |
| Local / Memory | 通过(mem = 零 fsync 地板,稳定环境高吞吐首选;持久化取舍由部署方自权衡) |
IO hints(WithHints,默认 NoBuffering):TierWal 页模型 4MB 页 = 大块顺序写,落 DIO 甜区;
磁盘 raft 部署建议 NoBuffering | WriteThrough(DIO+WT 全介质最优)——机械盘上 buffered
与 DIO 单独在选举窗口口径不达标(DIO 单独是反模式,p99.9=178ms;DIO+WT p99.9=0.43ms),
实测矩阵见性能契约①。读侧不受影响(重放经游标自管页缓存吸收)。
反模式
- 租约跨 await 持有——
BeginAppendBatch租约持 WAL 写锁(ref struct 编译器强制不逃逸);AppendAsync的背压 await 发生在底层批内部,租约本体不跨 await。 - 把 PersistedIndex 当集群 commitIndex——它是本地 fsync 水位;多数派确认归协议层。
- 跨枚举持有重放切片——
ReadFromSync产出的Data是页缓冲切片(仅枚举存活期有效); 复制读收集后发送须自行拷贝(或用ReadBatchLeaseSync并在租约存活期内消费)。 - 网络介质挂载 / virtual 不带 CarrierWriteThrough——DurabilityValidation 直接 fail-fast
(测量/非 raft 用途显式
WithDurabilityValidation(false))。 - 机械盘 DIO 单独用 / buffered 顶选举窗口——见上(DIO+WT 全介质最优)。
- opaque 容量配 0——TierWAL 必须显式
MetaOpaqueBytes > 0(底层 Settings 基类默认 0 = 无 opaque 区,段表/元数据搭车通道不可用;Options 默认 16KB 已兜底)。 - 高频 SnapshotAsync——快照 = 镜像生成 + 截断(raft 惯例 = leader 定期低频压缩)。
想深入
- 设计文档:
docs/design/tierwal-design.md、docs/design/tierwal-mirror-snapshot-design.md(内部仓) - 性能数据:
perf/tierwal.md(三契约 + 稳定性矩阵,随包发布) - 结构层积木:EntryLog / 存储引擎使用指南(TC.Tier.Runtime 包)