Lease 协议使用指南(六类型 · 三阶段 · 三态迭代)
给谁看:IO 引擎层(
Storage/IO)开发者、所有调SegmentTable.{X}Lease(...)的人。 回答什么:我要做追加/覆写/回收/截断/整理——用哪个 lease、怎么遍历 chunk、什么时候 Commit、 失败怎么办。
0. 一句话总纲与三条硬性要求
lease = IO 层与段表之间的唯一交互协议。每个操作类型是一个独立的类型化 lease, 对段表 / 物理段 / 稳态的要求由类型自身表达。
- 等待只有一个合法宿主:lease 协议。物理门(Empty→Ready)的等待点 = chunk 流水线第一拍 + 类型声明的提交扫尾。段表不等待、worker 不等待、池不等待。
- 类型即协议。禁止 kind 字节路由、禁止合并协议、禁止把某类型的要求放进共享路径。
- 段表是核心,物理段 = IO 层对段表统一事件的实现(见 segment-table.md)。
1. 选型表(需求 → lease 类型)
| 我要… | 用 | 关键语义 |
|---|---|---|
| 追加写新数据(地址由段表分配) | AppendLease(length) |
推双尾;物理门有 |
| 只分配地址不写(预留空间) | AllocateLease(length) |
无 lease 对象,返回 (Start, End);占位 Committed+sparse |
| 覆写已提交区间(地址已知) | WriteLease(start, length) |
不动水位;物理门有(快路径) |
| 回收中间区间(打洞归零抹数据) | ReclaimLease(from, to) |
[from,to) 可跨段——按段切 chunk 逐块 PunchHole;水位不动;无门 |
| 回收头部(删整段) | ReclaimHeadLease(to) |
跨段删除 + 尾段打洞;MinAddress 推进;无门 |
| 截断尾部(退水位) | ReclaimTailLease(newTail) |
仅退双尾(无打洞);独占双尾(不并发);无门 |
| 整理空洞(搬移压缩) | CompactLease(from, to) |
overlay 原子替换段(一把锁内 invalidate+replace) |
主推模型(性能最优,实测快 ~23%):一次 AllocateLease 大空间定地址 + 批量 WriteLease 写
(Write 不推 CommittedTail)。
2. 通用协议结构(全部类型共享,LeaseBase)
三阶段生命周期:
① 占住(构造即完成) ② 锁外 IO(你写数据的地方) ③ 终态(Commit/Rollback/Dispose)
AcquireExtentsForLease 遍历 chunk → 每块 IO → 整体 Commit() / Rollback()
逐 chunk 占区间(段表切分) chunk.Commit()/Rollback() 最后增量者触发 FinalizeTerminalCore
- chunk 终态仲裁 doneMask 位掩码 exactly-once:部分/整体 × 提交/回滚四路径共用单一仲裁—— 每个 chunk 恰好一次终态,重复 Commit/Rollback 是 no-op,不会重复扣账。
区间终态的分界线 = 物理真相(不是"成功/失败"——这是最容易记错的一点):
| 终态 | 适用场景 | 读门 | 为什么 |
|---|---|---|---|
Committed+sparse |
打洞成功 / Allocate 占位 | ✅ 可读(读零) | 物理上确实全是零 |
Committed |
Append/Write chunk 提交成功 | ✅ 可读(真实数据) | 内容为真 |
Wasted |
Append/Write chunk 回滚 | ❌ 阻断(可被 Write 覆写) | 地址已消费,但内容物理未知——宣称读零 = 读出垃圾 |
Aborted |
Reclaim 打洞失败 | ❌(只 Compact 修) | 永久洞 |
判定口诀:物理上真是零 → Committed+sparse;内容未知 → Wasted。"失败 = 提交 0"只在物理上真是零 的场景成立;回滚覆写旧数据的场景不成立。
- Dispose 语义:未终态 chunk 的回滚扫尾 + 终态收敛——
using忘了显式 Commit 不会泄漏区间。 - 物理门(
EnterChunkPhysicalGate):基类无门;Append/Write override 等 Empty→Ready;Reclaim 系无门。门在迭代器/索引器里自动过,调用者无需手动调。
3. chunk 遍历(API:ChunkScope——三态模式)
每个 lease 的区间被段表切成 ≥1 个 chunk(跨段必多块)。ChunkScope 是只读几何 + 分段终态:
Commit()(本块提交)/ Rollback()(本块回滚)。
using var lease = table.AppendLease(length);
// foreach——完整迭代器(推荐默认;自动过物理门)
foreach (var chunk in lease)
{
await WriteAsync(chunk.SegId, chunk.SegOff, data); // 锁外 IO(你的部分)
chunk.Commit(); // 本块终态
}
lease.Commit(); // 整体:推 CommittedTail
- 流水线规则:chunk 逐块提交可以乱序(doneMask 仲裁),但整体
lease.Commit()前所有块必须已 终态——Append 的扫尾会对未提交块补物理门再收尾。整块失败 →lease.Rollback()(或直接 Dispose)。 - 需要随机访问/重试单块时用下标
lease[i](索引器含物理门);需要操作枚举器本身时用显式迭代器 (MoveNext已过物理门,iter.CommitCurrent())。
4. 各类型速查卡(权威表压缩版)
AppendLease(追加——最频繁)
| 维度 | 语义 | ||
|---|---|---|---|
| 范围 | 从 AllocatedTail 分配 [start,end);Empty 段合法(注册即 Empty,不阻塞分配) |
||
| 物理门 | 有——每个 chunk IO 前等 Empty→Ready | ||
| chunk 提交 | Leased→Committed,AdvanceOffset;到顶自动 Ready→Full + OnSegmentFull + 预建下一段 |
||
| chunk 回滚 | →Wasted(可覆写空洞),水位照推(地址已消费不可逆) | ||
| 整体收敛 | AppendFinalize(End):推 CommittedTail(precise-prefix,max-CAS 幂等) |
WriteLease(覆写)
| 维度 | 语义 | ||
|---|---|---|---|
| 范围 | 覆写 [start,start+length);目标区间必须 ≤ CommittedTail 且可占(Committed/Wasted) | ||
| 物理门 | 有(显式声明;常态走快路径零开销) | ||
| chunk 提交 | Leased→Committed(去 sparse,落真实覆写) | ||
| chunk 回滚 | →Wasted | ||
| 整体收敛 | 无(目标区间本就 ≤ CommittedTail,推尾即错误前进) |
ReclaimLease(中间回收 = 段上打洞)
| 维度 | 语义 | ||
|---|---|---|---|
| 范围 | [from,to) 打洞(可跨段——逐块 PunchHole 物理归零抹除数据);区间保持 Committed+sparse | ||
| 物理门 | 无(作用于已提交区间,物理已存在) | ||
| chunk 提交 | →Committed+sparse(打洞成功终态记录) | ||
| chunk 回滚 | →Aborted(永久洞,只 Compact 修) | ||
| 三支柱 | 只动段区间;段/水位不变 |
ReclaimHeadLease(头部回收 = 打洞 + 跨段删除 + 推头)
| 维度 | 语义 | ||
|---|---|---|---|
| 范围 | [MinAddress, to):to 前整段删除 + to 段 [0,to.Offset) 打洞 | ||
| 物理门 | 无(删除不依赖物理就绪) | ||
| Commit/Rollback | 都推 MinAddress(物理删段不可逆) |
||
| 物理联动 | 删段文件由引擎在 lease 外执行(先 meta Remove+Flush 再删);lease 不做 IO |
ReclaimTailLease(尾截断 = 仅退双尾)
| 维度 | 语义 | ||
|---|---|---|---|
| 范围 | 截断 [newTail, AllocatedTail)——无跨段打洞,段区间状态机不参与(游标即裁决) | ||
| 前置 | MinAddress ≤ newTail < AllocatedTail;独占双尾(与其它 ReclaimTail 不并发;Append 退避不阻塞) |
||
| Commit/Rollback | 都退水位(物理截断不可逆) | ||
| 物理联动 | IO 层跟随 SetLength;段表侧到此为止 |
CompactLease(整理 = overlay 拷贝 + 段表原子替换)
使用契约:创建(独占占住全部目标区间)→ 完成全部物理 IO/搬移 → 最后一次性 Commit()。
区间码 CompactLeased 是独占模式——占住持续整个整理期间,不支持 chunk 级提交/释放(部分释放
会破坏"一把锁内全部段原子替换"的不变量);物理 IO 未全部完成不得提交(完整性绊线拦截 Pending chunk)。
代价模型:目标区间在整理全程对写者关闭(引擎侧 Compacting 排他)——大范围整理用 RangeCompact 分片。
独立协议:sealed class CompactLease : IDisposable——不继承 LeaseBase(无 chunk 流水线/
doneMask/物理门,模型是"占住 → 搬移 → 整体替换"两阶段)。调用方是引擎 Compact 子系统(compactor),
业务侧入口是引擎的 StartCompact() / StartRangeCompact()。
| 维度 | 语义 | ||
|---|---|---|---|
| 范围 | [from,to)——全量 Compact = [MinAddress, CommittedTail)(未写满的尾段不含未提交区);可跨段 | ||
| 区间码 | CompactLeased(Src=Compact,排他占住) |
||
| 物理门 | 无——overlay 拷贝的是已提交数据(物理已存在) | ||
| 阶段①(搬移) | 对每个 chunk 恰好调一次 SetReplacement(newMin, newMax, newGrowthLimit, state)(压缩后重建)或 MarkInvalid()(全部搬空删除)——互斥、一次性流转,未填 = Pending |
||
| 阶段②(提交) | Commit():先过完整性绊线(任何 chunk 仍 Pending → 拒绝提交抛异常)→ CAS Active→Committed → AtomicCompactReplace(一把 _mutationLock 内:invalidate 段 + 替换段槽位 Volatile.Write 发布)——中间状态不可见 |
||
| 整体终态 | Rollback()/Dispose():全部 chunk 释放占住 + CompactRollback;Dispose = 未终态自动回滚 |
5. Reclaim 家族辨析(三兄弟只差三件事——"跨段"不是差异点,三者区间都可跨段)
| 段上打洞 | 整段删除 | 水位线 | |
|---|---|---|---|
| ReclaimLease(中间) | ✓ 逐块打洞归零 | ✗(段都保留) | 不动 |
| ReclaimHeadLease(头) | ✓ 尾段部分区间 | ✓ to 之前的整段 | MinAddress 推进 |
| ReclaimTailLease(尾) | ✗ | 尾后段随尾消失 | 仅退双尾(回退≠打洞) |
6. 硬性要求与反模式
- 物理门不手动调——它在迭代器/索引器里。绕过迭代器直摸
ExtentsInternal= 绕过门 + 绕过仲裁。 - 禁止合并协议 / kind 路由——任一类型的要求泄漏成其它类型的隐藏前提。
- 跨段 lease 逐块处理,不要假设单 chunk(
growthLimit边界必然切块)。 - ❌ 对 Reclaim 打洞失败置之不理——Aborted 永久洞只 Compact 能修,洞多了读门会拦。
- ❌ ReclaimTail 当打洞用——运行期退尾不删区间记录;真要删数据走 Reclaim(中间)/启动修正。
- ❌ 把 lease 对象跨线程共享——lease 非线程安全;并发写各拿各的 lease(地址唯一性由 CAS 保证)。
- Compact 整体提交:
CompactLease没有 chunk 级部分提交语义。
7. 最小正确范式(含失败路径)
// 追加写(生产范式:try/using 覆盖失败路径)
try
{
using var lease = _table.AppendLease(data.Length, ct);
var off = 0L; // lease 内数据游标(chunk.SegOff 是段内相对量,跨段会归零——勿用它算 lease 内偏移)
foreach (var chunk in lease)
{
await WriteChunkAsync(chunk.SegId, chunk.SegOff, data.Slice((int)off, (int)chunk.Length), ct);
chunk.Commit(); // 成功一块提一块(乱序合法)
off += chunk.Length;
}
lease.Commit(); // 整体:推 CommittedTail(内部最后增量者收敛)
}
catch (OperationCanceledException) { throw; }
catch (Exception)
{
// 未 Commit 的 lease 经 Dispose 自动回滚扫尾(Wasted 空洞,地址不回收)——无需手动 Rollback
throw;
}
// 主推批量模型:Allocate 定地址 + Write 批量写(快 ~23%)
var (start, end) = _table.AllocateLease(totalLength, ct); // 纯地址(Committed+sparse 占位)
for (var off = 0L; off < totalLength; off += chunkSize)
{
using var w = _table.WriteLease(start + off, chunkSize, ct); // 覆写占位区间
foreach (var chunk in w) { await WriteChunkAsync(chunk.SegId, chunk.SegOff, ...); chunk.Commit(); }
w.Commit(); // Write 整体 Commit 无段表副作用(不推尾)——终态收敛在 lease 内部完成
}
// Compact(compactor 视角——业务侧经引擎 StartCompact()/StartRangeCompact(),不直接拿 lease)
using var lease = _table.CompactLease(from, to);
foreach (var chunk in lease.Chunks)
{
var packed = PlanPacking(chunk); // 搬移规划:数据前移压缩
if (packed.NewMaxOffset == 0)
chunk.MarkInvalid(); // 该段全部搬空 → 整段删除
else
chunk.SetReplacement(packed.NewMin, packed.NewMax, // 压缩后重建(新容量/水位)
packed.NewGrowthLimit, StableState.Ready);
CopyData(chunk, packed); // 物理搬移(overlay,锁外)
}
lease.Commit(); // 完整性绊线:有 chunk 漏填 SetReplacement/MarkInvalid → 此处抛,lease 仍 Active 可回滚
// 失败路径:异常未提交 → Dispose 自动回滚(全部占住释放,段表零改动)
8. 性能画像(速览)
单次 lease 全协议 ~1.5µs / 107-112B(纯地址 438ns;批量 WriteLease 最优);10 万次 Gen0=1-2、
区间表保持 1 条、p50 无退化;2 线程近线性零 Monitor 争用;IO 占比 Buffered ~7%、WriteThrough ~0.4%
——lease 不是写路径瓶颈,优化预算在 IO 引擎层。池化:LeaseFactory.Default = 每次 new(默认不池化
——对象小,池化成本更高);Pooled 仅诊断/对比用。
9. 决策速查
| 场景 | 做法 |
|---|---|
| 高频小写 | AppendLease 逐条(便捷)或 Allocate+Write 批量(最快) |
| 大块顺序写 | Allocate 大空间 + WriteLease 批量 |
| 覆写已知地址 | WriteLease(先确认区间 ≤ CommittedTail) |
| 释放空间不缩边界 | ReclaimLease 打洞 |
| 删最老数据 | ReclaimHeadLease |
| truncate 语义 | ReclaimTailLease |
| 空洞压缩 | CompactLease(整体提交) |
| 写失败 | 块级 chunk.Rollback() 或整体 Dispose(自动扫尾) |