Engine execution layer for V2 replication protocol: - RebuildInstaller: full state handoff (dirty map, WAL, superblock, flusher) - TruncateToLSN: exact safety predicate (checkpointLSN == truncateLSN), ErrTruncationUnsafe escalation to NeedsRebuild - SyncReceiverProgress: unconditional Store for post-rebuild alignment - V2StatusSnapshot: CommittedLSN = nextLSN-1 for sync_all V2 bridge real I/O executors: - TransferFullBase: TCP streaming + RebuildInstaller + second catch-up - TransferSnapshot: SHA-256 verified streaming to disk - TruncateWAL: ErrTruncationUnsafe detection + escalation - StreamWALEntries: rebuild-mode TCP apply Engine executor interfaces: - CatchUpIO.TruncateWAL, RebuildIO.TransferFullBase returns achievedLSN - CatchUpExecutor truncation-only skip, NeedsRebuild escalation - RebuildExecutor uses achievedLSN for progress tracking Design docs reorganized: superseded planning docs removed, protocol truths and closure map added. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
V2 语义建模与协议开发方法
日期:2026-04-02 状态:active 读者:架构设计、实现负责人、tester、reviewer
1. 文档目标
这份文档回答的不是“某个函数怎么改”,而是下面几个更上层的问题:
- 为什么
V2不应该主要依赖“多跑一些随机模拟”来建立正确性信心 - 为什么需要先定义状态机,再在其上叠加语义约束
- 什么叫“证明义务”,它和普通 testcase 有什么不同
- 如何把状态、约束、证明义务重新连接回实现、测试和 slice close
这份文档不是具体协议规格。
它是 V2 的方法论文档。
它的作用是把下面这条路线讲清楚:
- 先定义系统状态空间
- 再定义约束和语义
- 再导出必须被证明的义务
- 最后映射到实现、测试、review 和 phase slice
2. 为什么不能主要依赖 random simulation
随机模拟是有价值的,但它不是 closure framework。
它更适合:
- 发现意外交互
- 挖掘边缘 case
- 暴露直觉之外的坏结果
- 提高对某个设计的经验性信心
它不擅长直接回答下面这些问题:
- 系统真正承诺的 truth 是什么
- 哪些状态迁移是合法的,哪些必须被拒绝
- 哪些失败应该 fail closed,哪些可以自动恢复
- 为什么某个结果在协议上是正确的,而不是“测试刚好没炸”
- 哪些性质是必须长期保持的不变量
所以 V2 的基本方法不是:
- 多跑随机事件,直到感觉系统比较稳
而是:
- 先定义状态空间
- 先定义协议语义
- 先定义证明义务
- 再用 simulator、one-chain test、真实链路测试去验证这些义务
一句话:
- random simulation 是 discovery tool
- 不是 protocol closure 的主骨架
2.1 两种建立信心的路径(直观对比)
flowchart LR
subgraph A["以 random simulation 为主"]
A1[随机事件序列] --> A2[观察是否崩溃/出错]
A2 --> A3[扩大随机范围或时长]
end
subgraph B["以语义框架为主"]
B1[状态机 + 事件] --> B2[语义约束 / 不变量]
B2 --> B3[证明义务]
B3 --> B4[定向测试 / one-chain / 真实链路证据]
B4 --> B5[slice close]
end
要点:
A擅长发现意外,但不直接回答“协议承诺了什么”。B先定义可证目标,再用证据 close,而不是只靠“跑得久没炸”。
3. 五层抽象模型
为了把 V2 做成一个可推演、可 close、可落回开发的系统,可以把它分成五层。
3.0 五层堆叠(一览)
flowchart TB
L5["Layer 5:工程映射\n实现点 / 观测点 / 测试 / phase slice"]
L4["Layer 4:证明义务\n每条约束对应的最小证据"]
L3["Layer 3:语义约束\n不变量 / authority / boundary / convergence"]
L2["Layer 2:状态机\n状态对象 + 事件 + 合法迁移"]
L1["Layer 1:物理实现\ncode / goroutine / gRPC / blockvol"]
L1 --> L2
L2 --> L3
L3 --> L4
L4 --> L5
读图方式:从下往上是“从代码到语义”,从上往下是“从语义到落地”。
3.1 第一层:物理实现层
这一层是实际运行的工程对象:
mastervolume serverProcessAssignments()ControlBridgeRecoveryManagerCatchUpExecutorRebuildExecutor- heartbeat / gRPC path
blockvol/WAL/snapshot/flusher
这一层回答的是:
- 系统最终跑在哪里
- 哪些代码路径真正执行
但它本身不是协议语义。
3.2 第二层:状态机层
这一层定义系统有哪些正式状态对象,以及事件如何推动状态迁移。
这一层回答的是:
- 系统里真正的状态是什么
- 哪些事件会改变状态
- 状态如何从一个点迁移到另一个点
典型状态对象包括:
- 控制面状态:
epochrole- assignment truth
- stable
ServerID
- 恢复状态:
- sender
- session
- recovery owner
in_sync/needs_rebuild/catchup
- 数据边界状态:
CheckpointLSNWALHeadLSNreceivedLSNtargetLSNachievedLSN
- 对外可见状态:
- heartbeat truth
- reporting addresses
- externally visible ownership
典型事件包括:
AssignmentDeliveredEpochBumpedSessionCreatedSessionSupersededCatchUpCompletedRebuildCommittedTruncationEscalatedRepeatedAssignmentDeliveredHeartbeatCollectedCrash/Restart
3.3 第三层:语义约束层
这一层不再只是说“系统会怎么动”,而是说“系统应该满足什么规律”。
这一层回答的是:
- 哪些状态组合是允许的
- 哪些状态组合是禁止的
- 哪些迁移必须 fail closed
- 哪些 truth 最终必须收敛
这层通常包含五类约束:
- ownership 约束
- identity 约束
- boundary safety 约束
- convergence 约束
- idempotence 约束
这层的价值是把“会跑”变成“跑得对”。
3.4 第四层:证明义务层
这一层把语义约束变成有限个必须被证明的场景。
它回答的是:
- 每条约束最少需要哪些证据
- 哪些事件组合最能破坏这条约束
- 怎样设计 proof case,而不是瞎跑随机 case
证明义务不是随便写一个测试。 它是对某条语义约束的最小必要证明。
3.5 第五层:工程映射层
这一层把抽象重新落回工程:
- 哪条语义约束由哪个实现点负责
- 哪些观测点能证明它成立
- 哪些测试或 one-chain proof 覆盖它
- 哪个 phase / slice 对应关闭它
如果没有这一层,模型容易停在 PPT。 有了这一层,模型才能真正指导实现和 close。
4. 什么是“语义约束”
语义约束是叠加在状态机之上的规则。
它不是代码风格,也不是临时 if 判断。 它是系统长期必须满足的性质。
4.1 ownership 约束
目标是明确“当前谁有 authority”。
例子:
- 同一时刻,同一 replica 最多只能有一个 live recovery owner
- 新 session 生效后,旧 owner 不能继续提交结果
- supersede 后旧 goroutine 必须 drain
4.2 identity 约束
目标是明确“谁是谁”,而不是靠地址字符串猜身份。
例子:
- stable
ServerID不能在 wire 上丢失 ReplicaID应从<path>/<ServerID>构造,而不是从 transport address 推断- 缺失 stable identity 时应 fail closed,而不是随手 fallback
4.3 boundary safety 约束
目标是明确恢复边界必须在物理上成立。
例子:
- snapshot 边界必须和 manifest 对齐
- truncation 只有在安全条件成立时才能本地修正
- full-base rebuild 的完成边界必须和 runtime / engine accounting 对齐
4.4 convergence 约束
目标是防止多个 truth 长期分裂。
例子:
- master truth、runtime truth、heartbeat truth 最终必须收敛
- reassignment 后旧 truth 不应继续对外可见
achievedLSN、checkpoint、receiver progress 应在 accepted contract 下收敛
4.5 idempotence 约束
目标是保证重复不变更的输入不会不断产生额外副作用。
例子:
- repeated assignment 不应反复触发 recovery side effect
- repeated unchanged delivery 不应重复 relisten / restart
- repeated heartbeat delivery 不应破坏已收敛 truth
5. 什么是“证明义务”
证明义务不是“多写一些测试”。
证明义务的定义是:
- 为了证明某条语义约束成立,必须提供的最小证据单元
5.1 一个证明义务通常包含三部分
- 要保护的约束
- 最容易破坏它的事件组合
- 可以观察到结果的观测点
5.2 证明义务的例子
例子 A:唯一 owner
语义约束:
- 同一 replica 只能有一个 live owner
证明义务:
- epoch bump 后旧 owner 被 drain
- replacement 开始前旧 owner 已退出
- shutdown 后不再残留 active owner
例子 B:boundary safety
语义约束:
- 不安全 truncation 不得伪装成成功
证明义务:
- safe case 本地修正成功
- unsafe case 返回 escalation
- sender state 进入
needs_rebuild而不是错误地in_sync
例子 C:identity preservation
语义约束:
- stable ID 不得在 wire 上被地址 fallback 取代
证明义务:
- proto round-trip preserves stable ID
- real ingress path preserves stable ID
- missing stable ID fails closed
5.3 为什么证明义务比 random simulation 更有效
因为它是围绕“必须成立的语义”来构造场景,而不是随机撞运气。
它回答的是:
- 我们到底在证明什么
而不只是:
- 我们又跑过了一些 case
6. 从语义约束到 slice close
V2 的 phase / slice 应该由语义约束驱动,而不是由“某个模块看起来需要改”驱动。
建议每个 slice 都回答五个问题:
- 本 slice 关心哪些状态对象
- 本 slice 关心哪些关键事件
- 本 slice 要关闭哪条语义约束
- 本 slice 需要哪些证明义务
- 这些义务映射到哪些实现点和观测点
6.1 一个简单模板
Step 1:状态对象
例如:
- assignment truth
- recovery owner
- heartbeat truth
Step 2:关键事件
例如:
- epoch bump
- repeated assignment
- master-driven delivery
Step 3:语义约束
例如:
- unique owner
- no split truth
- idempotence
Step 4:证明义务
例如:
- old owner drained
- new truth becomes visible
- repeated delivery does not create new side effects
Step 5:工程映射
例如:
ProcessAssignments()RecoveryManagerCollectBlockVolumeHeartbeat()AssignmentsToProto/FromProto
7. simulator 在这个方法里的位置
simulator 仍然非常重要,但它处在语义框架之下。
它的价值主要是:
- 探索复杂交互
- 帮助寻找高风险事件组合
- 验证某条约束在更大状态空间里是否容易被打破
- 作为真实实现之前的设计验证层
它不应该替代:
- contract
- invariant
- proof obligation
- one-chain evidence
所以正确关系是:
- 先有语义框架
- 再用 simulator 扩大探索和验证覆盖
而不是反过来。
8. 直观理解:为什么这种方法比“逐渐修 bug”更强
很多工程系统的成长路径是:
- 先做一个能工作的版本
- 线上或测试出 bug
- 修一个点
- 再遇到新边界
- 再补一个 patch
这种路线可以快速落地,但容易产生:
- 隐含 truth
- patch pile
- 边界不清
- 系统很难解释为什么是对的
V2 这套方法的差异在于:
- 先抽象状态和事件
- 先定义 authority、boundary、convergence、idempotence
- 再把这些落回实现
- 做不到的地方缩窄 contract,而不是继续模糊化
这会让前期更慢,但长期更稳,也更适合做为开源协议工程方法。
9. 方法总结
V2 的推荐方法可以压缩成一句话:
- 先定义状态机,再定义语义约束,再定义证明义务,最后把这些映射回工程实现与测试。
进一步展开,就是:
- 状态机定义系统会怎么动
- 语义约束定义系统应该满足什么
- 证明义务定义我们必须提供哪些证据
- 工程映射定义这些证据如何在代码与测试里落地
这条路径不是为了让系统“更学术”。 它的目标是让系统在复杂故障和恢复场景下:
- 更可解释
- 更可 close
- 更不依赖运气
- 更不容易退化成 patch-driven correctness
10. V1 与 V2:哪些状态在 V1 中常未显式建模,导致结果不确定
这一节不是贬低 V1 工程价值。
它想说明的是:
- 很多传统路径能跑,是因为实现里堆了经验与 patch
- 但在协议层若没有显式状态,遇到边界场景时,系统行为会难以形式化预测
下面用“缺失的显式状态”来对照:这些缺口在 happy path 往往不明显,在 failover / rebuild / 重复控制消息 / identity 变化时会放大为不确定或 split truth。
10.1 对照表:显式状态与典型后果
| 维度 | V1 常见情况(概括) | 若未显式建模时的典型不确定 | V2 的显式化方向(概括) |
|---|---|---|---|
| 副本/节点身份 | 常隐含在 ip:port、连接串、临时约定里 |
failover 后“同名不同人”、重复地址、owner 混淆 | stable ServerID + ReplicaID = path/serverID + wire 携带 |
| 恢复 authority | 常隐含在“当前 goroutine/函数在跑” | 旧任务与新任务重叠、cancel 不到执行层 | session + RecoveryManager + supersede/drain 语义 |
| 控制代际 / 围栏 | epoch 不一定贯穿所有层 |
旧消息/旧计划仍生效、幽灵进度 | epoch + sender/session 失效规则 |
| rebuild 完成边界 | 常停在“拷完 extent / 跑完某段逻辑” | engine 认为完成 vs 本地实际 LSN 不一致 | achievedLSN 与 runtime checkpoint/receiver 对齐 |
| snapshot / tail 边界 | 常弱化为“尽量一致” | 边界漂移、静默接受错误镜像 | manifest + BaseLSN + fail-closed |
| replica-ahead / truncate | 常混在一个“修一下元数据”的路径里 | extent 已污染却宣称已修好 | safe / unsafe 分流 + escalate |
| 控制面 vs 运行时 vs 上报 | 常分散在多个模块,缺少统一 truth | master 以为 A,VS 报 B,内部还在跑 C | convergence 证明:ingress/runtime/heartbeat 对齐 |
| 重复 assignment / 心跳投递 | 常靠“再跑一次应该差不多” | 重复 side effect、重复 relisten | idempotence:同 truth 不重复触发 |
说明:
- 上表是对工程形态的概括,不是逐文件审计结论。
V1的具体代码路径里可能已经部分具备某些字段或行为,但若未上升到协议级显式对象,在 review 和演进时仍容易漂移。
10.2 不确定性的结构(Mermaid)
flowchart TB
subgraph missing["常见缺口:协议层未显式建模"]
M1[身份 truth 弱]
M2[恢复 owner 弱]
M3[完成边界弱]
M4[控制/运行/上报分裂]
end
subgraph symptoms["边界场景症状"]
S1[结果依赖时序/重试]
S2[同一输入不同次运行行为不同]
S3[表面成功但内部 truth 不一致]
S4[难以回答为什么是对的]
end
missing --> symptoms
10.3 和 V2 方法的关系
V2 的做法不是“多写一点 if”。
它是把上表里的缺口,尽量变成:
- 正式状态对象
- 语义约束
- 证明义务
- 工程映射与测试证据
这样“不确定”会从黑盒运气变成可命名、可测试、可 close 的缺口清单。
11. 推荐和哪些文档一起阅读
建议按下面顺序阅读:
protocol-development-process.mdv2-semantic-methodology.zh.mdv2-detailed-algorithm.zh.mdv2-protocol-closure-map.zh.mdv2-phase-development-plan.md
这样可以依次看到:
- 开发流程
- 方法论
- 具体算法
- 当前协议闭环地图
- phase 级执行计划
另见:v2-protocol-closure-map.zh.md 中的 truth 流水线图与 Phase 映射表。