30 秒看懂
Fly.io 工程师 Somtochi Onyekwere 介绍如何将 Corrosion 的 Plumtree 广播逻辑与套接字、定时器和随机性分离。调用方提供运行时,使确定性模拟成为可能;生产集成仍需另行测试。
- 1输入事件,输出动作协议处理事件,并将消息发送与定时器交给调用方。
- 2本地集群模拟按时间排序的事件堆在一个进程中驱动约 2000 个节点状态。
- 3故障也是事件测试可以让节点下线、丢弃消息,再在之后恢复节点。
- 4仍需测试集成即使协议正确,周边运行时处理副作用的方式不正确仍可能导致失败。
要点建议
- 分布式协议面临消息交付、时间和节点故障带来的非确定性。
- 协议状态机可以接收事件并请求副作用,而不必拥有套接字或时钟。
- 事件堆和虚拟时间可以在本地模拟大量节点。
- 注入随机性和故障可以让场景复现。
- 测试协议核心不能取代对生产集成的测试。
按章节阅读整理后的全文
分布式协议为何难以测试
网络、定时器和节点故障引入非确定性,而大型集成环境成本高昂。
我在 Fly.io 从事分布式系统和网络软件开发。这次演讲讨论如何组织分布式协议,让我们能够进行确定性测试,并在部署前更有信心地确认协议正常工作。
分布式系统难以测试,因为许多节点需要通过网络协调。消息可能延迟、乱序或丢失,定时器会触发,节点也可能不可用。即使发生这些故障,协议仍必须维持自己的假设。
大型集成测试运行成本高,也难以复现。拥有数千节点的集群涉及大量进程,而下一次运行时事件顺序可能不同。规模和非确定性都会增加故障排查难度。
将协议与副作用分离
协议接收事件并请求动作,由调用方提供运行时。
我们在修改 Fly.io 使用的开源 SQLite 复制系统 Corrosion 的广播协议时发现,论文中的协议本身相对简单。我们希望保留这个小型核心,同时能轻松改变条件、复现故障并复用实现。
协议代码经常将逻辑与线程创建、消息发送和定时器混在一起。如果实现内部会休眠,测试可能也不得不等待,从而带来不稳定。我们希望把这些副作用移到协议之外,建立清晰的边界,让测试可以替换运行环境。

我们的模型将协议视为状态机。它接收消息、节点上线或定时器触发等事件,并指出接下来应执行的动作。协议本身不拥有套接字或时钟;调用方决定如何发送消息和安排定时器。
这里的例子是 Plumtree,一个在集群中广播的 gossip 协议。节点向 eager peers 发送完整消息,后者继续转发。lazy peers 接收消息标识,必要时可以请求缺失的内容,作为回退路径。
在 Corrosion 中,广播请求等输入会交给 Plumtree 状态。协议可以要求调用方发送消息,或将内容交付给应用。调用方还负责提供时间,因此副作用都在核心逻辑之外。
一种可行的接口是返回所需动作的列表。我们选择使用由调用方实现的 trait,即接口。它暴露向节点或对等节点发送消息、交付内容、通知应用和安排定时器等副作用。

协议请求定时器后,由调用方负责安排。定时器触发时,调用方再把触发事件传回协议。消息也采用相同的分离方式:应用提供运行时,并决定如何执行副作用。
例如,Corrosion 会短暂缓冲消息,以便批量发送、提高效率。其他库使用者可以选择不同实现。集成层很小:只需提供接口,协议就会使用该环境。
模拟集群
本地事件队列和虚拟时间让多个节点状态运行起来,无需真实套接字或休眠。
这种边界对测试尤其有用,因为测试运行时可以与生产环境不同。生产环境可以使用线程和套接字;本地模拟则记录事件并将其排队,不需要真实网络,也不需要为每个模拟节点创建线程。
我们的测试用二叉堆按事件时间排序。一个循环取出最早的事件,将它应用到对应的协议状态,再把产生的消息、定时器或交付操作加入队列。映射表保存每个模拟节点的状态,使一台机器可以运行约 2000 个节点。

测试开始时,会选择一些节点在不同时间广播,并将这些事件加入队列。处理一次广播又可能产生更多发送、定时器请求和应用交付。循环不断处理后续事件,直到队列清空。
时间是虚拟的。模拟器不会等待真实定时器,而是跳到下一个事件的时间,并告诉协议定时器已经触发。这让一长串定时行为能够快速执行和重复。
故障与取舍
模拟可以注入故障、测量不同选择,但集成本身仍需要测试。

故障也是事件。测试可以让节点下线、丢弃发往该节点的消息、通知其他节点,并在之后恢复它。这样,我们就能检查协议对选定故障条件的响应。
我们还修改了 Plumtree,使它适应 Corrosion,因此需要测试修改带来的影响。论文采用的成员信息机制与我们的做法不同:在 Corrosion 中,节点知道完整成员列表。我们可以改变对等节点选择方式,例如是否偏向附近节点,并测量对广播交付时间的影响。
这种确定性测试使我们能够在上线前运行和重放协议行为。它不是多宇宙调试器,但提供了有用的本地模拟,以及系统地模拟外部副作用的方法。
这里有一个重要限制:测试协议核心并不能证明集成正确。如果调用方在发送时阻塞,可能无法及时将收到的消息传给协议。因此,周边集成仍需要自己的测试。
这种方法通常称为 Sans I/O。它不会消除复杂性,而是明确边界和责任。根据协议的不同,将请求的动作与副作用分离,可以让库更容易理解和复用。
我们还由应用注入随机性。生产环境使用操作系统的随机数,测试则使用带种子的随机源,使运行可复现。调用方必须提供环境并理解如何传入事件,因此设计增加了一些集成工作,但在我们的案例中,测试收益值得付出这些成本。
同频道的其他文章
40:04EN → 简体中文
51:06EN → 简体中文
39:12EN → 简体中文
22:33EN → 简体中文
34:11EN → 简体中文
28:15EN → 简体中文