本话题系列文章整理自 PingCAP NewSQL Meetup 第 26 期刘奇分享的《深度探索分布式系统测试》议题现场实录。文章较长,为方便你们阅读,会分为上中下三篇,本文为中篇。node
接上篇:
固然测试可能会让你代码变得没有那么漂亮,举个例子:python
这是知名的 Kubernetes 的代码,就是说它有一个 DaemonSetcontroller,这 controller 里面注入了三个测试点,好比这个地方注入了一个 handler ,你能够认为全部的注入都是 interface。好比说你写一个简单的 1+1=2 的程序,假设咱们写一个计算器,这个计算器的功能就是求和,那这就很难注入错误。因此你必需要在你正确的代码里面去注入测试逻辑。再好比别人 call 你的这个 add 的 function,而后你是否是有一个 error?这个 error 的问题是它可能永远不会返回一个 error,因此你必需要人肉的注进去,而后看应用程序是否是正确的行为。说完了加法,再说咱们作一个除法。除法你们知道可能有处理异常,那上面是否是能正常处理呢?上面没有,上面写着一个好比说 6 ÷ 3,而后写了一个 test,coverage 100%,可是一个除零异常,系统就崩掉了,因此这时候就须要去注入错误。大名鼎鼎的 Kubernetes 为了测试各类异常逻辑也采用相似的方式,这个结构体不算长,大概是十几个成员,而后里面就注入了三个点,能够在里面注入错误。mysql
那么在设计 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 可能有排第一的、排第二的,那咱们能够把这些全拿过来用来测试咱们的系统。sql
但对于有些应用程序而言,这时候就比较坑了。就是一个应用程序你得把它 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):
假设我一个磁盘坏了,假设我一个机器挂了,还有一个假设它不必定坏了也不必定挂了,好比说它着火了会怎么样?前两个月吧,是瑞士仍是哪一个地方的一个银行作测试,那哥们也挺逗的,人肉对着服务器这样吹气,来看监控数据那个变化,而后那边立刻开始报警。这还只是吹气而已,那若是更复杂的测试,好比说你着火从哪一个地方开始烧,先烧到硬盘、或者先烧到网卡,这个结果可能也是不同的。固然这个成本很高,而后也不是能 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 命令的时候,确定是可以把当前的目录显示出来。
这个程序干的是什么呢?就是 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% 的几率。这个例子是我昨天晚上作的,就是我要给你们一个直观的理解,可是分布式系统里面错误注入比这个要复杂。并且若是你遇到一个错误十年都没出现,你是否是太孤独了? 这个电影你们可能还有印象,威尔史密斯主演的,全世界就一我的活着,惟一的伙伴是一条狗。
实际上不是的,比咱们痛苦的人大把的存在着。
举 Netflix 的一个例子,下图是 Netflix 的系统。
他们在 2014 年 10 月份的时候写了一篇博客,叫《 Failure Injection Testing 》,是讲他们整个系统怎么作错误注入,而后他们的这个说法是 Internet Scale,就是整个多数据中心互联网的这个级别。你们可能记得 Spanner 刚出来的时候他们叫作 Global Scale,而后这地方能够看到,蓝色是注射点,黑色的是网络调用,就是全部这些请求在这些状况下面,全部这些蓝色的框框都有可能出错。你们能够想想,在 Microservice 系统上,一个业务调用可能涉及到几十个系统的调用,若是其中一个失败了会怎么样?若是是第一次第一个失败,第二次第二个失败,第三次第三个失败是怎么样的?有没有系统作过这样的测试?有没有系统在本身的程序里面去很好的验证过是否是每个能够预期的错误都是可预测的,这个变得很是的重要。这里以 cache 为例,就说每一次访问 Cassandra 的时候可能出错,那么也就给了咱们一个错误的注入点。
而后咱们谈谈 OpenStack
OpenStack fault-injection library:
大名鼎鼎的 OpenStack 其实也有一个 Failure Injection Library,而后我把这个例子也贴到这里,你们有兴趣能够看一下这个 OpenStack 的 Failure Injection。这之前你们可能不太关注,其实你们在这一点上都很痛苦, OpenStack 如今还有一堆人在骂,说稳定性太差了,其实他们已经很努力了。可是整个系统确实是作的异乎寻常的复杂,由于组件太多。若是你出错的点特别多,那可能会带来另一个问题,就是出错的点之间还能组合,就是先 A 出错,再 B 出错,或者 AB 都出错,这也就几种状况,还好。那你要是有十万个错误的点,这个组合怎么弄?固然如今还有新的论文在研究这个,2015 年的时候好像有一篇论文,讲的就是会探测你的程序的路径,而后在对应的路径下面去注入错误。
再来讲 Jepsen
Jepsen: Distributed Systems Safety Analysis
你们全部听过的知名的开源分布式系统基本上都被它找出来过 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。好比说我在三十分钟内能把它重现也是不错的,我并不须要在三秒内把它重现。因此,每前一步要付出相应的成本代价。
未完待续…