Files
seaweedfs/sw-block/design/v2-semantic-methodology.zh.md
T
pingqiuandClaude Opus 4.6 c7eb87c587 feat: Phase 09 — V2 execution primitives and production closure
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>
2026-04-02 16:25:23 -07:00

548 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# V2 语义建模与协议开发方法
日期:2026-04-02
状态:active
读者:架构设计、实现负责人、tester、reviewer
## 1. 文档目标
这份文档回答的不是“某个函数怎么改”,而是下面几个更上层的问题:
1. 为什么 `V2` 不应该主要依赖“多跑一些随机模拟”来建立正确性信心
2. 为什么需要先定义状态机,再在其上叠加语义约束
3. 什么叫“证明义务”,它和普通 testcase 有什么不同
4. 如何把状态、约束、证明义务重新连接回实现、测试和 slice close
这份文档不是具体协议规格。
它是 `V2` 的方法论文档。
它的作用是把下面这条路线讲清楚:
1. 先定义系统状态空间
2. 再定义约束和语义
3. 再导出必须被证明的义务
4. 最后映射到实现、测试、review 和 phase slice
## 2. 为什么不能主要依赖 random simulation
随机模拟是有价值的,但它不是 closure framework。
它更适合:
1. 发现意外交互
2. 挖掘边缘 case
3. 暴露直觉之外的坏结果
4. 提高对某个设计的经验性信心
它不擅长直接回答下面这些问题:
1. 系统真正承诺的 truth 是什么
2. 哪些状态迁移是合法的,哪些必须被拒绝
3. 哪些失败应该 fail closed,哪些可以自动恢复
4. 为什么某个结果在协议上是正确的,而不是“测试刚好没炸”
5. 哪些性质是必须长期保持的不变量
所以 `V2` 的基本方法不是:
- 多跑随机事件,直到感觉系统比较稳
而是:
1. 先定义状态空间
2. 先定义协议语义
3. 先定义证明义务
4. 再用 simulator、one-chain test、真实链路测试去验证这些义务
一句话:
- random simulation 是 discovery tool
- 不是 protocol closure 的主骨架
### 2.1 两种建立信心的路径(直观对比)
```mermaid
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 五层堆叠(一览)
```mermaid
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 第一层:物理实现层
这一层是实际运行的工程对象:
1. `master`
2. `volume server`
3. `ProcessAssignments()`
4. `ControlBridge`
5. `RecoveryManager`
6. `CatchUpExecutor`
7. `RebuildExecutor`
8. heartbeat / gRPC path
9. `blockvol` / `WAL` / `snapshot` / `flusher`
这一层回答的是:
- 系统最终跑在哪里
- 哪些代码路径真正执行
但它本身不是协议语义。
### 3.2 第二层:状态机层
这一层定义系统有哪些正式状态对象,以及事件如何推动状态迁移。
这一层回答的是:
1. 系统里真正的状态是什么
2. 哪些事件会改变状态
3. 状态如何从一个点迁移到另一个点
典型状态对象包括:
1. 控制面状态:
- `epoch`
- `role`
- assignment truth
- stable `ServerID`
2. 恢复状态:
- sender
- session
- recovery owner
- `in_sync` / `needs_rebuild` / `catchup`
3. 数据边界状态:
- `CheckpointLSN`
- `WALHeadLSN`
- `receivedLSN`
- `targetLSN`
- `achievedLSN`
4. 对外可见状态:
- heartbeat truth
- reporting addresses
- externally visible ownership
典型事件包括:
1. `AssignmentDelivered`
2. `EpochBumped`
3. `SessionCreated`
4. `SessionSuperseded`
5. `CatchUpCompleted`
6. `RebuildCommitted`
7. `TruncationEscalated`
8. `RepeatedAssignmentDelivered`
9. `HeartbeatCollected`
10. `Crash` / `Restart`
### 3.3 第三层:语义约束层
这一层不再只是说“系统会怎么动”,而是说“系统应该满足什么规律”。
这一层回答的是:
1. 哪些状态组合是允许的
2. 哪些状态组合是禁止的
3. 哪些迁移必须 fail closed
4. 哪些 truth 最终必须收敛
这层通常包含五类约束:
1. ownership 约束
2. identity 约束
3. boundary safety 约束
4. convergence 约束
5. idempotence 约束
这层的价值是把“会跑”变成“跑得对”。
### 3.4 第四层:证明义务层
这一层把语义约束变成有限个必须被证明的场景。
它回答的是:
1. 每条约束最少需要哪些证据
2. 哪些事件组合最能破坏这条约束
3. 怎样设计 proof case,而不是瞎跑随机 case
证明义务不是随便写一个测试。
它是对某条语义约束的最小必要证明。
### 3.5 第五层:工程映射层
这一层把抽象重新落回工程:
1. 哪条语义约束由哪个实现点负责
2. 哪些观测点能证明它成立
3. 哪些测试或 one-chain proof 覆盖它
4. 哪个 phase / slice 对应关闭它
如果没有这一层,模型容易停在 PPT。
有了这一层,模型才能真正指导实现和 close。
## 4. 什么是“语义约束”
语义约束是叠加在状态机之上的规则。
它不是代码风格,也不是临时 if 判断。
它是系统长期必须满足的性质。
### 4.1 ownership 约束
目标是明确“当前谁有 authority”。
例子:
1. 同一时刻,同一 replica 最多只能有一个 live recovery owner
2. 新 session 生效后,旧 owner 不能继续提交结果
3. supersede 后旧 goroutine 必须 drain
### 4.2 identity 约束
目标是明确“谁是谁”,而不是靠地址字符串猜身份。
例子:
1. stable `ServerID` 不能在 wire 上丢失
2. `ReplicaID` 应从 `<path>/<ServerID>` 构造,而不是从 transport address 推断
3. 缺失 stable identity 时应 fail closed,而不是随手 fallback
### 4.3 boundary safety 约束
目标是明确恢复边界必须在物理上成立。
例子:
1. snapshot 边界必须和 manifest 对齐
2. truncation 只有在安全条件成立时才能本地修正
3. full-base rebuild 的完成边界必须和 runtime / engine accounting 对齐
### 4.4 convergence 约束
目标是防止多个 truth 长期分裂。
例子:
1. master truth、runtime truth、heartbeat truth 最终必须收敛
2. reassignment 后旧 truth 不应继续对外可见
3. `achievedLSN`、checkpoint、receiver progress 应在 accepted contract 下收敛
### 4.5 idempotence 约束
目标是保证重复不变更的输入不会不断产生额外副作用。
例子:
1. repeated assignment 不应反复触发 recovery side effect
2. repeated unchanged delivery 不应重复 relisten / restart
3. repeated heartbeat delivery 不应破坏已收敛 truth
## 5. 什么是“证明义务”
证明义务不是“多写一些测试”。
证明义务的定义是:
- 为了证明某条语义约束成立,必须提供的最小证据单元
### 5.1 一个证明义务通常包含三部分
1. 要保护的约束
2. 最容易破坏它的事件组合
3. 可以观察到结果的观测点
### 5.2 证明义务的例子
#### 例子 A:唯一 owner
语义约束:
- 同一 replica 只能有一个 live owner
证明义务:
1. epoch bump 后旧 owner 被 drain
2. replacement 开始前旧 owner 已退出
3. shutdown 后不再残留 active owner
#### 例子 B:boundary safety
语义约束:
- 不安全 truncation 不得伪装成成功
证明义务:
1. safe case 本地修正成功
2. unsafe case 返回 escalation
3. sender state 进入 `needs_rebuild` 而不是错误地 `in_sync`
#### 例子 C:identity preservation
语义约束:
- stable ID 不得在 wire 上被地址 fallback 取代
证明义务:
1. proto round-trip preserves stable ID
2. real ingress path preserves stable ID
3. missing stable ID fails closed
### 5.3 为什么证明义务比 random simulation 更有效
因为它是围绕“必须成立的语义”来构造场景,而不是随机撞运气。
它回答的是:
- 我们到底在证明什么
而不只是:
- 我们又跑过了一些 case
## 6. 从语义约束到 slice close
`V2` 的 phase / slice 应该由语义约束驱动,而不是由“某个模块看起来需要改”驱动。
建议每个 slice 都回答五个问题:
1. 本 slice 关心哪些状态对象
2. 本 slice 关心哪些关键事件
3. 本 slice 要关闭哪条语义约束
4. 本 slice 需要哪些证明义务
5. 这些义务映射到哪些实现点和观测点
### 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()`
- `RecoveryManager`
- `CollectBlockVolumeHeartbeat()`
- `AssignmentsToProto/FromProto`
## 7. simulator 在这个方法里的位置
simulator 仍然非常重要,但它处在语义框架之下。
它的价值主要是:
1. 探索复杂交互
2. 帮助寻找高风险事件组合
3. 验证某条约束在更大状态空间里是否容易被打破
4. 作为真实实现之前的设计验证层
它不应该替代:
1. contract
2. invariant
3. proof obligation
4. one-chain evidence
所以正确关系是:
1. 先有语义框架
2. 再用 simulator 扩大探索和验证覆盖
而不是反过来。
## 8. 直观理解:为什么这种方法比“逐渐修 bug”更强
很多工程系统的成长路径是:
1. 先做一个能工作的版本
2. 线上或测试出 bug
3. 修一个点
4. 再遇到新边界
5. 再补一个 patch
这种路线可以快速落地,但容易产生:
1. 隐含 truth
2. patch pile
3. 边界不清
4. 系统很难解释为什么是对的
`V2` 这套方法的差异在于:
1. 先抽象状态和事件
2. 先定义 authority、boundary、convergence、idempotence
3. 再把这些落回实现
4. 做不到的地方缩窄 contract,而不是继续模糊化
这会让前期更慢,但长期更稳,也更适合做为开源协议工程方法。
## 9. 方法总结
`V2` 的推荐方法可以压缩成一句话:
- 先定义状态机,再定义语义约束,再定义证明义务,最后把这些映射回工程实现与测试。
进一步展开,就是:
1. 状态机定义系统会怎么动
2. 语义约束定义系统应该满足什么
3. 证明义务定义我们必须提供哪些证据
4. 工程映射定义这些证据如何在代码与测试里落地
这条路径不是为了让系统“更学术”。
它的目标是让系统在复杂故障和恢复场景下:
1. 更可解释
2. 更可 close
3. 更不依赖运气
4. 更不容易退化成 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)
```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”。
它是把上表里的缺口,尽量变成:
1. 正式状态对象
2. 语义约束
3. 证明义务
4. 工程映射与测试证据
这样“不确定”会从**黑盒运气**变成**可命名、可测试、可 close 的缺口清单**。
## 11. 推荐和哪些文档一起阅读
建议按下面顺序阅读:
1. `protocol-development-process.md`
2. `v2-semantic-methodology.zh.md`
3. `v2-detailed-algorithm.zh.md`
4. `v2-protocol-closure-map.zh.md`
5. `v2-phase-development-plan.md`
这样可以依次看到:
1. 开发流程
2. 方法论
3. 具体算法
4. 当前协议闭环地图
5. phase 级执行计划
另见:`v2-protocol-closure-map.zh.md` 中的 truth 流水线图与 `Phase` 映射表。