Fault Injection Platform Design and Implementation Practice
The article details the design and deployment of a Java‑focused fault‑injection platform that uses Attach agents and JVM‑Sandbox to inject controllable pod‑ and request‑level faults—such as latency, exceptions, and return‑value errors—through dynamic templates, enabling fine‑grained, production‑safe chaos testing for e‑commerce services.
随着公司业务的不断扩张,用户流量不断提升,研发体系的规模和复杂性也随之增加,线上服务的稳定性也越来越重要,因此有必要搭建一个提供安全、高效、基于生产环境的故障演练系统,为线上服务保驾护航。
关于故障演练的建设理念,业界已经有了非常多的文章,但是涉及到具体的技术实现方面与落地,很少介绍。本文将基于故障演练系统,从设计到落地整个实践过程,来详细介绍下故障演练系统是具体如何设计,以及如何落地的。
对于容器级别的故障,我司已经有了较为成熟的产品混沌实验平台,但是针对我们电商事业部(主要语言为 Java),依旧有不少痛点问题无法避免,例如在实验时想对特殊用户产生故障行为,针对自动化测试平台的请求产生故障行为,在使用 RPC 组件调用下游时可以针对具体请求产生故障行为等,基于此我们研发了基于 Java 场景的故障演练平台。
考虑到只需要针对 Java 应用进行故障演练,同时尽可能少的避免研发同学有接入和改造成本,所以采用了 Java Attach 技术,底层采用的是阿里开源的 JVM-SANDBOX,基于此开发了 Achilles Agent,在目标方法的前后加上了自定义的拦截器,从而织入故障代码。
再往上层就是故障演练平台的开发了,包括应用管理、用例管理、故障配置的动态下发、故障的调试和结果管理以及场景演练。那么 agent 与故障演练平台是如何交互的呢?
环境准备:对于想要使用故障演练的系统,必须要先为应用完成环境初始化,考虑到我们需要不定时地制造故障,所以采用了 Java Attach Agent 技术,为了避免字节码修改过程中引起的 CPU 飙升导致用户访问超时问题,我们会先对应用进行摘流,确保 agent 已经装载完毕后,再将应用接流。故障 agent 会在目标代码执行前后添加两个拦截器,用来执行故障。
故障执行:当接收到来自故障平台的请求后,会先判断这条请求是一个正常请求还是故障请求,判断条件即是请求头中是否包含指定的故障标识,如果不是故障请求则正常处理,是故障请求会先查询故障,然后根据故障条件判断是否需要执行故障,最后按照优先级顺序执行筛选后的故障。
故障响应:执行完故障之后,会根据用户在故障 case 中配置的响应表达式解析是否满足预期,将结果存储至数据库中,还会存储调用过程中产生的故障明细日志,方便研发排查问题。
故障行为分类:按照故障的生命周期,我们将故障分成了两类:一种是 PodFault,另一种是 RequestFault。pod 级别表现为跟应用相关,比如说 CPU 满载,JVM 堆内存溢出这种故障行为,request 级别故障表现为跟一次请求相关,请求结束那么本次故障行为也就终止了,一般有网络延时故障、异常故障、返回值故障等。
网络延时故障:在调用下游时模拟网络延时,假设依赖某个下游服务的 RT 突然增高,而调用方系统并未设置超时时间或者未使用线程池,导致调用方的容器线程一直阻塞,进而使整个服务不可用。
异常故障:让指定方法抛出异常,一般用于验证服务是否可以降级。
返回值故障:让指定方法返回特定返回值,一般用于测试等场景。
故障类型分类:当前系统内置的故障类型都是针对第三方组件的调用,如 HttpClient、Redis、MySQL,结合不同的故障行为,我们就可以组合多种故障类型,例如通过 HttpClient 插件和网络延时故障就可以组合模拟使用 HttpClient 时发生了网络阻塞异常,从而验证整个接口的 RT 和响应值是否符合预期。
这里举个实际 case 来说明下:假设原来调用下游的超时时间是 300ms,此时用户注入了一个网络延时故障(基础延时 200ms,波动范围在 30ms 内),在执行故障之前会首先计算具体延时时间,这里假设是 227ms,然后开始执行故障,即让程序 sleep 227ms,然后把此次调用的耗时时间调整为 300ms - 227ms = 73ms,如果没有其他故障则最后执行目标方法。
故障爆炸范围:在设计之初,我们调研了市面上很多的故障演练产品,发现都有一个弊端,故障爆炸范围不可控或者很难控,这个范围要做到随着用户的需求表现出可大可小的范围。下面举几个例子来阐述下:小范围故障:比如说当前系统正在测试,只想针对调用下游的某一个接口产生超时异常,但又不影响其他接口的测试,希望将故障行为绑定到具体的接口或者测试账号上,通常用于为测试同学提供一些快捷的异常 case 验证手段。大范围故障:比如说想验证某个应用对某些组件表现出百分百超时,比如说某个服务依赖 Redis 做缓存,可以将 Redis 组件表现为全部超时,用来查看服务响应是否正常,通常用于梳理服务的强弱依赖。
除了上面说的两个比较宽泛的故障范围,实际我们电商事业部还面临着更多更复杂的场景,比如说当前我们已经有了一套自动化的测试工具,能否让这些测试请求也具备故障演练的效果。
综合以上种种因素,我们在设计时对故障的爆炸范围有了精确的控制,它是如何实现的呢?首先在为目标机器安装故障 agent 时,需要指定一个故障模板,这个我们也称之为全局故障模板或者应用故障模板,顾名思义,这个模板中的故障是对所有请求都可以生效使用的,如下图所示:与之相对应的是请求级别故障模板,也即这个故障是绑定到具体的测试请求上的,相当于通过故障演练平台发起的请求都可以单独指定故障,如下图所示:根据上面的应用级别故障和请求级别故障,再配合故障模板中的 expression 表达式,我们就可以组合多种异常行为,从而更加细粒度的控制故障爆炸范围。
除此之外,为了支持第三方平台也能使用应用级别故障,我们开发了特殊来源的 source header,一旦识别到来自于特定的第三方请求调用,也享有故障效果。
那支不支持在生产环境上对真实用户模拟出故障效果呢?当然也是支持的,我们提供了是否强制执行故障功能,但会有一些额外限制条件,比如说最多影响多少条数据或者影响时间范围,否则的话粒度太大可能就会造成非预期的生产事故了。
正常来说,只有故障演练平台的请求才具备故障行为能力,如下图所示:为了让正常请求也具备故障能力,可以在应用级别的故障模板中开启强制故障,从而产生故障效果,强制故障会有一个自检操作,比如说故障只允许运行半小时或者最多产生10条故障响应,那么到期后会自动删除,如下图所示:故障的响应断言我们采用了 groovy 脚本的方式,方便研发对不同接口的返回值做自定义特殊处理,如下图所示:如果在故障模板里开启了 verbose 功能,那么响应中还会附加故障明细日志,方便研发更精准地查看故障到底有没有执行,在哪一步执行等等信息,如下图所示:有了上面的基础,我们就可以为多个应用批量地完成场景演练了,首先需要创建好一个测试场景集合,顾名思义就是包含了 n 多个测试请求,然后添加故障应用,最后创建场景演练,在场景演练里我们可以模拟出服务雪崩等现象,即将某个测试场景进行轮数的放大和期望 QPS 的提升,从而查看某个故障是否会引发服务雪崩,场景演练如下图所示:通过对现有混沌试验平台的不足,我们研发了电商自有的故障演练平台,针对性地解决了故障爆炸范围不可控,故障行为难纠错等问题,目前已经接入的核心应有有 30+ 个,故障 case 录入了300+个,未来会着手自动化演练、可视化演练、容灾多活演练等方面。
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.