测试开发之路 混沌工程的秘密 (一)

孙高飞 · December 22, 2019 · Last by zhaohongjie replied at January 03, 2020 · 3456 hits

前言

最近一年时间里混沌工程的概念在国内火了一把, 据说是因为阿里云出现了一次比较严重的事故后,业界就开始重视这方面的东西。各个大会上不时的都会有关于混沌工程的议题出现,我们在网络上也能见到相关的文章。介于我目前也在负责我们项目中的相关工作, 所以我打算用几篇文章来详细讲解混沌工程的实践经验。

什么是混沌工程

混沌这个词来源于英文chaos的翻译。我记得是源自于多年前Netflix在AWS上发布的一款名叫chaos monkey的服务, 这个服务会按一定的策略随机的让线上的服务停止服务(就是所谓的杀服务),注意这里是在线上杀服务,而不是在测试环境。 这个服务的目的是倒逼业务团队做好自己的高可用特性。 所以混沌工程在早期其实一直都被称呼为高可用测试,或故障演练。其目的是验证即便遇到各种事故导致某些服务停摆,系统依然可以依靠其高可用特性正常提供服务。 之所以用chaos(混沌)这个单词是代表其不确定性,我理解的意思是不确定何时何地发生何种故障后,会对系统造成何种影响。 而我们的目的就是通过测试,把这些不确定性变为可以确定的信息并推进修复这些问题。

高可用

考虑到这种测试类型偏冷门,大家可能没接触过高可用相关内容,而混沌工程又离不开高可用。所以这里简单介绍下高可用的知识, 后续详细讲解测试策略的时候会再展开高可用的各种策略。 高可用架构最关键的是两个字:冗余。 既同一个服务部署多个实例共同支撑对外服务(跟负载均衡很像哈, 其实高可用的基础之一就是负载均衡), 如下图:

比如我们有一个业务模块, 为了实现高可用,我们在不同的机器上启动了3个相同的实例,这3个实例是一模一样的,任何一个实例都有单独对外提供服务的能力。 而这时候一般都会有一个类似路由的模块,用户的请求都发送给这个路由模块, 而路由模块会负责把请求平均分发到下面的这3个实例上。 这样如果有一个实例由于某个故障而停止服务时,系统发现后路由模块便会停止向故障实例分发流量,而是由其他2个实例继续提供服务。如下图:


PS: 好的系统会设置故障自动恢复机制,比如发现故障后会尝试重新拉起这个实例,甚至是部署到另外一台节点上以规避节点级故障,所以冗余不仅仅是服务实例的冗余,也是机器节点的冗余,以防某些节点出现故障后,冗余节点能够顶替上。

上面就是高可用的基本原理, 当然想要做到高可用没那么简单, 其中有一个很关键的步骤就是系统如何发现服务故障。 只有及时的发现故障才能尽快的切换流量以避免更大的损失,而发现故障并不是一个容易的事情。 如果故障发现的晚了, 比如故障出现15分钟后系统才探测到并开始切换流量。 那么这15分钟的延迟时间内用户的请求依然会受到影响, 那么这就不符合设计初衷(当然这世界上没有百分之百的高可用, 我们要确保用户收到的影响在可接受范围内)。 或者系统根本就没有发现实例的故障而无法触发流量切换机制,那么这种情况造成的影响就更大了。 所以其实高可用测试,最重要的是测试系统的故障发现能力。不论我们向系统中注入何种异常,其目的都是希望系统能够自动检测并触发后续的流量切换等机制。 所以在实施混沌工程之前, 了解自家系统的健康检查机制是很重要的, 只有了解其原理才能更好的在合适的时机注入相应的异常。 比如我之前负责测试的产品, 很多较为严重的高可用缺陷都不是测试出来的, 而是了解了高可用架构设计后直接分析出来的。

PS: 由于微服务架构大行其道,系统中服务数量越来越多,在服务间交互容易出现问题的同时, 做混沌工程的工作量越来越大(因为要针对每个服务做这种测试), 所以测试前线分析其高可用架构逻辑并推演出可能的缺陷是很重要的,也是能很大程度减少工作量的方法。

健康检查

接下来讲述业界常用的两种健康检查的方式来介绍这里面的注意点。 首先第一个是以zk和etcd架构为代表的服务自注册架构。如下:

在这种系统里一般都会有一个类似zk的中间件, 启动的实例将自己的信息注册到zk中,路由模块到zk中获取服务实例的信息并开始分发流量。 而每个实例都会周期性的向zk写入自己的健康信息(包括时间戳)。路由模块会根据写入信息的时间来判断服务是否健康。如果某个实例出现故障,它无法向zk写入自己的信息时,路由模块会发现它上一次写入的时间已经超过了设定的超时时间, 所以它判断该服务已不可用。 这种架构强调的是路由模块和实例之间没有强耦合关系, 一切都是zk中的注册信息为准。

第二种是以k8s针对服务的健康检查为代表的主动探测机制(k8s的健康检查机制还是略复杂的,它既有前面说的自注册架构式的机制,也有现在要说的主动检测机制),k8s提供了两种健康检查的探针。 这两种探针的使用方式一般都是由业务实例提供一个健康检查接口, 监控检查接口中会去检查当前服务的健康状态, 比如这个接口会去尝试连接mysql来判断当前节点与mysql节点的网络是否通畅,这样在出现与mysql所在节点的网络故障时系统可以及时发现, 当然也可以直接提交一个查询甚至是一个无伤大雅的业务级别的写入请求来保证业务级别的健康状态。这里不详细展开k8s的细节,有兴趣的同学可以自行百度。总之在这种机制下, 各个服务都可以定制自己的健康检查接口供系统调用, 系统发现其异常后便会触发流量切换等机制。

重试

刚才说明了一下主动和被动两种常用的健康检查机制, 但是这两个机制都无法解决一个问题,就是健康检查的延迟。 之前也说过在高可用中用最短的时间探测到异常是非常非常非常重要的,但是我也同样说过这世界上没有百分百的高可用。 不论使用何种机制都无法避免故障发生到系统探知这中间出现的延迟时间。 根据异常的不同,业务的不同,系统架构的不同我们所面对的延迟时间也是不一样的。 有些时候很快,是秒级的。 有些时候较慢,是分钟级的。 为了保证在这段时间内的请求不会失败, 一般会在系统中额外增加一个failover机制,你可以理解一个比较简单的failover就是重试, 当在这段延迟时间内如果有请求发送到了故障实例中, 这自然会出现失败或者超时, 那么上层业务可以选择重试几次, 路由模块的流量分发一般都是随机的,如果第一次运气不好发送到了故障实例上,那么我们就重试第二次,甚至三次。 当然如果脸黑到重试的时候一直发到了故障节点上那我实在说不出啥了😂
当然这种重试机制不是银弹, 重试意味着增加了用户请求的延迟,是比较影响用户体验的。 所以还是尽量缩短健康检查的延迟方位王道,重试不过是补救措施,而且不是所有请求都能重试的,很多关键的写请求是不能重试的(非常危险)。

测试理念

上面花了大量的篇幅在讲高可用的设计原理是为了接下来要讲述的测试过程做一个良好的铺垫。 知道了自家产品在高可用这方面的设计原理后, 才能更针对性的设计case。 这里面的测试理念就和很多人理解的不一样了。 混沌工程在明面上宣传的时候都过度宣传了混沌二字, 导致很多人理解混沌工程的测试方案是偏随机性的故障注入,目光也都放到了故障注入工具上,这导致打从测试目的上就出现了偏差。 混沌工程也是一个测试方法论, 既然是测试方法,就会有标准的输入以及预期的结果,尽量剥离不确定性以及随机性,所以测试的过程很多时候其实是很反混沌的,讲究的是在要求的特定环境,特定服务,特定阶段下注入特定的异常,预期系统会有特定的反应。 这样在出现问题的时候,才能更准确的让故障生效并排查出究竟是哪里的问题。

  • 比如我曾经见过有人在做这些事情的时候没有注重隔离性, 也就是他把好几个服务都部署在了同一个机器上,尤其这几个模块还是相互依赖耦合的,然后在这个机器上注入了一个网络高延迟的异常。 这样带来的问题就是外围自动化测试用例挂了一堆,在排查的时候他不知道哪些case的失败究竟是因为哪个模块在高可用上没有做好。
  • 也见过由于不理解高可用的机制, 所以在模拟网络延迟的时候模拟了一个600ms的延迟异常,她觉得延迟很高了, 但是他们的健康检查接口的超时时间是1s。也就是说这个故障是触发不了高可用机制的, 所以他测试出了系统在1s网络延迟的情况下的容错能力,但却没有测试到流量切换的场景。(PS: 它也注入了CPU过载,kill 进程,丢包等故障,但都没有触发由于超时而带来的流量切换等后续机制)

所以我才说混沌二字现在有些被过于解读了,过于强调了随机性和不确定性。 却让人容易忽略其中的基本测试原则。 那么如果我们面临一个这样的测试需求,我们应该怎么做呢?

  • 了解产品的高可用架构设计和机制: 再详细了解之后,其实你自己心里就已经大概有数, 你的系统在什么故障能够稳定容错, 什么故障是碰见了就会跪的(当然特别成熟的系统基本是不会有这种这么明显的漏洞的)
  • 其次是配置检查:每个服务部署后关于这里的配置(比如k8s的探针配置)是否存在, 我曾经遇见过有业务模块忘记配置探针而导致的高可用崩坏。 还要计算相关配置的内容是否符合预期。比如不论使用何种健康检查机制,通过设置的超时时间,重试次数。计算出故障出现后系统能探测并反应过来的时间。判断这个延迟时间是否在需求的容忍范围内。
  • 准备全链路的自动化测试方案:混沌工程绝对不仅仅是故障注入, 我们要测试出故障发生后对系统的整体影响。 这也是混沌二字的精神体现, 我们是无法预测给一个服务注入故障后会对其他服务造成什么样的影响,毕竟各个服务间都是有依赖关系的,也许一个故障对这个服务本身的影响不大, 但是对于依赖它的其他服务可能是会有影响的, 没有针对整个系统级别的测试的话,我们是做不出这样的判断的。 所以后期最好还是要有全链路的自动化测试手段才比较靠谱。 大厂的同学可以用流量回放, 小厂的同学可以自己撸API或者UI自动化。
  • 准备故障注入工具:在这个linux内核高速发展,开源项目到处开花的时代。 故障注入已经不再是个事了, 即便你不去用各种开源项目。 单纯linux内核自带模块都足以支撑你日常通用故障演练了。比如cpu,io, 磁盘负载可以用dd命令, 模拟断网可以用iptables命令, 模拟网络延迟,丢包等可以用tc命令。
  • 高可用测试环境准备, 这个没什么好说的了, 没环境那还测个毛线了。 当然这里要注意隔离性, 早期测试的时候故障等级都比较低, 是不会模拟大范围故障的。 所以早期争取做到一个故障只对一个服务生效。 所以推荐容器化部署, 容器是使用cgroups和linux名称空间做的有效隔离。 绝对是故障演练的利器。
  • 故障分级:故障是分等级的, 比较低比如服务 crash这种单实例级别故障, 比较高的比如在一个节点上有多个服务,但是整个节点出现故障导致上面所有服务都不可用的这种影响较大的故障,再往上升级比如测试双机房高可用的时候,模拟把一整个机房都弄挂掉的场景。 请根据自己项目的需求安排故障等级和优先级并依次推进。
  • 实施执行。

当然除了上面这些还有其他需要注意的地方, 但是我们放到后续在讲,比如很多时候要模拟非通用的运维级别异常, 比如直接向代码中注入异常等等。

结尾

今天的内容偏理论。 其实我也是打算整个内容都比较着重的讲理论的, 所以想知道怎么实现故障注入工具的同学可能会失望了。 当然后面还是会讲怎么注入异常的,那些命令和开源项目都怎么用(包括jvm-sandbox注入java异常) 以及自己封装工具的注意点。 但是~~ 我想说的是异常注入其实真的没啥好讲的, 现在这个时代想给你的系统注入点异常实在太容易了,很多故障就是一个命令的事。 所以还没接触过混沌工程的小伙伴真的不用向以前那样看见故障演练就觉得模拟故障好难啊, 我是不是做不了啊这些没用的担心。 过于关注工具本身反而会让你忽视其他重要的东西。 好了今天就讲这么多吧,剩下我们下回分解。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 8 条回复 时间 点赞

高速发展,错别字……

恒温 回复

已改~~~ 不要在意这些细节~~~

有启发,赞

高可用架构最关键的是两个字:冗余。应该还有一句话,故障自动转移,高可用=冗余+故障自动转移。😀
故障自动转移=负载均衡中间件来保证。
冗余= 主从+集群+哨兵等架构来保证。
从上层的DNS到下层的DB,差不多都是这个套路。说起来好像是很简单似的-_-||,最近也在朝这个方向学习,感谢楼主高质量的分享。

0x7C00 回复

后面讲了流量切换等后续failover流程的事哈。 不过你说的对, 我是应该在一开始就强调出来

好文,看不懂的都是好文

hexianl 回复

😂

深有同感

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up