Table of Contents

Mirror 使用指南——字节镜像 / 版本链 / 整体 vs 分页

给谁看:需要整体状态基线外存的组件开发者(镜像备份 / SortedIndex 桥接主存储 / 跨重启恢复)。 回答什么:WholeMirror vs PagedMirror 怎么选 / 三拍会话 / 版本链 / 2PC Abort 语义 / 恢复。 本篇只讲机制——帧字节布局见源码注释(Structures/Mirror/Contracts/MirrorFrame.cs)。 定位:Structures/ 6 子族之一,搭配件;与 Snapshot 同族异坐标系(版本号截断 vs 字节截断)。


0. 一句话总纲

Mirror = 字节镜像 + 版本链 + 2PC 六件套(Abort = 丢弃未确认会话)。 WholeMirror 整体流式(三拍会话:BeginSession → AppendChunk × N → EndSession,不知尺寸、零缓冲、CRC64 边写边累积);PagedMirror 逐页增量(WritePage 一页一帧,可乱序,CRC32C);EndSession flush 才落盘;版本链 N=2 轮替(当前 + 上一 checkpoint,更老即收)。

最小可用代码(WholeMirror,单 checkpoint + 2PC 提交)

using var mirror = new WholeMirror(fs, new WholeMirrorSettings { MetaPolicyKind = MetaPolicyKind.Managed });
mirror.Initialize();                                              // 后台恢复
mirror.WaitForReady();                                            // 等就绪

var head = mirror.BeginSession();                                 // 帧三拍①:圈头 + 写瘦头
mirror.AppendChunk(stateBytes);                                   // 帧三拍②:追加 payload
mirror.EndSession();                                              // 帧三拍③:CRC 收官 + flush
mirror.Prepare(seq: 1);                                           // 2PC①:数据 fsync + meta 落 LastPreparedSeq
mirror.ConfirmCommitted(seq: 1);                                  // 2PC②:CAS 推进 + 链头推进 + N=2 回收

int n = mirror.ReadChunk(mirror.HighestVersionAddress, 0, dst);   // 读已提交链头

最小模型四步即覆盖 Mirror 全部核心机制(帧三拍 + flush + 2PC + 链头推进 + N=2 回收);恢复时构造 + Initialize + WaitForReady 三步自动重建链头。


1. 定位

  • 角色:整体状态基线外存。与 Ring/Log 主结构搭配——主结构是数据真相源,Mirror 是基线镜像(恢复时按基线截断 + 增量重放)。
  • 形态差异
    • WholeMirror:整体单链 checkpoint。每 checkpoint 一个完整新版本,整帧流式写入(不知总长也能写),适合"整体快照外存 / SortedIndex 桥接主存储"。
    • PagedMirror:per-page 多链。每 checkpoint 内每页一个帧(按 PageId 寻址独立链),可乱序写入,适合"按页增量镜像"。
  • 与 Snapshot 同族异坐标系
    • Mirror 按版本号截断——保留 N=2 个 checkpoint 版本,更老物理回收;恢复按版本号识别"最后一批未裁决 record"。
    • Snapshot 按字节截断——纯流式形态,无版本概念(详见 snapshot.md)。
  • 桥接器 WholeMirrorPersistence(位于 Runtime/DataMirror/):ITransferPersistence 的内置桥接器,自持一个 WholeMirror(三段式相位 ↔ 三拍会话;Confirm 挂 WriteFooter,Abort 挂尾截断)。:索引族(HashIndex/SortedIndex)现已改用自建主存储(PersistenceKind.Builtin),镜像通道注入面已退役——见 index.md §6;此桥接器仍保留给其他需"借镜像存储能力"的组件(按 Runtime/COORDINATION.md §6 铁律 8 桥接模式)。

1.1 选型速查

维度 WholeMirror PagedMirror
链拓扑 全局单链(MirrorChainKind.Single per-page 多链(MirrorChainKind.PerKey,按 PageId
写门面 三拍会话:BeginSession → AppendChunk × N → EndSession(流式,不知尺寸) 逐页 WritePage(每页一帧,可乱序)
CRC 算法 CRC64(FLAG_CRC64,magic WMHD/WMFT) CRC32C(FLAG_CRC32C,magic PMVH/PMFT)
恢复快路径 尾锚直达(Locate(尾 magic, Last) + PreviousVersion 回跳 N=2) 全走链按 PageId 重建(每页独立链头)
适用形态 整体快照外存 / 桥接 SortedIndex 主存储 按页增量镜像(页粒度更新)
N=2 保留窗口 全局第二新地址 所有页 keepAddr 最小值(误删某页在用版本由全局水位兜底)
flush 时机 EndSession 内部 flush 逐页不 flush,Prepare 统一 flush
写并发模型 单线程会话(多生产者经队列汇聚) 单线程会话,多链在会话内可乱序

选型口诀:整体镜像(一整段状态序列化)→ WholeMirror;按页镜像(页粒度独立更新,部分页不变即跳过)→ PagedMirror。两者均与 SortedIndex 同协议(2PC 六件套 + meta 三模式),可在不同 checkpoint 域并存。


2. Settings 全参数

MirrorSettings.cs + Settings.cs 基类。两子类配置项差异:PagedMirrorSettingsLogPageSizeBitsWholeMirrorSettings 无专属字段。

参数 类型 WholeMirror 默认 PagedMirror 默认 说明
MainEngine StorageEngineOptions 单段稀疏(enableSegmentation=false, preallocateFile=false, segmentGrowthLimit=64MB 同左 Mirror 版本链是单段追加流——分段默认关,稀疏按需增长
Name string "tc.mirror" "tc.mirror" 引擎名(便捷构造默认值)
PreallocateFile bool false false 创建时不预分配文件
DeleteOnClose bool false false 跨重启组合必须 false(否则恢复时真相源被清)
MetaPolicyKind MetaPolicyKind Disabled Disabled meta 持久化策略;Managed 起独立 meta 引擎,Transport 嵌入主流,Disabled 不持久化水位
MetaOpaqueBytes int 0 0 外部 opaque 容量;启动后不可改
LogPageSizeBits int 22(PageSize=4MB) 仅 PagedMirror;PageSize = 1 << LogPageSizeBits,与源结构页对齐

N=2 轮替是固定策略(保留当前 + 上一 checkpoint),不设配置项。段增长上限 DefaultSegmentGrowthLimit = 64MB 是 const 常量。

便捷构造(推荐起点):

var wholeSettings   = new WholeMirrorSettings();                                   // 默认单段稀疏
var pagedSettings   = new PagedMirrorSettings { LogPageSizeBits = 12 };            // PageSize=4096(测试轻量)

完整构造(自定义段几何 / hints / 清理策略):

var settings = new WholeMirrorSettings(
    new StorageEngineOptions("kv.mirror", 64L << 20, enableSegmentation: false)
        .WithDeleteOnClose(false))
{
    MetaPolicyKind   = MetaPolicyKind.Managed,
    MetaOpaqueBytes  = 256,
};

3. 构造与生命周期

Mirror 子类无 Create 工厂(区别于 RingOfLong.Create 那种封闭类型一步就绪)——直接 new + Initialize + WaitForReady 三步。Initialize 启动后台恢复(非阻塞);WaitForReady 等恢复完成。Dispose 幂等。

using var mirror = new WholeMirror(fs, settings);
mirror.Initialize();          // 启动后台恢复(OnInitializeBegin:主引擎 + Managed meta 引擎并行启动)
mirror.WaitForReady();        // 等恢复核心完成(meta Load → 三级回退 → 悬干裁决 → 链头重建)
// 此处 IsReady=true,可读写

桥接形态WholeMirrorPersistence):构造零 IO,首次 TryOpenWrite/TryOpenRead 触发 Initialize + WaitForReady——checkpoint / 恢复路径天然是等待点。

关键状态查询

属性 类型 语义
IsReady bool 恢复完成可服务
CurrentVersion long 已提交 checkpoint 版本号(单调;空卷=0)
HighestVersionAddress LogicalAddress 已提交链头地址
HasCommittedVersion bool 是否已有已提交版本——Empty 是合法地址(首 record 在地址 0),不能用地址值判有无
LowestVersionAddress LogicalAddress 回收边界(最老保留版本地址;Empty=未回收过)

跨实例恢复示例——同一卷上第二实例构造期尾锚扫盘恢复链头:

// 第一实例:写两个 checkpoint 后 Dispose(卷上保留数据)
var vol = new TestVolume();
var settings1 = new WholeMirrorSettings(
    new StorageEngineOptions("test.0", 1L << 24, enableSegmentation: false).WithDeleteOnClose(false));
LogicalAddress v2Addr;
using (var mirror1 = new WholeMirror(vol.Fs, settings1))
{
    mirror1.Initialize();
    mirror1.WaitForReady();
    WriteCheckpoint(mirror1, 4096, 0x11);
    mirror1.Prepare(1);
    mirror1.ConfirmCommitted(1);
    WriteCheckpoint(mirror1, 4096, 0x22);
    mirror1.Prepare(2);
    mirror1.ConfirmCommitted(2);
    v2Addr = mirror1.HighestVersionAddress;
}

// 第二实例(同卷):尾锚扫盘恢复链头
var settings2 = new WholeMirrorSettings(
    new StorageEngineOptions("test.0", 1L << 24, enableSegmentation: false).WithDeleteOnClose(true));
using var mirror2 = new WholeMirror(vol.Fs, settings2);
mirror2.Initialize();              // 后台启动恢复
mirror2.WaitForReady();            // 等恢复完成

// 恢复后 mirror2.CurrentVersion == 2;HighestVersionAddress == v2Addr(尾锚直达最新数据帧)
vol.Dispose();

关键点:跨重启组合必须 DeleteOnClose=false(否则恢复时真相源被清)。Initialize 非阻塞——后台启动恢复,WaitForReady 才是等待点。桥接形态(WholeMirrorPersistence)将 Initialize + WaitForReady 延迟到首次 TryOpenWrite/TryOpenRead,因为 checkpoint / 恢复路径天然是等待点。

3.1 生命周期钩子链(继承 LifecycleBase<MirrorRecoveryHints>

阶段 钩子 MirrorBase 实现
Initialize 第一阶段【前】 OnInitializeBegin _engine.Initialize() + Managed meta 引擎并行启动
恢复算法工厂 CreateRecovery 返回 new DefaultMirrorRecovery(this)
依赖 join WaitForDependenciesAsync 主引擎 + meta 引擎双 await(全异步轨,零同步阻塞)
恢复核心 OnRecoveryCoreAsync meta Load → 三级回退 → 悬干裁决 → 链头重建
Dispose DisposeOverride / DisposeOverrideAsync 释放帧 IO 缓冲 + 基类资源(幂等)

生命周期详细机制(CAS 闸门 / 状态机 / 进度上报 / MarkReady)见 src/TC.Tier.Core/docs/lifecycle.md,本篇只讲 Mirror 侧职责。

3.2 桥接形态(WholeMirrorPersistence)

ITransferPersistence 的内置桥接器(3a 托管模式——构造零 IO,首次使用启动恢复)。:索引族已退役此桥接(改用自建主存储 PersistenceKind.Builtin,见 index.md §6);此桥接器仍保留给其他需"借镜像存储能力"的组件消费。

阶段 WholeMirrorPersistence 行为
构造 new WholeMirror(fileSystem, settings, ...) 自持;零 IO
TryOpenWrite/TryOpenRead 首次 EnsureStarted() 触发 mirror.Initialize() + WaitForReady()(惰性启动)
写会话 三段式相位 ↔ WholeMirror 三拍会话(BeginSession 一族——尺寸后知)
Confirm WriteFooter(checkpoint 单人写完即提交)
Abort 挂未写尾 Dispose(镜像会话尾截断回退,悬干弃置)
读侧 Verify = 账面机制——内容有效性由消费方格式裁决
传输上限 会话打开声明 maxTransferBytes(默认 128K),单次超限直接抛
并发 单写者(并发双开 → TryOpenWrite false,非抛)
Dispose 转发 _mirror.Dispose()(幂等)

详见 src/TC.Tier.Runtime/DataMirror/WholeMirrorPersistence.cs


4. API 详表

Mirror 子族 API 按形态分三组:WholeMirror 三拍会话 / PagedMirror 逐页写读 / 共有 2PC 六件套。所有 API 同步执行(引擎 IO 同步原生);异步轨 PrepareAsync / AbortAsync 实质等价同步(引擎 flush/截断原生仅同步)。

4.1 WholeMirror——三拍会话 + 读门面

API 签名 行为
BeginSession LogicalAddress BeginSession() 分配会话版本号 + 帧三拍之一(圈头区 + 写瘦头——不预留、不知尺寸)。返回帧头地址(调用方持有,用于后续读 / 定位)
AppendChunk void AppendChunk(ReadOnlySpan<byte> chunk) 帧三拍之二:顺序追加 payload chunk(圈地 + 复写 + CRC64 边写边累积——零缓冲不驻内存)。单线程契约:逐段追加
EndSession LogicalAddress EndSession() 帧三拍之三:CRC 收官落尾 + _engine.Flush()。返回帧尾末地址。帧保持 pending 直到 ConfirmCommitted / Abort
ReadChunk int ReadChunk(LogicalAddress frameHead, long offsetInPayload, Span<byte> dst) 按 payload 偏移读切片(拷贝交付)。返回实际读取字节数
GetFrameInfo MirrorFrameInfo? GetFrameInfo(LogicalAddress frameHead) 帧几何账目(payload 长 = 尾位 − 头 − 头结构——零长度字段,长度是推导的事实)
GetPayloadLength long GetPayloadLength(LogicalAddress frameHead) 帧 payload 长度(推导)。无有效帧尾返回 0
Verify bool Verify(LogicalAddress frameHead) 流式分段重读重算 CRC(头 + 体 + 尾前缀 对 尾 Crc 字段——大帧不驻内存)

三拍契约BeginSessionAppendChunk × NEndSession 必须同一线程顺序执行;EndSession 之后才能开下一个会话(否则抛"上一帧未收官")。EndSession 只 flush 数据,不推进 LastCommittedSeq——2PC 提交点走 ConfirmCommitted

三拍会话完整示例——大镜像零缓冲流式写:

using var mirror = new WholeMirror(fs, new WholeMirrorSettings());
mirror.Initialize();
mirror.WaitForReady();

// 任意总长——边写边知,EndSession 时才完成 CRC 收官
const long TotalSize = 10000;
var data = new byte[TotalSize];
Array.Fill(data, 0xAB);

var frameHead = mirror.BeginSession();                    // 帧三拍之一:圈头 + 写瘦头
mirror.AppendChunk(data.AsSpan(0, 6000));                 // 帧三拍之二:追加任意段
mirror.AppendChunk(data.AsSpan(6000));                    // 继续追加(无需告知总长)
var frameEnd = mirror.EndSession();                       // 帧三拍之三:CRC 落尾 + flush

// 几何推导(零长度字段)+ CRC 流式验证
mirror.GetPayloadLength(frameHead).Should().Be(TotalSize, "像长 = 尾位 − 头 − 头结构(推导)");
mirror.Verify(frameHead).Should().BeTrue("写后 CRC64 应一致");

// 按 payload 偏移读切片(拷贝交付,跨 await 也安全)
var dst = new byte[TotalSize];
int n = mirror.ReadChunk(frameHead, 0, dst);
// n == TotalSize;dst 与 data 逐字节相等

说明

  • BeginSession 不知镜像总尺寸即可圈头——CRC64 边写边累积,EndSession 收官落尾时才需要全帧一致;大镜像(GB 级)零缓冲不驻内存。
  • EndSession 后帧仍 pending(未裁决);走 2PC 时由 ConfirmCommitted / Abort 决定最终可见性。不走 2PC 的单 checkpoint 用法,EndSession 后即可读(committed 链头仍按 HighestVersionAddress 取)。
  • 读侧 ReadChunk 拷贝交付——跨 await / 跨线程消费合法;零拷贝形态请用 Ring 的 GetValueSpan(同栈内合法,详见 ring.md)。

4.2 PagedMirror——逐页写读

API 签名 行为
WritePage LogicalAddress WritePage(long page, long startPage, ReadOnlySpan<byte> pageBytes, long logicalAddress = 0) 写一页(追加该页版本链新帧)。首个写入自动开启 checkpoint 会话。pageBytes > PageSize 截断;< PageSizeFLAG_LAST_PARTIAL。返回帧头地址
ReadPage (int bytesRead, bool isValid) ReadPage(long page, long startPage, Span<byte> dest) 读该页 committed 链头:帧几何推导 payload 长 → 读 payload → 全帧 CRC32C 验证。无该页 / 空链 / 帧损毁返回 (0, false)
PageSize int PageSize(只读) 1 << LogPageSizeBits

多链语义page 是 per-page 链键(独立链头 + 第二新)。startPage 是会话起始页(语义锚点,不参与寻址)。可乱序:会话内 WritePage(2, ...)WritePage(0, ...)WritePage(1, ...) 合法,多链互不影响。会话内每页首写记录 previous = Invalid(链尾哨兵——Empty 是合法 seg0@0,不能当哨兵)。

逐页写读示例——乱序写 + 多链原子提交:

using var mirror = new PagedMirror(fs, new PagedMirrorSettings { LogPageSizeBits = 12 });
mirror.Initialize();
mirror.WaitForReady();

// 乱序写三页——多链互不影响;首写自动开启 checkpoint 会话
byte[] fills = [0x00, 0x11, 0x22];
mirror.WritePage(2, 0, MakePage(4096, fills[2]), logicalAddress: 200);
mirror.WritePage(0, 0, MakePage(4096, fills[0]), logicalAddress: 0);
mirror.WritePage(1, 0, MakePage(4096, fills[1]), logicalAddress: 100);
mirror.Prepare(seq: 1);                  // 多链整体 Prepare(数据 flush + meta 落盘)
mirror.ConfirmCommitted(seq: 1);          // 多链整体 Confirm(逐页链头推进)

// 每页读自己链头——乱序写不影响读
for (byte p = 0; p < 3; p++)
{
    var dst = new byte[4096];
    var (n, valid) = mirror.ReadPage(p, 0, dst);
    // n == 4096;valid == true;dst[0] == fills[p]
}

末页短 payloadpageBytes.Length < PageSize 时 codec 自动置 FLAG_LAST_PARTIALReadPage 返回实际字节数(不补齐),调用方按 (bytesRead, isValid) 处理。

4.3 共有——水位 / 版本号 / 2PC 六件套

API 签名 行为
Prepare void Prepare(long seq) 2PC Prepare:_engine.Flush() + WriteMeta()(记录 LastPreparedSeq + 悬干水位)。数据 fsync 先于 meta fsync——断电时 meta 绝不标记 data 未落盘的 commit 点
PrepareAsync ValueTask PrepareAsync(long seq, CancellationToken ct) Prepare 异步轨(实质同步完成)
ConfirmCommitted void ConfirmCommitted(long seq) CAS 推进 LastCommittedSeq + 推进链头(子类 OnConfirmSession)+ N=2 立即头截断回收最老 + 刷新 meta + 触发回调。seq ≤ current 时 no-op
Abort void Abort(long seq) 尾截断回退悬干新 checkpoint:物理丢弃 [_committedChainEnd, AllocatedTail) + 回退会话链头状态 + meta。seq ≤ LastAbortedSeq 时幂等 no-op
AbortAsync ValueTask AbortAsync(long seq, CancellationToken ct) Abort 异步轨
OnCommitted void OnCommitted(long seq, Action callback) 注册提交回调(已提交到更高 seq 时立即同步触发;锁外触发避免死锁)
ReclaimOldVersions void ReclaimOldVersions() N=2 头截断显式入口(ConfirmCommitted 自动调;业务也可显式调)。保留窗口由子类 ComputeRetainFloor 决定
((ITransactionParticipant)mirror).LastCommittedSeq long 已提交 seq(Volatile.Read)
((ITransactionParticipant)mirror).LastPreparedSeq long 已 Prepare seq(Volatile.Read)

Abort 语义 = 丢弃未确认会话:会话 active 才回退;committed 链头未被会话触碰(只清 pending,不碰 committed);尾截断物理丢弃悬干(引擎退化 AllocatedTail);LastPreparedSeq 回退到 LastCommittedSeq。


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

契约 / 类型 用途 注入通道
IMirrorCodec 帧格式 codec(Header/Footer 读写 + magic/CRC 模式校验 + 链拓扑声明) 构造期由子类自带(WholeMirror.Codec / PagedMirror.Codec),不暴露注入——格式固定
MirrorMetaHeader meta 水位 Header(12B 纯规范:magic "MMHD" + version + flags + payload 长度段) MetaPolicyFactory<MirrorMetaHeader, MirrorMetaPayload> 注入策略
MirrorMetaPayload meta 水位 Payload(48B:版本链两端 + LastCommittedSeq + LastPreparedSeq) 同上
MirrorFrameHeader 帧头(32B:magic + version + flags + PageId + LogicalAddress + MirrorVersion) 不注入——盘上格式
MirrorFrameFooter 帧尾(40B:magic + version + flags + PreviousVersion + MirrorVersion + Crc) 不注入——盘上格式
MirrorFrameInfo 帧几何账目值类型(head→footer 对 + 头尾字段快照) 读 API 返回
MirrorRecoveryHints 恢复提示(HighestVersionAddress? + LastCommittedSeq? Initialize(hints) 注入;优先级最高(高于 meta、扫盘)
MetaPolicyFactory<MirrorMetaHeader, MirrorMetaPayload> meta 策略工厂委托 构造参数 metaPolicyFactory——默认按 MetaPolicyKind 内联三模式(Managed/Transport/Disabled)
IMetaTransport Transport 模式的 meta 传输 构造参数 metaTransport——未注入回落到内部 MetaHost(嵌入主流的 IS_META 帧)
IRecovery<MirrorRecoveryHints> 恢复算法实例 构造参数 recovery——默认 DefaultMirrorRecovery(基类内置)
ITransactionParticipant 2PC 六件套(Prepare/ConfirmCommitted/OnCommitted/Abort/LastCommittedSeq/LastPreparedSeq MirrorBase 实现此接口,经 TransactionLog 协调器编排

6. 持久化机制

6.1 版本链追加

每次 checkpoint 写一个完整新版本追加到流尾,不重写旧帧。链经 PreviousVersion(帧尾字段)回跳:

  • WholeMirror:全局单链,PreviousVersion = 上一 checkpoint 链头(首版本 = Invalid——Empty 是合法 seg0@0,不能当哨兵)。
  • PagedMirror:per-page 多链,PreviousVersion = 该页上一帧头(页首版本 = Invalid)。

帧长任意字节边界——v2 流式帧教义:格式零长度字段,帧长 = 尾位 − 头(推导的事实)。下一帧起点 = 本帧尾末(AllocatedTail 恒贴已写尾,帧间零间隙)。

6.2 EndSession flush

WholeMirror.EndSession 内部调 _engine.Flush()——数据落盘到设备(fsync)。非 2PC 用法在此即持久(单 checkpoint 自包含);2PC 用法下 Prepare 再 flush 是幂等加固。

PagedMirror 逐页不 flush(性能优先)——WritePageEndFrame,落盘由 Prepare 统一 flush。

为何 Prepare 是 2PC 持久化点:数据 fsync 先于 meta fsync——断电时 meta 绝不标记 data 未落盘的 commit 点(设计决策:data fsync → meta fsync 严格偏序,崩溃一致性核心保证)。崩溃在 Prepare 后 Commit 前:seq 未推进 → 恢复按 meta 裁决尾截断悬干(一致)。

6.3 N=2 轮替回收

ConfirmCommitted 自动调 ReclaimOldVersions:保留窗口 = 当前 + 上一 checkpoint,更老物理回收(_engine.ReclaimHead 头截断——punch hole 清零旧帧 magic,链头重建后失效帧不可再定位)。

  • WholeMirror:ComputeRetainFloor 返回全局第二新地址(无第二新回退链头自身 = 不回收)。
  • PagedMirror:返回所有页 keepAddr 的最小值(引擎 MinAddress 是全局水位,只能推进到所有页都同意回收的地址——否则误删某页在用版本)。
  • keepAddr == Empty 时基类守卫保守跳过本轮(Empty 是合法地址,下轮补收——正确性优先)。

6.4 Transport 嵌入 meta 帧

MetaPolicyKind.Transport 模式下,meta block 作为 IS_META 帧追加进镜像流(统一帧机制——meta 帧是宿主流的一等公民):

  • 写:基类 WriteEmbeddedMetaBlockCore 走帧三拍 + FLAG_ENTRY_IS_META(不参与数据版本链——PreviousVersion=Invalid、版本号用当前值不推进、EndFrame 对 IS_META 跳过 OnRecordAppended),只追加在流尾供找回。
  • 读:ScanLastEmbeddedMetaBlockCore 前向帧走链单步原语扫描收集 IS_META 帧(结构完好即收——payload 交 meta policy 自校验)。
  • 默认回落:未注入 IMetaTransport 时基类自装配 MetaHost 嵌套类(实现 IMetaTransport)——无需独立 meta 引擎 / 独立文件。

6.5 2PC 协议

Prepare_engine.Flush() + WriteMeta()(LastPreparedSeq + 悬干水位落 meta)。 ConfirmCommitted:CAS 推进 LastCommittedSeq + 子类 OnConfirmSession(链头推进)+ N=2 头截断 + 刷新 meta + 触发回调。 OnCommitted:注册链式回调(已提交到更高 seq 时立即同步触发)。 Abort:见下。

6.6 Abort 语义 = 丢弃未确认会话

if (seq <= LastAbortedSeq) return;           // 幂等
if (_sessionActive) {
    OnAbortSession();                         // 子类清 pending(WholeMirror=ClearPendingFrame;PagedMirror=_sessionWrites.Clear())
    if (_lastRecordEnd > _committedChainEnd) {
        _engine.ReclaimTail(_committedChainEnd);  // 物理丢弃悬干(引擎退化 AllocatedTail)
        _lastRecordEnd = _committedChainEnd;
    }
    _sessionActive = false;
}
LastPreparedSeq = LastCommittedSeq;          // 回退 Prepare 点
LastAbortedSeq = seq;
WriteMeta();

关键不变量:committed 链头未被会话触碰(Abort 不动 committed);尾截断只丢悬干;LastPreparedSeq 回退到 LastCommittedSeq。

2PC Abort 演示——v1 提交后 v2 写完 + Prepare → Abort:

using var mirror = new WholeMirror(fs, new WholeMirrorSettings());
mirror.Initialize();
mirror.WaitForReady();

// v1:写完 → Prepare → Confirm(落定提交)
WriteCheckpoint(mirror, 8192, 0x11);          // BeginSession + AppendChunk + EndSession
mirror.Prepare(seq: 1);
mirror.ConfirmCommitted(seq: 1);
var v1Addr = mirror.HighestVersionAddress;    // 已提交链头

// v2:写完 → Prepare → Abort(悬干整体回退)
WriteCheckpoint(mirror, 8192, 0x22);
mirror.Prepare(seq: 2);
mirror.Abort(seq: 2);                          // 尾截断物理丢弃 [committedChainEnd, AllocatedTail)

// 不变量 1:链头回退到 v1
mirror.HighestVersionAddress.Should().Be(v1Addr, "Abort 应回退链头到上一已提交 checkpoint");
// 不变量 2:版本号不推进
mirror.CurrentVersion.Should().Be(1, "Abort 不推进 checkpoint 版本号");
// 不变量 3:LastPreparedSeq 回退到 LastCommittedSeq
((ITransactionParticipant)mirror).LastPreparedSeq.Should().Be(1);
// 不变量 4:已提交 checkpoint 不受 Abort 影响——v1 CRC 完整可读
var dst = new byte[8192];
mirror.ReadChunk(v1Addr, 0, dst);
dst[0].Should().Be(0x11, "已提交 checkpoint 不受 Abort 影响");
mirror.Verify(v1Addr).Should().BeTrue("Abort 尾截断后 v1 CRC 完整");

WholeMirror vs PagedMirror Abort 差异:机制在基类统一执行;子类 OnAbortSession 只清各自的 pending 状态:

  • WholeMirror:ClearPendingFrame()(清 _hasPendingFrame / _pendingFrameHead / _pendingFrameFooter)。
  • PagedMirror:_sessionWrites.Clear()(清会话内逐页写入记录——committed 各页链头未被会话触碰)。

PagedMirror Abort 的多链原子性:会话 2 写 page 0 + page 1 两页后 Abort,两页的 v2 写入一起回退,page 0/1 各自链头仍指向 v1——单链故障不会污染多链整体


7. 恢复协议

7.1 三级回退(DefaultMirrorRecovery)

Initialize → 后台恢复核心 OnRecoveryCoreAsync(hints, ct)

  1. hints 优先MirrorRecoveryHints.HighestVersionAddress / LastCommittedSeq——上层已知水位直接注入。
  2. meta Load(O(1) 水位)MetaPolicyKind != Disabledawait MetaPolicy.LoadAsync → 读 MirrorMetaPayload(版本链两端 + LastCommittedSeq + LastPreparedSeq)。
  3. 扫盘兜底ScanAndRebuild 按版本号定位链头——Single 走尾锚快速路径(Locate(尾 magic, Last) 直达最新数据帧 + PreviousVersion 回跳 N=2 第二新),PerKey 全走链按 PageId 重建各链头。

7.2 悬干裁决

meta.LastPreparedSeq > LastCommittedSeq → 最后一批帧(按最高会话版本号识别)视为悬干,_engine.ReclaimTail 物理丢弃。无 meta(Disabled / 损坏)= 无裁决依据,扫到状态视为已提交。

7.3 按版本号截断(与 Snapshot 恢复差异)

维度 Mirror Snapshot
截断坐标系 版本号(会话版本号识别"最后一批未裁决 record") 字节(按 CommittedTail 截断)
保留版本数 N=2(当前 + 上一 checkpoint) 1(最新基线)
悬干回退 meta prepared > committed 时尾截断物理丢弃 无版本概念——纯流式
多链 PagedMirror per-page 独立链头(按 PageId 重建) 不适用

7.4 帧判定链零长度依赖

恢复扫盘的"什么是有效帧"裁决链——格式本身不含长度字段,靠三件套判定:

判据 假命中的兜底
候选提名 双 magic 值匹配(头 magic + 尾 magic) 假 magic 必经 CRC 裁决淘汰
结构校验 Version == CurrentVersion + flags 的 CRC 算法位(CRC64/CRC32C) 版本/flags 不符直接否
头尾版本一致 footer.MirrorVersion == header.MirrorVersion 假尾缩窗继续倒扫
权威帧 最新帧(WholeMirror 尾锚路径)走 CRC 全验 旧代只验结构不读体——旧代损坏不遮新代

关键性质:帧长任意字节边界(v2 alignment=1);payload 埋诱饵(payload 内塞双 magic)不影响本帧 CRC(覆盖域原样入算);旧代损坏不遮新代(尾锚只看最新方向 + PreviousVersion 链对旧代只验结构不读体)。

7.5 恢复 hints 注入

var hints = new MirrorRecoveryHints
{
    HighestVersionAddress = knownHeadAddr,    // 已知链头(上层注入,优先于扫盘)
    LastCommittedSeq      = 100,             // 已知提交点
};
mirror.Initialize(hints);                    // 注入恢复提示
mirror.WaitForReady();

7.6 整体流程一览

  ┌─────────────────────────────────────────────────────────────┐
  │ Initialize() ──> OnInitializeBegin (主引擎 + meta 引擎并行启动) │
  ├─────────────────────────────────────────────────────────────┤
  │          WaitForDependenciesAsync (双引擎 join)              │
  ├─────────────────────────────────────────────────────────────┤
  │           OnRecoveryCoreAsync(hints, ct) 恢复核心           │
  │  1. MetaPolicy.LoadAsync → 读 MirrorMetaPayload (O(1) 水位) │
  │  2. 三级回退:hints → meta → 扫盘定位链头                    │
  │  3. 悬干裁决:meta.LastPreparedSeq > LastCommittedSeq →     │
  │     尾截断物理丢弃(按最高会话版本号识别)                    │
  │  4. ScanAndRebuild:Single 走尾锚快速路径;PerKey 全走链    │
  │     按 PageId 重建各链头                                     │
  ├─────────────────────────────────────────────────────────────┤
  │             MarkReady → IsReady=true 可读写                 │
  └─────────────────────────────────────────────────────────────┘

性能特性:Managed meta 模式下恢复 O(1)(读水位块即得链头);Disabled / meta 损坏走扫盘——WholeMirror 尾锚路径直达最新帧(一次 Locate(尾 magic, Last) + 倒扫本帧头 + PreviousVersion 回跳 N=2 第二新),PagedMirror 全走链按 PageId 重建(每页独立链头,扫盘代价与页数线性)。


8. 反模式

# 反模式 后果 正确
1 会话未 EndSession 跨 await 帧三拍单线程契约破坏;下个 BeginSession 抛"上一帧未收官" 三拍同线程顺序完成;跨 await 前 EndSession
2 跨实例组合 DeleteOnClose=true 跨重启恢复时引擎被清,真相源丢失 跨重启组合 DeleteOnClose=false;测试一次性卷才用 true
3 绕过 2PC 直接靠 EndSession 当提交 LastCommittedSeq 不推进,恢复时该 checkpoint 视为悬干被截断 提交点走 Prepare + ConfirmCommitted
4 Abort 后读会话内地址 悬干已物理丢弃,帧头 magic 被 punch hole 清零 Abort 后只读 HighestVersionAddress(已提交链头)
5 HighestVersionAddress == Empty 判"无版本" 首 record 就在 Empty(合法地址空间起点),误判为无版本 HasCommittedVersion 标志判存在性
6 WholeMirror 多线程并发 AppendChunk 帧三拍单线程契约破坏,CRC 累积错乱 单线程顺序追加;多生产者经队列汇聚到单写者
7 PagedMirror 跨会话不 Confirm 就写下页 上次会话 pending 帧未裁决,链头推进错乱 每 checkpoint 会话写完走 Prepare + ConfirmCommitted/Abort
8 给 Disabled meta 调 SetOpaqueMeta InvalidOperationException(禁用即报错) 配置 MetaPolicyKind=Managed/Transport 再写 opaque
9 跨 checkpoint 会话复用同 seq Prepare/Confirm CAS 推进错乱(seq ≤ current no-op) 每 checkpoint 单调推进 seq;悬挂用独立 seq
10 未调 ReclaimOldVersions 期望自动回收 旧 checkpoint 累积,版本链变长(ConfirmCommitted 已自动调,但显式调合法) ConfirmCommitted 内置自动回收;如有自定义触发点,调 ReclaimOldVersions 即可

9. 想深入?指路

想懂什么 去哪
源码 src/TC.Tier.Runtime/Structures/Mirror/MirrorBase.* partials + WholeMirror* / PagedMirror* + Contracts/
帧字节布局 / Codec 内部 Structures/Mirror/Contracts/MirrorFrame.cs + IMirrorCodec.cs 源码注释
横切:meta 协议(opaque 搭车 / Managed/Transport/Disabled 三模式) meta.md
横切:Session 协调协议(写 / 检查点 / 悬挂裁决) session.md
横切:StorageEngine 使用(读写 / 水位 / Compact) storage-engine.md
横切:段表机制 segment-table.md
横切:lease 协议 lease-protocol.md
同族:Ring 主结构 ring.md
同族:Log 主结构 log.md
同族:Index 索引族 index.md
同族:VersionedMetadata versioned-metadata.md
同族:Snapshot(字节截断形态) snapshot.md
WholeMirrorPersistence 桥接器源码(索引族已退役,仍保留给其他借镜像能力的组件) src/TC.Tier.Runtime/DataMirror/WholeMirrorPersistence.cs 源码注释
总览(6 子族速查 + KV 组合) structures.md

机制级细节(8 水位全表、帧字节布局、CRC 增量算法、2PC 内部状态机、扫盘重同步策略)按需从源码与设计稿查阅——使用面不需要这些细节;能力全集以类型 XML 注释为准。

9.1 配套阅读顺序建议

顺序 文档 目的
1 structures.md 6 子族全貌 + 选型坐标
2 本篇(mirror.md Mirror 子族机制 + 两种形态
3 storage-engine.md 引擎读写 / 水位 / Compact 基础
4 meta.md opaque 搭车 / 三模式
5 session.md 跨结构 2PC 协调协议
6 snapshot.md 同族异坐标系对比

核心要点回顾:Mirror 是字节镜像 + 版本链 + 2PC 六件套;选型看颗粒度(整体流式 → WholeMirror,按页增量 → PagedMirror);提交点必走 Prepare + ConfirmCommitted(EndSession 只 flush 数据不推进 seq);Abort = 丢弃未确认会话(committed 链头不被触碰);恢复按版本号截断(与 Snapshot 字节截断异坐标系)。

9.2 FAQ

  • Q:为什么没有 Create 工厂? A:Mirror 子类构造 + Initialize + WaitForReady 三步即就绪(区别于 Ring 封闭类型的 Create 一步就绪)。桥接形态 WholeMirrorPersistence 将此三步推迟到首次使用。
  • Q:非 2PC 用法(单 checkpoint)如何提交? A:EndSession 已 flush;不走 Prepare/Confirm 时数据已持久,但 LastCommittedSeq 不推进——恢复时该 checkpoint 视为悬干可能被截断。生产用法必走 2PC
  • Q:WholeMirror 与 Snapshot 怎么选? A:需要 N=2 版本链 / 多链 / 2PC → Mirror;纯流式字节截断 / 无版本概念 → Snapshot(同族异坐标系,详见 snapshot.md)。
  • Q:跨实例恢复需要配 meta 吗? A:不需要。MetaPolicyKind=Disabled 时走扫盘兜底(WholeMirror 尾锚 O(最新帧),PagedMirror 全走链按 PageId 重建);Managed 模式下 meta O(1) 加速。