杭州每刻科技有限公司是一家领先的智能云财务产品和解决方案服务商,产品采用最新的微服务架构,内部业务逻辑十分复杂。对于一个大型的微服务应用,保证系统的健壮性是一件非常重要但具有挑战的话题。因此每刻科技引入了 PingCAP 云原生混沌工程测试平台 Chaos Mesh 来解决在微服务健壮性测试过程中遇到的痛点。本文分享了 Chaos Mesh 在每刻科技实际场景的使用经验。
作者:杭州每刻科技质量保证部 崔星海
引言
一个大型的微服务应用就像一个混沌的世界,保证系统的健壮性是一件非常重要但具有挑战的话题。在微服务系统中存在大量的服务实例,当部分服务实例出现问题时,微服务应用需要具有较高的容错性,通过重试,断路,自愈等手段保证系统能够继续对外正常提供服务。因此在应用发布到生产系统强需要对系统进行充分的健壮性测试。
对微服务应用进行健壮性测试的一个最大的困难是如何对系统故障进行模拟。在一个部署了成百上千微服务的测试环境中,如果想通过对应用、主机或者交换机进行设置来模拟微服务之间的通信故障是非常困难的。
每刻业务痛点
杭州每刻科技有限公司是一家领先的智能云财务产品和解决方案服务商,内部业务逻辑十分复杂,又有很多外部对接的情况,所以遇到了多种测试场景无法构建、线上问题无法复现等痛点。于是考虑引进混沌测试,通过多方面的场景,去解决测试过程中的一些问题。
比如,每刻某个业务是使用 Java 异步方式实现的,如果遇到业务处理失败的情况,会进行业务异常处理,这时候如果恰巧遇到服务刚启动或者服务进程结束,又会进行业务回滚,由于某些原因,又可能会导致回滚失败。总得来说就是,访问接口失败,进入异常,异常处理又失败,导致回滚,回滚又失败,这个时候就会有一些脏数据需要去手动清理。虽然在业务代码中有对这种故障的应对措施,但无法通过常规的测试手段去验证是否按预期执行,这个时候就需要使用混沌工程的测试方法。
为什么选择 Chaos Mesh?
每刻之所以选择 Chaos Mesh 主要源于其能在以下 2 个方面,解决每刻在微服务健壮性测试过程中遇到的痛点:
故障的不可预测以及异常的不可复现性
在分布式计算的世界中,故障可能随时随地发生在你的集群上。传统情况下,我们使用单元测试和集成测试来确保系统可以投入生产。但是,随着集群规模的扩大,复杂性的增加以及数据量以 PB 级增长,这些测试无法涵盖所有内容。
Chaos Mesh 充分考虑了分布式系统可能出现的故障,提供全面、细粒度的故障类型,通过人为地在集群中注入故障来检测集群对故障的处理以及恢复能力。目前,Chaos Mesh提供的混沌实验分为基础资源类型故障(例如 NetworkChaos, HTTPChaos, StressChaos, KernelChaos)、平台类型故障(例如 GCPChaos)和应用层故障(例如 JVMChaos)三大类,几乎涵盖了分布式测试体系中基础故障模拟的绝大多数场景。详情可参考 Chaos Mesh 文档。
专为 Kubernetes 设计
在容器世界中,Kubernetes 是绝对的领导者。本质上,Kubernetes 是用于云的操作系统。Chaos Mesh 基于 Kubernetes CRD (Custom Resource Definition) 构建,原生支持 Kubernetes 环境,提供了强悍的自动化能力。
基于 Chaos Mesh 的混沌实验
针对上面常规测试手段无法覆盖的故障场景,基于 Chaos Mesh 得到了完美实现。首先,在平台上去注入对应的方法异常故障,这个时候逻辑走到了异常处理的代码块,再注入故障,进入回滚,继续对回滚逻辑注入故障,到此就完成了这个异常场景的可测性。
每刻的不少业务是使用 Java 异步方式实现的。因此 JVM 故障注入是我们使用最多的混沌实验类型。Chaos Mesh 通过 Byteman 模拟 JVM 应用故障,主要支持以下类型的故障:
- Exception(抛出异常)
- GC (Java 垃圾回收)
- Latency (方法延迟)
- Return
JVM 注入的流程是这样的:用户填写配置提交 -> 创建 crd 资源到 K8s 集群 -> Chaos Mesh 获取到实验配置,向应用所在服务器上部署的 chaos daemon 发送请求 -> chaos daemon 执行 bminstall 安装 byteman java agent, 执行 bmsubmit 命令注入混沌实验。
下面将通过几个 JVM 应用故障的注入案例来详细说明。在进入具体类型之前,需要在 Chaos Dashboard 里选择集群和 JVM 实验大类。
案例 1:Exception
参数:
- Class:全类名的路径
- Method:方法名称
- Port:指 Byteman agent 使用的端口,要保证不和其他服务的端口冲突
参数:
- 命名空间选择器:应用所处的命名空间
- 标签:应用所属的标签,具体可参考 Kubernetes 对标签的定义
提交之后如果正常注入后,会有如下展示:
调用接口后,查看到我们注入的方法报错,报错信息就是我们注入的报错:
可以发现, 在方法 com.maycur.dingtalk.facade.UnifyFormFacade.getApprovalUnifyForms 的位置抛出了前面注入的异常 java.lang.NullPointerException。
案例 2:Latency(方法延迟)
参数:
- Latency:延迟时间(ms)
- Class:全类名的路径
- Method:方法名称
- Port:指 Byteman agent 使用的端口,要保证不和其他服务的端口冲突
其他的参数填写跟例 1 中 Exception 一样
可以明显的看到,接口很明显的延迟了,说明我们的注入是成功的。
案例 3:修改接口返回
由于 Chaos Mesh 的 RETURN 故障注入方式,只支持基本数据类型以及 String 类型的返回值,但实际情况中,我们的数据返回的大多都是一个对象,甚至是 List 包了一层对象。这样就需要我们自己去写一段脚本了。
脚本是 Byteman 原生支持的规则配置文件,可以参考 Byteman 文档。比如下图,待审批列表的,返回了一个 List 包了一个对象:
那么对应的 btm 脚本如下:
RULE return new info
CLASS com.maycur.dingtalk.facade.UnifyFormFacade
METHOD getApprovalUnifyForms
AT EXIT
BIND
Page:com.github.pagehelper.PageInfo=\$!;
UnifyFormList:com.maycur.dingtalk.dto.UnifyFormListDto=Page.getList().get(0);
IF true
DO
UnifyFormList.setInvoiceBagCode(\"123\");
return Page;
ENDRULE
可以看到接口返回已经被修改:
解释一下上图代码的意思:
- CLASS:全类路径;
- METHOD:方法名;
- AT EXIT:表示这段脚本在函数 return 的时候被触发;
- BIND:将上下文中的数据绑定到变量中, 以便后文使用;
- Page:com.github.pagehelper.PageInfo=\$!:定义了一个名为 Page 类型为com.github.pagehelper.PageInfo 的变量, 其值为\$!, 即该函数的返回值;
- UnifyFormList:com.maycur.dingtalk.dto.UnifyFormListDto=Page.getList().get(0):定义了名为UnifyFormList的变量, 类型为com.maycur.dingtalk.dto.UnifyFormListDto 值为 Page.getList().get(0);
- DO:表示脚本被触发时执行的操作;
- UnifyFormList.setInvoiceBagCode(\"123\"):通过 setter 设置修改后的值;
- return Page:返回 Page 对象;
简单来说,首先获取到该函数的返回值(一个列表),然后获取列表的第一个元素,然后通过该元素所属的类型提供的 set 方法修改它的值,从而达到修改函数返回值的目的。Byteman 提供了强大的功能,对于复杂的故障功能实现,具体可参考 Byteman 规则文档。
总结
从开始调研 Chaos Mesh 到正式使用,前前后后总共经历了一个多月的时间,遇到最大的问题就是环境兼容的问题,各种方法都试过了,最终升级服务搞定,详情见 issue #2588。
到正式使用后就很没遇到什么困难了,简单的混沌实验可以通过界面点击来实现,复杂一些的实验可以通过 Byteman 脚本来实现。
后续计划将 Chaos Mesh 引入到公司所有的服务中来,主要用于测试服务之间调用的异常结果处理以及对接外部系统的健壮性测试。
欢迎大家加入 Chaos Mesh 社区,加入 CNCF Slack 下的 Chaos Mesh 频道: project-chaos-mesh,一起参与到项目的讨论与开发中来!大家在使用过程中发现 Bug 或缺失什么功能,也可以直接在 GitHub ( https://github.com/chaos-mesh ) 上提 Issue 或 PR。
目录