Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions content/post/2026-02-19-troupe-2.smd
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
.title = "组合性:Troupe 如何驯服分布式系统的复杂度",
.date = @date("2026-02-19T15:00:00+0800"),
.author = "sdzx",
.layout = "post.shtml",
.draft = false,
.custom = {
.math = true, // 如果你要用到数学公式,请 设置 math 为 true; 否则可以忽略
.mermaid = false, // 如果你要用到 mermaid 图表,请设置 mermaid 为 true; 否则可以忽略
},
---

在分布式系统的世界里,我们经常面临一个两难困境:**业务逻辑越复杂,代码就越容易失控**。传统的编程方式要求每个节点(角色)独立实现协议逻辑,导致随着协议数量、角色数量的增加,维护成本呈指数级上升。最终,系统变得像一团乱麻——修改一个细节需要同步所有角色的代码,调试一个跨角色的问题需要追踪多个独立实现的状态机。

[Troupe](https://github.com/sdzx-1/troupe) 的出现,正是为了打破这一困境。它的核心武器,不是简单的状态机抽象,而是**组合性**。组合性让 Troupe 从一个小小的协议库,蜕变为一套能够构建极其复杂分布式系统的“构造语言”。本文将深入探讨组合性如何解决传统难题,以及它带来的复杂度革命。

## 一、传统方式:逻辑分散与维护噩梦

想象一个简单的三角色协议:Alice、Bob、Charlie 需要协作完成某项任务。在传统实现中,你需要分别编写三份代码:

- `alice.zig` 包含 Alice 发送请求、接收响应、处理超时的逻辑。
- `bob.zig` 包含 Bob 接收请求、处理、发送响应的逻辑。
- `charlie.zig` 类似,但视角不同。

如果协议有 M 个状态、N 个角色,那么你需要维护 N 份几乎相同但又不同的状态机代码。当协议演化(比如增加一个重试分支),所有 N 份代码都必须同步修改——稍有疏忽,就会导致角色间的状态不一致。更糟的是,这些协议往往不是孤立运行的,它们会与成员管理、故障恢复等协议交织在一起。结果,每个角色的代码都变成了一个大泥球,混杂着多个协议的标志位、回调、事件处理。

这种分散式的实现导致了几大痛点:

- **重复劳动**:同一份逻辑写 N 遍。
- **同步成本**:修改需要协调 N 个文件。
- **一致性风险**:稍有不慎,各角色状态机产生分歧。
- **测试爆炸**:需要测试每个角色以及它们之间的交互,组合数随角色和协议数量指数增长。
- **认知负担**:理解整个系统需要同时追踪 N 个独立的代码库。

## 二、组合性的核心思想:定义一次,处处演绎

Troupe 彻底颠覆了上述模式。它将协议定义为**类型化的状态机**,所有角色的行为都从这一个定义中派生。你不再需要为 Alice、Bob、Charlie 分别写代码,只需要编写一份“剧本”——而剧本本身是可组合的。

### 1. 协议即类型
每个协议状态是一个 tagged union,它的每个字段代表一种可能的消息,而消息的“下一状态”通过 `Data(NextState)` 类型参数指定。例如:

```zig
const Ping = union(enum) {
ping: Data(u32, Pong),
};
```

这个定义同时蕴含了“Alice 发送 ping”和“Bob 接收 ping”两种视角。运行时,`Runner` 根据当前角色自动分发正确的行为。

### 2. 协议作为组合子
协议可以通过类型嵌套实现无缝拼接。一个协议的“出口”(某个状态)可以直接作为另一个协议的“入口”:

```zig
PingPong(.alice, .bob, TwoPhaseCommit(.charlie, .alice, .bob).Begin)
```

这段代码表达了一个简单的组合:先执行 Alice 和 Bob 之间的 pingpong,结束后自动进入 Charlie 协调的两阶段提交。这种嵌套是**类型安全**的——编译器会展开并验证所有路径。

### 3. 跨协议同步自动化
当协议嵌套时,角色自动划分为“内部角色”(参与当前协议的)和“外部角色”(等待的)。当内部协议到达外部可见状态(通过 `extern_state` 声明)时,`internal_roles[0]` 会自动向所有外部角色发送 `Notify` 消息,通知它们新状态。这一机制将跨协议同步的责任从开发者转移给了框架,且通过编译期检查保证通知的完整性。

### 4. 编译期验证
组合后的状态图会在编译期由 `reachableStates` 遍历,检查每个状态的发送者、接收者、角色覆盖、上下文类型一致性等。任何结构上的错误(如分支状态未通知所有内部角色)都会直接导致编译失败。这意味着组合后的系统不仅是合法的,而且是**可证明合法**的。

## 三、复杂度降维:从 O(N·M) 到 O(M)

让我们用数学语言描述这种变化。设:
- \(R\) = 角色数量
- \(P\) = 协议数量(每个协议有若干状态)
- \(S_i\) = 第 i 个协议的状态数
- \(T\) = 协议间的连接数(切换次数)

**传统方式**下,每个角色需要实现它参与的所有协议逻辑,且这些实现必须手动同步。总代码复杂度大致为:
\[
O(R \times \sum S_i + R \times T)
\]
更重要的是,维护成本随 \(R\) 和 \(T\) 指数增长——因为任何修改都需要同步到所有角色的代码中,且角色间的交互测试组合数呈组合爆炸。

**Troupe 方式**下,协议定义一次,角色行为自动派生;协议组合通过类型声明完成,无需手动编写切换逻辑。总代码复杂度大致为:
\[
O(\sum S_i + T)
\]
这里的 \(T\) 是组合声明中的嵌套层数,由编译器展开。维护成本与角色数 \(R\) 无关——增加新角色只需在 `Context` 中添加对应字段,所有协议逻辑自动适用。

当 \(R\) 和 \(P\) 变大时,这种差异会急剧放大。一个 10 角色、20 协议、50 次切换的系统,传统方式可能需要数万行分散的、难以维护的代码,而 Troupe 可能只需数百行声明。更重要的是,Troupe 的代码天然就是系统的**完整规范**——你无需阅读多个文件来理解整体行为,只需看顶层组合声明即可。

## 四、实例:random-pingpong-2pc 中的多协议交响

在 `random-pingpong-2pc.zig` 示例中,我们可以看到组合性的威力:

```zig
charlie_as_coordinator: Data(void, PingPong(.alice, .bob, PingPong(.bob, .charlie, PingPong(.charlie, .alice, CAB(@This()).Begin).Ping).Ping).Ping)
```

这短短几行定义了一个复杂的协议序列:三个 pingpong 依次在 Alice-Bob、Bob-Charlie、Charlie-Alice 之间执行,最后进入由 Charlie 协调的两阶段提交。在传统实现中,你需要:
- 为每个角色编写 pingpong 的参与逻辑(每个角色可能既是客户端又是服务器)。
- 在 Alice 的代码中处理“先和 Bob pingpong,然后等待 Bob 和 Charlie pingpong 结束,最后参与 2pc”。
- 类似地处理 Bob 和 Charlie 的代码。
- 处理跨协议同步:当 pingpong 序列结束时,如何通知未参与的角色(这里是 Selector)?

而在 Troupe 中,这一切都被压缩为类型声明。编译器会展开这个嵌套,生成完整的状态图,并自动安排跨协议通知(当整个序列结束时,Selector 会收到通知)。开发者只需关注协议本身的逻辑,无需操心编排和同步。

## 五、组合性的哲学意义:从运行时编排到设计时规范

组合性的真正价值,在于它**将分布式系统的“编排”从运行时转移到了设计时**。在传统系统中,协议切换、角色同步、状态分发都是在运行时通过消息传递完成的——这本身就是分布式问题的源头。Troupe 则把这些责任提升到类型系统层面:组合关系在编译时固定,同步机制由框架自动生成。

这种做法体现了软件工程的一条黄金法则:**能提前解决的问题,不要留到运行时**。Troupe 把组合的正确性检查提前到编译期,把跨协议同步的逻辑自动化,让开发者能够专注于协议的核心逻辑,而不是被无穷的编排细节淹没。

从认知层面看,组合性大大降低了理解系统的门槛。你不再需要阅读每个角色的代码来拼凑整体行为,只需要看顶层的组合声明——它就像一张地图,清晰地展示了协议之间的连接关系。这种“声明式”的编程风格,让复杂系统变得可读、可推理。

## 六、结论:组合性才是 Troupe 最大的价值

确定性保证了系统不会乱,编译期验证保证了系统不会错,但**组合性保证了你能构建足够复杂的系统**。没有组合性,前两者只能用于玩具协议;有了组合性,你才能用它编写真实的、多阶段、多角色的分布式应用——从简单的 pingpong 到复杂的交易系统、共识协议链。

Troupe 的组合性设计,将分布式系统的复杂度从**乘数级**降为**加数级**,极大地提升了人类能够驾驭的分布式逻辑的上限。它告诉我们,面对分布式系统的混沌,我们不必束手就擒——通过类型系统的巧妙运用,我们可以将混沌装进一个确定性的盒子里,然后用组合的乐高积木搭建出任何复杂的系统。