为什么需要更加智能的猴子
在 Chaos Tools and Techniques for Testing the TiDB Distributed NewSQL Database 这篇文章里面,我提到了我们现在正在参考 Netflix Automating Failure Testing Research at Internet Scale 这篇 Paper,做一些探索性的工作,主要就是想让我们的测试发现更多的 bug。
这里,首先来说说为啥需要更加智能的猴子。在 TiDB 的测试里面,我们会随机的注入很多错误,来看系统是否符合预期,但现在这些其实是很初级的,很多注入其实是我们自己手工设计,然后按照一定的规则去跑的,这样其实并不能够保证所有的分支能够覆盖到。所以,如果这时候,有一只更加智能的猴子,它可以把你所有的代码和部署架构都爬一遍,自动的做一些坏事,这将是多么美好的一件事情。
为了达成这样的目标,一个简单的做法就是采用最常见的 brute force,也就是暴力遍历所有的分支,然后生成相应的规则去注入测试。但这个通常只对比较简单的系统有效果。假设一个超级大的系统有 100 个组件,我们的搜索空间就是 2 的 100 次方,这个是一个非常恐怖的数量了,所以我们需要一种更好的方式来限制我们的搜索空间。
Lineage-driven Fault Injection
Netflix 就面临着这样的问题,然后他们就发现了 Lineage-driven Fault Injection,然后找到这篇 Paper 的作者 Peter Alvaro 跟他们一起合作,看能不能将 Netflix 的猴子弄得更加智能。
Lineage-driven Fault injection 的目标是发现容错系统的 bug,一个不能容错的系统,其实我们也没必要去折腾,因为谁都知道,它挂掉整个系统就不能服务了。在论文里面,作者将其叫做 Molly。
对于 Molly 来说,核心思想就是根据系统正确的输出,来回溯,是否有 failures 能够让系统没法正确的输出。
Molly 流程如下:
- 从一次成功的结果开始,反向推倒。
- 问为什么能产生这次正确的结果,形成一个 Lineage。
- 将这个 Lineage 编码成一个布尔表达式
- 对这个布尔表达式求解,生成一个错误假设
- 进行注入测试
- 不停的重复直到发现一个容错的 bug
Lineage Example
这么说实在是有点太抽象,所以我还是决定直接用 Netflix 自己举得例子来做一次重复的说明。假设有一个服务,需要进行如下的处理:
这里,A 代表 API,R 代表 Ratings,P 代表 Playlist,B 代表 Bookmark。(A or R or P or B)
表示我们可以随机选择 A,R,P, 或者 B 一个点来进行错误注入,那么可能会有如下几种结果:
- 请求失败了,也就是我们发现了一个 failure,那么我们就可以裁剪包括这个 failure 的搜索空间了,因为只要包括这个 failure 的,就一定会失败。
- 请求成功了,也就是这个 failure point 并不是关键的点。
- 请求成功了,但这时候是一个后备系统来处理了这次 failure。也就是我们俗称的 failover 或者 fallback。
这里我们选择 R 进行注入,发现请求成功了,那么就能得到下面的图:
在这个图的公式 (A or P or B) and (A or P or B or R)
里面,右边的 (A or P or B or R)
就是最开始的公式,然后对 R 进行取反去掉了,就得到 (A or P or B)
,然后用 and
组合起来。
然后我们队 P 进行注入,得到下面图:
这里虽然对 P 进行了注入,但请求仍然能够成功,因为我们有一个备份系统 PlaylistFallback,也就是上面说的情况 3 的情况,也就是说,这里我们有了一个新的点需要去注入,也就是 PF。整个公式变成了 (A or PF or B) and (A or P or B) and (A or P or B or R)
。
然后我们不停的重复上面的过程,直到找不到注入点了。对于上面的例子,我们假设已经没有新的注入点了,那么我们最终的注入集合就是 [{A}, {PF}, {B}, {P, PF}, {R, A}, {R, B}...]
,然后我们就开始实际的参考这些集合进行注入。因为 Molly 针对的是 fault-tolerant 系统,也就是意味着,当我们对这些点进行错误注入的时候,如果整个请求失败,那就证明我们这个点并没有做到 fault-tolerant,我们需要改进了。
挑战
虽然 Molly 原理比较简单,但 Netflix 在实践的时候,还是遇到了一些问题,
第一个就是如何定义『成功』。上面说到,Molly 是会根据成功正确的输出来进行反向推导,但我们如何去定义这个『成功』?通常,我们可以依赖 HTTP status,譬如 200 就知道是成功了,但有一些系统,它失败也仍然会返回 200,只是把 error message 放到 HTTP body 里面,这个就比较麻烦。另外,有时候,返回 200 并不一定代表没问题,譬如用户等了很长一段时间才收到 200,所以需要更加多维度的指标来评估『成功』,在 Netflix 里面,现在很多都通过 metric 来进行衡量。
另一个问题就是『幂等性』,因为 Molly 需要的是一个可重放的请求,但大家都知道,『人不能两次踏进同一条河流』,一个请求重现的时候也可能不会走到相同的路径,但 Molly 却需要保证。为了解决这个问题,Netflix 采用了一个非常原始但足够简单高效的做法,就是对用 Falcor 框架生成的请求做 mapping。
还有一个问题就是测试的时候不能太激进,注入太频繁有可能影响线上系统了,这里我真的很佩服 Netflix,敢拿用户当小白鼠测试,我们可没这么大的胆量。所以只能自己内部吃自己的狗食。
我们能做什么?
Netflix 跟 Peter Alvaro 的合作成果还是比较不错的,他们发现了几个非常隐藏的 bug。那么这些对我们有什么借鉴意义呢?
对于 TiDB 来说,我们的组件其实比较简单,加上周边的生态工具组合还不到 10 个,这个量级的服务其实用 brute force 就可以了。而我们也定义好了我们自己的 Lineage,进行注入。当然,Molly 这套我们也在研究,只是短期觉得没必要这么复杂。
另一个可以参考的就是 Molly 在出错的时候会生成 failure trace 以及提供非常详细的信息,这个我们已经借鉴用在了自己的内部测试系统上面。
我个人其实还有一个想法,算是比较激进的,就是跟社区合作,看能不能将我们我们的测试服务跟使用 TiDB 的用户进行整合。也就是说,我们不光测试 TiDB,也帮助大家一起测试完善整个系统。
自由和责任
写在最后,在 Applying Failure Testing Research 里面,提到了 Netflix 的文化,就是 “Freedom and Responsibility”,翻译成中文就是『自由和责任』。在 Netflix 这个公司,会给与程序员非常大的自由去干你想干的事情,但同时,你也必须承担起相应的责任,这些事情并不是随便让你玩玩,就当是过家家的,你有责任去将你的事情落地,变成能更好的帮助公司发展的生产力,即使最终证明这件事情不能 work。我个人觉得,就是因为有这样的文化,才促使 Netflix 在技术和商业上面能有这么多突破。
在 PingCAP,自由和责任也是一种文化,我们鼓励大家去自由折腾,但同时也明确要求大家需要对自己的折腾负责。也正是因为这样,才让我们无论是在技术上面,还是市场上面,都在高速迭代和发展。如果你认同我们这样的文化,欢迎联系我:tl@pingcap.com