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 基类。两子类配置项差异:PagedMirrorSettings 增 LogPageSizeBits,WholeMirrorSettings 无专属字段。
| 参数 | 类型 | 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 字段——大帧不驻内存) |
三拍契约:BeginSession → AppendChunk × N → EndSession 必须同一线程顺序执行;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 截断;< PageSize 记 FLAG_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]
}
末页短 payload:pageBytes.Length < PageSize 时 codec 自动置 FLAG_LAST_PARTIAL,ReadPage 返回实际字节数(不补齐),调用方按 (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(性能优先)——WritePage 只 EndFrame,落盘由 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):
- hints 优先:
MirrorRecoveryHints.HighestVersionAddress/LastCommittedSeq——上层已知水位直接注入。 - meta Load(O(1) 水位):
MetaPolicyKind != Disabled时await MetaPolicy.LoadAsync→ 读MirrorMetaPayload(版本链两端 + LastCommittedSeq + LastPreparedSeq)。 - 扫盘兜底:
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) 加速。