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 超 PageSize 抛 InvalidOperationException。
- 批量
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 = 禁用条数维度。 |
三维度阈值仅用于构造默认 GroupCommitPolicy(commitPolicy 参数为 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. 持久化机制
[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 → FlushPage → OnPageFlushed |
前一页换出时即持久化(数据 + 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 |
FlushUntilAsync → engine.Flush |
| ② 推进水位 |
CommitCore(commitTarget) → AppendMeta + meta.Commit |
CommitCoreAsync → AppendMetaAsync + meta.CommitAsync |
| ③ 唤醒等待者 |
Monitor.PulseAll(_commitLock) |
同 |
6.4.2 守卫矩阵(防自递归 / 防 Dispose 篡改)
| 守卫 |
触发条件 |
行为 |
_inCommit 重入守卫 |
CommitCore → AppendMeta 写 meta entry 触发页满 → FlushWindow → OnPageFlushed → CommitCore |
重入时跳过 CommitCore——防无限递归栈溢出(meta entry 是提交记录本身,不需要再触发提交)。 |
IsDisposed 守卫 |
FlushOnDispose 期间的页 flush |
不再触发 commit——Dispose 职责是把脏页冲到盘上(避免数据丢失),但不应隐式篡改 commit 边界(未 commit 的末页重启后按未 commit 处理,符合 WAL 语义)。 |
committedTail ≤ _committedOffset 守卫 |
末页重复 flush |
不再 commit——避免末页重复 flush 时无谓 commit。 |
6.4.3 水位不变量
| 不变量 |
语义 |
CommittedOffset ≤ FlushedTail |
已 commit 必已落盘——CommitWithFlush 先 FlushUntil 后 CommitCore,保证此序。 |
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 驱动 |
MetaPolicyKind |
meta 引擎 |
典型用途 |
Disabled |
无 |
不持久化水位——SetOpaqueMeta 抛 InvalidOperationException,ReadOpaqueMeta 恒空。 |
Managed |
独立 meta 引擎(同构 Create) |
跨重启单值状态 + opaque 搭车水位线原子落盘。 |
Transport |
嵌入式 meta entry 写入 log 流(MetaHost),或注入 IMetaTransport 外部传输 |
DeltaLog 临时 delta 文件场景(缺省)。 |
opaque 登记:SetOpaqueMeta(bytes) stage 进策略缓冲,随下次水位提交原子落盘(同一块、同一 CRC)。无独立 opaque 提交路径。需确定性持久化点调 CommitAsync(一块 = 当前水位 + opaque)。
7. 恢复协议
| 优先级 |
来源 |
适用 |
① 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 临时文件场景近似水位)。 |
DeltaLog 默认 MetaPolicyKind.Transport——meta entry 作为 IsMeta entry 嵌入 log 流(MetaHost.WriteBlock 走 AppendCore(isMeta: true);ReadLastBlock 走 cursor 找最后 IsMeta entry 的 payload)。无需独立 meta 引擎——临时 delta 文件场景语义(delta 用完即弃,meta 跟随 log 流一起存在/消失)。
7.4 2PC 悬干裁决
恢复时还原 LastPreparedSeq / LastCommittedSeq / PreparedTailAddress——TransactionLog.LoadAndReconcile 据此判 LastPreparedSeq > LastCommittedSeq(悬干)并驱动 Abort 按 PreparedTailAddress 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. 想深入?指路
机制级细节(8 水位全表、帧字节布局、dump 协议、2PC 内部状态机)按需从源码与设计稿查阅——使用面不需要这些细节;能力全集以类型 XML 注释为准。