VersionedMetadata 使用指南——版本链 N=2 轮转回收 / 单值元数据
给谁看:需要跨重启单值状态(水位/配置/锚点)的组件开发者 回答什么:VersionedMetadata 是什么 / Settings / 版本链 N=2 / 同步 vs 异步持久化 / 外部隔离传输 本篇只讲结构本身——Meta 横切协议(Disabled/Managed/Transport + IMetaTransport)见 meta.md 定位:Structures/ 6 子族之一,搭配件(与 Ring/Log 主结构搭配,跨重启单值状态)
0. 一句话总纲
VersionedMetadata = 版本链追加式单值 + N=2 轮转回收 + 崩溃安全(写一半 = 旧版本完好)。
- 单值:每次
Write把整条 payload 当一个版本追加到磁盘版本链,链头即当前值; - 内存工作副本(N≥2 对齐槽)让
Read零 IO、Abort零 IO(回退到上一版本); - 落盘 = 引擎
Allocate + Write + Flush,崩溃写到一半 = 链头 magic/CRC 不匹配 = 旧版本完好(断链天然容错); PayloadSize须 ≥ 托管块上界(3a 外部隔离传输场景,见 §2、§8 反模式 1)。
1. 定位
| 维度 | 内容 |
|---|---|
| 与主结构的关系 | 与 Ring/Log 主结构搭配——承载跨重启的单值状态(如 Ring 的 CommittedTailAddress 锚点、Session 的检查点水位、TimeSeries 的 dense 块水位) |
| 与 Meta 横切件的关系 | VersionedMetadata 是 Meta 横切件在结构层的具体实现——结构自身的水位(HighestVersionAddress 等)经 Meta 横切件持久化;横切协议(三模式 + IMetaTransport)见 meta.md |
| 与 Index 的差异 | Index 派生数据(可重建、判等闭环、依赖真相源);VersionedMetadata 是单值状态本体(追加流版本链,N=2 轮转回收) |
| 与 Mirror 的差异 | Mirror 是字节镜像(不知尺寸的整流式 AppendChunk);VersionedMetadata 是定长/变长单值(每次 Write 一条 record) |
两种消费形态:
- 直接用——跨重启单值状态:构造 + Initialize + WaitForReady,按
Write/Read/Persist用。 - 3a 外部隔离传输——把别的结构的 meta 块托管到 VersionedMetadata:用内置适配器
MetadataMetaTransport(源码Meta/MetadataMetaTransport.cs),勿自写单槽文件(见 §8 反模式 2)。
为什么单值用版本链而不是单槽覆盖写:
- 崩溃安全免费——单槽覆盖写要自理 torn write(写一半崩 = 数据撕裂),版本链追加 + 链头 magic/CRC 校验天然容错;
- Abort 零 IO——回退上一版本只切内存镜像,不触盘;
- 空间有界——N=2 轮转回收(
ReclaimOldVersions),不会无界增长; - 跨结构复用——VersionedMetadata 自身实现
ITransactionParticipant,可注册进TransactionLog跨结构原子提交(3a 场景直接用ext.Storage)。
2. Settings 全参数
VersionedMetadataSettings(继承 MetadataSettings ← Settings 基类公共 meta 配置):
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
MainEngine |
StorageEngineOptions |
"tc.metadata",单段 16MB,不分段,不预分配 | 主引擎选项。Metadata 版本链默认单段稀疏按需增长(元数据是结构体级小数据) |
PayloadSize |
int |
必填 | 元数据结构体字节数(写入 payload 大小,自动向上对齐到扇区)。3a 托管场景须 ≥ meta 块上界(12B 头 + 水位 struct + MetaOpaqueBytes + 4B 尾) |
MaxPayloadSize |
int? |
null |
变长档写入上限。null = 固定档(Write 截断/补零到 PayloadSize);非 null 且 > PayloadSize = 变长档(Write 允许 ≤ 上限任意长度,record 按实际长度落盘,超上限 fail-fast 抛) |
MaxMemoryVersions |
int |
2 | 内存多版本保留窗口(N≥2 是 Abort 零 IO 底线,可配更高支持 MVCC)。构造时强制 Math.Max(2, value) |
MetaPolicyKind |
MetaPolicyKind |
Disabled |
meta 横切件三模式:Disabled(扫盘恢复)/ Managed(独立 .meta 引擎)/ Transport(外部 IMetaTransport)——见 meta.md §1 |
MetaOpaqueBytes |
int |
0 | 外部 opaque 区容量(写侧约束,不参与盘上几何)。3a 托管场景按调用方需要配置 |
跨重启改
PayloadSize合法:恢复载入的历史版本按其盘上真实PayloadLength完整交付(不补零、不截断);本次运行的新版本按新PayloadSize几何落盘。版本链混尺寸由各 record 头部自述几何支撑。
MaxPayloadSize何时用:keyed 分区元数据(如 TimeSeries dense 水位块——n 随序列增长,每次 Write 的块长动态)。固定档消费者(Queue/Blob 等)逐字节行为不变。
3. 构造与生命周期
VersionedMetadata 无 Create 工厂——显式三步:构造(= 配置,零 IO)→ Initialize(同步启动后台恢复,立即返回)→ WaitForReady(观测等待恢复完成)。Dispose 幂等。
3.1 直接消费(最常见)
using TC.Tier.Core.IO;
using TC.Tier.Runtime.Structures.Metadata;
// ① 文件系统(介质平权——换介质 = 换一根 spec)
using var fs = TierFs.New("memory:"); // 或 "local:///path/to/dir"
// ② 配置(PayloadSize 必填;其余默认值即生产值)
var settings = new VersionedMetadataSettings(
new StorageEngineOptions("session.ckpt", 16L * 1024 * 1024, enableSegmentation: false))
{
PayloadSize = 64, // 单值结构体字节数
MetaPolicyKind = MetaPolicyKind.Managed, // O(1) 恢复水位;Disabled 走扫盘
};
// ③ 构造 + 启动 + 等就绪
using var md = new VersionedMetadata(fs, settings);
md.Initialize(); // 同步 void,启动后台恢复后立即返回
md.WaitForReady(); // 观测等待恢复完成(载入链头版本到内存镜像)
3.2 Write + Read + Abort(写读编排)
// ① 写——返回版本号(单调递增);本调用按落盘策略决定是否立即 flush
long v1 = md.Write(stackalloc byte[] { /* 64B 结构体 */ });
// v1 == 1(首次 Write 推进到 1;恢复载入过历史版本则从载入版本号续推)
// ② 读——零 IO;首次 Write 前 = 加载版本(盘上真实大小),Write 后 = 现热区
Span<byte> buf = stackalloc byte[64];
int n = md.Read(buf); // n = min(当前内容长度, buf.Length)
// ③ 零拷贝视图——仅 epoch 存活期内有效;跨 await 必须改走 Read(dst)
Span<byte> view = md.AsSpan(); // 同步栈内合法
ref var head = ref md.GetRef<long>(); // 强类型首字段引用(热路径零校验)
// ④ 显式持久化点——不注入 SyncPersistencePolicy 时 Write 不自动落盘,需手动触发
md.Persist(); // 把内存镜像追加为版本链新版本 + flush + 写 meta
// ⑤ 2PC 编排(与 TransactionLog 协同——Prepare 即持久化点)
const long seq = 100;
md.Prepare(seq); // 追加新版本 + flush + 写 meta(LastPreparedSeq = seq)
md.ConfirmCommitted(seq); // CAS 推进 LastCommittedSeq;新版本正式成为当前
// 失败路径:
md.Abort(seq); // 内存回退到上一版本(零 IO)+ ReclaimTail 回退悬干新版本
// ⑥ 头截断(后台周期调)——保留最近 N=MaxMemoryVersions 个版本,更老的从链尾方向回收
md.ReclaimOldVersions();
3.3 异步启动(注入恢复 hints)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var hints = new MetadataRecoveryHints
{
HighestVersionAddress = knownHead, // 上层最强知识——优先级最高(> meta > 扫盘)
LastCommittedSeq = knownSeq,
};
md.Initialize(hints); // 恢复核心:启动主引擎就绪 → meta.Load → 三级回退
await md.WaitForReadyAsync(cts.Token);
3.4 Dispose
// 显式释放(幂等);底层引擎、meta 引擎、epoch、热区对齐内存、缓冲池一并释放
md.Dispose();
// 或异步释放(MetaPolicy 走异步轨)
await md.DisposeAsync().ConfigureAwait(false);
生命周期与全部 Structures 同构(详见 src/TC.Tier.Core/docs/lifecycle.md):OnInitializeBegin 启动主引擎 +(Managed 模式)并行启动 meta 引擎;恢复核心 WaitForDependenciesAsync 双 await 引擎就绪 → MetaPolicy.LoadAsync → 三级回退定位链头 → 加载链头版本到内存镜像。
变长档写读:
MaxPayloadSize启用后 Write 允许 ≤ 上限的任意长度,record 按实际长度落盘。CurrentPayloadLength探测当前内容长度(固定档恒 PayloadSize;变长档随块增长)后分配读缓冲;AsSpan()/GetSpan<T>()自动按当前实际长度切片。
3.5 文件系统与介质
IFileSystem经组合根TierFs注入——介质平权:memory:/local:///path/s3://bucket等 spec 切换零代码改动;- 引擎名隔离同 fs 子目录——多结构同 fs 时引擎名须唯一(如
"my-log"/"my-log.meta"); DeleteOnClose:跨重启组合(如 3a 托管)须false(默认),否则 Dispose 时清盘;测试场景可设true自动回收;SectorSize从_fs.Volume静态属性读取——构造期可用,padding/热区分配安全。
4. API 详表
| API | 签名 | 说明 |
|---|---|---|
| 写 | long Write(ReadOnlySpan<byte> data) |
把 data 写入内存镜像,推进版本号,按落盘策略触发持久化。固定档:长截短补;变长档:超 MaxPayloadSize 抛。返回新版本号(单调递增;恢复后从载入版本续推) |
| 持久化 | void Persist() |
强制把内存镜像作为新版本追加到磁盘版本链 + flush + 写 meta。内容未变跳过(防重复/防缩容零覆写) |
| 读(拷贝) | int Read(Span<byte> dst) |
零 IO 读当前内容。加载版本优先(首次 Write 前按盘上真实大小交付);Write 后 = 热区。返回实际读取字节数 |
| 读(零拷贝) | Span<byte> AsSpan() |
当前内容 0-copy Span 视图。仅 epoch 存活期内有效——并发写/Abort 可能使其失效 |
| 读(强类型引用) | ref T GetRef<T>() where T : unmanaged |
当前内容首字节按 T 重解释的引用(热路径 GetRefUnsafe 零校验) |
| 读(强类型 Span) | Span<T> GetSpan<T>() where T : unmanaged |
按 T 重解释的 Span。要求内容大小与 sizeof(T) 对齐——历史大小 ≠ PayloadSize 时可能抛 |
| 热路径读 | int ReadNoEpoch(Span<byte> dst) |
不含 epoch 进出的读(供已持 epoch 的 scope/batch 内调)。裸调危险 |
| 当前长度 | int CurrentPayloadLength { get; } |
加载版本按盘上真实长度 / 热区按最近 Write 长度。分配读缓冲前探测用 |
| 水位·链头 | LogicalAddress HighestVersionAddress { get; } |
链头/最新版本地址 |
| 水位·链尾 | LogicalAddress LowestVersionAddress { get; } |
链尾/最老版本地址(头截断回收边界) |
| 水位·版本号 | long CurrentVersion { get; } |
当前版本号(单调递增,0 = 空链/无数据) |
| 扇区大小 | uint SectorSize { get; } |
引擎扇区大小(padding/对齐计算用) |
| 头截断 | void ReclaimOldVersions() |
回收旧版本磁盘空间——保留最近 N=MaxMemoryVersions 个版本,更老的从链尾方向 ReclaimHead(epoch 保护:等 readers 退出后回收) |
| Opaque 写 | void SetOpaqueMeta(ReadOnlySpan<byte> data) |
登记外部 opaque meta 进策略缓冲,随下次水位提交原子落盘。Disabled 抛 InvalidOperationException;超 MetaOpaqueBytes 抛 ArgumentException |
| Opaque 读 | ReadOnlySpan<byte> ReadOpaqueMeta() |
读最近已提交块内的 opaque 字节视图。无数据/Disabled 时为空 Span |
| 2PC | 见下表 | ITransactionParticipant 六件套 |
2PC 六件套(ITransactionParticipant,对齐 Log/Ring 范式,独立协议与数据写正交):
| 成员 | 语义 |
|---|---|
Prepare(long seq) / PrepareAsync(long seq, CancellationToken ct) |
记录回退快照 → 追加新版本到磁盘链头 + flush + 写 meta。内容未变跳过追加。数据 flush 原生同步,异步版仅 meta 走 MetaPolicy.CommitAsync |
ConfirmCommitted(long seq) |
CAS 推进 LastCommittedSeq + 持久化 meta(刷新水位)+ 清空 Abort 回退快照 + 触发回调。seq ≤ current 时 no-op |
Abort(long seq) / AbortAsync(long seq, CancellationToken ct) |
内存窗口回退到上一版本(零 IO)+ 尾截断 ReclaimTail 回退悬干新版本 + flush + 写 meta。幂等——恢复时可能对同一 seq 多次调 |
OnCommitted(long seq, Action callback) |
注册提交回调(链式触发)。已提交到更高 seq 时立即同步触发 |
long LastCommittedSeq { get; } |
当前已提交 seq(恢复/运行时读) |
long LastPreparedSeq { get; } |
最近 Prepare 的 seq。恢复裁决悬干用(> 协调者 committedSeq = 悬干,丢弃) |
5. 自定义注入工厂
构造参数支持注入(默认实现满足绝大多数场景;替换走命名委托/接口注入,禁匿名 lambda):
| 注入项 | 接口/类型 | 默认实现 | 注入方式 |
|---|---|---|---|
| Codec | IMetadataCodec |
VersionedMetadata.Codec(私有嵌套 sealed,JIT 去虚化) |
经 protected MetadataBase 构造第一参数——子类继承传 |
| 恢复算法 | IRecovery<MetadataRecoveryHints> |
DefaultMetadataRecovery(三级回退:hints → meta → 扫盘) |
构造参数 recovery |
| Meta 策略工厂 | MetaPolicyFactory<MetadataMetaHeader, MetadataMetaPayload> |
CreateMetaPolicyDefault(按 MetaPolicyKind 映射 Managed/Transport/Disabled) |
构造参数 metaPolicyFactory——命名委托,禁匿名 lambda |
| Meta 传输 | IMetaTransport |
Transport 模式回落 MetaHost(meta 块作为 IS_META record 嵌入版本链流) |
构造参数 metaTransport——3a 外部隔离用 MetadataMetaTransport 或自实现 |
| Epoch | LightEpoch |
内部 new 自管(进 Resources 统一释放) |
构造参数 epoch——多结构共享同一实例时注入 |
| 持久化策略 | IPersistencePolicy |
不注入 = 写路径不自动落盘(仅 Prepare/Persist 强制 flush) | 构造参数 persistencePolicy——SyncPersistencePolicy / AsyncPersistencePolicy |
| Logger | ILogger |
null | 构造参数 logger |
Contracts 类型(Structures/Metadata/Contracts/):
| 类型 | 角色 |
|---|---|
MetadataRecordFields |
版本 record 业务字段包(Flags/PayloadLength/PaddingLength/PreviousVersion/MetadataVersion),codec 读写 header 时传参 |
MetadataMetaHeader |
meta 水位 Header(12B 纯规范:Magic/Version/Flags/PayloadLength/PaddingLength),走 [BinaryLayout] 源生成 codec |
MetadataMetaPayload |
meta 水位 Payload(48B:HighestVersionAddress/LowestVersionAddress/LastCommittedSeq/LastPreparedSeq) |
MetadataRecoveryHints |
恢复提示(HighestVersionAddress? / LastCommittedSeq?),外部主动注入——最高优先级 |
IMetadataCodec |
版本 record codec 接口(HeaderSize/Magic/WriteHeader/TryReadHeader/FillCrc/VerifyCrc/ReadVersion 等) |
IPersistencePolicy / SyncPersistencePolicy / AsyncPersistencePolicy |
落盘策略——见 §6 |
6. 持久化机制
6.1 版本链 N=2 轮转回收
- 磁盘:版本链追加流——每次落盘 =
Allocate(recordSize).Start+Write(addr, span)+Flush。record 几何 =[Header 42B][Payload 实际长][Padding 扇区对齐],CRC32C in Header(覆盖 Header 除 Crc 自身 + Payload + Padding)。 - 内存:N 个对齐槽(
AlignedMemoryManager,pinned native,零 GC)。Write在覆盖 [0] 前先把 [0] 滑到 [1](保留为 Abort 零 IO 回退源)。N≥2 是底线——MaxMemoryVersions强制Math.Max(2, value)。 - 轮转回收:
ReclaimOldVersions按MaxMemoryVersions保留窗口——[keepAddr, highest]= 最近 N 个版本,更老的从链尾方向ReclaimHead。epoch 保护(BumpCurrentEpoch等 readers 退出后回收)。 - 逐 record 自身几何推进:链上 record 尺寸可能不同(PayloadSize 跨重启变更后新旧混尺寸),扫描/回收按每条 record 头部自述的
PayloadLength+PaddingLength跳进——统一 record 步进会落在旧 record 中段,断链静默丢数据。
版本 record 42B Header 布局(MetadataHeader,源生成器 [BinaryLayout] 生成 codec):
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 4 | MagicValue |
魔数 = RecordMagic.VersionedMetadata,块身份校验 |
| 4 | 2 | Version |
头版本号(major=1, minor=0),ValidEquals 校验 |
| 6 | 2 | Flags |
DefaultFlags = FLAG_CRC32C \| FLAG_PAYLOAD_4B;meta record 叠加 FLAG_ENTRY_IS_META |
| 8 | 4 | PayloadLength |
元数据结构体字节数(payload 实际长度) |
| 12 | 2 | PaddingLength |
扇区对齐填充字节数 |
| 14 | 16 | PreviousVersion |
版本链指针,指向上一版本(链尾为 LogicalAddress.Empty) |
| 30 | 8 | MetadataVersion |
版本号(单调递增;调用方按版本号寻址/恢复定位) |
| 38 | 4 | Crc |
CRC32C(覆盖 Header 除 Crc 自身 + Payload + Padding) |
6.2 同步 vs 异步持久化策略对比
| 策略 | ShouldPersist(version) |
何时落盘 | 适用 | 代价 |
|---|---|---|---|---|
| 不注入(默认) | (不调) | 仅 Prepare / Persist 显式触发 |
上层用 2PC 编排提交点(如 Session) | Write 不自动落盘——崩在 Prepare 前 = 丢失 |
SyncPersistencePolicy |
恒 true | 每次 Write 立即 AppendVersionToDisk + flush |
单值独立持久化(不参与 2PC) | 每次 Write 强制 IO(最高一致性,最低吞吐) |
AsyncPersistencePolicy |
恒 false | 后台批量 flush / Prepare / Persist 显式触发 |
高频写、容忍批量延迟落盘 | 后台线程批量;崩在 flush 前 = 丢失未提交版本 |
Prepare无论何种策略都强制 flush——2PC 协议要求 Prepare 即持久化点(WriteAsync 不保证落盘)。
6.3 内存热窗口(Abort 零 IO 的关键)
Write 在覆盖热区 [0] 前,先把 [0] 滑到 [1],使 [1] 始终持有上一版本镜像:
[0] = 当前版本(最近 Write 的目标)
[1] = 上一版本(Abort 零 IO 回退源;写覆盖前的镜像)
[2..N-1] = 更老版本(MaxMemoryVersions > 2 时支持 MVCC)
- 拷贝顺序从高索引往低索引(
[N-1] ← [N-2] ← ... ← [1] ← [0]),避免[0]→[1]覆盖[1]旧值前未读到; - 变长档槽增长:
EnsureHotSlotSize(needed)按需重分配整组槽(max(needed, 当前×2)对数摊薄,上限_hotCapacity),搬移既有镜像;固定档恒跳过(槽尺寸 = PayloadSize); - 冷热分离:恢复载入的历史版本按盘上真实
PayloadLength从自持PinnedBufferPool租借只读缓冲,不进按当前配置分配的读写热区——历史大小 ≠ 当前 PayloadSize 时不补零、不截断;首次 Write 后当前内容切到热区,加载版本归还池(两次 Write 后不可达); - Abort 回退分支:会话内有 Write(
_currentVersion > _baseVersion)时——两次以上写走热-热回退([1]→[0]);单次写且前置 = 加载版本走冷-热回退(_serveLoaded = true,回到恢复载入的历史版本完整大小)。
6.4 崩溃安全
天然容错——无需 WAL:
- 写到一半(torn write):链头 record magic/CRC 不匹配 → 恢复时
TryReadHeader返回 false → 链断在上一合法 record(旧版本完好); - 内存镜像未落盘:恢复时从盘上链头载入(旧版本);
- Prepare 后 Commit 前:磁盘链头是 Prepare 追加的新版本,但
LastCommittedSeq未推进——恢复时LastPreparedSeq > LastCommittedSeq即悬干,Abort触发尾截断ReclaimTail回退(按 record 自身几何 +_lastAppendedRecordSize)。
6.5 外部隔离传输(3a 托管)
把别的结构的 meta 块托管到 VersionedMetadata——用内置适配器 MetadataMetaTransport:
using TC.Tier.Core.IO;
using TC.Tier.Runtime.Meta;
using TC.Tier.Runtime.Structures;
using TC.Tier.Runtime.Structures.Metadata;
using var fs = TierFs.New("local:///var/lib/myapp");
// ★ PayloadSize 须 ≥ meta 块上界:12B 头 + 水位 struct + MetaOpaqueBytes + 4B 尾
// MetadataMetaPayload = 48B;按调用方 MaxMetaOpaqueBytes 估算后留余量更稳
var hostSettings = new VersionedMetadataSettings(
new StorageEngineOptions("my-log.meta", 1L << 20, enableSegmentation: false))
{
PayloadSize = 4096, // 留足余量(远大于 64 + MetaOpaqueBytes)
MetaPolicyKind = MetaPolicyKind.Disabled, // 托管结构自身水位走扫盘(无嵌套 meta)
};
// 构造 + 启动(适配器内部 Initialize;首次读写 WaitForReady 就绪)
using var ext = new MetadataMetaTransport(fs, hostSettings);
// 注入到 Log/Ring/Mirror 等主结构的 settings.MetaPolicyKind = Transport 时使用
var logSettings = new EntryLogSettings(
new StorageEngineOptions("my-log", 64L << 20)) { MetaPolicyKind = MetaPolicyKind.Transport };
using var log = new EntryLog(fs, logSettings, metaTransport: ext);
// ★ 适配器语义:WriteBlock = Write + Persist + flush + ReclaimOldVersions(N=2 轮转有界)
// ReadLastBlock = 内存镜像零 IO 读 + 按统一布局自述裁剪为变长精确块
// 需要 2PC 跨结构原子提交时经 ext.Storage(VersionedMetadata 自身已实现 ITransactionParticipant)
WriteBlock 即 meta fsync 点(data 在调用链先行落盘);3a 语义下主结构主流水位线零影响——meta 块写入与主结构引擎完全解耦(详见 meta.md §6.3)。
7. 恢复协议
DefaultMetadataRecovery(继承 RecoveryBase<MetadataRecoveryHints> 模板,三级回退——对齐 meta.md §6.2 全结构统一优先级):
① WaitForDependenciesAsync:等主引擎 + meta 引擎(Managed 模式)双 await 全异步轨就绪
② MetaPolicy 装配(恢复核心钩子内——依赖引擎就绪)
③ meta.LoadAsync(水位 O(1);Disabled no-op)
④ 三级回退定位链头:
1. hints(外部注入 HighestVersionAddress——最高优先级)
2. meta payload(HighestVersionAddress + LastCommittedSeq + LowestVersionAddress——O(1))
3. 扫盘按版本号定位 Head(无 meta / meta 损坏兜底)
⑤ LoadVersionToMemory(head):按 record 自身 PayloadLength 从自持 PinnedBufferPool 租借只读缓冲
——历史大小 ≠ 当前 PayloadSize 时不补零不截断,完整交付
扫盘:从 MinAddress 正向扫,按每条 record 自身 PayloadLength+PaddingLength 跳进,magic 不匹配 = 链结束。最后一条合法数据 record = 链头(最高地址 = 最新写入)。跳过 IS_META record(Transport 自流嵌入模式的 meta block 不参与数据版本号定位,几何跳进照常)。
链头判定细节:
- 不用地址值
== LogicalAddress.Empty判断"有没有找到"——Empty 是合法地址(地址空间起点,首版本链头就是 Empty);用headFound布尔标志; - 链头 = 最高地址的数据 record——不用版本号比较:同一版本号可能被多次持久化(Write 落盘 + Prepare 再落盘),版本号相同但地址递增,最新地址才是当前链头;
- 扫盘 64KB 页步进(
ScanProbePageSize)用于 magic 首扫定位起点;几何跳进后逐 record 头校验;最大扫盘 64MB 防失控。
与 Meta 横切件配合:
- 结构自身水位(HighestVersionAddress 等)经 Meta 横切件持久化(MetaPolicyKind 三模式);
- 3a 场景下 VersionedMetadata 自身被
MetadataMetaTransport包成别的结构的 meta 介质——结构自身的水位走Managed(嵌套.meta.meta引擎)或Disabled(扫盘)。
8. 反模式
| # | 反模式 | 后果 / 正确做法 |
|---|---|---|
| 1 | PayloadSize < 托管块上界(3a 场景) |
WriteBlock 抛 ArgumentException(fail-fast 不截断)。正确:PayloadSize ≥ 12B 头 + 水位 struct(48B)+ MetaOpaqueBytes + 4B 尾,留余量更稳 |
| 2 | 自写单槽文件存 meta(3a 场景) | 独自写盘要自理 torn write 原子性、落盘顺序(data 先 meta 后)、崩溃恢复一致性、2PC 提交链路。正确:用 MetadataMetaTransport 托管到 VersionedMetadata(版本链天然容错 + N=2 轮转有界 + 2PC 已实现) |
| 3 | 绕过 PersistencePolicy 自改落盘时机 |
一致性失守——Async 策略下 Write 不落盘,崩在后台 flush 前 = 丢失。正确:需要每次 Write 落盘就注入 SyncPersistencePolicy;上层编排走 2PC(Prepare+ConfirmCommitted) |
| 4 | 跨 await 持有 AsSpan() / GetRef<T>() 视图 |
视图仅 epoch 存活期内有效——并发写/Abort 可能使其失效。正确:跨 await 走 Read(dst)(拷贝交付) |
| 5 | 用地址值 == LogicalAddress.Empty 判断"链头未找到" |
Empty 是合法地址(地址空间起点,首版本链头就是 Empty)。正确:恢复代码用 headFound 布尔标志(已是这样实现的) |
| 6 | 统一 record 步进扫描版本链 | PayloadSize 跨重启变更后新旧混尺寸,统一步进会落在旧 record 中段 → 静默断链丢数据。正确:按每条 record 头部自述 PayloadLength+PaddingLength 跳进 |
| 7 | Disabled 模式下 SetOpaqueMeta 期望被接受 |
抛 InvalidOperationException(禁用即报错,不静默吞)。正确:需要 opaque 就配 Managed/Transport |
| 8 | 把 meta 块当独立提交路径(自流嵌入 3b 之外的语义) | 已废除——自拍水位独立成块 = 并发水位回退 + 被内部提交冲掉。正确:opaque 只登记,随水位提交原子落盘(meta.md §4) |
9. 想深入?指路
| 想懂什么 | 去哪 |
|---|---|
| 源码 | src/TC.Tier.Runtime/Structures/Metadata/(VersionedMetadata / MetadataBase.* / Contracts/) |
| Meta 横切协议(三模式 + IMetaTransport + opaque 搭车 + 块格式) | meta.md |
| Session 协调协议(写/读/检查点三 op、悬挂裁决) | session.md |
| 引擎使用(Allocate/Write/Flush/Reclaim/Compact) | storage-engine.md |
| 段几何 / 段表 | segment-table.md |
| Lease 协议 | lease-protocol.md |
| 同族结构 | ring.md / log.md / index.md / mirror.md / snapshot.md |
| 总览(七种结构选型 / 组合 KV / 反模式) | structures.md |
机制级细节(record 42B header 字段全表、扫盘 64KB 页步进、_hotSlotSize 增长策略、Abort 回退分支条件)按需从源码 XML 注释与 docs/design/ 设计稿查阅——使用面不需要这些细节;能力全集以类型 XML 注释为准。
10. FAQ
| 问题 | 答案 |
|---|---|
Write 后立刻 Read 不到新值? |
不会——Write 同步更新内存镜像,Read 零 IO 读内存。但磁盘上不一定有——不注入 SyncPersistencePolicy 时 Write 不自动落盘,崩在 Persist/Prepare 前磁盘链头仍是旧版本(恢复后读旧值)。 |
MaxMemoryVersions 配 1 行不行? |
不行——构造强制 Math.Max(2, value)。N=1 时 Write 覆盖前无可保留的旧版本镜像,Abort 只能冷-热回退到加载版本(无法热-热回退到上一 Write)。N=2 是 Abort 零 IO 的几何底线。 |
跨重启改 PayloadSize 安全吗? |
安全——历史版本按盘上真实 PayloadLength 从自持池租借只读缓冲,不补零不截断完整交付;本次运行的新版本按新 PayloadSize 几何落盘。版本链混尺寸由每条 record 头部自述几何支撑(扫描/回收按各自 PayloadLength+PaddingLength 跳进)。 |
Prepare 后 Abort 之间还能 Read 到新值吗? |
能——Prepare 已追加新版本到磁盘链头并 flush,但 LastCommittedSeq 未推进;内存镜像也是新值。Abort 后才回退(内存 + 磁盘链头 + 版本号一并回退到 _prepareSnapshotAddress)。 |
Disabled 模式恢复慢吗? |
慢——扫盘 O(链长/盘),最大扫 64MB。GB/TB 级单值状态强烈建议开 Managed(O(1) 读水位)。VersionedMetadata 单值场景一般几 KB 到 MB,扫盘可接受;3a 托管场景按主流结构大小判断。 |
变长档下 AsSpan() 返回的 span 长度? |
当前内容长度(加载版本按盘上真实 PayloadLength / 热区按最近 Write 长度)。不是 PayloadSize,也不是 MaxPayloadSize——按当前实际镜像切片。CurrentPayloadLength 是探测入口。 |