段表使用指南(SegmentTable)
给谁看:IO 引擎层(
Storage/)开发者、lease 消费侧、恢复/Checkpoint 接入者。 回答什么:段表持有什么、我能碰什么、事件怎么接、回调怎么发、什么绝对不要做。
0. 一句话总纲
段表 = 地址空间的唯一真相之源:它持有三支柱(水位线 / 段 / 段区间),只发事件、只收回调, 不等待、不做 IO、不关心物理文件。物理段是 IO 层对段表统一事件的实现。
统一事件(ISegmentHandler,段表 → IO 层)
段表(真相) ────────────────────────────────────────────→ IO 层(物理资产)
三支柱 ←──────────────────────────────────────────── worker 建段 / 预备池 /
建段回调(CreateSegmentCallback,IO 层 → 段表) meta / 删文件
1. 三支柱(调用者必须建立的心智模型)
| 支柱 | 内容 | 关键规则 |
|---|---|---|
| 水位线 | MinAddress(头)/ CommittedTail(终态前缀)/ AllocatedTail(分配) |
架在区间表上的粗游标;外部只读;不变量链 MinAddress ≤ CommittedTail ≤ AllocatedTail |
| 段 | 每段 MinOffset/MaxOffset 两条段内水位 + StableState + GrowthLimit |
RealSize = MaxOffset − MinOffset 是数据大小,GrowthLimit 是容量——两者不是一回事;IsFull ⟺ MaxOffset ≥ GrowthLimit(派生属性) |
| 段区间 | 每段一份区间表(ExtentStateCode:在途带 Src → 终态) |
lease 协议的核心状态机;可占性 IsOccupiable = Committed || Wasted |
读门:区间的可读性看区间表(IsRangeFullyReadable),不是看水位游标——游标是粗裁剪,
区间表才是逐字节真相(游标 vs 区间双轨,勿混用)。
2. 段稳态(StableState)——准入语义
| 稳态 | 含义 | 进入 | 离开 |
|---|---|---|---|
Empty |
逻辑已注册、物理未建——物理门关(不接受 chunk IO) | 注册即 Empty(AppendSegmentRaw) |
唯一出口:建段回调 |
Ready |
物理就绪,chunk IO 合法 | 回调成功 TryMarkReady |
到顶自动→Full;→Compacting;→Invalid |
Full |
MaxOffset ≥ GrowthLimit:不可扩容、可覆写 |
AdvanceOffset 到顶(仅 Ready→Full) |
→Compacting;→Invalid |
Compacting |
整理排他 overlay | Compact 进入 | Compact 完成 |
Broken |
建段失败终态——物理门永久关,不重试;不可分配:分配区间不得含其任何字节,尾在 Broken 段内/请求跨段时烧洞跳过 | 回调失败 TryMarkBroken |
终态(地址空间墓碑) |
Invalid |
已删除:准入吊销、文件不存在 | ReclaimHead/Compact/启动修正 | 终态(幂等) |
三条设计决策(勿当缺陷):
- 运行期 Full 只经 Ready→Full。Empty 期被 Allocate 占位推满的段不转 Full 态——满语义由派生
IsFull承担;Empty→Full 直达只存在于启动恢复装配。 - 回调双分支幂等:成功/失败均 CAS,非 Empty 一律 no-op——重复/迟到回调不打断已迁移段。
- Compact 后的段槽复用是设计路径:整理压缩空洞 → 中间段 Invalid 出服务 → 退尾再分配时重注册 同 segId——这是地址空间回收复用机制。
3. 关键 API(调用者视角)
| 类别 | 成员 | 说明 |
|---|---|---|
| 只读查询 | AllocatedTail / CommittedTail / MinAddress / SegCount / MaxSegId / GetSegment(segId) / TryGetSegment / IsRangeFullyReadable / GetExtentRanges |
全部无锁安全;GetSegment 返回只读 SegmentView(不存在返回 Hollow) |
| 地址算术 | AdvanceAddress / RetreatAddress / GetDistance(+ SegmentGrowthLimit) |
纯计算,不改状态——语义见 §3.1 |
| lease 入口 | AppendLease / AllocateLease / WriteLease / ReclaimLease / ReclaimHeadLease / ReclaimTailLease / CompactLease |
外部改状态的唯一合法通道,见 lease-protocol.md |
| 协调(IO 层专用) | WaitSegmentReady / CreateSegmentCallback / PulseAllSegmentsReady |
建段协调;WaitSegmentReady 的唯一调用方是 lease 协议(物理门) |
| 持久化 | SaveAddressTable / LoadAddressTable(一对)+ SetStartupTails(启动水位) |
恢复/落盘专用,见 §3.2 |
| 锁开放面 | ExecuteUnderLock / TryGetLock |
三级锁中必须开放的两级,见 §4 |
3.1 地址算术(三个接口,纯计算无状态)
| 接口 | 语义 | 边界规则 |
|---|---|---|
AdvanceAddress(start, length) |
前进 length 字节,跨段进位 | 只前进(回退调 Retreat);返回的 Extension 恒为 0——调用方须显式保留 start.Extension;恰好填满一段时停驻段末边界 (segId, GrowthLimit) |
RetreatAddress(start, length) |
回退 length 字节,跨段借位 | 只回退;回退到 MinAddress 之前返回 LogicalAddress.Invalid;落点恒为真实字节位置 |
GetDistance(from, to) |
from→to 字节距离,跨段累加 | from > to 返回负值;不做 AllocatedTail 上界校验(调用方保证合法) |
SegmentGrowthLimit(segId) |
取段生长上限 | 段存在用段的;Hollow 段用生命周期上限——地址空间连续,被回收段在逻辑地址上仍占位 |
三个算术共享同一条不变量:跨段进位/借位按每段各自的 GrowthLimit 计算(Compact 后段大小可能 不同,不用全局值)。
区间表示规范(半开 [start, end)——统一规范):
- 一切区间都是半开:覆盖
start ≤ b < end;end是"已占用字节之后第一个位置"。 - 边界规范形:恰好填满一段时,边界停驻段末
(seg, GrowthLimit)——旧哨兵形态(seg+1, 0)(把段末写成"下一段头")已废除(存量盘兼容接受,新算术永不产出);(N, 0)保留唯一身份 = 段首字节 / 地址空间原点。 - 半开的理由(定稿):空区间天然是
start == end(零特判);Append 写起点就是 end 本身(热路径 零+1);全库水位比较按半开格建设。
3.2 持久化(一对读写 + 一个水位修正)
| 接口 | 语义 | 时机 |
|---|---|---|
SaveAddressTable(IAddressTableWriter) |
落盘段表:逐段 SegmentSpec + footer(双尾水位是权威) |
Checkpoint / 关闭 |
LoadAddressTable(IAddressTableReader) |
装配:三段式(头部 → 段载荷循环 → 尾部直读) | 恢复期,一次性 |
SetStartupTails(StartupParameters) |
启动期双尾水位设定(可大可小:小=截断回收;大=覆盖旧数据推水位)。单值=双尾同址;双值=扫盘恢复形态 | 仅启动阶段(首次 Allocate 之前,之后调用直接抛) |
恢复顺序硬性要求:LoadAddressTable → SetStartupTails(若扫盘结果与持久化水位不一致)→ 首次
Allocate 锁定运行期。无持久化启动:构造 → SetStartupTails 定双尾 → 直接 Allocate——LoadAddressTable
全程可选。(IO 引擎侧的 EngineRecoveryHints 由引擎恢复流程翻译为 SetStartupTails;两层类型不共用。)
4. 锁模型(三级——开放两级,区间锁不开放)
| 级 | 锁 | 保护对象 | 开放面 | 谁用 |
|---|---|---|---|---|
| 表级 | _mutationLock(Monitor) |
段数组/索引结构变更 | ✅ ExecuteUnderLock(Action) |
外部批量自洽结构操作 |
| 段级 | SegmentLock(SpinRWLock 写偏向) |
段级读写互斥 / Compact/截断排他 | ✅ TryGetLock(segId, out SpinRWLock?)(裸锁) |
读者与写者互斥、Compact 与一切互斥 |
| 段内 | 区间锁(struct SpinLock,微秒级临界区) | 段内区间表 | ❌ 不开放——lease 协议封装 | 只有 lease 协议 |
为什么区间锁不开放:区间表的每次变更都是 lease 三阶段协议的一部分(占住→锁外 IO→提交/回滚)—— 开放区间锁 = 外部绕过协议直改区间表 = 状态机与事件契约脱钩。需要区间排他?拿一个 lease,协议替你 持锁。
锁序(必须遵守):_mutationLock > 段 SpinRWLock > 区间 SpinLock——只能从上往下拿,不可逆序
(逆序 = 死锁)。SpinRWLock 排他临界区内绝对禁止 await(线程关联原语,跨 await 释放即泄漏;
共享锁可跨 await 长持,读计划锁即此用法)。
与物理门的分工:锁管互斥(谁在改),单向状态闩(§2 Empty→Ready/Broken/Invalid 的
_physicalReady)管协调(等谁就绪)——纯状态协调不用锁。
5. 事件契约(段表 → IO 层,ISegmentHandler)
| 事件 | 时机 | IO 层职责 |
|---|---|---|
OnSegmentCreate(segId, growthLimit, isHighPriority) |
注册时 / 段满预建下一段 | 正式建段或池预建(build-gate single-flight:同一 segId 恰好一次物理构建) |
OnSegmentFull(segId, finalSize, growthLimit) |
段满(含占位推满——派生 IsFull 判定) |
写 Full-meta + 补池 |
OnSegmentDelete / OnSegmentReplace / OnSegmentReclaim |
删段/Compact 替换/回收通知 | 物理联动(引擎子系统自管) |
SubmitBackgroundWork(work) |
段表自洽低频任务 | worker 顺序执行 |
回调契约(IO 层 → 段表):物理构建完成后调 CreateSegmentCallback(segId, success)。建段任务唯一
职责 = 为「已注册且 Empty」的段完成物理构建并回调;非 Empty / 已摘索引 → 作废绝不重建。
6. 生命周期与阶段门禁
- 构造(
SegmentTableSettings:GrowthLimit/MinSegId/IndexCapacity/SpinMilliseconds/WarnEvery/EnableSingleSegment)→ 启动阶段(LoadAddressTable(可选)+SetStartupTails,仅 Allocate 之前 可调)→ 运行期(首次 Allocate 成功即 CAS 锁定,不可逆)。 - handler 传入与否决定出生态:带 handler 出生 Empty(等物理回调);
null(纯内存/测试)出生 Ready。
7. 硬性要求(违反 = 数据丢失 / 死锁 / 复活已删段)
- 外部永远拿
SegmentView,不碰Segment引用;改状态唯一入口 = lease 协议。 - 段表不等待、不做 IO。任何"等建段完成再…"的诉求都是 lease 协议的事(物理门),绝不往段表/ worker 里加等待。
- 建段 single-flight:同一 segId 的物理构建恰好一次(取用、守卫、声明三者原子同临界区)。
- 索引/数组扩容 build-then-publish(先填 -1 后
Volatile.Write单点发布),禁用Array.Resize—— 其零填充窗口会被无锁读者读到幽灵索引。 - 发布-读取 acquire/release 全对称(ARM 弱序合规):写侧全
Volatile.Write发布;读侧对索引 字段与槽位均Volatile.Read(任一环 plain load 在 ARM 上可见中间态;x64 TSO 恰安全但不得依赖)。 WaitSegmentReady唯一调用方 = lease 协议;有界放弃(预算/告警走 Settings)。
8. 反模式(禁止重蹈)
- ❌ Invalid 段重建:持过期复查的任务去 claim 已回收段 = 复活已删文件 + 容量重计。守卫与声明 同临界区,四态(Consumed/Claimed/InFlight/Abandoned)一锤定音。
- ❌ 锁内的"正确"证明不了无锁读者的中间态:共享数组变更先想读者看到什么。
- ❌ 把
StableState.Full当满判据:满 = 派生IsFull;StableState是准入/物理语义,不是容量语义。 - ❌ 运行期调
SetStartupTails/ 在Allocate后裸写水位——直接抛。 - ❌ 给段表加物理概念(文件路径、句柄、池深度……)——物理是 IO 层的事。
9. 最小正确范式(IO 引擎接线)
// ① 引擎构造段表:带 handler(出生 Empty),等待参数走 Settings
_table = new SegmentTable(
new SegmentTableSettings(growthLimit: SegmentGrowthLimit, SpinMilliseconds: 30_000),
SegmentHandler, // ISegmentHandler 实现(事件 → worker/池)
Logger);
// ② handler 收到注册事件 → 入队正式建段(或池命中同步转正)
public void OnSegmentCreate(int segId, long growthLimit, bool isHighPriority)
{
if (TryConsumePooledSegment(segId)) // 池命中:物理现成
_table.CreateSegmentCallback(segId, success: true); // 同步转正(幂等)
else
EnqueueCreateTask(segId, growthLimit, isHighPriority); // worker 正式建
}
// ③ worker 建段任务体:只对「已注册且 Empty」构建,成败都回调,绝不等待/重试
private async ValueTask EnsureSegmentPhysicalAsync(int segId, long growthLimit, CancellationToken ct)
{
switch (TryConsumeOrClaimPhysicalBuild(segId, out var gate)) // 取用/守卫/声明同临界区
{
case PhysicalBuildClaim.Consumed:
_table.CreateSegmentCallback(segId, success: true); return;
case PhysicalBuildClaim.Claimed:
try { await CreateSegmentCoreAsync(segId, growthLimit, ct);
_table.CreateSegmentCallback(segId, success: true); }
catch (Exception ex) { Logger.LogError(ex, "建段失败 {SegId}", segId);
_table.CreateSegmentCallback(segId, success: false); } // 幂等,重复不抛
finally { CompletePhysicalBuild(segId, gate, pooled: false); }
return;
case PhysicalBuildClaim.InFlight: // 池在途——其完成者代执行同一回调
case PhysicalBuildClaim.Abandoned: // 非 Empty/已摘索引——绝不重建
default: return;
}
}
纯内存/测试:handler: null → 段出生即 Ready,无物理协调(SegmentTableLeaseProtocolTests 全套示例)。
10. 决策速查
| 我要… | 用 |
|---|---|
| 查某段是否存在/多大 | GetSegment/TryGetSegment → SegmentView(只读) |
| 判断区间可读 | IsRangeFullyReadable(读门 = 区间表,不是游标) |
| 地址前进/回退/距离 | AdvanceAddress / RetreatAddress / GetDistance(§3.1——注意返回 Extension 恒 0) |
| 跨段区间切分 | GetExtentRanges |
| 改任何状态(写/收/删) | lease 入口 → lease-protocol.md |
| 段表落盘/装配/启动水位 | SaveAddressTable / LoadAddressTable / SetStartupTails(§3.2,仅启动阶段) |
| 批量结构变更对外不可见 | ExecuteUnderLock(表级,§4) |
| 段级读写/Compact 互斥 | TryGetLock(段级裸锁,§4) |
| 区间排他 | 不直接拿锁——走 lease 协议(§4 锁模型) |
| 诊断 | GetActiveLeases/SnapshotSegmentExtents(只读;ForceRelease 仅受控回滚) |