Ring 使用指南——追加式 KV 环 / RCU 原地更新 / 环形驱逐
给谁看:用 Ring 作 KV 真相源 / WAL 的组件开发者 回答什么:Ring 是什么 / Settings 全参数 / 8 水位 / Codec 注入 / 写读 API / 恢复 / 反模式 本篇只讲机制——帧字节布局见源码;性能数字见
perf/structures-perf-baseline.md定位:Structures/ 6 子族之一,主结构(数据真相源),与 Log 并列 2 主结构
0. 一句话总纲
Ring = 固定槽数环形页池 + 全局单调逻辑地址 + 8 水位指针 + RCU 原地更新 + 环形驱逐
- 追加写返回
LogicalAddress(16B 结构SegId+Extension+Offset)= 取值句柄,地址永不重写 - 同尺寸 value 经
UpdateValue原地覆写(RCU 语义),同槽位复用 - 环形满自动驱逐最老页(Head 推进);地址空间永远单调向前
- 8 水位指针经 CAS128 原子推进(cmpxchg16b 快路径 / 非对齐分片锁兜底),并发 Flush/Truncate 不会回退
- 地址运算只用
engine.GetDistance/engine.CalculationAddress(铁律:LogicalAddress.Offset跨段无意义,不参与算术)
1. 定位
- 与 Log 同为主结构:Ring 是数据真相源(record 流),Log 是操作流;二者均以
LogicalAddress为一等公民 - 与 Index 组合:Ring=真相源 / Index=派生数据;写编排正序:先 Ring.Write → 再 Index.Insert
- 与 Mirror/Snapshot 关系:Ring 自带
OpenSnapshotReader/Writer区间导出/导入能力(数据快照),与独立的mirror.md/snapshot.md形态无引用关系 - 与 Meta 关系:外部 opaque 经
SetOpaqueMeta随水位线原子落盘,是 Index 锚点 W 的搭车通道 - 与 StorageEngine 关系:结构层是 internal 使用者;文件系统 = 构造传哪个
IFileSystem(详见storage-engine.md)。引擎自恢复 MinAddress/AllocatedTail;Ring 自管 8 水位 - 与横切协议:meta 持久化经统一
IMetaPolicy(见meta.md);2PC 跨结构原子经ITransactionParticipant(协调器见session.md)
2. Settings 全参数
BlittableRingSettings 是 RingSettings 的 sealed 派生——无额外字段(key 长度由 TKey 类型参数提供 sizeof(TKey))。所有可配置项在基类 RingSettings。
| 参数 | 默认 | 说明 |
|---|---|---|
PageSize |
AlignmentConst.Alignment32M |
页大小(字节)。校验:2 的幂 / [4KB, 1GB]。DIO 对齐契约下限 |
MemorySize |
AlignmentConst.Alignment16G |
总内存容量(字节)。校验:≥ PageSize 且整除为 2 的幂页数。PageCount = MemorySize/PageSize 是派生量,不在 Settings 直配 |
MaxPageCount |
8192 |
页数上界。页数过多(PageSize 太小)扫描/驱逐开销随页数线性增长,校验时拒绝 |
MutableFraction |
0.9 |
mutable 区占比 (0,1) 开区间。决定 mutable→readonly 转换点(ReadOnlyLag = MutableFraction × (PageCount-1) × PageSize) |
Preallocate |
false |
构造时是否全量预分配所有页(false = 懒分配,按需 Allocate) |
OverflowPolicy |
Disabled |
溢出策略(WiscKey 式 KV 分离)。Disabled=Value 内联主 record;Enabled=Value > MinOverflowSize 时分离到溢出引擎 |
MinOverflowSize |
0 |
Value 溢出阈值(字节)。Enabled 时 0 = 全部溢出 |
ClockCacheCapacity |
null |
冷页缓存槽位。null=按 ColdReadRatio 派生(默认);显式设置覆盖 ratio,向上取整到 2 的幂 |
ColdReadRatio |
0.25 |
冷回源比例 [0.0, 1.0]。0.0=不缓存(每次冷读走设备);1.0=缓存全部页(近乎全热) |
ColdRecordBufferLimit |
AlignmentConst.Alignment1M |
单条 record 部分页回源的 buffer 上限。超过此值时回退到整页路径(LoadColdPage)。校验:须 ≥ HeaderSize |
MainEngine |
— | StorageEngineOptions,主引擎配置(引擎名 / 段大小 / 预分配 / 删 onClose / DirectIo / Hints 等)。构造时直传 |
MetaPolicyKind |
— | meta 策略模式(Managed / Transport / Disabled)。Managed=独立 meta 引擎;Transport=外部传输(未注入回落 MetaHost 嵌入 ring 流);Disabled=不持久化(opaque 拒写) |
MetaOpaqueBytes |
256 |
opaque 槽字节数(结构级缺省;初始化器可覆盖)。上限受 IMetaPolicy 容量约束 |
构造校验(fail-fast,RingBase.Validate——源码 Structures/Ring/RingBase.cs):
PageSize是 2 的幂 / [4KB, 1GB]MemorySize ≥ PageSize且整除为 2 的幂页数PageCount ≤ MaxPageCountSegmentGrowthLimit ≥ PageSizeMutableFraction ∈ (0,1)开区间ColdRecordBufferLimit ≥ HeaderSize
3. 构造与生命周期
3.1 [RingKey] 源生成器机制
开放泛型 BlittableRing<TKey> 的构造是 protected internal——消费面只经 [RingKey] 生成的封闭薄类(编译期封闭 + CreateAsync 工厂)。内置 long 已在 TC.Tier.Runtime 程序集声明,RingOfLong 开箱即用;自定义 Key 在消费程序集加一行:
[assembly: RingKey(typeof(OrderId))] // 产出 RingOfOrderId / HashOfOrderId / BTreeOfOrderId ...
分析器诊断不满足 unmanaged 约束的声明。绕过 [RingKey] 直接继承开放泛型 = CS0122 编译错(internal 不可见时绑定器误绑 IO 扩展同名方法)。
3.2 CreateAsync 工厂 = 构造 + Initialize + WaitForReadyAsync 一步就绪
using var fs = TierFs.New("memory:");
var settings = new BlittableRingSettings(new StorageEngineOptions("kv-ring", 64L << 20))
{
PageSize = 4096, MemorySize = 64 * 1024, MutableFraction = 0.5,
};
await using var ring = await RingOfLong.CreateAsync(settings, fs); // 就绪可读写
或分步(观察恢复进度 / 注入恢复窗口):
var ring = new RingOfOrderId(settings, fs);
ring.Initialize(); // 触发 OnInitializeBegin → 三引擎并行启动 + 恢复
ring.WaitForReady(); // 等恢复完成(默认四级回退)
3.3 写读最小用例(正序两步写 / 两段合口径读)
static LogicalAddress KvPut(RingOfLong ring, IIndex<long> index, long key, ReadOnlySpan<byte> value)
{
var addr = ring.Write(key, value); // ① 真相源先写,得地址
index.Insert(key, addr, LogicalAddress.Empty); // ② 索引后插(指向该地址)
return addr;
}
static bool KvTryGet(RingOfLong ring, IIndex<long> index, long key, Span<byte> buf, out int len)
{
len = 0;
var addr = index.Find(key); // ① 索引命中得地址
if (addr == LogicalAddress.Empty) return false;
len = ring.GetValue(addr, buf); // ② 真相源按址取值
return true;
}
3.4 Dispose
幂等(CAS 排他门 _disposeGate)。落盘契约(存储底线):先把 mutable 区 [FlushedUntilAddress, TailAddress) 落盘(FlushUntilCore 免门直走)→ 写 meta → 释放 meta 策略 → 释放页 native 内存。各步异常吞掉互不阻断(尽力落盘)。Dispose 与并发读写经 EnsureReady/EnsureNotDisposing fail-fast 隔离。
4. API 详表(按功能分组)
4.1 写
| 方法签名 | 用途 |
|---|---|
LogicalAddress Write(TKey key, ReadOnlySpan<byte> value) |
公开写入。大 value 自动溢出(按 OverflowPolicy)。永不返回 Empty |
LogicalAddress Write(TKey key, ReadOnlySpan<byte> prefix, ReadOnlySpan<byte> value) |
分段写入(免临时数组拼接);Overflow=Enabled 时超阈值抛——用连续 value 的 Write/WriteAsync |
LogicalAddress WriteTombstone(TKey key) |
写墓碑(FLAG_RINGRECORD_TOMBSTONE + 空 value);恢复时 RecordKey.IsTombstone 过滤 |
ValueTask<LogicalAddress> WriteAsync(TKey key, ReadOnlyMemory<byte> value, CancellationToken ct = default) |
公开异步写。inline 快路径同步完成(零 async 开销);溢出慢路径真异步 |
ValueTask<LogicalAddress> WriteTombstoneAsync(TKey key, CancellationToken ct = default) |
异步墓碑写(同步完成) |
void UpdateValue(LogicalAddress addr, ReadOnlySpan<byte> newValue) |
RCU 原地更新。四分支翻转:overflow→overflow/inline→overflow/overflow→inline(受原 inline 槽容量约束,超抛)/inline→inline |
ValueTask UpdateValueAsync(LogicalAddress addr, ReadOnlyMemory<byte> newValue, CancellationToken ct = default) |
异步版。纯 inline 翻转同步完成;溢出翻转走 WriteOverflowAsync |
WriteBatch BeginWriteBatch() |
批量写(ref struct,using 包裹)。多写者各持独占窗口段,批内无锁推进;详见 §4.6 |
WriteBatch.Append 签名:
LogicalAddress Append(TKey key, ReadOnlySpan<byte> value);
LogicalAddress Append(TKey key, scoped ReadOnlySpan<byte> prefix, scoped ReadOnlySpan<byte> value);
同一 WriteBatch 不可并发调用;不同批各持窗口可并发。Dispose 释放 epoch。
4.2 读(内容自愈读)
判据 = SafeSnapshotTail(addr < 水位 → 页池直读;≥ → 设备读 / 自愈回退)。热直读经 magic + CRC 校验,失败自动回退设备重读——驱逐复用/撕裂/残留不产生假数据(读到设备权威快照或空/NotFound,由上层定夺)。
| 方法签名 | 返回 / 语义 |
|---|---|
int GetValue(LogicalAddress addr, Span<byte> destination) |
拷贝交付;0 = 无有效记录(未命中/墓碑/校验失败) |
bool TryGetValue(LogicalAddress addr, Span<byte> destination, out int written) |
拷贝交付;false = 未命中/墓碑/校验失败。跨 await 消费必须走此形态 |
ReadOnlySpan<byte> GetValueSpan(LogicalAddress addr) |
零拷贝切片——仅同步栈内合法。空 span = 无有效记录。溢出 record 回退 thread-static 缓冲拷贝 |
RecordKey<TKey> GetKey(LogicalAddress addr) |
拉到 key(+ value 长度 + flags + 地址)。无有效记录抛 InvalidOperationException(fail-fast) |
bool TryGetKey(LogicalAddress addr, out TKey key) |
读 key 的 NotFound 形态。false = 无有效记录 |
ValueTask<RecordKey<TKey>> GetKeyAsync(LogicalAddress addr, CancellationToken ct = default) |
异步单条 key 读;热区同步快路径,冷区真异步整页回源 |
ValueTask<int> GetValueAsync(LogicalAddress addr, Memory<byte> destination, CancellationToken ct = default) |
异步 value 拷贝读;溢出走 ReadOverflowAsync |
ValueTask<(TKey Key, bool Success)> TryGetKeyAsync(LogicalAddress addr, CancellationToken ct = default) |
异步 key 读的 NotFound 形态 |
void GetRecords<THandler>(ReadOnlySpan<LogicalAddress> addresses, THandler handler) where THandler : IReadOnlyRecordHandler<TKey> |
批量读(按页号聚簇,减少冷区回源次数)。无有效记录的地址跳过(不回调 handler.Handle) |
RingRecordFields GetFields(LogicalAddress addr) |
读 record header 字段(轻量) |
Span<byte> GetSpan(LogicalAddress addr, int length) |
不暴露 byte*,页池 native 内存可写窗口。调用方保证页未驱逐回收 |
void EnterEpoch() / void ExitEpoch() |
epoch 读保护协议实现(IEpochProtected)。读路径已退役——读安全性由内容自愈读不变式承担;写/驱逐 drain 仍使用 |
long GetByteDistance(LogicalAddress from, LogicalAddress to) |
公开字节距离(队列积压/滞后 lag 度量原语) |
4.3 8 水位(全列)
| 水位 | 含义 | 持久化层 |
|---|---|---|
BeginAddress |
头截断边界(= engine.MinAddress,此地址前数据已回收) | 持久化层(meta 必存) |
FlushedUntilAddress |
落盘边界(此地址前已写引擎,恢复可信数据上限 W) | 持久化层(meta 必存) |
SafeReadOnlyAddress |
安全只读水位——epoch 保护下 readonly 区推进目标(读者可安全读至此处) | 持久化层(meta 必存) |
ReadOnlyAddress |
只读水位——此地址前页面冻结(等待 flush 后驱逐) | 持久化层(meta 必存) |
TailAddress |
写游标——下一条 record 的分配地址(TryAllocate 单调推进) |
持久化层(meta 必存) |
HeadAddress |
驱逐边界——此地址前的旧页已被回收(head 随 flush/驱逐推进) | 内存水位层(meta 可选存,恢复时从 Begin 重建) |
SafeHeadAddress |
安全驱逐边界(epoch 保护下的驱逐推进目标) | 内存水位层 |
ClosedUntilAddress |
关页边界——此地址前的页已关闭(新写入不得落在其前) | 内存簿记层(永不落盘,恢复时初始化为 SafeHeadAddress) |
辅助:TakeSafeSnapshotTail()(lock 内采集——之前全部 header 完整可见,扫描游标构造快照 + 读路径热冷分档判据)。8B 距离水位 _safeSnapshotTailDist/_headDist 经 Interlocked CAS-max 发布 / Volatile 读,热路径无锁无撕裂。
4.4 截断
| 方法签名 | 语义 |
|---|---|
void TruncatePrefix(LogicalAddress address, bool truncateDevice = false) |
头截断——推进 BeginAddress。前置:address ≤ FlushedUntilAddress(否则抛——未落盘数据会丢)。truncateDevice=true 调 engine.ReclaimHead(失败不阻断逻辑截断) |
void TruncateSuffix(LogicalAddress address) |
尾截断——D2 决策唯一异常路径(放松"地址单调不回退"铁律)。回退四件套:物理回收(engine.ReclaimTail)+ 内存水位条件回退(各字段独立判断)+ 页池清零 + 冷缓存失效。守卫:address 不得落入已驱逐区(< SafeHeadAddress) |
4.5 快照
| 方法签名 | 语义 |
|---|---|
IRingSnapshotReader OpenSnapshotReader(LogicalAddress begin = default, LogicalAddress end = default) |
创建快照读取器(pull 模式)。默认区间 [BeginAddress, TailAddress)。冷热透明分流(FlushedUntilAddress 判据) |
IRingSnapshotWriter OpenSnapshotWriter(LogicalAddress begin, LogicalAddress end) |
创建快照写入器(push 模式)。begin..end 超出当前 AllocatedTail 时先整段预留 Allocate。填完所有数据须调 Complete 推进 TailAddress 到 end |
IRingSnapshotReader:long Length / int Read(Span<byte> buf) / ValueTask<int> ReadAsync(Memory<byte> buf, CancellationToken ct),页级流式支持 GB/TB。
IRingSnapshotWriter:void Write(ReadOnlySpan<byte> buf) / ValueTask WriteAsync(ReadOnlyMemory<byte> buf, CancellationToken ct) / void Complete() / ValueTask CompleteAsync(CancellationToken ct)。
4.6 批量写
BeginWriteBatch() 返回 ref struct WriteBatch(using 包裹)。窗口模型:每个批 = 独占页段窗口——领取时在 _tailLock 内把 tail 一次性推进到窗口尾(预留段,批独占);批内 record 分配完全无锁(页内游标推进,零锁零 EnsureSpace)。多写者各持窗口并发写——锁竞争从"每 record 一次"降到"每窗口一次"。
4.7 扫描
| 方法签名 | 语义 |
|---|---|
IRingScanCursor OpenScanCursor(LogicalAddress begin = default, LogicalAddress end = default) |
打开扫描游标(默认区间 [BeginAddress, TailAddress);工厂注入或默认 SequentialRingScanCursor)。整段持 epoch |
IAsyncEnumerable<(TKey Key, LogicalAddress Address, bool IsTombstone)> ScanAsync(LogicalAddress begin, LogicalAddress end, CancellationToken ct = default) |
范围扫描异步迭代器(包装 OpenScanCursor,冷区真异步 IO)。仅吐用户数据 record(过滤 FLAG_ENTRY_IS_META);墓碑旗标随三元组吐出 |
IAsyncEnumerable<(TKey Key, LogicalAddress Address, bool IsTombstone)> ScanAsync(CancellationToken ct = default) |
全量扫描(BeginAddress 到 TailAddress) |
long SpanPages(LogicalAddress from, LogicalAddress to) |
两地址间页跨度(P1 扫描模式选择器成本模型输入) |
LogicalAddress GetFlushedWatermark() |
IKeyResolver 契约——派生结构后台持久化的 footer 锚点 W(= FlushedUntilAddress) |
4.8 2PC(ITransactionParticipant 六件套)
| 方法签名 | 语义 |
|---|---|
void Prepare(long seq) / ValueTask PrepareAsync(long seq, CancellationToken ct) |
事务准备:落盘数据 + meta 至"悬干"状态。Transport 回落 MetaHost = 1 次 fsync(meta record 随数据同页 flush 原子落盘);Managed/注入 Transport/Disabled = 2 次 fsync(FlushUntil 先 + WriteMeta 后)。经 _prepareGate 串行 |
void ConfirmCommitted(long seq) |
推进 LastCommittedSeq,触发回调;推进 _txRollbackTail(提交边界,随 meta 持久化为 CommittedTailAddress) |
void OnCommitted(long seq, Action callback) |
注册 seq 回调;已提交到更高 seq 立即同步触发 |
void Abort(long seq) / ValueTask AbortAsync(long seq, CancellationToken ct) |
2PC 回滚——TruncateSuffix(_txRollbackTail) 回退到上一已确认提交边界,丢弃本轮悬干数据。守卫矩阵:seq ≤ LastCommittedSeq=no-op;seq ≠ LastPreparedSeq=陈旧;无既有提交边界/无悬干数据 = 仅复位记账 |
LastCommittedSeq / LastPreparedSeq 公开只读(恢复 / 运行时读)。
4.9 Opaque meta 搭车
| 方法签名 | 语义 |
|---|---|
void SetOpaqueMeta(ReadOnlySpan<byte> data) |
写外部 opaque meta(stage 进策略缓冲,随下一次水位提交原子落盘)。唯一消费者形态:TierKV 组合层登记 index 镜像锚点 W。MetaPolicyKind=Disabled 时拒写 |
ReadOnlySpan<byte> ReadOpaqueMeta() |
读最近已提交块;空 = 未写入/未开启 meta 持久化——空即答案 |
IMetaPolicy<RingMetaHeader, RingMetaPayload> MetaPolicy { get; } |
构造期装配(永非 null 纯读) |
5. 自定义注入工厂
| 接口 / 类型 | 职责 | 默认实现 | 自定义注意 |
|---|---|---|---|
IRingCodec |
record 三段式 codec(header/CRC/字段偏移/单字段读) | BlittableRing.Codec |
暴露 HeaderSize / Alignment / Magic / CrcOffset / PayloadLengthOffset;TryReadHeaderLight 热路径轻量门(magic+长度上界+Flags),免全量 header 解码——本进程写入页池热读专用;设备回源/恢复走全量 TryReadHeader |
RingRecordFields |
record 业务字段包(Flags / PayloadLength / PaddingLength / PreviousAddress)—— readonly record struct |
— | codec 读写 header 时传参 |
RecordKey<TKey> |
Key + ValueLength + Flags + Address 组合返回值 | — | IsOverflow / IsMeta / IsTombstone 旗标属性 |
IKeyResolver<TKey> |
Index 注入 Ring 时必需的判等闭环契约 | RingBase<TKey> 实现 |
TryGetKey / GetFlushedWatermark / ScanAsync 三成员;扫描详见 §4.7 |
IRingScanCursor |
扫描游标,扩 IStructureScanCursor |
SequentialRingScanCursor(嵌套 sealed) |
CurrentAddress / NextAddress / BeginAddress / EndAddress / GetFields() / CurrentRecordSize;自定义经 RingCursorFactory<out TRingScanCursor> 委托注入 |
RingCursorFactory<out TRingScanCursor> |
扫描游标工厂委托 (begin, end) => cursor |
null = 默认 SequentialRingScanCursor |
自定义经构造参数 cursorFactory 注入 |
IRingSnapshot |
快照统一抽象(pull/push 分离) | RingSnapshot<TRing>(嵌套) |
Reader(begin, end) / Writer(begin, end);自定义经构造参数 ringSnapshot 注入 |
RingSnapshotReaderFactory / RingSnapshotWriterFactory |
快照读写器工厂委托 | null = 默认 RingSnapshotReader / RingSnapshotWriter |
自定义经构造参数 snapshotReaderFactory / snapshotWriterFactory 注入 |
IRingSnapshotReader |
快照读取器——pull 模式,页级流式 | — | Length / Read(Span) / ReadAsync(Memory, ct);IDisposable + IAsyncDisposable |
IRingSnapshotWriter |
快照写入器——push 模式,Complete 收口 |
— | Write(ReadOnlySpan) / WriteAsync(ReadOnlyMemory, ct) / Complete() / CompleteAsync(ct) |
OverflowPolicy(enum) |
溢出策略 Disabled / Enabled |
Disabled |
Enabled = WiscKey 式 value 分离,溢出引擎与主引擎同 fs |
RingMetaPayload(struct) |
持久化层 6 指针 + LastCommittedSeq + LastPreparedSeq + OverflowTailAddress + KeySize + CommittedTailAddress |
— | [BinaryLayout] 源生成器产出 codec;旧块短 payload 零扩展降级 |
RingMetaHeader(struct) |
meta header 规范字段(MagicValue / Version / Flags / PayloadLength / PaddingLength,12B) |
— | Magic="RMHD",CurrentVersion=major 1/minor 0 |
AddressInfo(internal struct) |
溢出指针(Address + Size,24B) |
— | 旧版 8B 位压缩在 LogicalAddress(16B) 模型下不适用 |
OverflowRecordHeader(internal struct) |
溢出帧头 18B(Magic+Version+Flags+PayloadLen+PadLen+Crc32C) | — | Magic "OVRF",Alignment=4 |
IReadOnlyRecordHandler<in TKey> |
批量读回调接口 | — | Handle(LogicalAddress address, TKey key, int valueLength, ushort flags)——flags 是 RecordFlags 内部位 |
MetaPolicyFactory<RingMetaHeader, RingMetaPayload> |
meta 策略工厂委托 kind => policy |
CreateMetaPolicyDefault |
Managed / Transport(未注入 transport 回落 MetaHost)/ Disabled 三模式 |
IMetaTransport |
Transport 模式的外部 meta 传输 | MetaHost(嵌套,回落:meta record 嵌入 ring 流) |
自定义经构造参数 metaTransport 注入 |
IRecovery<RingRecoveryHints> |
恢复策略 | DefaultRingRecovery(四级回退) |
自定义经构造参数 recovery 注入;裸写 IRecovery 与 LifecycleBase 闸门不同步——继承 RecoveryBase<RingRecoveryHints> |
LightEpoch |
epoch 保护 | 自建 | 自定义共享经构造参数 epoch 注入(ResourceOwnership.Referenced) |
6. 持久化机制
6.1 页池状态机(4 态)
mutable → readonly → flushed → evicted
↑ │ │ │
│ │ │ │
└──────────┴─────────────┴────────────┘
(slot 循环复用:pageSeq & PageCountMask)
- mutable:当前写者活跃区,
TryAllocate推进 TailAddress - readonly:页面冻结,等待 flush。
ReadOnlyAddress推进经PageAlignedShiftReadOnlyAddress(页边界对齐) - flushed:已写引擎(
AsyncFlushPages把页内容 Write 回它对应的引擎地址——同地址 100% 确定)。FlushedUntilAddress推进 - evicted:旧页回收(head 推进
FreePage)。HeadAddress/SafeHeadAddress推进
页池固定 N 槽(PageCount = MemorySize/PageSize),slot = pageSeq & PageCountMask 循环复用。地址单调递增永不回退,写满一圈淘汰旧页(FASTER hybrid log 模型)。_emptyPageCount 默认 PageCount-1(最大 mutable 区);_headOffsetLagBytes = (PageCount-1) × PageSize 决定 mutable 区大小 + 自动驱逐触发点;_readOnlyLagBytes = MutableFraction × (PageCount-1) × PageSize 决定 mutable→readonly 转换点。
6.2 FlushedUntilAddress 推进路径
FlushUntil(untilAddress)/FlushUntilAsync:持久化契约面(写穿 + fsync)。_flushGate串行门保证并发 flush 区间不重叠——「读水位快照 → 迭代刷页 → 推进水位」三步原子,避免两个并发 flush 的区间重叠触发关页链Shift→OnPagesClosed→FreePage把对方迭代中的页置 nullFlushUntilTail()/FlushUntilTailAsync:tail 原子快照(tail 在_tailLock内原子读取,读与用之间不被并发推进混入;快照之后的并发写入由后续 flush 覆盖,水位 CAS128 单调)WriteThroughUntil(untilAddress):容量写穿(驱逐链专用,零 fsync)——页复用前数据已在设备,进程崩溃安全,断电窗口归上层 checkpoint/Committed 档承担
flush 路径必调 InvalidateColdPageCache(页内容随追加演进,flush 后缓存快照即陈旧——W6 缺陷根因修复:不失效则同页后续冷读永久供陈旧快照,resolver 回读 key 不匹配 → Find=Empty)。
在途 flush 登记与关页 worker 互斥(_ongoingCloseLock 内登记区间,worker 走到与在途区间相交的页即停止本轮回收,由下一次关页触发续走)。
6.3 meta 持久化(IRingMetaPolicy)
WriteMeta/WriteMetaAsync:更新 meta 缓冲(水位 + payload)+Commit落盘。flushedUntilOverride参数:Transport 回落 MetaHost 时的 1-fsync Prepare 优化——先记 dataTail 再随数据同页 flush,避免 Managed 那种"先 flush 再记水位"的 2 次 fsyncWriteMetaRecord(private protected):把内层 meta block 作为FLAG_ENTRY_IS_METArecord 追加到 TailAddress(纯内存写:分配 tail + 写 header + 写 payload + CRC + Seal,零 device IO;落盘靠 Prepare 末尾 FlushUntil)。flags 顺序约定:写 header 时就设全IS_META|VALID|SEALED,再 FillCrc(CRC 覆盖含 flags 字段),最后 Seal 幂等BuildMetaPayload:6 持久化指针 +LastCommittedSeq+LastPreparedSeq+OverflowTailAddress+KeySize+CommittedTailAddressMetaLayout(sealed 嵌套):实现IMetaLayout<RingMetaHeader, RingMetaPayload>——12B 规范 header + 结构化 payload + opaque 插槽;旧块短 payload 零扩展降级(新字段读默认值,优雅不抛)MetaHost(sealed 嵌套):Transport 未注入传输时的回落——meta record 作为FLAG_ENTRY_IS_META追加到 ring 流末尾;读回 = 倒序扫 ring 流找最后一条有效 IS_META record 的 payload
详见 meta.md。
6.4 与 StorageEngine 关系
- 主引擎构造期
Create(纯装配零 IO),OnInitializeBegin内Initialize(非阻塞) - 溢出引擎(
OverflowPolicy=Enabled时构造,引擎名.overflow后缀,与主引擎同 fs、同几何) - meta 引擎(
MetaPolicyKind=Managed时构造,引擎名.meta后缀,段大小 ≥ 1MB,不分段) - 三引擎在
OnInitializeBegin并行启动,WaitForDependenciesAsync异步 join;引擎就绪由DefaultRingRecovery.RecoverAsync开头WaitForReadyAsync保证 - 水位线归结构层:引擎自恢复 MinAddress/AllocatedTail,结构水位走 meta/扫盘,不下传
_dataStart构造时由首次Allocate确定(GetDistance锚点,100% 确定);pageSeq = GetDistance(_dataStart, addr) >> PageSizeBits;pageIntra = GetDistance(_dataStart, addr) & PageSizeMask- 热路径快路径:同段(Ring 单段是常态)时
DistanceFromDataStart= 纯 long 减法,零引擎调用;跨段才 fallback 到engine.GetDistance
7. 恢复协议
7.1 四级回退(DefaultRingRecovery.OnRecoveryCoreAsync)
① RingRecoveryHints.RecoveredTail(外部主动注入最高优先级)
→ hints.FlushedUntilAddress(次选)
② meta(O(1) 正路):MetaPolicy.LoadAsync → RingMetaPayload
+ KeySize 锚点校验(防拿错特化开同一卷,fail-fast)
+ 2PC 水位还原(LastCommittedSeq/LastPreparedSeq/CommittedTailAddress)
③ 引擎 CommittedTail(已落盘区域)
④ 扫盘找 torn write 边界:
MagicLocator 倒序找最后含 magic 的页(Monotone 快速档)
→ ForwardScanRecordsAsync 前向逐页扫 record,CRC 校验求精
→ 同页撕裂重同步(坏帧按对齐粒度步进续扫,不连带丢同页后续有效帧)
7.2 RingRecoveryHints 注入
public readonly struct RingRecoveryHints
{
public LogicalAddress? BeginAddress { get; init; }
public LogicalAddress? HeadAddress { get; init; }
public LogicalAddress? FlushedUntilAddress { get; init; }
public LogicalAddress? RecoveredTail { get; init; } // 上层快照场景注入,优先于扫盘
public LogicalAddress? OverflowTailAddress { get; init; } // 溢出引擎尾
}
7.3 自愈读(read-protection-tiering v2)
不变式(读安全性由不变式承担,不依赖调用方编排):
- I1 写穿先行:
addr < FlushedUntilAddress→ 数据已在引擎 - I2 水位内完整:
addr < SafeSnapshotTail→ 页池必有完整 record(写者写完才推进水位) - I4 内容指纹:magic + CRC32C 校验
读分档判据 = IsHotRecord(dist):head ≤ addr < SafeSnapshotTail → 页池直读;addr < head → 页已回收(强制设备读,防同形记录复用别名假数据);addr ≥ SafeSnapshotTail → 设备读。热直读经 magic+CRC 校验失败自动回退设备重读——页池与设备互为自愈。
7.4 锚点 W 与 Index 协议
① Ring.CreateAsync → 恢复水位;锚点 W = ring.ReadOpaqueMeta()
(无锚点 / 损坏 / W 越过当前尾 → 回退 BeginAddress——宁可旧多重放)
② index 拉流重放:ScanAsync(W, TailAddress) 逐条 Insert 自建
(Hash/SortedIndex 有主存储帧时优先载帧,见 structures.md §4)
锚点 W = 索引上次主存储 dump/重放完成的水位,经 Ring 的 opaque 随水位线原子落盘;每次 dump/重放后 ring.SetOpaqueMeta(W) 即可。详见 meta.md。
7.5 溢出引擎尾恢复
RecoverOverflowTail 三级回退(同主流程):
hints.OverflowTailAddress注入- meta 的
OverflowTailAddress字段 OpenSequentialReader扫描溢出帧(逐帧 magic + Skip payload,恢复容忍不验 CRC,上层读时再校验)- 引擎
AllocatedTail近似
恢复后引擎尾对齐:_overflowTailAddress 落后于引擎物理尾时(撕裂写预留/Wasted 区间),ReclaimTail 退齐——否则新溢出帧从撕裂区中间分配,与遗留 Wasted 区间相交被读门拦截。
8. 反模式
| # | 反模式 | 后果 / 正解 |
|---|---|---|
| 1 | 写编排反序:先 index.Insert 再 ring.Write |
索引指向不存在的数据。正序:先 ring.Write 得地址 → 再 index.Insert |
| 2 | 零拷贝 span 跨 await:GetValueSpan 返回值跨 await 持有 |
span 仅同步栈内合法;跨 await/跨线程消费必须走拷贝 API(TryGetValue/GetValue)——API 形态强制,不依赖约定 |
| 3 | 绕过 [RingKey] 直接继承开放泛型:new BlittableRing<TKey>(...) |
CS0122 编译错(protected internal 闸门);封闭薄类是唯一消费面 |
| 4 | 跨实例组合 DeleteOnClose=true |
跨重启组合必须 DeleteOnClose=false(否则索引重建时真相源被清) |
| 5 | 给地址直达读加会话/索引:GetValue(addr) 前先 EnterScope 等 |
地址即句柄——GetValue(addr) 永久零税。读路径已退役 epoch 保护,调用方编排不必要 |
| 6 | TruncatePrefix 越过 FlushedUntilAddress |
抛 InvalidOperationException——未落盘数据会丢。截断点必须 ≤ FlushedUntilAddress |
| 7 | TruncateSuffix 落入已驱逐区(< SafeHeadAddress) |
抛——事务窗口横跨已驱逐页,无安全回退边界。Abort 前置窗口内不应发生大规模驱逐 |
| 8 | WriteBatch 跨 await 持有:ref struct 不跨 await,但 using 范围跨 await |
写锁语义——批窗口持 epoch,跨 await 持有 = epoch 嵌套警告。批在同步栈内开闭 |
| 9 | Overflow=Enabled 时分段写超阈值 | Write(key, prefix, value) 抛 InvalidOperationException——用连续 value 的 Write/WriteAsync |
| 10 | UpdateValue overflow→inline 回退时超原 inline 槽容量 |
抛——newValue.Length > originalInlineCapacity。原 inline 槽容量 = allocated - HeaderSize - keyLen |
| 11 | MetaPolicyKind=Disabled 时 SetOpaqueMeta | 抛——未开启 meta 持久化,opaque 登记被拒。配置 Managed/Transport 或移除 opaque 写入 |
| 12 | 跨特化开同一卷(RingOfLong 开 RingOfGuid 的卷) |
ValidateKeySizeAnchor fail-fast——盘上 KeySize 与实例 TKey 不符,避免静默 key 损坏 |
9. 想深入?指路
| 想懂什么 | 去哪 |
|---|---|
| 总览(七结构选型 / 组合 KV / 恢复模型) | structures.md |
| 横切协议(opaque 搭车 / 块格式 / 自定义传输 / 锚点 W) | meta.md |
| Session 协调协议(写/读/检查点三 op、悬挂裁决、故障模型) | session.md |
| 引擎使用(读写/水位/恢复/Compact/段表) | storage-engine.md / segment-table.md |
| Lease 协议(raft 共识 lease) | lease-protocol.md |
| 同族其他结构 | log.md / index.md / versioned-metadata.md / mirror.md / snapshot.md |
| 性能基线(点查/写/恢复/并发扩展) | perf/structures-perf-baseline.md |
| 源码(base/Settings/Contracts/各 partial) | src/TC.Tier.Runtime/Structures/Ring/ |
机制级细节(CAS128 水位基座、帧字节布局、溢出分块骨架、2PC 内部状态机、撕裂重同步扫描器)按需从源码注释与设计稿(docs/design/)查阅——使用面不需要这些细节;能力全集以类型 XML 注释为准。