本文以近期开源的Dragonboat多组Raft库为例,介绍Dragonboat这样一个典型分布式系统是如何做测试的。Dragonboat以Go实现,能在普通硬件上提供每秒1000万次以上的强一致读写,它是目前github.com上速度最快的功能完整的多组Raft开源库。欢迎大家试用,并请点Star支持:Dragonboat
最大的误导
常看到有系统吹捧自己可靠的方法是说某大型活动用了它,或说是某某公司某内部项目用了,从而得出可靠的结论,生产环境俨然成了廉价公关软文口中的测试平台。其实众所周知,某活动全场意外当机重启的节点数少之又少,磁盘毁损一整年才2-4%,而故障性的网络分区在很多DevOps岗的整个职业生涯里也就遇到几次而已,以至于多年来各一线公司网络分区造成故障的故事收集起来也才写满几页纸。某活动扛住了或是某项目用了,这些完全不是软件可靠与否的充要条件。
事实其实是残酷的。曾阅读过国内排名前四的某司一个共识库,30分钟代码读下来找到多处数据丢失毁损的bug,实现则是很典型的那种打死也不肯写测试的全裸奔模式。对于软件,任何无法用代码来验证的廉价营销式说辞,不说、不看、不听:
相较于廉价的文宣,Dragonboat对系统正确性满心敬畏,老老实实以完备的测试方案、开源的测试代码、公开可验证的测试结果数据三者来提供切实的保障。
常规测试
常规测试部分,Dragonboat做了:
- 数万行全手写测试代码,Raft协议3000行核心代码拥有过万行测试代码护航
- Go内建的race detector测试
- 各类静态检查,及早发现如错误返回值未处理等可能问题
- 使用go-fuzz对所有网络与本地输入做随机输入fuzz测试
Dragonboat内各Package的测试覆盖率基本均在90%以上。以Raft协议实现来看,不正确地删改一行代码基本就能触发多个测试错误。这些测试代码,连同下面要介绍的monkey testing,nightly build时在race detector被打开的情况下运行。对于长期被人诟病的Golang的error处理方式,的确容易因为人为疏忽造成error返回有漏检的可能,但仅gometalinter一个软件收录的静态检测工具就有多种能对付它。对各输入的Fuzz testing初听来或许有些多此一举,但一跑Fuzz testing几十秒就发现bug的例子比比皆是,Dragonboat开发中也曾遇到。
基于Raft协议的自测
Raft协议对内部数据有严格限定要求,比如显而易见的就有:
- 所有entry的Index值始终应该是连续且严格递增的
- 所有entry的Term值应该是单向的
- 当Index与Term确定时,entry内容是唯一确定的
这些都提供较小代价下运行时自测的机会。仅以第一项为例,在Dragonboat中它被落实到多个点位上,对应用透明的进行自测:
- 在节点完成了协议规定的检查,即将append log时
- 在entry被commit以后,准备由Raft协议返回供复制状态机执行时
- 复制状态机即将执行entry,由该entry Index对比当前最新已执行的entry的Index值时
这些自测在Dragonboat中无法通过任何设置予以关闭,甚至在Benchmark跑分时也严格限定必须进行。
磁盘文件IO测试
磁盘文件IO要做正确有多难,可以先看两个事实:
假设文件系统的可靠是天经地义的吧?很遗憾,这种假设也是高危动作。
TS Pillai的这张总结图表直观显示文件IO做正确有多难。
为解决这些磁盘文件IO的挑战,Dragonboat首先选择不自作聪明的到处自己去做文件操作,把存储尽可能交给RocksDB,并对基于RocksDB的系统加以各类测试:
- 使用ScyllaDB的charybdefs实现的磁盘错误注入测试
- 使用自动开关的掉电数据完整性测试
前者可以模拟诸如RocksDB试图读一个sst文件的内容时第二次读操作返回错误,帮助检查Dragonboat是否按照设计正确地处理这样的IO错误。掉电测试检查fsync是否被正确配置(如MacOS上是否fcntl(fd, F_FULLFSYNC)了)与调用,是否IO逻辑上有丢数据的问题。
看了上述介绍,可能有人觉得这是小题大做,从Turbo C就开始玩的文件操作有啥难?hehe,文件操作方面是git、ZooKeeper的作者经验多,还是您更牛?
Monkey Testing
Monkey Testing有时也称为Chaos Engineering,目的在于自动测试系统在各组件失效当机情况下系统是否依旧能按设计提供应有的服务。与Fuzz testing的随机数据输入不同,Monkey Testing / Chaos Engineering着眼于随机破坏性事件对系统的影响。
在Dragonboat的monkey testing中,各种随机破坏性事件的组合被注入到一个多节点的测试环境里,在一年多的自动测试期间,导致了百亿数量级次数的Raft节点重启事件,发现并修正了大量Raft协议实现与相关辅助功能的bug。整个测试流程及其耗时,堪称地狱模式。具体的,在monkey testing中,被注入的随机事件有:
- 随意的停止各节点
- 随意删除节点所有Raft数据
- 随意丢弃传输中的消息
- 随意网络分割节点暂时阻断通讯
在上述大量注入的随机破坏性事件前提下,同时在上述多节点测试环境上运行大量Raft组实例,进行Raft的读写测试。该monkey testing环境同时内建一组三个节点的Drummer系统,三个Drummer节点观测、维护各Raft组健康信息,并在发现Raft组的成员失效以后,试图在其它节点上通过Raft组成员变更,新增并启动一个新的Raft成员,替换已失效的Raft成员。
上述三节点的Drummer本身也是一个基于Dragonboat的Raft实现的无单点系统,且在monkey testing中同样会被注入上述随机错误。Drummer的上述监控、修复Raft组的业务逻辑是在自身同样面对大量被注入的随机破坏性事件的前提下完成的,这进一步验证了此类具体实际业务逻辑下,Dragonboat的Raft实现的可靠性。
在一个节点平均存活仅几分钟的情况下,在几台服务器上每晚便可完成千万次量级的节点随机失效与重启测试。在此及其严酷的测试环境中,同时向系统施加Raft读写请求,配合大量后台的Raft快照保存与快照恢复操作,严格的后验检查确保:
- Jepsen的Knossos和porcupine检查,绝无违反称为linearizability的强一致性
- Raft组在有Quorum的时候需可用
- 用户应用状态机状态一致
- Raft组成员一致
- 磁盘上保存的Raft Entry Log一致
一部分Jepsen可读格式的edn log已被公布,可供大家使用各自选定的Linearizability checker检验:https://github.com/lni/knossos-data