***(PingCAP)分布式系统测试那些事

(唐刘)分布式测试框架jepsen

2015-12-12 siddontang(唐刘@PingCAP) Hello Clojure - Jepsen

***(PingCAP)分布式系统测试那些事_第1张图片

jepsen是一个分布式测试库,我们可以使用它对某个分布式系统执行一系列操作,并最终验证这些操作是否正确执行。

jepsen已经成功验证了很多分布式系统,我们可以在它的源码里面看到相关系统的测试代码,包括mysql-cluster,zookeeper,elasticsearch等。

为什么要研究jepsen,主要在于我们需要进行分布式数据库tidb的测试,自己写一套分布式测试框架难度比较大,并且还不能保证在分布式环境下面自身测试框架是不是对的,于是使用一个现成的,经过验证的测试框架就是现阶段最好的选择了,于是我们就发现了jepsen。

jepsen是使用clojure进行开发的,所以这也就是为什么我要学习clojure的原因,不过比较郁闷的是,学了几天,还是没有看懂太多的代码,只能慢慢不断摸索了。

(略)


(刘奇)《深度探索分布式系统测试》

摘要:本话题系列文章整理自 PingCAP NewSQL Meetup 第 26 期刘奇分享的《深度探索分布式系统测试》议题现场实录。文章较长,为方便大家阅读,会分为上中下三篇,本文为上篇。

2016-11-01 刘奇分布式系统测试那些事儿(上)理念
2016-11-10 刘奇 分布式系统测试那些事儿(中)错误注入
2016-12-07 刘奇分布式系统测试那些事儿(下)信心的毁灭与重建

今天主要是介绍分布式系统测试。对于 PingCAP 目前的现状来说,我们是觉得做好分布式系统测试比做一个分布式系统更难。就是你把它写出来不是最难的,把它测好才是最难的。大家肯定会觉得有这么夸张吗?那我们先从一个最简单的、每个人都会写的 Hello world 开始。

A simple “Hello world” is a miracle
We should walk through all of the bugs in:

  • Compiler
  • Linker
  • VM (maybe)
  • OS

其实这个 Hello world 能够每次都正确运行已经是一个奇迹了,为什么呢?
首先,编译器得没 bug,链接器得没 bug ;
然后我们可能跑在 VM 上,那 VM 还得没 bug;
并且 Hello world 那还有一个 syscall,那我们还得保证操作系统没有 bug;
到这还不算吧,我们还得要硬件没有 bug。

所以一个最简单程序它能正常运行起来,我们要穿越巨长的一条路径,然后这个路径里面所有的东西都不能出问题,我们才能看到一个最简单的 Hello world。

但是分布式系统里面呢,就更加复杂了。比如大家现在用的很典型的微服务。假设你提供了一个微服务,然后在微服务提供的功能就是输出一个 Hello world ,然后让别人来 Call。

A RPC “Hello world” is a miracle
We should walk through all of the bugs in:

  • Coordinator (zookeeper, etcd)
  • RPC implementation
  • Network stack
  • Encoding/Decoding library
  • Compiler for programming languages or [protocol buffers, avro, msgpack, capn]

那么我们可以看一下它的路径。我们起码需要依赖 Coordinator 去做这种服务发现,比如用 zookeeper,etcd ,大家会感觉是这东西应该很稳定了吧?但大家可以去查一下他们每一次 release notes,里边说我们 fix 了哪些 bug,就是所有大家印象中非常稳定的这些东西,一直都在升级,每一次升级都会有 bug fix。但换个思路来看,其实我们也很幸运,因为大部分时候我们没有碰到那个 bug,然后 RPC 的这个实现不能有问题。当然如果大家深度使用 RPC,比如说 gRPC,你会发现其实 bug 还是挺多的,用的深一点,基本上就会发现它有 bug。还有系统网络协议栈,去年 TCP 被爆出有一个 checksum 问题,就是 Linux 的 TCP 协议栈,这都是印象中永远不会出问题的。再有,编解码,大家如果有 Go 的经验的话,可以看一下 Go 的 JSON 历史上从发布以来更新的记录,也会发现一些 bug。还有更多的大家喜欢的编解码,比如说你用 Protocol buffers、Avro、Msgpack、Cap'n 等等,那它们本身还需要 compiler 去生成一个代码,然后我们还需要那个 compiler 生成的代码是没有 bug 的。然后这一整套下来,我们这个程序差不多是能运行的,当然我们没有考虑硬件本身的 bug。

其实一个正确的运行程序从概率上来讲(不考虑宇宙射线什么的这种),已经是非常幸运的了。当然每一个系统都不是完善的,那通常情况下,为什么我们这个就运行的顺利呢?因为我们的测试永远都测到了正确的路径,我们跑一个简单的测试一定是把正确的路径测到了,但是这中间有很多错误路径其实我们都没有碰到。然后我不知道大家有没有印象,如果写 Go 程序的时候,错误处理通常写成 if err != nil,然后 return error ,不知道大家写了多少。那其它程序、其它的语言里就是 try.catch,然后里面各种 error 处理。就是一个真正完善的系统,最终的错误处理代码实际上通常会比你写正常逻辑代码还要多的,但是我们的测试通常 cover 的是正确的逻辑,就是实际上我们测试的 cover 是一小部分。

那先纠正几个观念,关于测试的。就是到底怎么样才能得到一个好的、高质量的程序,或者说得到一个高质量的系统?

Who is the tester ?

  • Quality comes from solid engineering.
  • Stop talking and go build things.
  • Don’t hire too many testers.
    • Testing is owned by the entire team. It is a culture, not a process.
  • Are testers software engineers? Yes.
  • Hiring good people is the first step. And then keep them challenged.

我们的观念是说先有 solid engineering 。我觉得这个几乎是勿庸置疑的吧,不知道大家的经验是什么?然后还有一个就是不扯淡,尽快去把东西 build 起来,然后让东西去运转起来。我前一段时间也写了一个段子,就是:“你是写 Rust 的,他是写 Java 的,你们这聊了这么久,人家 Rust (编译速度慢) 的程序已经编译过了,你 Java 还没开始写。”原版是这样的:“你是砍柴的,他是放羊的,你们聊了一天,他的羊吃饱了,你的柴呢?”然后最近还有一个特别有争议的话题:CTO 应该干嘛。就是 CTO 到底该不该写代码,这个也是众说纷纭。因为每一个人都受到自己环境的局限,所以每个人的看法都是不一样的。那我觉得有点像,就是同样是聊天,然后不同人有不同的看法。

Test automation

  • Allow developers to get a unit test results immediately.
  • Allow developers to run all unit tests in one go.
  • Allow code coverage calculations.
  • Show the testing evolution on the dashboards.
  • Automate everything.

我们现在很有意思的一个事情是,迄今为止 PingCAP 没有一个测试人员,这是在所有的公司看来可能都是觉得不可思议的事情,那为什么我们要这么干?因为我们现在的测试已经不可能由人去测了。究竟复杂到什么程度呢?我说几个基本数字大家感受一下:我们现在有六百多万个 Test,这是完全自动化去跑的。然后我们还有大量从社区收集到的各种 ORM Test,一会我会提到这一点。就是这么多 Test 已经不可能是由人写出来的了,以前的概念里面是 Test 是由人写的,但实际上 Test 不一定是人写的,Test 也是可以由机器生成的。举个例子,如果给你一个合法的语法树,你按照这个语法树去做一个输出,比如说你可以更换变量名,可以更换它的表达式等等,你可以生成很多的这种 SQL 出来。

Google Spanner 就用到这个特性,它会有专门的程序自动生成符合 SQL 语法的语句,然后再交给系统去执行。如果执行过程中 crash 了,那说明这个系统肯定有 bug。但是这地方又蹦出另外一个问题,就是你生成了合法的 SQL 语句,但是你不知道它语句执行的结构,那你怎么去判断它是不是对的?当然业界有很聪明的人。我把它扔给几个数据库同时跑一下,然后取几个大家一致的结果,那我就认为这个结果基本上是对的。如果一个语句过来,然后在我这边执行的结果和另外几个都不一样,那说明我这边肯定错了。就算你是对的,可能也是错的,因为别人执行下来都是这个结果,你不一样,那大家都会认为你是错的。

所以说在测试的时候,怎么去自动生成测试很重要。去年,在美国那边开始流行一个新的说法,叫做 “怎么在你睡觉的时候发现 bug”。那么实际上测试干的很重要的事情就是这个,就是自动化测试是可以在你睡觉的时候发现 bug。好像刚才我们还提到 fault injection ,好像还有 fuzz testing。然后所有测试的人都是工程师,因为只有这样你才不会甩锅。

这是我们现在坚信的一个事情,就是所有的测试必须要高度的自动化,完全不由人去干预。然后很重要的一个就是雇最优秀的人才,同时给他们挑战,就是如果没有挑战,这些人才会很闲,精力分散,然后很难合力出成绩。因为以现在这个社会而言,很重要一个特性是什么?就是对于复杂性工程需要大量的优秀人才,如果优秀的人才力不往一处使力的话,这个复杂性工程是做不出来的。我今天看了一下龙芯做了十年了,差不多是做到英特尔凌动处理器的水平。他们肯定是有很优秀的人才,但是目前还得承认,我们在硬件上面和国外的差距还比较大,其实软件上面的差距也比较大,比如说我们和 Spanner 起码差了七年,2012 年 Spanner 就已经大规模在 Google 使用了,对这些优秀的作品,我们一直心存敬仰。

我刚才已经反复强调过自动化这个事情。不知道大家平时写代码 cover 已经到多少了?如果 cover 一直低于 50%,那就是说你有一半的代码没有被测到,那它在线上什么时候都有可能出现问题。当然我们还需要更好的方法去在上线之前能够把线上的 case 回放。理论上你对线上这个回放的越久你就越安全,但是前提是线上代码永远不更新,如果业务方更新了,那就又相当于埋下了一个定时炸弹。比如说你在上面跑两个月,然后业务现在有一点修改,然而那两个又没有 cover 住修改,那这时候可能有新的问题。所以要把所有的一切都自动化,包括刚才的监控。比如说你一个系统一过去,然后自动发现有哪些项需要监控,然后自动设置报警。大家觉得这事是不是很神奇?其实这在 Google 里面是司空见惯的事情,PingCAP 现在也正在做。

Well… still not enough ?

  • Each layer can be tested independently.
  • Make sure you are building the right tests.
  • Don’t bother great people unless the testing fails.
  • Write unit tests for every bug.

这么多还是不够的,就是对于整个系统测试来讲,你可以分成很多层、分成很多模块,然后一个一个的去测。还有很重要的一点,就是早期的时候我们发现一个很有意思的事情。就是我们 build 了大量 Test,然后我们的程序都轻松的 pass 了大量的 Test,后来发现我们一个 Test 是错的,那意味着什么?意味着我们的程序一直是错的,因为 Test 会把你这个 cover 住。所以直到后来我们有一次觉得自己写了一个正确的代码,但是跑出来的结果不对,我们这时候再去查,发现以前有一个 Test 写错了。所以一个正确的 Test 是非常重要的,否则你永远被埋在错误里面,然后埋在错误里面感觉还特别好,因为它告诉你是正确的。

还有,为什么要自动化呢?就是你不要去打扰这些聪明人。他们本身很聪明,你没事别去打扰他们,说“来,你过来给我做个测试”,那这时候不断去打扰他们,是影响他们的发挥,影响他们做自己的挑战。

这一条非常重要,所有出现过的 bug,历史上只要出现过一次,你一定要写一个 Test 去 cover 它,那这个法则大家应该已经都清楚了。我看今天所在的人的年龄,应该《圣斗士星矢》是看过的,对吧?这个圣斗士是有一个特点的,所有对他们有效的招数只能用一次,那这个也是一样的,就保证你不会被再次咬到,就不会再次被坑到。我印象中应该有很多人 fix bug 是这样的:有一个 bug 我 fix 了,但没有 Test,后来又出现了,然后这时候就觉得很奇怪,然后积累的越多,最后就被坑的越惨。

这个是目前主流开源社区都在坚持的做法,基本没有例外。就是如果有一个开源社区说我发现一个 bug,我没有 Test 去 cover 它,这个东西以后别人是不敢用的。

Code review

  • At least two LGTMs (Looks good to me) from the maintainers.
  • Address comments.
  • Squash commit logs.
  • Travis CI/Circle CI for PRs.

简单说一下 code review 的事情,它和 Test 还是有一点关系,为什么?因为在 code review 的时候你会提一个新的 pr,然后这个 pr 一定要通过这个 Test。比如说典型的 Travis CI,或者 CircleCI 的这种 Test。为什么要这样做呢?因为要保证它被 merge 到 master 之前你一定要发现这个问题,如果已经 merge 到 master 了,首先这不好看,因为你要 revert 掉,这个在 commit 记录上是特别不好看的一个事情。另外一个就是它出现问题之前,你就先把它发现其实是最好的,因为有很多工具会根据 master 自动去 build。比如说我们会根据 master 去自动 build docker 镜像,一旦你代码被 commit 到 master,然后 docker 镜像就出来了。那你的用户就发现,你有新的更新,我要马上使用新的,但是如果你之前的 CI 没有过,这时候就麻烦了,所以 CI 没过,一定不能进入到 CD 阶段。

Who to blame in case of bugs?
The entire team.

另外一个观念纠正一下,就是出现 bug 的时候,责任是谁的?通常我见过的很多人都是这样,就说“这个 bug 跟我没关系,他的模块的 bug”。那 PingCAP 这边的看法不一样,就是一旦出现 bug,这应该是整个 team 的责任,因为你有自己的 code review 机制,至少有两个以上的人会去看它这个代码,然后如果这个还出现问题,那一定不是一个人的问题。

除了刚才说的发现一些 bug,还有一些你很难定义,说这是不是 bug,怎么系统跑的慢,这算不算 bug,怎么对 bug 做界定呢?我们现在的界定方式是用户说了算。虽然我们觉得这不是 bug,这不就慢一点吗,但是用户说了这个东西太慢了,我们不能忍,这就是 bug,你就是该优化的就优化。然后我们团队里面出现过这样的事情,说“我们这个已经跑的很快了,已经够快了”,对不起,用户说慢,用户说慢就得改,你就得去提升。总而言之,标准不能自己定,当然如果你自己去定这个标准,那这个事就变成“我这个很 OK 了,我不需要改了,可以了。”这样是不行的。

Profiling

  • Profile everything, even on production
    • once-in-a-lifetime chance
  • Bench testing

另外,在 Profile 这个事情上面,我们强调一个,即使是在线上,也需要能做 Profile,其实 Profile 的开销是很小的。然后很有可能是这样的,有一次线上系统特别卡,如果你把那个重启了,你可能再也没有机会复现它了,那么对于这些情况它很可能是一辈子发生一次的,那一次你没有抓住它,你可能再也没有机会抓住它了。当然我们后面会介绍一些方法,可以让这个能复现,但是有一些确实是和业务相关性极强的,那么可能刚好又碰到一个特别的环境才能让它出现,那真的可能是一辈子就那么一次的,你一定要这次抓住它,这次抓不住,你可能永远就抓不住了。因为有些犯罪它一辈子只犯一次,它犯完之后你再也没有机会抓住它了。

Embed testing to your design

  • Design for testing or Die without good tests
  • Tests may make your code less beautiful

再说测试和设计的关系。测试是一定要融入到你的设计里面,就是在你设计的时候就一定要想这个东西到底应该怎么去测。如果在设计的时候想不到这个东西应该怎么测,那这个东西就是正确性实际上是没法验证的,这是非常恐怖的一件事情。我们把测试的重要程度看成这样的:你要么就设计好的测试,要么就挂了,就没什么其它的容你选择。就是说在这一块我们把它的重要性放到一个最高的程度。

(未完待续...)

2016-11-10 刘奇 分布式系统测试那些事儿(中)错误注入

(接上篇)当然测试可能会让你代码变得没有那么漂亮,举个例子:


***(PingCAP)分布式系统测试那些事_第2张图片

这是知名的 Kubernetes 的代码,就是说它有一个 DaemonSetcontroller,这 controller 里面注入了三个测试点,比如这个地方注入了一个 handler ,你可以认为所有的注入都是 interface。比如说你写一个简单的 1+1=2 的程序,假设我们写一个计算器,这个计算器的功能就是求和,那这就很难注入错误。所以你必须要在你正确的代码里面去注入测试逻辑。再比如别人 call 你的这个 add 的 function,然后你是不是有一个 error?这个 error 的问题是它可能永远不会返回一个 error,所以你必须要人肉的注进去,然后看应用程序是不是正确的行为。说完了加法,再说我们做一个除法。除法大家知道可能有处理异常,那上面是不是能正常处理呢?上面没有,上面写着一个比如说 6 ÷ 3,然后写了一个 test,coverage 100%,但是一个除零异常,系统就崩掉了,所以这时候就需要去注入错误。大名鼎鼎的 Kubernetes 为了测试各种异常逻辑也采用类似的方式,这个结构体不算长,大概是十几个成员,然后里面就注入了三个点,可以在里面注入错误。

那么在设计 TiDB 的时候,我们当时是怎么考虑 test 这个事情的?

首先一个百万级的 test 不可能由人肉来写,也就是说你如果重新定义一个自己的所谓的 SQL 语法,或者一个 query language,那这个时候你需要构建百万级的 test,即使全公司去写,写个两年都不够,所以这个事情显然是不靠谱的。但是除非说我的 query language 特别简单,比如像 MongoDB 早期的那种,那我一个“大于多少”的这种,或者 equal 这种条件查询特别简单的,那你确实是不需要构建这种百万级的 test。但是如果做一个 SQL 的 database 的话,那是需要构建这种非常非常复杂的 test 的。这时候这个 test 又不能全公司的人写个两年,对吧?所以有什么好办法呢?MySQL 兼容的各种系统都是可以用来 test 的,所以我们当时兼容 MySQL 协议,那意味着我们能够取得大量的 MySQL test。不知道有没有人统计过 MySQL 有多少个 test,产品级的 test 很吓人的,千万级。然后还有很多 ORM, 支持 MySQL 的各种应用都有自己的测试。大家知道,每个语言都会 build 自己的 ORM,然后甚至是一个语言的 ORM 都有好几个。比如说对于 MySQL 可能有排第一的、排第二的,那我们可以把这些全拿过来用来测试我们的系统。

但对于有些应用程序而言,这时候就比较坑了。就是一个应用程序你得把它 setup 起来,然后操作这个应用程序,比如 WordPress,而后再看那个结果。所以这时候我们为了避免刚才人肉去测试,我们做了一个程序来自动化的 Record---Replay。就是你在首次运行的时候,我们会记录它所有执行的 SQL 语句,那下一次我再需要重新运行这个程序的时候怎么办?我不需要运行这个程序了,我不需要起来了,我只需要把它前面记录的 SQL record 重新回放一遍,就相当于是我模拟了程序的整个行为。所以我们在这部分是这样做的自动化。

那么刚刚说了那么多,实际上做的是什么?

实际上做的都是正确路径的测试,那几百万个 test 也都是做的正确的路径测试,但是错误的路径怎么办?很典型的一个例子就是怎么做 Fault injection。硬件比较简单粗暴的模拟网络故障可以拔网线,比如说测网络的时候可以把这个网线拔掉,但是这个做法是极其低效的,而且它是没法 scale 的,因为这个需要人的参与。

然后还有比如说 CPU,这个 CPU 的损坏概率其实也挺高的,特别是对于过保了的机器。然后还有磁盘,磁盘大概是三年百分之八点几的损坏率,这是一篇论文里面给出的数据。我记得 Google 好像之前给过一个数据,就是 CPU、网卡还有磁盘在多少年之内的损坏率大概是什么样的。

还有一个大家不太关注的就是时钟。先前,我们发现系统时钟是有回跳的,然后我们果断在程序里面加个监测模块,一旦系统时钟回跳,我们马上把这个检测出来。当然我们最初监测出这个东西的时候,用户是觉得不可能吧,时钟还会有回跳?我说没关系,先把我们程序开了监测一下,然后过段时间就检测到,系统时钟最近回跳了。所以怎么配 NTP 很重要

然后还有更多的,比如说文件系统,大家有没有考虑过你写磁盘的时候,磁盘出错会怎么办?好,写磁盘的时候没有出错,成功了,然后磁盘一个扇区坏了,读出来的数据是损坏的,怎么办?大家有没有 checksum ?没有 checksum 然后我们直接用了这个数据,然后直接给用户返回了,这个时候可能是很要命的。如果这个数据刚好存的是个元数据,而元数据又指向别的数据,然后你又根据元数据的信息去写入另外一份数据,那就更要命了,可能数据被进一步破坏了。

所以比较好的做法是什么?

Fault injection

  • Hardware
    • disk error
    • network card
    • cpu
    • clock
  • Software
    • file system
    • network & protocol

Simulate everything

模拟一切东西。就是磁盘是模拟的,网络是模拟的,那我们可以监控它,你可以在任何时间、任何的场景下去注入各种错误,你可以注入任何你想要的错误。比如说你写一个磁盘,我就告诉你磁盘满了,我告诉你磁盘坏了,然后我可以让你 hang 住,比如 sleep 五十几秒。我们确实在云上面出现过这种情况,就是我们一次写入,然后被 hang 了为 53 秒,最后才写进去,那肯定是网络磁盘,对吧?这种事情其实是很吓人的,但是肯定没有人会想说我一次磁盘写入然后要耗掉 53 秒,但是当 53 秒出现的时候,整个程序的行为是什么?TiDB 里面用了大量的 Raft,所以当时出现一个情况就是 53 秒,然后所有的机器就开始选举了,说这肯定是哪儿不对,重新把 leader 都选出来了,这时候卡 53 秒的哥们说“我写完了”,然后整个系统状态就做了一次全新的迁移。这种错误注入的好处是什么?就是知道当出错的时候,你的错误能严重到什么程度,这个事情很重要,就是 predictable,整个系统要可预测的。如果没有做错误路径的测试,那很简单的一个问题,现在假设走到其中一条错误路径了,整个系统行为是什么?这一点不知道是很吓人的。你不知道是否可能破坏数据;还是业务那边会 block 住;还是业务那边会 retry?

以前我遇到一个问题很有意思,当时我们在做一个消息系统,有大量连接会连这个,一个单机大概是连八十万左右的连接,就是做消息推送。然后我记得,当时的 swap 分区开了,开了是什么概念?当你有更多连接打进来的时候,然后你内存要爆了对吧?内存爆的话会自动启用 swap 分区,但一旦你启用 swap 分区,那你系统就卡成狗了,外面用户断连之后他就失败了,他得重连,但是重连到你正常程序能响应,可能又需要三十秒,然后那个用户肯定觉得超时了,又切断连接又重连,就造成一个什么状态呢?就是系统永远在重试,永远没有一次成功。那这个行为是不是可以预测?这种错误当时有没有做很好的测试?这都是非常重要的一些教训。

硬件测试以前的办法是这样的(Joke):

***(PingCAP)分布式系统测试那些事_第3张图片

假设我一个磁盘坏了,假设我一个机器挂了,还有一个假设它不一定坏了也不一定挂了,比如说它着火了会怎么样?前两个月吧,是瑞士还是哪个地方的一个银行做测试,那哥们也挺逗的,人肉对着服务器这样吹气,来看监控数据那个变化,然后那边马上开始报警。这还只是吹气而已,那如果更复杂的测试,比如说你着火从哪个地方开始烧,先烧到硬盘、或者先烧到网卡,这个结果可能也是不一样的。当然这个成本很高,然后也不是能 scale 的一种方案,同时也很难去复制。

这不仅仅是硬件的监控,也可以认为是做错误的注入。比如说一个集群我现在烧掉一台会怎么样?着火了,很典型的嘛,虽然重要的机房都会有这种防火、防水等各种的策略,但是真的着火的时候怎么办?当然你不能真去烧,这一烧可能就不止坏一台机器了,但我们需要使用 Fault injection 来模拟。

我介绍一下到底什么是 Fault injection。给一个直观的例子,大家知道所有人都用过 Unix 或者 Linux 的系统,大家都知道,很多人习惯打开这个系统第一行命令就是 ls 来列出目录里面的文件,但是大家有没有想过一个有意思的问题,如果你要测试 ls 命令实现的正确性,怎么测?如果没有源代码,这个系统该怎么测?如果把它当成一黑盒这个系统该怎么测?如果你 ls 的时候磁盘出现错误怎么办?如果读取一个扇区读取失败会怎么办?

这个是一个很好玩的工具,推荐大家去玩一下。就是当你还没有做更深入的测试之前,可以先去理解一下到底什么是 Fault injection,你就可以体验到它的强大,一会我们用它来找个 MySQL 的 bug。

libfiu - Fault injection in userspace
It can be used to perform fault injection in the POSIX API without having to modify the application's source code, that can help to test failure handling in an easy and reproducible way.

那这个东西主要是用来 Hook 这些 API 的,它很重要的一点就是它提供了一个 library ,这个 library 也可以嵌到你的程序里面去 hook 那些 API。就比如说你去读文件的时候,它可以给你返回这个文件不存在,可以给你返回磁盘错误等等。最重要的是,它是可以重来的。

举一个例子,正常来讲我们敲 ls 命令的时候,肯定是能够把当前的目录显示出来。

***(PingCAP)分布式系统测试那些事_第4张图片

这个程序干的是什么呢?就是 run,指定一个参数,现在是要有一个 enable_random,就是后面所有的对于 IO 下面这些 API 的操作,有 5% 的失败率。那第一次是运气比较好,没有遇到失败,所以我们把整个目录列出来了。然后我们重新再跑一次,这时候它告诉我有一次读取失败了,就是它 read 这个 directory 的时候,遇到一个 Bad file descriptor,这时候可以看到,列出来的文件就比上面的要少了,因为有一条路径让它失败了。接下来,我们进一步再跑,发现刚列出来一个目录,然后下次读取就出错了。然后后面再跑一次的时候,这次运气也比较好,把这整个都列出来了,这个还只是模拟的 5% 的失败率。就是有 5% 的概率你去 read、去 open 的时候会失败,那么这时候可以看到 ls 命令的行为还是很 stable 的,就是没有什么常见的 segment fault 这些。

大家可能会说这个还不太好玩,也就是找找 ls 命令是否有 bug 嘛,那我们复现 MySQL bug 玩一下。

Bug #76020
InnoDB does not report filename in I/O error message for reads
fiu-run -x -c "enable_random name=posix/io/*,probability=0.05" bin/mysqld --basedir=/data/ushastry/server/mysql-5.6.24 --datadir=/data/ushastry/server/mysql-5.6.24/76020 --core-file --socket=/tmp/mysql_ushastry.sock --port=15000
2015-05-20 19:12:07 31030 [ERROR] InnoDB: Error in system call pread(). The operating system error number is 5.2015-05-20 19:12:07 7f7986efc720 InnoDB: Operating system error number 5 in a file operation.InnoDB: Error number 5 means 'Input/output error'.2015-05-20 19:12:07 31030 [ERROR] InnoDB: File (unknown):

'read' returned OS error 105. Cannot continue operation

这是用 libfiu 找到的 MySQL 的一个 bug,这个 bug 是这样的,bug 编号是 76020,是说 InnoDB 在出错的时候没有报文件名,那用户给你报了错,你这时候就傻了对吧?这个到底是什么地方出错了呢?然后这个地方它怎么出来的?你可以看到它还是用我们刚才提到的 fiu-run,然后来模拟,模拟的失败概率还是这么多,可以看到,我们的参数一个没变,这时把 MySQL 启动,然后跑一下,出现了,可以看到 InnoDB 在报的时候确实没有报 filename ,File : 'read' returned OS error,然后这边是 auto error,你不知道是哪一个文件名。

换一个思路来看,假设没有这个东西,你复现这个 bug 的成本是什么?大家可以想想,如果没有这个东西,这个 bug 应该怎么复现,怎么让 MySQL 读取的东西出错?正常路径下你让它读取出错太困难了,可能好多年没出现过。这时我们进一步再放大一下,这个在 5.7 里面还有,也是在 MySQL 里面很可能有十几年大家都没怎么遇到过的,但这种 bug 在这个工具的辅助下,马上就能出来。所以** Fault injection 它带来了很重要的一个好处就是让一个东西可以变得更加容易重现**。这个还是模拟的 5% 的概率。这个例子是我昨天晚上做的,就是我要给大家一个直观的理解,但是分布式系统里面错误注入比这个要复杂。而且如果你遇到一个错误十年都没出现,你是不是太孤独了?

这个电影大家可能还有印象,威尔史密斯主演的,全世界就一个人活着,唯一的伙伴是一条狗。

***(PingCAP)分布式系统测试那些事_第5张图片

实际上不是的,比我们痛苦的人大把的存在着。

举 Netflix 的一个例子,下图是 Netflix 的系统。

***(PingCAP)分布式系统测试那些事_第6张图片

他们在 2014 年 10 月份的时候写了一篇博客,叫《 Failure Injection Testing 》,是讲他们整个系统怎么做错误注入,然后他们的这个说法是** Internet Scale,就是整个多数据中心互联网的这个级别。大家可能记得 Spanner 刚出来的时候他们叫做 Global Scale,然后这地方可以看到,蓝色是注射点,黑色的是网络调用**,就是所有这些请求在这些情况下面,所有这些蓝色的框框都有可能出错。大家可以想一想,在 Microservice 系统上,一个业务调用可能涉及到几十个系统的调用,如果其中一个失败了会怎么样?如果是第一次第一个失败,第二次第二个失败,第三次第三个失败是怎么样的?有没有系统做过这样的测试?有没有系统在自己的程序里面去很好的验证过是不是每一个可以预期的错误都是可预测的,这个变得非常的重要。这里以 cache 为例,就说每一次访问 Cassandra 的时候可能出错,那么也就给了我们一个错误的注入点。

然后我们谈谈 OpenStack

**OpenStack fault-injection library: **
https://pypi.python.org/pypi/os-faults/0.1.2

大名鼎鼎的 OpenStack 其实也有一个 Failure Injection Library,然后我把这个例子也贴到这里,大家有兴趣可以看一下这个 OpenStack 的 Failure Injection。这以前大家可能不太关注,其实大家在这一点上都很痛苦, OpenStack 现在还有一堆人在骂,说稳定性太差了,其实他们已经很努力了。但是整个系统确实是做的异乎寻常的复杂,因为组件太多。如果你出错的点特别多,那可能会带来另外一个问题,就是出错的点之间还能组合,就是先 A 出错,再 B 出错,或者 AB 都出错,这也就几种情况,还好。那你要是有十万个错误的点,这个组合怎么弄?当然现在还有新的论文在研究这个,2015 年的时候好像有一篇论文,讲的就是会探测你的程序的路径,然后在对应的路径下面去注入错误
再来说 Jepsen

Jepsen: Distributed Systems Safety Analysis

***(PingCAP)分布式系统测试那些事_第7张图片

大家所有听过的知名的开源分布式系统基本上都被它(Jepsen)找出来过 bug。但是在这之前大家都觉得自己还是很 OK 的,我们的系统还是比较稳定的,所以当新的这个工具或者新的方法出现的时候,就比如说我刚才提到的那篇能够线性 Scale 的去查错的那篇论文,那个到时候查错力就很惊人了,因为它能够自动帮你探测。另外我介绍一个工具 Namazu,后面讲,它也很强大。

这里先说Jepsen, 这货算是重型武器了,无论是 ZooKeeper、MongoDB 以及 Redis 等等,所有这些全部都被找出了 bug,现在用的所有数据库都是它找出的 bug,最大的问题是小众语言 closure 编写的,扩展起来有点麻烦。我先说说 Jepsen 的基本原理,一个典型使用 Jepsen 的测试通过会在一个 control node上面运行相关的 clojure 程序,control node 会使用 ssh 登陆到相关的系统 node(jepsen 叫做 db node)进行一些测试操作。

当我们的分布式系统启动起来之后,control node 会启动很多进程,每一个进程都能使用特定的 client 访问到我们的分布式系统。一个 generator 为每一个进程生成一系列的操作,比如 get/set/cas,让其执行。每一个操作都会被记录到 history 里面。在执行操作的同时,另一个 nemesis 进程会尝试去破坏这个分布式系统,譬如使用 iptable 断开网络连接等,当所有操作执行完毕之后,jepsen 会使用一个 checker 来分析验证系统的行为是否符合预期。PingCAP 的首席架构师唐刘写过两篇文章介绍我们实际怎么用 Jepsen 来测试 TiDB,大家可以搜索一下,我这里就不详细展开了。

FoundationDB

  • It is difficult to be deterministic
    • Random
    • Disk Size
    • File Length
    • Time
    • Multithread

FoundationDB 这就是前辈了,2015 年被 Apple 收购了。他们为了解决错误注入的问题,或者说怎么去让它重现的这个问题,做了很多事情,很重要的一个事情就是** deterministic **。如果我给你一样的输入,跑几遍,是不是能得到一样的输出?这个听起来好像很科学、很自然,但是实际上我们绝大多数程序都是做不到的,比如说你们有判断程序里面有随机数吗?有多线程吗?有判断磁盘空间吗?有判断时间吗?你再一次判断的时候还是一样的吗?你再跑一次,同样的输入,但行为已经不一样了,比如你生了一个随机数,比如你判断磁盘空间,这次判断和下次判断可能是不一样的。
所以他们为了做到“我给你一样的输入,一定能得到一样的输出”,花了大概两年的时间做了一个库。这个库有以下特性:它是个单线程的,然后是个伪并发的。为什么?因为如果用多线程你怎么让它这个相同的输入变成相同的输出,谁先拿到锁呢?这里面的问题很多,所以他们选择使用单线程,但是单线程本身有单线程的问题。而且比如你用 Go 语言,那你单线程它也是个并发的。然后它的语言规范就告诉我们说,如果一个 select 作用在两个 channel 上,两个 channel 都 ready 的时候,它会随机的一个,就是在语言定义的规范上面,就已经不可能让你得到一个 deterministic 了。但还好 FoundationDB 是用 C++ 写的。

FoundationDB

  • Single-threaded pseudo-concurrency.
  • Simulated the implementation of all the external communication.
  • Determinism.
  • Disasters happen more frequently here than in the real world.

另外FoundationDB 模拟了所有的网络,就是两个之间认为通过网络通讯,对吧?实际上是通过它自己模拟的一套东西在通讯。它里面有一个很重要的观点就是说,如果磁盘损坏,出现的概率是三年百分之八的话,那么在用户那出现的概率是三年百分之八。但是在用户那一旦出现了,那证明就很严重了,所以他们对待这个问题的办法是什么?就是我通过自己的模拟系统让它每时每刻都在产生。它们大概是每两分钟产生一次磁盘损坏,也就是说它比现实中的概率要高几十万倍,所以它就觉得它调的技术 more frequently,就是我这种错误出现的更加频繁,那网卡损坏的概率是多少?这都是极低的,但是你可以用这个系统让它每分每秒都产生,这样一来你就让你的系统遇到这种错误的概率是比现实中要大非常非常多。那你重现,比如说现实中跑三年能重现一次,你可能跑三十秒就能重现一次。

但对于一个 bug 来说最可怕的是什么?就是它不能重现。发现一个 bug,后来说我 fix 了,然后不能重现了,那你到底 fix 了没有?不知道,这个事情就变得非常的恐怖。所以通过 deterministic 肯定能保证重现,我只要把我的输入重放一次,我把它录下来,每一次我把它录下来一次,然后只要是曾经出现过,我重放,一定能出现。当然这个代价太大了,所以现在学术界走的是另外一条路,不是完全 deterministic,但是我只需要它 reasonable。比如说我在三十分钟内能把它重现也是不错的,我并不需要在三秒内把它重现。所以,每前一步要付出相应的成本代价。

(未完待续...)

2016-12-07 刘奇分布式系统测试那些事儿(下)信心的毁灭与重建

(接中篇)
ScyllaDB 有一个开源的东西,是专门用来给文件系统做 Failure Injection 的, 名字叫做 CharybdeFS。如果你想测试你的系统,就是文件系统在哪不断出问题,比如说写磁盘失败了,驱动程序分配内存失败了,文件已经存在等等,它都可以测模拟出来。

CharybdeFS: A new fault-injecting file system for software testing
Simulate the following errors:

  • disk IO error (EIO)
  • driver out of memory error (ENOMEM)
  • file already exists (EEXIST)
  • disk quota exceeded (EDQUOT)

再来看看 Cloudera,下图是整个 Cloudera 的一个 Failure Injection 的结构。

***(PingCAP)分布式系统测试那些事_第8张图片

一边是 Tools,一边是它的整个的 Level 划分。比如说整个 Cluster, Cluster 上面有很多 Host,Host 上面又跑了各种 Service,整个系统主要用于测试 HDFS, HDFS 也是很努力的在做有效的测试。然后每个机器上部署一个 AgenTEST,就用来注射那些可能出现的错误。看一下它们作用有多强大。

Cloudera: Simulate the following errors:

  • Packets loss/corrupt/reorder/duplicate/delay
  • Bandwidth limit: Limit the network bandwidth for the specified address and port.
  • DNSFail: Apply an injection to let the DNS fail.
  • FLOOD: Starts a DoS attack on the specified port.
  • BLOCK: Blocks all the packets directed to 10.0.0.0/8 (used internally by EC2).
  • SIGSTOP: Pause a given process in its current state.
  • BurnCPU/BurnIO/FillDISK/RONLY/FIllMEM/CorruptHDFS
  • HANG: Hang a host running a fork bomb.
  • PANIC: Force a kernel panic.
  • Suicide: Shut down the machine.

数据包是可以丢的,可以坏的,可以 reorder 的,比如说你发一个 A,再发一个 B,它可以给你 reorder,变成先发了 B 再发了 A,然后看你应用程序有没有正确的处理这种行为。接着发完一次后面再给你重发,然后可以延迟,这个就比较简单。目前这个里面的大部分,TiKV 都有实现,还有带宽的限制,就比如说把你带宽压缩成 1M。以前我们遇到一个问题很有意思,发现有人把文件存到 Redis 里面,但 Redis 是带多个用户共享的,一个用户就能把整个 Redis 带宽给打满了,这样其他人的带宽就很卡,那这种很卡的时候 Redis 可能出现的行为是什么呢?我们并不需要一个用户真的去把它打满,只要用这种工具,瞬间就能出现我把你的带宽限制到原来的 1%,假设别人在跟你抢带宽,你的程序行为是什么?马上就能出来,也不需要配很复杂的环境。这极大的提高了测试效率,同时能测试到很多 corner case。

然后 DNS fail。那 DNS fail 会有什么样的结果?有测过吗?可能都没有想过这个问题,但是在一个真正的分布式系统里面,每一点都是有可能出错的。还有 FLOOD,假设你现在被攻击了,整个系统的行为是什么样的?然后一不小心被这个 IP table 给 block 了,该怎么办。这种情况我们确实出现过。我们一上来并发,两万个连接一打出去,然后发现大部分都连不上,后来一看 IP table 自动启用了一个机制,然后把你们都 block。当然我们后面查了半个小时左右,才把问题查出来。但这种实际上应该是在最开始设计的时候就应该考虑的东西。

如果你的进程被暂停了,比如说大家在云上跑在 VM 里面,整个 VM 为了升级,先把你整个暂停了,升级完之后再把你恢复的时候会怎么样?那简单来讲,就是如果假设你程序是有 GC 的,GC 现在把我们的程序卡了五秒,程序行为是正常的吗?五十秒呢?这个很有意思的就是,BurnCPU,就是再写一个程序,把 CPU 全占了,然后让你这个现在的程序只能使用一小部分的 CPU 的时候,你程序的行为是不是正常的。正常来讲,你可能说我 CPU 不是瓶颈啊,我瓶颈在 IO,当别人跟你抢 CPU,把你这个 CPU 压的很低的时候,到 CPU 是瓶颈的时候,正常你的程序的这个行为是不是正常的?还有 IO,跟你抢读的资源,跟你抢写的资源,然后 filedisk 把磁盘写满,写的空间很少。比如说对数据库而言,你创建你的 redo log 的时候,都已经满了会怎么样?然后我突然把磁盘设为只读,就你突然一个写入会出错,但是你接下来正常的读写行为是不是对的?很典型的一个例子,如果一个数据库你现在写入,磁盘满了,那外面读请求是否就能正常响应。 Fill memory,就是瞬间把这个 memory 给压缩下来,让你下次 malloc 的时候可能分布不到内存。这个就和业务比较相关了,就是破坏 HDFS 的文件。其它的就是 Hang、Panic,然后还有自杀,直接关掉机器,整个系统的行为是什么样的?

现在比较痛苦的一点是大家各自为政,每一家都做一套,但是没有办法做成一个通用的东西给所有的人去用。包括我们自己也做了一套,但是确实没有办法和其他的语言之间去 share,最早提到的那个 libfu 库实际上是在 C 语言写的,那所有 C 相关的都可以去 call 那个库。

Distributed testing

  • Namazu
    • ZooKeeper: Found ZOOKEEPER-2212, ZOOKEEPER-2080 (race): (blog article)
    • Etcd: Found etcdctl bug #3517 (timing specification), fixed in #3530. The fix also resulted a hint of #3611, Reproduced flaky tests {#4006, #4039}
    • YARN: Found YARN-4301 (fault tolerance), Reproduced flaky tests{1978, 4168, 4543, 4548, 4556}

然后 Namazu。大家肯定觉得 ZooKeeper 很稳定呀, Facebook 在用、阿里在用、京东在用。大家都觉得这个东西也是很稳定的,直到这个工具出现了,然后轻轻松松就找到 bug 了,所有的大家认为的这种特别稳定的系统,其实 bug 都还挺多的,这是一个毁三观的事情,就是你觉得东西都很稳定,都很 stable,其实不是的。从上面,我们能看到 Namazu 找到的 Etcd 的几个 bug,然后 YARN 的几个 bug,其实还有一些别的。

How TiKV use namazu

  • Use nmz container / non-container mode to disturb cluster.
    • Run container mode in CI for each commit. (1 hour)
    • Run non-container mode for a stable version. (1 week+)
  • Use extreme policy for process inspector
    • Pick up some processes and execute them with SCHED_RR scheduler. others are executed with SCHED_BATCH scheduler
  • Use [0, 30s] delay for filesystem inspector

接下来说一下 TiKV 用 Namazu 的一些经验。因为我们曾经在系统上、在云上面出现过一次写入磁盘花了五十几秒才完成的情况,所以我们需要专门的工具模拟这个磁盘的抖动。有时候一次写入可能确实耗时比较久,那这种时候是不是 OK 的。大家如果能把这种东西统统用上,我觉得还能为很多开源系统找出一堆 bug。

稍微介绍一下我们现在运行的基本策略,比如说我们会用 0 到 30 秒的这个 delay (就是每一次你往文件系统的交互,比如说读或者写,那么我们会给你产生随机的 0 到 30 秒的 delay ),但我们正常应该还是需要去测三十秒到几分钟的延迟的情况,是否会让整个系统崩掉了。

How TiKV simulate network transport

  • Drop/Delay messages randomly
  • Isolate Node
  • Partition [1, 2, 3, 4, 5] -> [1, 2, 3] + [4, 5]
  • Out of order messages
  • Filter messages
  • Duplicate and send redundant messages

怎么模拟网络呢?假设你有网络,里面有五台机器,那我现在想做一个脑裂怎么做?不能靠拔网线对吧?比如在 TiKV 的测试框架中,我们就可以直接通过 API 把 5 个节点脑裂成两部分,让 1, 2, 3 号节点互相联通,4, 5 号节点也能联通,这两个分区彼此是隔离的,非常的方便。其实原理很简单,这种情况是用程序自己去模拟,假如是你发的包,自动给你丢掉,或者直接告诉你 unreachable,那这个时候你就知道这个网络就脑裂了,然后你怎么做?就是只允许特定类型的消息进来,把其他的都丢掉,这样一来你可以保证有些 bug 是必然重现的。这个框架给了我们极大的信心用来模拟并重现各种 corner case,确保这些 corner case 在单元测试中每次都能被覆盖到。

How to test Rocksdb

  • Treat storage as a black box.
  • Three steps(7*24):
    • Fill data, Random kill -9
    • Restart
    • Consistent check.
  • Results:
    • Found 2 bugs. Both fixed

然后说说我们怎么测 RocksDB。 RocksDB 在大家印象中是很稳定的,但我们最近发现了两个 bug。测的方法是这样的:我们往 RocksDB 里面填数据,然后随机的一段时间去把它 kill 掉,kill 掉之后我们重启,重新启动之后去检测我们刚才 fail 的 data 是不是一致的,然后我们发现两个可能造成数据丢失的 bug,但是官方的响应速度非常快,几天就都 fix 了。可是大家普遍运行的是这么 stable 的系统,为什么还会这么容易找到 bug?就说这个测试,如果是一直有这个测试的 cover,那么这两个 bug 可能很快就能够被发现。

这是我们一个基本的,也就是当成一个纯黑盒的测。大家在测数据库的时候,基本也是当黑盒测。比如说 MySQL 写入数据,kill 掉,比如说我 commit 一个事务,数据库告诉我们 commit 成功,我把数据库 kill 掉,我再去查我刚才提交的数据一样能查到。这是一个正常的行为,如果查不到,说明整个系统有问题。

More tools

  • american fuzzy lop
***(PingCAP)分布式系统测试那些事_第9张图片

其实还有一些更加先进的工具,大家平时觉得特别稳定的东西,都被摧残的不行。Nginx 、NGPD、tcpdump 、LibreOffice ,如果有用 Linux 的同学可能知道,还有 Flash、sqlite。这个东西一出来,当时大家很兴奋,说怎么一下子找了这么多 bug,为什么以前那么稳定的系统这么不堪一击,会觉得这个东西它还挺智能的。就比如说你程序里面有个 if 分支,它是这样的,假如你程序有一百条指令,它先从前面一直走,走到某条分支指令的时候,它是一直持续探索,一个分支走不下去,它会一直在这儿持续探索,再给你随机的输入,直到我探索进去了,我记下来了下次我知道我用这个输入可以进去特定的分支。那我可以再往下走,比如说你 if 分支进去之后里面还有 if ,那你传统手段可能探测不进去了但它可以,它记录一下,我这个可以进去,然后我重来,反正我继续输入这个,我再往里面走,一旦我探测到一个新的分支,我再记住,我再往里面走。所以它一出来的时候大家都说这个真厉害,一下发现这么多 bug。但最激动的不是这些人,最激动的是黑客,为什么?因为突然有很多栈溢出、堆溢出漏洞被发现了,然后就可以写一堆工具去攻击线上的这么多系统。所以很多的技术的推进在早期的时候是黑客做出来,但是他们的目的当然不一定是为了测试 bug,而是为了怎么黑一个系统进去,这是他们当时做的,所以这个工具也是非常强大、非常有意思的,大家可以拿去研究一下自己的系统。

大家印象里面各种文件系统是很稳定的,可是当用 American fuzzy lop 来测试的时候,被惊呆了。 Btrfs 连 5 秒都没有坚持到就跪了,大家用的最多的 Ext4 是最坚挺的,也才抗了两个小时!!!

***(PingCAP)分布式系统测试那些事_第10张图片

再来说说 Google,Google 怎么做测试对外讲的不多,最近 Chrome team 开源了他们的 Fuzz 测试工具 OSS-Fuzz,这个工具强大的地方在于自动化做的极好

  • 发现 bug 后自动创建 issue
  • bug 解决后自动 verify

更惊人的是 OSS-Fuzz 集群一周可以跑 ~4 trillion test cases 更多细节大家可以看这篇文章:『Announcing OSS-Fuzz: Continuous Fuzzing for Open Source Software』

另外有些工具能让分布式系统开发人员的生活变得更美好一点
Tracing tools may help you

  • Google Dapper
  • Zipkin
  • OpenTracing

还有 Tracing,比如说我一个 query 过来,然后经过这么多层,经过这么多机器,然后在不同的地方,不同环节耗时多久,实际上这个在分布式系统里面,有个专门的东西做 Tracing ,就是 distribute tracing tools。它可以用一条线来表达你的请求在各个阶段耗时多长,如果有几段,那么分到几个机器,分别并行的时候好了多长时间。大体的结构是这样的:


***(PingCAP)分布式系统测试那些事_第11张图片

这里是一个具体的例子:


***(PingCAP)分布式系统测试那些事_第12张图片

很清晰,一看就知道了,不用去看 log,这事其实一点也不新鲜,Google 十几年前就做了一个分布式追踪的工具。然后开源社区要做一个实现叫做 Zipkin,好像是 java 还是什么写的,又出了新的叫 OpenTracing,是 Go 写的。我们现在正准备上这个系统,用来追踪 TiDB 的请求在各个阶段的响应时间。

最后想说一下,大家研究系统发现 bug 多了之后,不要对系统就丧失了信心,毕竟bug 一直在那里,只是从前没有发现,现在发现得多了,总体上新的测试方法让系统的质量比以前好了很多。好像有点超时了,先聊到这里吧,还有好多细节没法展开,下次再聊。

-本系列完结-

***(PingCAP)分布式系统测试那些事_第13张图片
ItDB——分布式 NewSQL 数据库

官网:http://www.pingcap.com/

延展阅读:

  • 分布式系统测试那些事儿——理念
  • 分布式系统测试那些事儿——错误注入
  • 分布式系统测试那些事儿——信心的毁灭与重建

你可能感兴趣的:(***(PingCAP)分布式系统测试那些事)