形式化验证与 TLA+:行为的数学建模
2015 年 4 月,AWS 的工程师在《Communications of the ACM》上发表了一篇论文1,记录了 TLA+ 在 DynamoDB 复制协议中发现的一个极端 bug——它经过了设计审查、代码审查和大量测试,没有人发现它。TLC 模型检验器给出的最短反例轨迹包含 35 个高层步骤。论文指出:在足够大的规模下,这种复合事件的低概率并不能构成防御——历史上 AWS 在生产环境中确实观察过同等复杂的组合。
本文讨论的问题只有一个:怎样在代码落地之前,用数学方法证明一个系统设计没有逻辑错误。
一、测试的边界
先看一个简单的问题:一个全局计数器 counter,两个线程各自执行 100 次递增。最终 counter 的值是多少?
学过并发编程的人知道答案:不一定是 200。counter += 1 不是原子操作——它包含读、加、写三步。两个线程同时读到同一个值,各自加 1 后写回,结果相当于丢失了一次递增。
这是并发编程中最基础的竞态条件。常规对策是加锁、用原子指令、或者按并发模型重新设计。
但这里有一个更根本的问题:你怎么确认你的对策是正确的?
测试可以让你更自信,但无法穷举。两个线程各执行 100 步——不考虑更复杂的交错,仅指令级的交错组合就已经是天文数字。线程数更多、步骤稍复杂之后,状态空间急剧膨胀,任何基于采样的验证手段都碰不到边界。
这不是测试方法论的问题,也不是工程师不够认真。这是一个组合数学问题:
系统的状态空间随并发组件的数量呈指数增长——这叫状态空间爆炸(state space explosion)。
2015 年,Kyle Kingsbury 在 Jepsen 分布式系统正确性分析中对 MongoDB 的复制协议进行了系统性测试2。当时 MongoDB 使用的复制协议(protocol v0)以 wall-clock 时间戳作为操作排序的依据。在时钟偏斜的网络分区场景下,一个被隔离的节点如果时钟较快,可能在选举中胜出,导致已经被多数派确认的写入被静默丢弃。Jepsen 的测试显示:在 4525 次以 majority write concern 提交的写入中,有 93 次被确认后丢失。这不是一个可以通过增加测试用例数量来解决的问题——它是协议设计的逻辑缺陷。
另一个更著名的案例是 Therac-25 放射治疗机事故(1985-1986)3:软件中的竞态条件导致 6 名患者接受了超过百倍剂量的辐射,其中 3 人死亡。事后调查发现,触发条件依赖于操作员在特定时间窗口内按下特定按键组合——在测试期间从未被复现。
