Table of Contents

Log 使用指南——WAL 页帧流 / 组提交 / 单写者

给谁看:用 Log 作 WAL / checkpoint delta 的组件开发者 回答什么:EntryLog vs DeltaLog 怎么选 / Settings 全参数 / 组提交策略 / 单写者硬约束 / 恢复 本篇只讲机制——帧字节布局见源码;性能数字见 perf/log-perf-baseline.md 定位:Structures/ 6 子族之一,主结构(操作流),与 Ring 并列 2 主结构


0. 一句话总纲

Log = 单写者页帧流 + 组提交三维策略 + 恢复水位 + 不跨页 entry

  • Append 返回 entry 起始 LogicalAddress;单条 entry 含 header+payload+padding 超 PageSizeInvalidOperationException
  • 批量 BeginAppendBatch() 走双页 ping-pong 让渡轨——页满不阻塞写线程,唯一等待点 = 双页都在途的背压。
  • 提交 = CommitAsync(推进 CommittedOffset + meta.Commit + fsync);Flush 只落盘不推进水位。
  • 恢复三级回退:LogRecoveryHints → meta 持久化水位 → 扫盘验帧。
  • 多生产者必须经队列汇聚到唯一写线程——_writeLock 是并发安全网,不是并发许可。

1. 定位

  • 主结构:Log 与 Ring 并列为主结构——Ring = 数据真相源(record 流),Log = 操作流(WAL/redo 流)。
  • 两种形态
    • EntryLog:通用 per-entry 顺序日志,WAL 是其典型用途。CRC64 in-header,22B 头。Meta 默认 Disabled,组提交三维策略默认启用。
    • DeltaLog:KV 增量 checkpoint delta 流,绑定 DeltaLogCodec(不可替换)。CRC32C in-header,18B 头。Meta 默认 Transport(嵌入式 meta entry 写入 log 流——临时 delta 文件场景)。
  • 与 Metadata 关系:DeltaLog 与 versioned-metadata.md 配合承载 checkpoint delta + 跨重启水位。
  • 与 StorageEngine 关系:Log 接入 IStorageEngine(逻辑地址 = 物理地址),IO 底层 = 持久化的内存——Log 不碰段/对齐/落盘细节,只在引擎地址空间上做 record 格式(codec)+ group commit 调度。引擎使用见 storage-engine.md

1.1 EntryLog vs DeltaLog 形态对比

维度 EntryLog DeltaLog
用途 通用 per-entry 顺序日志(WAL 典型) KV 增量 checkpoint delta 流
Codec EntryLog.Codec(CRC64 in-header,22B 头,4B 对齐) DeltaLog.Codec(CRC32C in-header,18B 头,4B 对齐)——绑定不可替换
MaxEntrySize 4MB(1 << 22 4MB(1 << 22
Meta 缺省 Disabled Transport(嵌入式 meta entry 写入 log 流——临时 delta 文件场景)
组提交策略 默认启用(GroupCommitPolicy 三维度,构造 commitPolicy=null 时启用) 不启用专属策略(无 ICommitPolicy 注入参数)
CommittedOffset 暴露(commit 边界水位) 不暴露(基类无此字段)
提前提交循环 启动(StartEarlyCommitLoop——CommitInterval ≠ -1ms 时) 不启动
恢复钩子 OnLogRecovered override 设 CommittedOffset = TailAddress + 启提前提交循环 默认空(无依赖恢复结果的装配)
构造注入 commitPolicy / cursorFactory / metaPolicyFactory / metaTransport / clock cursorFactory / metaPolicyFactory / metaTransport(无 commitPolicy / clock)

选型判据:需要 WAL 持久性调度(raft log / redo log)→ EntryLog;承载 checkpoint delta(与 VersionedMetadata 配合)→ DeltaLog。


2. Settings 全参数

基类 LogSettings(承 Settings)+ 各实现类 sealed 子类。Settings 基类字段被所有结构共享(meta 三策略、主引擎选项)。

2.1 通用字段(LogSettings + Settings

字段 类型 默认 说明
LogPageSizeBits int 22(4MB) 页 = 攒批缓冲 + DIO 对齐提交单位。PageSize = 1 << LogPageSizeBits单 entry 不跨页——单 entry 必须 ≤ PageSize,大对象由调用方分片成多条 entry。
MetaPolicyKind MetaPolicyKind Disabled meta 持久化策略:Disabled(无 meta)/ Managed(独立 meta 引擎)/ Transport(嵌入式 meta entry 写入 log 流,或注入 IMetaTransport 外部传输)。
MetaOpaqueBytes int 0 外部可写入 opaque 区容量(字节)。启动后不可改。只做写侧约束——不参与盘上布局(meta 块四段自描述)。
MainEngine StorageEngineOptions "tc.log" 主引擎选项(引擎名/段几何/预分配/关闭行为)。
Name string "tc.log" 存储引擎名。
PreallocateFile bool true 创建时预分配文件空间。
DeleteOnClose bool false 关闭时删除存储引擎。跨重启组合必须 false

2.2 EntryLog 专属字段(EntryLogSettings

字段 类型 默认 说明
CommitInterval TimeSpan 10ms 组提交时间阈值。0 = 每次 Append 立即提交;-1ms(InfiniteTimeSpan)= 禁用时间维度(攒页/手动提交场景)。
MaxUnflushedBytes long 64KB 组提交字节阈值。0 = 字节维度立即满足;long.MaxValue = 禁用字节维度。
MaxUnflushedCount int 1000 组提交条数阈值。0 = 条数维度立即满足;int.MaxValue = 禁用条数维度。

三维度阈值仅用于构造默认 GroupCommitPolicycommitPolicy 参数为 null 时启用)。三场景靠阈值配置表达(无需枚举选模式):

场景 CommitInterval MaxUnflushedBytes MaxUnflushedCount 说明
典型 WAL(默认) 10ms 64KB 1000 三维度兜底,崩溃窗口 ≤10ms
单条强制 0 0 0 每次 Append 立即提交(宜配 PersistenceMode.WriteThrough)
手动/2PC -1ms long.MaxValue int.MaxValue 不自动提交,靠 CommitAsync / TransactionLog 驱动

2.3 DeltaLog 专属字段(DeltaLogSettings

无额外专属配置。构造时 MetaPolicyKind 缺省强制为 Transport(嵌入式 meta——临时 delta 文件场景语义)。两个构造重载均如此;调用方初始化器仍可覆盖。


3. 构造与生命周期

3.1 生命周期(全部结构统一)

  • 三步装配:构造 → Initialize(hints)WaitForReady(Log 族无工厂;封闭结构才有 CreateAsync 一步就绪);异步调用方用 WaitForReadyAsync
  • Initialize 启动双引擎并行启动(主引擎 + Managed meta 引擎,均非阻塞)。
  • Dispose 幂等——观察在途让渡刷盘(含补跑延迟提交链)、刷末页、释放双页缓冲与 frame 池,最后走基类编排。

3.2 单写者硬性约束

_writeLock并发安全网,不是并发许可——保证并发调用不损坏状态(水位/TailAddress 零改动),不改变单写者语义。多生产者必须经队列汇聚到唯一写线程。批协议背压慢路的 Monitor.Exit 窗口内禁止其他写者插队 Begin——批协议消费形态 = 单写者(raft 适配器 = wal 唯一追加方)。

3.3 代码示例

① 装配(三步就绪)

using var fs = TierFs.New("memory:");
var settings = new EntryLogSettings("raft.wal")
{
    LogPageSizeBits = 20,         // 1MB 页
    CommitInterval = TimeSpan.FromMilliseconds(10),
    MaxUnflushedBytes = 64 * 1024,
    MaxUnflushedCount = 1000,
    MetaPolicyKind = MetaPolicyKind.Managed,
    MetaOpaqueBytes = 64,
};
// 构造 + Initialize + WaitForReady(恢复三级回退在此完成);异步调用方用 await log.WaitForReadyAsync(ct)
using var log = new EntryLog(fs, settings);
log.Initialize();
log.WaitForReady();

② Append + CommitAsync

// 单写者:多生产者必须经队列汇聚到唯一写线程
LogicalAddress addr = log.Append(stackalloc byte[128]);   // 返回 entry 起始地址
// 默认 GroupCommitPolicy 三维度兜底;末页未 commit 数据崩溃后丢失(WAL 语义)
// 显式提交(如 raft Apply 前需确认持久化):
await log.CommitAsync(ct);
// 等待特定 entry 已 commit:
await log.WaitForCommitAsync(addr, ct);
// 提交错误查询(后台循环错误不冒泡到调用方):
Exception? lastErr = log.LastCommitError;

③ Replay(恢复 / 重放已 commit)

// 只重放 ≤ CommittedOffset 的 entry(WAL 一致性语义)
// verifyCrc=false(默认):仅验 Magic,约快 10-50×;true = 每条全量验 CRC(完整性审计)
long count = log.Replay(LogicalAddress.Empty, static (payload, isMeta, addr) =>
{
    if (isMeta) return;                 // 跳过嵌入式 meta entry
    ApplyEntry(payload, addr);          // 零拷贝 Span——禁止跨回调持有
}, verifyCrc: false);

④ 批量追加(高吞吐——页满让渡不阻塞)

// 批(ref struct)绝不跨 await 持有(持写锁);using 包裹保证 Dispose 释放锁
using var batch = log.BeginAppendBatch();
foreach (var entry in entries)
{
    // 页满让渡轨——当前页交异步刷盘、立即切另一页继续写
    // 唯一等待点 = 双页都在途的背压(罕见 = 页边界 × 双页在途)
    LogicalAddress addr = batch.Append(entry);
}
// Dispose 回写宿主 _pageUsed + 释放写锁——批内未 commit,靠默认 GroupCommitPolicy 或显式 CommitAsync

⑤ 2PC 事务(跨结构原子提交)

// 协调器 = Transactions/TransactionLog(写独立 commit record 文件驱动 Prepare/Confirm/Abort)
// Log 是参与者之一,与 Ring / Index 等共同 Prepare → ConfirmCommitted / Abort
log.OnCommitted(seq, () => ApplyNotify(seq));    // 注册 seq 提交回调(已提交到更高 seq → 立即同步触发)
log.Prepare(seq);                                // FlushUntil(tail) + AppendMeta(tail)——进入悬干状态
// ... 其他参与者 Prepare ...
// 协调器写 commit record → 各参与者 ConfirmCommitted(seq)
log.ConfirmCommitted(seq);                       // 推进 LastCommittedSeq + _txRollbackTail
// 失败路径:
log.Abort(seq);                                  // TruncateSuffix 回退到 _txRollbackTail,丢弃本轮悬干数据

4. API 详表

4.1 写

方法 签名 说明
Append LogicalAddress Append(ReadOnlySpan<byte> payload) 同步追加单条 entry,返回起始地址。超单页抛 InvalidOperationException。热路径 non-virtual。
AppendAsync ValueTask<LogicalAddress> AppendAsync(ReadOnlyMemory<byte> payload, CancellationToken ct = default) 异步对等版。当前实现同步完成(写路径锁内完成;页满 IO 重叠让位于锁串行)。
BeginAppendBatch AppendBatch BeginAppendBatch() 开启批量追加(ref struct,using 包裹)。批持写锁(批内本地游标零锁;Dispose 释放)。
AppendBatch.Append LogicalAddress Append(ReadOnlySpan<byte> entry) 批内同步追加。页满让渡轨(异步刷盘、不阻塞写线程);双页都在途背压同步等。
AppendBatch.AppendAsync ValueTask<LogicalAddress> AppendAsync(ReadOnlySpan<byte> entry, CancellationToken ct = default) 批内异步追加。三档:页有空间=同步快路;页满无背压=让渡同步完成;双页在途=唯一真异步点(孤儿认领协议)。
Flush void Flush() 同步屏障:提交当前页(末页)+ engine.Flush(fsync 落盘)。不推进 CommittedOffset
FlushAsync ValueTask FlushAsync(CancellationToken ct = default) 异步屏障——真异步:数据写在写锁外等待(屏障 IO 不占写锁)。
CommitAsync ValueTask CommitAsync(CancellationToken ct = default) 显式提交:flush 落盘 + 推进 CommittedOffset + meta.Commit。保证 §7 不变量 CommittedOffset ≤ FlushedTail
WaitForCommitAsync ValueTask WaitForCommitAsync(LogicalAddress untilAddress, CancellationToken ct = default) 阻塞等待 CommittedOffset ≥ untilAddress。

4.2 读 / 扫描

方法 / 成员 签名 说明
OpenCursor ILogCursor OpenCursor(LogicalAddress startAddress = default, LogicalAddress endAddress = default, bool verifyCrc = false, SnapshotMode snapshotMode = Consistent, bool leasePages = false) 打开扫描游标——引擎 OpenSequentialReader + PageFrame 整页校验。默认 endAddress = FlushedTail(已落盘水位)。
Replay long Replay(LogicalAddress fromAddress, EntryReplayHandler handler, bool verifyCrc = false) 回调式重放已 commit entry(零装箱)。便捷重载 Replay(handler, verifyCrc)
ReplayAsync ValueTask<long> ReplayAsync(LogicalAddress fromAddress, AsyncEntryReplayHandler handler, bool verifyCrc = false, CancellationToken ct = default) 异步对等版。
ILogCursor.MoveNext bool MoveNext() 同步推进到下一条 entry。
ILogCursor.MoveNextAsync ValueTask<bool> MoveNextAsync(CancellationToken ct = default) 异步推进。
ILogCursor.CurrentAddress LogicalAddress 当前 entry 起始地址。
ILogCursor.CurrentPayload ReadOnlySpan<byte> 当前 entry payload(零拷贝——指向读帧内)。非 lease 模式禁跨 MoveNext 持有
ILogCursor.CurrentPayloadMemory ReadOnlyMemory<byte> Memory 视图。lease 模式有效至 Dispose;非 lease 禁跨 MoveNext。
ILogCursor.CurrentIsMeta bool 当前 entry 是否为 meta。

4.3 截断

方法 签名 说明
TruncatePrefix void TruncatePrefix(LogicalAddress address) 头截断:推进 BeginAddress 到 address。引擎 ReclaimHead 回收物理段。EntryLog retention 用。
TruncateSuffix bool TruncateSuffix(LogicalAddress address) 尾截断:回退 TailAddress 到 address。引擎 ReclaimTail + 页缓冲复位。EntryLog raft leader 切换用。返回 false = 地址无效。

4.4 水位

成员 签名 说明
BeginAddress LogicalAddress 起始地址 = engine.MinAddress(水位读引擎,Log 不自存)。
TailAddress LogicalAddress 当前写游标 = 最后一个已追加 entry 尾地址(含页缓冲内未落盘 entry)。
FlushedTail(internal) LogicalAddress 已落盘水位 = 最后一个已确认写完成 frame 的尾地址。游标扫描终点。
CommittedOffset(EntryLog) LogicalAddress 已 commit 的最高地址(commit 边界水位)。保证 ≤ FlushedTail
LastCommitError(EntryLog) Exception? 最近一次提交错误。上层应周期查询以发现持久化失败。
LastCommittedSeq long 当前已提交序号(-1 = 未参与事务)。
LastPreparedSeq long 最近一次 Prepare 的 seq(恢复判定悬空事务用)。

4.5 2PC(ITransactionParticipant 六件套)

跨结构原子提交由 ITransactionParticipant 暴露,协调器 = Transactions/TransactionLog(写独立 commit record 文件驱动 Prepare/Confirm/Abort)。上层编排归 session.md

方法 签名 说明
Prepare void Prepare(long seq) 事务准备:FlushUntil(tail) 数据落盘 + AppendMeta(tail) 写 meta(同一尾快照)。进入悬干状态——ConfirmCommitted 前崩溃恢复时丢弃。
PrepareAsync ValueTask PrepareAsync(long seq, CancellationToken ct) 异步对等版。
ConfirmCommitted void ConfirmCommitted(long seq) 确认提交:推进 LastCommittedSeq + 推进 _txRollbackTail + 触发 OnCommitted 回调。
Abort void Abort(long seq) 2PC 回滚:TruncateSuffix 回退到上一已确认提交边界,丢弃本轮悬干数据。守卫矩阵:seq ≤ LastCommittedSeq → no-op;陈旧 Abort → 仅复位记账;无既有提交边界 → 仅复位记账。
AbortAsync ValueTask AbortAsync(long seq, CancellationToken ct) 异步对等版。
OnCommitted void OnCommitted(long seq, Action callback) 注册 seq 的提交回调。已提交到更高 seq → 立即同步触发。

5. 自定义注入工厂(Contracts)

接口 / 类型 用途 内置实现
ILogCodec entry 头尾编解码——注入 LogBase 消除虚分发。HeaderSize(EntryLog=22B / DeltaLog=18B)/ Alignment(4B)/ MaxEntrySize(4MB)/ WriteHeader / TryReadHeader EntryLog.Codec / DeltaLog.Codec(均 internal sealed,构造期绑定)。
ICommitPolicy 提交策略——EntryLog group 提交引擎按策略判定"何时"触发提交(不决定"怎么"提交)。bool ShouldCommit(in CommitSnapshot) GroupCommitPolicy(internal 默认——三维度阈值兜底);恒提交/手动提交语义经自定义实现表达。
LogCursorFactory<out TLogCursor> 扫描游标工厂委托。参数:startAddress / endAddress / verifyCrc / snapshotMode(Consistent=快照隔离 / DirtyRead=游标锁)/ leasePages(true=换页不还池,CurrentPayloadMemory 有效至 Dispose)。 默认 PageFrameCursor(内嵌 LogBase.Cursor.cs)。
EntryReplayHandler / AsyncEntryReplayHandler 重放回调委托。参数:payload(零拷贝 Span)/ isMeta / entryAddress / ct。
ILogCursor 扫描游标接口(承 IStructureScanCursor)。CurrentAddress / EndAddress / CurrentPayload / CurrentPayloadMemory / CurrentEntryLength / CurrentIsMeta + MoveNext / MoveNextAsync PageFrameCursor(内嵌)。
LogMetaHeader meta 块规范头(12B,三段式 [Header 12B][Payload 80B+opaque][Crc32Footer 4B])。
LogMetaPayload meta payload(80B):BeginAddress / TailAddress / CommittedOffset / LastCommittedSeq / LastPreparedSeq / PreparedTailAddress(2PC Abort 回退点)。
CommitSnapshot 提交判定快照(喂给 ICommitPolicy):UnflushedBytes / UnflushedCount / SinceLastCommit。

6. 持久化机制

6.1 页帧格式(LogPageFrameHeader)

[LogPageFrameHeader 8B][entry data 扇区对齐][Crc32Footer 4B][padding 撑到扇区对齐]
  • LogPageFrameHeader(8B):MagicValue("LPGF")+ DataLength(页内有效数据字节数,不含 header/footer)。源生成器自动生成 codec。
  • Crc32Footer(4B):CRC32C 覆盖 [header + data](不含 footer 自身与 padding)。
  • entry 布局:[EntryLogHeader 22B / DeltaLogHeader 18B][payload][padding 4B 对齐]——单 entry 不跨页(超 PageSize 抛)。

6.1.1 地址空间窗口模型(Allocate + CalculationAddress + Write)

Log 不用 engine.Append(pwrite 后才知地址——无法满足"entry 写入时即知地址"),改用三层结构:

操作 IO
地址空间窗口 engine.Allocate(SpaceAllocSize) → 大区间 CAS 推水位 零 IO
页(PageSize 攒批单位) engine.CalculationAddress(_spaceStart, _spaceWriteOffset) 精确推算 entry 地址 纯逻辑
entry 写入 engine.Write(addr, frame) 在已分配空间内覆写(lease 保护,DIO 对齐) 真正 IO
  • SpaceAllocSize = PageSize × 16(一个窗口放 16 页 frame,摊薄 Allocate(CAS) 开销)。
  • 窗口剩余放不下时 ReclaimTail 退回旧窗口未用空间 + Allocate 新窗口(窗口换代 _spaceGeneration++,在途写失败回滚只认同代——跨代回滚会覆写新窗口游标)。
  • DIO 三重对齐(IOMode.Enabled 下硬责任):fileOffset(Allocate 返回,段内扇区对齐)/ length(AlignUp 到扇区,padding 补零)/ buffer 地址(AlignedMemoryManager 扇区对齐分配)。

6.1.2 双页 ping-pong 让渡轨

路径 调用 语义
同步轨 FlushPage Append/Flush/CommitAsync 屏障路径 原地同步写——_inFlightFlush 等待后内联写新页
让渡轨 FlushPageYield 批协议页满 / 异步 AppendAsync 组帧 → 先行推进地址(frame 尺寸已知)→ 启动纯数据写任务 → 立即切另一页继续写(IO 重叠)
观察点 DrainInFlightSync/Async 下次让渡 / Flush / Dispose 等纯数据写完成 → 补跑延迟提交链(OnPageFlushed

让渡轨状态机:Writing → Flushing[IO 在途] → 空闲;双页 ping-pong 不变。_inFlightFlush纯数据写任务——永不触碰 _writeLock、永不内联跑提交链(批持锁等待在途刷盘时内嵌提交链 = 自锁死)。提交链延迟到观察点(DrainInFlightSync/Async)在锁语义内补跑。

6.2 组提交策略(GroupCommitPolicy 三维度)

维度 字段 0 值语义 禁用值
字节量 MaxUnflushedBytes 字节维度立即满足(每次 Append 触发) long.MaxValue
时间 Interval 时间维度立即满足 -1ms(InfiniteTimeSpan)
记录数 MaxUnflushedCount 条数维度立即满足 int.MaxValue

判定:UnflushedBytes ≥ MaxUnflushedBytes || (Interval ≠ -1ms && SinceLastCommit ≥ Interval) || UnflushedCount ≥ MaxUnflushedCount——任一满足即触发。三个全 0 = 每次 Append 立即提前提交。

6.3 两层提交模型(职责分离)

触发 语义 可配置
底层页提交契约(恒成立) 跨页 Append / Flush / Dispose → FlushPageOnPageFlushed 前一页换出时即持久化(数据 + meta + CommittedOffset 推进)。最坏情况:写满一页才提交。 否(不可关)
可选提前提交(注入式优化) ICommitPolicy 在页未满时提前触发 降延迟。注入 null = 仅依赖底层页契约;注入 GroupCommitPolicy = 三维度提前提交。

6.4 CommitAsync vs Flush

API 落盘(fsync) 推进 CommittedOffset meta.Commit 典型用途
Flush / FlushAsync 仅落盘,不推进水位(如读前确保可见)
CommitAsync 显式提交(raft Apply 前确认持久化)
OnPageFlushed(底层契约) ✓(已落盘) 页换出同步提交(恒成立)
后台提前提交循环 ✓(仅推进到 FlushedTail) 时间维度兜底(Interval 到期)

崩溃一致性顺序:data fsync 先于 meta fsync——CommitWithFlush = FlushUntil(engine.Write + engine.Flush 落盘 data)→ CommitCore(AppendMeta + meta.Commit 落盘 meta)。断电时 meta 绝不会标记一个 data 尚未落盘的 commit 点。

6.4.1 提交执行链(同步轨 / 异步轨)

阶段 同步轨 CommitWithFlush 异步轨 CommitWithFlushAsync
① 落盘 data FlushUntil(commitTarget)engine.Flush FlushUntilAsyncengine.Flush
② 推进水位 CommitCore(commitTarget)AppendMeta + meta.Commit CommitCoreAsyncAppendMetaAsync + meta.CommitAsync
③ 唤醒等待者 Monitor.PulseAll(_commitLock)

6.4.2 守卫矩阵(防自递归 / 防 Dispose 篡改)

守卫 触发条件 行为
_inCommit 重入守卫 CommitCoreAppendMeta 写 meta entry 触发页满 → FlushWindowOnPageFlushedCommitCore 重入时跳过 CommitCore——防无限递归栈溢出(meta entry 是提交记录本身,不需要再触发提交)。
IsDisposed 守卫 FlushOnDispose 期间的页 flush 不再触发 commit——Dispose 职责是把脏页冲到盘上(避免数据丢失),但不应隐式篡改 commit 边界(未 commit 的末页重启后按未 commit 处理,符合 WAL 语义)。
committedTail ≤ _committedOffset 守卫 末页重复 flush 不再 commit——避免末页重复 flush 时无谓 commit。

6.4.3 水位不变量

不变量 语义
CommittedOffset ≤ FlushedTail 已 commit 必已落盘——CommitWithFlushFlushUntilCommitCore,保证此序。
FlushedTail ≤ TailAddress 已落盘水位 ≤ 当前写游标(含内存页内未 flush 的 entry)——让渡轨先行推进 TailAddress 后两者脱钩。
BeginAddress ≤ CommittedOffset commit 边界不得越过头截断——TruncatePrefix 后不夹回(head 回收不影响 commit)。
截断后 CommittedOffset ≤ TailAddress TruncateSuffix / Abort 后 EntryLog override OnTailTruncated / OnAborted 夹回——否则 Replay 越界跳段报 Segment not found。

6.4.4 内置 ICommitPolicy 对照

策略 ShouldCommit 返回 用途
GroupCommitPolicy(默认——构造注入 null 即得) 三维度任一满足即 true WAL 典型场景(默认 10ms/64KB/1000)
自定义实现(恒 true) 恒 true 单条强制——每次 Append 立即提交(宜配 WriteThrough)
自定义实现(恒 false) 恒 false 手动 / 2PC——靠 CommitAsync / TransactionLog 驱动

6.5 meta 持久化三策略

MetaPolicyKind meta 引擎 典型用途
Disabled 不持久化水位——SetOpaqueMetaInvalidOperationExceptionReadOpaqueMeta 恒空。
Managed 独立 meta 引擎(同构 Create) 跨重启单值状态 + opaque 搭车水位线原子落盘。
Transport 嵌入式 meta entry 写入 log 流(MetaHost),或注入 IMetaTransport 外部传输 DeltaLog 临时 delta 文件场景(缺省)。

opaque 登记:SetOpaqueMeta(bytes) stage 进策略缓冲,随下次水位提交原子落盘(同一块、同一 CRC)。无独立 opaque 提交路径。需确定性持久化点调 CommitAsync(一块 = 当前水位 + opaque)。


7. 恢复协议

7.1 三级回退(与 Metadata/Mirror/Snapshot 统一)

优先级 来源 适用
LogRecoveryHints 外部主动注入(最高优先级) 上层快照场景注入 TailAddress;DeltaLog 临时文件场景注入 FileSize(近似水位)。
② meta 持久化水位 MetaPolicy.LoadAsync 读最后一块 meta LogMetaPayload.TailAddress > Empty 时启用。同时还原 LastCommittedSeq / LastPreparedSeq / PreparedTailAddress(2PC 悬干裁决依据)。
③ 扫盘 engine.MinAddress 前向走帧到最后一个有效帧尾 兜底——逐帧验证 magic + dataLen + footer CRC,撕裂/坏帧止步于前一有效帧。

恢复后 ReconcileEngineTail:退到 Log 真实尾 + 帧边界 padded end 对齐(meta/hints 记录的尾是末帧数据尾,padding 之后才能续写首个新 frame,否则 cursor 读完末帧 CRC 跳过 padding 后找不到新帧 header)。EntryLog 的 EntryLogRecovery.OnLogRecovered 在恢复后设 CommittedOffset = TailAddress(恢复后天然一致)+ 启动提前提交循环。

7.2 LogRecoveryHints 字段

字段 类型 说明
TailAddress LogicalAddress? 已知写游标(上层快照场景注入;优先于扫盘)。
BeginAddress LogicalAddress? 已知头截断边界(retention 场景注入)。
FlushedUntilAddress LogicalAddress? 已知落盘边界。
CommittedOffset LogicalAddress? 已知 commit 边界(EntryLog 场景注入)。
FileSize long? 已知文件大小(非地址,DeltaLog 临时文件场景近似水位)。

7.3 嵌入式 meta + 临时文件(DeltaLog 形态)

DeltaLog 默认 MetaPolicyKind.Transport——meta entry 作为 IsMeta entry 嵌入 log 流(MetaHost.WriteBlockAppendCore(isMeta: true)ReadLastBlock 走 cursor 找最后 IsMeta entry 的 payload)。无需独立 meta 引擎——临时 delta 文件场景语义(delta 用完即弃,meta 跟随 log 流一起存在/消失)。

7.4 2PC 悬干裁决

恢复时还原 LastPreparedSeq / LastCommittedSeq / PreparedTailAddress——TransactionLog.LoadAndReconcile 据此判 LastPreparedSeq > LastCommittedSeq(悬干)并驱动 AbortPreparedTailAddress TruncateSuffix 回退(跨崩溃可用)。

7.5 扫盘算法细节

阶段 实现
起点 engine.MinAddress(已知帧边界)——非页起点(帧可跨扫描页,落在上一帧中段填充零里会断链)。
帧推进 OpenSequentialReader(Consistent 快照隔离,usePageCache=true)逐帧读 header + data + CRC + padding。
CRC 验收 每帧验证 footer CRC32C(cover = header + dataLen,与 Cursor 同口径)——撕裂/坏帧止步于前一有效帧。
物理截断帧容忍 ReclaimTail 打洞后 committed 边界落在帧中——读不满按实际数据解析(截断点前的 entry 完整保留);无数据 = 真 EOF。截断帧无 CRC/padding——直接按实际解析。
终止 magic 不连续 / dataLen 越界 / CRC 失败 = 空洞/EOF——返回最后一个有效帧尾。

cursor 起点 = 任意 LogicalAddress(不必帧边界):探测分流——帧头 magic 验证通过 → 正常页解析(帧头对齐,零 skip);entry 头 → 无帧头模式(cursor 内跳过 < startAddress 的 entry,页尾靠 TryDetectFrameBoundary 扇区探测下一页帧头)。断点续传重放用——避免每次从 MinAddress 全量重扫。


8. 反模式(组合层必守)

# 反模式 后果 正解
1 Log 并发 Append 单写者硬约束破坏——水位/TailAddress 错乱 多生产者经队列汇聚到唯一写线程(_writeLock 是安全网,非并发许可)
2 批(ref struct)跨 await 持有 持写锁跨 await = 自锁死 / SynchronizationLockException 批内 AppendAsync 背压慢路 await 在宿主侧(孤儿认领协议),消费方续体线程不可预期
3 单 entry 超 PageSize InvalidOperationException 大对象调用方拆分成多条 entry(单 entry 不跨页契约)
4 跨 MoveNext 持有 CurrentPayload Span(非 lease 模式) 换页即还池——span 指向已归还的池缓冲 用 lease 模式(leasePages=true,CurrentPayloadMemory 有效至 Dispose)或拷贝交付
5 不查询 LastCommitError 后台循环提交错误不冒泡到调用方——持久化失败静默 上层周期查询 LastCommitError 发现持久化失败
6 MetaPolicyKind=Disabled 时 SetOpaqueMeta InvalidOperationException(禁用即报错,不静默吞) Managed / Transport,或移除 opaque 写入
7 绕过组提交策略手动 CommitAsync 末页未 commit 数据崩溃后丢失(符合 WAL 语义,但可能误以为已持久化) 显式 CommitAsync 用于 raft Apply 前确认;常规写入信任默认 GroupCommitPolicy
8 跨结构组合 DeleteOnClose=true 跨重启组合必须 false(否则索引重建时真相源被清) 跨重启组合 DeleteOnClose=false(同 structures.md §5)
9 截断后期望 CommittedOffset 自动夹回 EntryLog 已 override OnTailTruncated / OnAborted 夹回——子类不自管会违反 §7 不变量 信任基类夹回逻辑;自定义 LogBase 子类须 override 夹自管水位
10 opaque 期望独立提交路径 旧 WriteOpaqueMeta 自拍 TailAddress 独立成块 = 并发水位回退 + 被内部提交冲掉,已废除 opaque 跟随水位线提交链(SetOpaqueMeta stage → 下次 CommitAsync 原子落盘)

9. 想深入?指路

想懂什么 去哪
总览(七种结构怎么选怎么组合) structures.md
引擎使用(读写/水位/恢复/Compact) storage-engine.md
meta 统一协议(opaque 搭车/块格式/自定义传输) meta.md
Session 协调协议(写/读/检查点三 op、悬挂裁决、故障模型) session.md
段表 / lease 协议(raft 共识层) segment-table.md / lease-protocol.md
同族结构 ring.md / index.md / versioned-metadata.md / mirror.md / snapshot.md
性能基线(Append 吞吐 / 重放速率 / 组提交延迟) perf/log-perf-baseline.md
帧字节布局 / 双页让渡轨状态机 / 2PC 内部状态机 源码注释(Structures/Log/ 各 partial)与设计稿(docs/design/

机制级细节(8 水位全表、帧字节布局、dump 协议、2PC 内部状态机)按需从源码与设计稿查阅——使用面不需要这些细节;能力全集以类型 XML 注释为准。