不用说,这又是一篇带有作者浓厚感情色彩的招聘帖子。这次开的招聘贴是 Test 架构开发工程师。根据这么多年的观察,我发现很多程序员是不喜欢测试的,总觉得测试是一个费时费力不讨好,不能提升自己开发能力、技术能力的事情。对于这个,我只能说,『呵呵,too young,too simple!』。
我本人甚至坚信:『无论做什么系统,尤其是分布式系统,测试是基石。尤其是在我们这边,测试是一个非常核心的产品竞争力。』作为一个分布式数据库,你如何让用户放心把数据存到你的系统上面,让用户相信他们的数据是安全的,这个很大程度上面只能靠测试。所以你看,测试的重要性不言而喻了。
那么,作为一个测试架构开发的工程师,到底要做什么事情呢?
常规测试
Unit Test
首先,我们来看看最基本的测试,也就是大家非常熟悉的,单元测试。单元测试看起来很简单,但门道可不少,要写好其实不容易。弄得不好,就容易陷入一个误区,就是为了提高测试覆盖率,加了很多自以为很不错但其实特别复杂的测试,给维护带来了很严重的负担。
关于如何写单元测试,网上有很多介绍,就不详细说明了。这里提一个我自己喜欢的方式,就是 Table Driven Test。对于一个函数,可能有很多不同的输入参数,会 cover 到不同的分支,对应不同的输出结果,通常的写法很可能就是为每一种输入,调用对应函数,在判断结果,重复代码很多,这里用 Table Driven 的方法就比较清晰明了。
另外,现在我们还碰到的一个测试问题其实就是对一些逻辑里面 timeout 的处理,最通用的做法就是在外面 sleep 一段时间,等到 timeout 触发相关的逻辑,再判断。但这个其实是有问题的,在一些 CI 系统,尤其是 Travis CI,给你的测试资源是非常少的,有可能 timeout 的那个线程跑的慢,导致外面 sleep 结束的时候还没触发到相应的逻辑,结果测试失败。如何更优雅的去解决这些问题,都是单元测试的挑战。
Integration Test
说完了,单元测试,我们来说说集成测试,毕竟单元测试只能是针对特定的模块的,整体能够一起工作还是要靠集成测试。集成测试其实就跟自己的业务相关度比较大了。但因为我们是 MySQL 兼容协议,所以非常幸运的,我们能重用市面上面很多的测试案例,当跑过了这些测试案例,就很大程度上面能让我们确定,我们的兼容性已经很不错了,譬如我们在很早的时候就跑过了 sqllogic test,这个的 case 集可是非常的恐怖的。
当然,光有现成的还是不够的,我们还要学着自己造轮子,也就是根据我们实际的业务特别,自动的生成输入输出,打到我们的系统里面,验证结果。譬如,我们可以构造一个银行转账的 case,100 个人,每个人初始 100 块钱,两两互相随机转账,无论什么时候,总的金额一定是 10000 元,如果多了或者少了,证明我们的程序就有 bug 了。
Hello Chaos
上面的测试,顶多就是功能性的测试,验证下我们的程序在正常的情况下面是不是有问题,如果只是这样,那这个产品是不靠谱的。尤其是在分布式领域,系统的不确定性太多了,以至于我们需要更加激进一点,给系统里面放一只猴子来捣乱。
Failpoint
最开始前面我提到了单元测试,虽然单元测试的覆盖率是一个很重要的指标,但有些时候,一些逻辑无论你的单元测试怎么写,都很难覆盖到,因为它需要很多错误条件来触发。这里,我们就可以直接使用另一个机制,Failpoint。
Failpoint 的原理很简单,就是在一些关键代码地方,注入一个 failure 代码,里面有一些条件,当系统运行到这些 failure 代码里面并且发现相应的条件已经满足,就直接执行 failure 定义的操作,可能是 sleep 10,也可能是直接 panic 这样的。
更进一步,当我们程序里面注入了非常多的 failure 之后,我们可以显示的在程序运行的时候,准备好一系列 failure list,依次的发给程序去触发 failure,如果这时候程序出现了非预期的行为,譬如我上面提到的转账总金额不对,那么我们就知道程序有 bug,而刚好,我们可以用发现问题的这个 failure list 来复现。
Failure Injection
Failpoint 这是代码级别的注入,除开代码,我们还可以做的更多。对于分布式系统来说,网络分区其实是一个很常见的问题,所以我们可以直接在测试的时候使用 iptables 和 tc 这样的工具来进行网络干扰,譬如增加 latency,将一些节点隔离,或者将一些节点弄成单向不通这样。或者用 iperf,nc 这样的工具把网络带宽打满,看系统的反映。
另一个要考虑的就是磁盘损坏,毕竟坏盘太常见了,这里我们可以让系统跑在 fuse filesystem,随机的让文件 API 返回错误,也可以使用 debugfs 来干这个事情。当然,还有更极端的,直接 rm 干掉一些数据文件,或者强制将一些文件弄坏,观察系统的反映。
Linearizability Test
因为我们的系统是一个强一致性的系统,所以一定要满足线性一致性。现阶段,Jepsen 是这块测试的最通用的选择,Jepsen 的原理也是很简单的,启动集群,对系统注入 failure,同时跑一系列的操作,最后恢复集群,验证这些操作是否满足线性一致性。虽然简单,但 Jepsen 已经给很多系统发现了 bug,以至于几乎所有的分布式系统现在都要有 Jepsen 的测试了,当然也包括我们。
后面我们会在 Jepsen 上面添加更多的 case,同时也会基于一个纯 Go 的 Porcupine 项目来进行线性一致性的验证,毕竟 Go 是我们这边最熟悉的语言。
Chaos Engineering
好了,我们再进一步了,直接将上面这些上升到工程吧。这个也就是我之前提到的混沌工程,我们先观察系统的正常状态,然后开始根据一些预先设计的 case 来干扰系统,观察系统的反映,从而在系统真的出现问题的时候,能快速知道到底是哪里引起的。
更牛逼的,是学习 Netflix,根据现有系统的稳态,直接学习,自动的生成 failure injection,观察系统是不是出现了问题。
可以看到,在这里,只要你敢想,没什么不能做的,就怕你的想象力不丰富,想的东西没把系统搞死。
Enhancement
通常弄到 Chaos Engineering 这一个阶段,系统的稳定性已经很不错了,但我个人觉得我们还应该做的更多,所以也开始了一些理论研究的工作。
TLA+
对于很多功能实现来说,第一个问题,可能就是,『你如何证明你的设计是对的?』对于这个,TLA+ 以及相关的 Coq 等就是一个非常好的工具。
关于 TLA+,大家可以去看相关的介绍,现阶段最流行的两个一致性协议,Paxos 和 Raft 都经过了 TLA+ 的认证,我们这边也进行了很多相关的工作,譬如使用 TLA+ 来证明分布式事务的设计的正确性。当然,后面我们还需要做的更多,我们的目标是对于现有系统的很多设计,以及后续的设计,都使用 TLA+ 来进行证明。
Symbolic Execution
当我们代码里面有很多的异常处理,或者 panic 的时候,一个比较关心的问题就是我们在什么时候才会进入到这样的异常逻辑。当然,可以通过 failpoint 来,但有时候,如果我们直接通过分析代码,就能知道什么样的参数能触发到这样的逻辑,这个就很减轻很多测试工作。现阶段,我们正在研究 symbolic execution 的就是在干这个事情。
总结
上面就是现在我们正在进行的,以及未来后面会做的事情,可以看到,东西还是非常多的,挑战还是非常大的。这些如果都弄出来,对大家的能力的锻炼会非常的大,当然也能更好的提升我们自己产品的质量。
如果你对这块有很浓厚的兴趣,喜欢折腾瞎搞系统,同时喜欢理论证明,欢迎联系我。我的邮箱 [email protected]。