产品一致性架构(TierKv / TierTimeSeries / TierBlob)
本文回答:三个产品怎么保证一致性、不丢不重、原子性与崩溃恢复——数据真源与派生结构的 分层、分配-持久分离、2PC 提交点语义、恢复对账,以及每产品的一致性语义边界与崩溃窗口。 它是 raft-consistency.md(产品 × raft) 的本地产品面(无 raft 时的一致性),也是 persistence-architecture.md(跨层 持久化)的产品消费面。
与使用指南的关系:tierkv.md / tierseries.md / tierblob.md 是怎么用;本文是为什么一致。
代码引用为仓库相对路径(Kv/… = src/KernLab.Tier.Products/Kv/…)。
0. 三产品共同骨架
三个产品建立在同一条底层教义上:数据结构是"数据真源 + 可重建派生结构"的两层模型,
原子性由 2PC(ITransactionParticipant)在提交点收口,恢复靠"持久化真源 + 重放对账"。
| 产品 | 数据真源(持久化权威) | 派生/查询结构 | 水位/元数据真源 | 原子性载体 |
|---|---|---|---|---|
| TierKv | Ring(append-only 记录流) | Hash/BTree/SkipList 主索引 + 可选范围索引(可重建) | Ring opaque(版本高水位 + 过期扫描水位) | Ring 2PC Prepare/Confirm + 索引 CAS 换绑可见性 |
| TierTimeSeries | Ring(样本 record 流) | BTree 时间索引(键 (ts,addr)) | VersionedMetadata(TrimmedUntil/计数) | 水位 VersionedMetadata 作 2PC 参与者 |
| TierBlob | 数据 = StreamSnapshot;对象表 = 表引擎版本链记录帧流 | 无索引(地址直达) | 对象表(meta O(1) 水位)+ 数据引擎三水位 | 表参与者 Prepare/Confirm/Abort + undo 链 |
1. TierKv
1.1 写路径原子性:append-only 换绑(Kv/TierKv.cs:14-22)
- Ring 是数据唯一真源,主索引是可重建派生结构("派生重放教义"——任何索引切换/重建都不丢数据)。
- 写 = 「Ring 追加新记录 → 索引 CAS 换绑地址」——不覆盖、版本历史天然保留;Delete = 墓碑
写入 + 索引清槽(恢复重放跳过已删 key)。索引换绑唯一写入口经版本保护区包络(
TierKv.cs:362-387)。
1.2 会话读一致性三档(Kv/KvSessionContracts.cs:6-25)
None:读恒走索引看最新。ReadMyWrites(缺省):写立即应用 + 本地写集,读优先命中写集 (本会话最后一次写胜出他人并发覆盖)。Serializable:同步写临界区在版本保护下执行。- 会话创建即取单调版本号(会话单线程契约,内部零锁)。
1.3 多 key 原子批(全或无,TierKv.cs:472-536)
- 会话侧
BeginAtomicBatch→ 批内写走暂存(本会话读可见、他人与索引不可见)→CommitBatchAsync/AbortBatch。 - 引擎侧编排:Ring 追加全部暂存 → 2PC Prepare(整体落盘悬空)→ ConfirmCommitted(提交点)→
索引换绑(可见性)。崩溃任一窗口全或无:Prepare 前/悬空期崩溃 = 恢复丢弃(索引零污染);
Confirm 后崩溃 = 记录已提交、索引由派生重放自建(
TierKv.cs:464-468)。
1.4 完成语义三档与组提交(Kv/TierKvContracts.cs:12-24)
FireAndForget(内存档,持久化由 checkpoint 承担)/Committed(写级不丢显式旋钮)/WaitForPending(会话 pending 收口)。- 组提交摊薄 fsync(
FlushGroupedAsync,TierKv.cs:756-808——floor 只推实刷值,防持久化契约破坏)。
1.5 Functions RMW(读-改-写,TierKv.cs:580-633)
三流统一「读折叠 → Format → 追加 → 索引 CAS 换绑」;miss→Initial、hit→InPlace 先行(false 回落 Copy)、折叠 false→Error 零写入;旧记录恒保留(版本历史是多出能力)。
1.6 TTL(惰性读删 + 时钟防线,TierKv.cs:93-115,1752-1767)
- 值帧统一
[tag 0xC8][8B 过期 ticks][payload]("全量封装不变式")。 - 惰性读删:读路径判定过期即不存在。
- 时钟倒流防线(公开面易忽略的关键):
NowUtcTicks经单调水位钳制——墙钟后跳/倒流时已判 过期项不复活(NTP 校时跳变下的 TTL 语义防线)。
1.7 CAS(地址即版本 / fencing token,TierKv.cs:1816-1881)
- 地址版/值版;
_casGate串行 compare→append→换绑窗口。 - 关键语义边界:并发 CAS 恰一成功;但 CAS×普通 Put 仍是 last-writer-wins(CAS 不是全局 写锁——防丢失保证仅对其他 CAS 调用方成立)。过期绑定视为不存在。TTL+CAS = 「比较 + 写入 + 挂过期」单一原子单元,获取即租约(fencing token = NewAddress)。
1.8 版本号单调性(Kv/KvVersionAllocator.cs)
- opaque 24B 布局 =
HighWater(8B) + Magic "TKV1"(4B) + SweepSegId + SweepOffset——魔数代际守卫, 演进换值;NextVersion()全局单调(Interlocked);Restore只升不降守卫。 - 持久化 = 版本高水位搭 Ring opaque 随 2PC Prepare 同块原子落盘(
TierKv.cs:502-508); 恢复续接先于 Ready(分配版本恒大于历史)。
1.9 恢复 / 检查点(帧物化 + 增量重放,TierKv.cs:243-296,833-870)
- 检查点三拍:① Ring 全量落盘 → ② 主索引帧落盘 → ③ 版本图入 opaque 随 2PC Prepare 原子落盘。
- 崩溃后重开 = 索引帧物化(O(索引))+ 增量重放
(W, Tail](非全量);帧缺失/损坏 fail-safe 回退全量重放(数据等价)。恢复正确性不依赖索引(派生物可重建)。 - PITR 回滚到指定历史版本排期 W6.1+。
1.10 回收与过期强删(TierKv.cs:1488-1610)
ReclaimAsync:低频全表扫判定存活 → 存活最小地址为安全下界 → TruncatePrefix 逻辑回收; 未落盘尾不回收(clamp 至 FlushedUntil)。- 过期强删:水位推进式(不做逐条墓碑化)——增量扫描找最老未过期绑定 → 截断前缀 → 扫描 水位随 2PC Prepare 原子落盘;"先截断后持久化"——崩溃窗口内水位落后于截断点 = 下轮重扫 (幂等安全)。过期与回收强删均不发事件。
1.11 Watch(无丢无重续传,TierKv.cs:1378-1429 + Kv/KvWatchHub.cs)
历史补扫 + 实时直通,地址开区间续传;锁内发布水位去重 + 有界通道慢订阅者断连(写路径零反压); 游标越回收线抛(etcd compacted 同义);恢复期重置发布水位。
2. TierTimeSeries
2.1 组合配方(TimeSeries/TierTimeSeries.cs:18-33)
Ring(数据真源,append-only)× BTree 时间索引(键 (ts,addr),乱序吸收/范围序/最新点查)× VersionedMetadata 水位(retention 边界原子持久)。单序列与 dense 多序列共用同一流程面。
2.2 写入 / 乱序吸收 / 守卫(TierTimeSeries.cs:137-162,211-220)
- Append:Ring 写 record + 写后即知插索引(内存可见;落盘由显式 Flush/trim 前置 flush 承担)。
- 乱序天然吸收(BTree 有序插入,无后台排序工序)。
- fail-fast 两守卫:
ts < TrimmedUntil(已回收区间,数据必丢)+ 超出MaxOutOfOrderPast(防老样本钉住索引)。
2.3 查询一致性(TierTimeSeries.cs:311-320)
- 索引在 = 索引序交付(正确时间序);
Indexed=false降档 = 地址序交付(显式降档契约—— 乱序写入下 Ring 地址序 ≠ 时间序,直接 Scan 会交付乱序流)。索引是正确性必需而非优化。
2.4 retention / 治理(截断先行、水位随后,TierTimeSeries.cs:480-535,671-708)
TruncateAsync:前置 flush → 索引前缀截断 → Ring TruncatePrefix → 水位持久(原子提交, 单调不回退)。- 崩溃窗口语义:截断先行、水位随后——窗口 = 已截断但水位未推进 → 重启对账以数据事实 为准(TrimmedUntil = min(水位, 首样本 ts))。后台 retention 循环(TTL/MaxBytes 取更晚锚); dense 逐序列推进。
2.5 dense 多序列(逐序列一致性边界,SeriesRegistry.cs:57-117)
每序列 O(1) 侧账(~64B),惰性注册 + 容量护栏;MinPinnedAddress(慢序列钉住——快序列回收
不越雷池);逐序列 trim → Ring 截断下限 = 全体序列钉住地址 min;恢复按 envelope SeriesId 路由。
2.6 恢复对账三步(spec §7,TierTimeSeries.cs:819-1018)
① 水位载入 → ② 索引启动(Ring Ready 后)→ ③ 对账:4a 悬空索引清理(地址 < Begin 或 > Tail)→ 4b 缺条重建(Ring 扫 [Begin, Tail) 不在索引的重插)→ 4c 水位 min 校正。崩溃窗口:record 已落、 索引未持久 → 恢复对账重插(at-least-durable)。
2.7 水位 2PC 参与者(TierTimeSeries.cs:813)
GetWatermarkParticipant() 返回水位标准 ITransactionParticipant——"业务效果与 trim 水位同域"
(rollup 管道事务化形态,spec §10)——跨结构原子的现成入口。
3. TierBlob
3.1 组合配方与句柄模型(Blob/TierBlob.cs:13-21)
StreamSnapshot 数据引擎(对象 = CRC64 帧流,句柄 = 起始 LogicalAddress)× 对象表(版本链记录 帧流,meta 真相源)。帧扇区对齐不变式(物理 = 逻辑布局,重启恒等映射读回成立,定长读免 帧解析 +14 直达)——恢复正确性的地基。
3.2 对象登记持久化收口(句柄不丢,Blob/BlobObjectTable.cs:11-21,329-334)
- 对象表 = 表引擎(
{name}.blob.meta)上每条登记/删除/标损 = 一帧[Header 14B][payload 48B+补零][Footer 28B],CRC64 帧级自校验。 - 每条登记 = 帧闭环 → 表引擎 Prepare(fsync + 水位 meta)→ ConfirmCommitted——表水位恒领先/ 对齐登记事实,任何崩溃窗口下已返句柄不丢。
3.3 崩溃对账(TierBlob.cs:420-484)
① 表重放(meta O(1) 水位 + 定步长逐帧重放,步长不齐 fail-fast)→ ② 数据尾由表推导(登记帧 几何权威,不采信 Backward 扫描——稀疏介质伪 footer 会把尾推上天)+ TruncateSuffix 纠偏 → ③ Active 对象逐个验帧(尾级 footer magic+TotalLength O(1) 或 DeepVerifyOnRecovery 整帧 CRC64) 损坏标墓碑显式可见。孤儿帧(Complete 后登记前崩溃)= 不可见,落在推导尾之上被后续写入 覆盖(at-least-once:崩溃 Put 由调用方重试产生新对象)。
3.4 Session 2PC(业务写 + 对象登记原子,TierBlob.cs:380-414)
GetParticipant() → BlobTableParticipant:Prepare = 表引擎 Prepare + 开启会话域(此后登记延迟
提交挂 undo 链);Confirm = 统一 fsync + 提交水位;Abort = 表帧尾截断 + 内存镜像逆序回退
(延迟删除回退 = 对象复活)。undo 链捕获 previous(BlobObjectTable.cs:155-170,246-263)。
会话期间 PutAsync/DeleteAsync 自动挂入事务。
3.5 写入单通道 / Complete / Abort(Blob/BlobSessions.cs)
- 产品写闸
_writeGate串行(结构层写尾单会话契约);不同对象并行写 = Runtime 迭代候选。 - Complete/Abort 显形:Dispose 未 Complete = Abort(尾截断回滚);定长会话须恰好写满(不足/超过 Complete 拒绝并回滚);不定长流容量终判。
- Delete = 表墓碑 + 空间保留;物理回收逐墓碑 extent 打洞 + 死亡前缀头截断(Active 区间零触碰)。
4. 底层依赖(统一契约)
| 依赖 | 提供 |
|---|---|
ITransactionParticipant(Contracts/Transactions) |
2PC 统一契约:Prepare(落盘悬空)/ ConfirmCommitted(提交点)/ Abort(幂等回滚) |
RingBase.Transaction |
Ring 2PC:Prepare 落盘数据+meta(Transport 回落宿主 = 1 次 fsync);Abort = TruncateSuffix 回退到上一提交边界 |
RingBase.Recovery |
恢复四级回退:hints → meta(O(1)+KeySize 锚点+2PC 事务水位还原) → 引擎 CommittedTail → 扫盘找 torn write 边界 |
RingBase.Watermarks |
7 个水位指针 CAS128 单调推进(无 TOCTOU) |
VersionedMetadata |
版本链存储(内存工作副本 + 磁盘版本链 + 多版本保留 Abort 零 IO)——TimeSeries 水位/Blob 对象表基座 |
SessionManager |
组合域统一协调协议层(物化 → Prepare-all → Confirm-all、悬挂裁决)——Blob/TimeSeries 多域事务 |
EntryLog |
TierWal 的底层 WAL 结构(raft 复制日志面);三产品直接用 Ring,不直接用 EntryLog |
5. 一致性语义核心主线(三产品统一)
- 数据真源与派生结构分层(Kv 索引 / TimeSeries 索引 / Blob 无索引)——"派生结构可重建、 正确性不依赖它"是全仓恢复语义的根。
- 分配-持久分离(Append/Put/Complete 返回即内存可见;持久化 = 显式 Flush / checkpoint / Committed / 2PC Prepare)——三产品同一惯例。
- 2PC 提交点语义(Prepare 落盘悬空 → Confirm 提交点 → 可见性;崩溃窗口 = 悬空丢弃 / 已确认保留)——原子批、Blob 登记、水位/业务同域均基于此。
6. 相关文档
- 使用指南:tierkv.md / tierseries.md / tierblob.md(同目录)
- 产品 × raft 一致性:raft-consistency.md
- 跨层持久化稳定性:persistence-architecture.md
- 底层结构:Ring/session/versioned-metadata(
src/KernLab.Tier.Runtime/docs/) - 内部设计稿(不随包发布):tierkv-design / tc-tier-timeseries-spec / tierblob-spec / tierkv-ttl-cas-addressread-design