如果故障选择了你……
作者 | 叶飞、穹谷
导读:总以为混沌工程离你很远?但发生故障的那一刻不是由你来选择的,而是那一刻来选择你,你能做的就是为之做好准备。混沌工程在阿里内部已经应用多年,而ChaosBlade这个开源项目是阿里多年来通过注入故障来对抗故障的经验结晶。为使大家更深入的了解其实现原理以及如何扩展自己所需要的组件故障注入,我们准备了一个系列对其做详细技术剖析:架构篇、模型篇、协议篇、字节码篇、插件篇以及实战篇。
原文标题《技术剖析 Java 场景混沌工程实现系列(一)| 架构篇》
前言
在分布式系统架构下,服务间的依赖日益复杂,很难评估单个服务故障对整个系统的影响,并且请求链路长,监控告警的不完善导致发现问题、定位问题难度增大,同时业务和技术迭代快,如何持续保障系统的稳定性和高可用性受到很大的挑战。
我们知道发生故障的那一刻不是由你来选择的,而是那一刻来选择你,你能做的就是为之做好准备。所以构建稳定性系统很重要的一环是混沌工程,在可控范围或环境下,通过故障注入,来持续提升系统的稳定性和高可用能力。
ChaosBlade(Github 地址:https://github.com/chaosblade-io/chaosblade) 是一款遵循混沌工程实验原理,提供丰富故障场景实现,帮助分布式系统提升容错性和可恢复性的混沌工程工具,可实现底层故障的注入,特点是操作简洁、无侵入、扩展性强。 其中 chaosblade-exec-jvm (Github 地址:https://github.com/chaosblade-io/chaosblade-exec-jvm )项目实现了零成本对 Java 应用服务故障注入。其不仅支持主流的框架组件,如 Dubbo、Servlet、RocketMQ 等,还支持指定任意类和方法注入延迟、异常以及通过编写 Java 和 Groovy 脚本来实现复杂的实验场景。
为使大家更深入的了解其实现原理以及如何扩展自己所需要的组件故障注入,分为六篇文章对其做详细技术剖析:架构篇、模型篇、协议篇、字节码篇、插件篇以及实战篇。本文将详细介绍 chaosblade-exec-jvm 的整体架构设计,使用户对 chaosblade-exec-jvm 有一定的了解。
系统设计
Chaosblade-exec-jvm 基于 JVM-Sanbox 做字节码修改,执行 ChaosBlade 工具可实现将故障注入的 Java Agent 挂载到指定的应用进程中。Java Agent 遵循混沌实验模型设计,通过插件的可拔插设计来扩展对不同 Java 组件的支持,可以很方便的扩展插件来支持更多的故障场景,插件基于 AOP 的设计定义通知Advice
、增强类Enhancer
、切点PointCut
,同时结合混沌实验模型定模型ModelSpec
、实验靶点Target
、匹配方式Matcher
、攻击动作Action
。
Chaosblade-exec-jvm 在由make build
编译打包时下载 JVM-Sanbox relase 包,编译打包后 chaosblade-exec-jvm 做为 JVM-Sandbox 的模块。在加载 Agent 后,同时监听 JVM-Sanbox 的事件来管理整个混沌实验流程,通过Java Agent 技术来实现类的 transform 注入故障。
原理剖析
在日常后台应用开发中,我们经常需要提供 API 接口给客户端,而这些 API 接口不可避免的由于网络、系统负载等原因存在超时、异常等情况。使用 Java 语言时,HTTP 协议我们通常使用 Servlet 来提供 API 接口,chaosblade-exec-jvm 支持 Servlet 插件,注入超时、自定义异常等故障能力。本篇将通过给 Servlet API 接口 注入延迟故障能力为例,分析 chaosblade-exec-jvm 故障注入的流程。
对 Servlet API 接口/topic
延迟3秒,步骤如下:
// 挂载 Agent blade prepare jvm --pid 888 {"code":200,"success":true,"result":"98e792c9a9a5dfea"} // 注入故障能力 blade create servlet --requestpath=/topic delay --time=3000 --method=post {"code":200,"success":true,"result":"52a27bafc252beee"} // 撤销故障能力 blade destroy 52a27bafc252beee // 卸载 Agent blade revoke 98e792c9a9a5dfea
1. 执行过程
以下通过 Servlet 请求延迟为例,详细介绍故障注入的过程。
- ChaosBlade 下发挂载命令,挂载 Sandbox 到应用进程,激活 Java Agent,例如
blade p jvm --pid 888
。 - 挂载 Sandbox 后加载 chaosblade-exec-jvm 模块,加载插件,如 ServletPlugin、DubboPlugin 等。
- 匹配 ServletPlugin 插件的切点、注册事件监听,HttpServlet 的 doPost、doGet 方法。
- ChaosBlade 下发故障规则命令
blade create servlet --requestpath=/topic delay --time=3000 --method=post
。 - 匹配故障规则,如 --requestpath=/topic ,访问 http://127.0.0.1/topic 规则匹配成功。
- 匹配故障规则成功后,触发故障,如延迟故障、自定义异常抛出等。
- ChaosBlade 下发命令卸载 JavaAgent,如
blade revoke 98e792c9a9a5dfea
。
2. 代码剖析
1)挂载 Agent
blade p jvm --pid 888
该命令下发后,将在目标 Java 应用进程挂在 Agent ,触发 SandboxModule onLoad() 事件,初始化 PluginLifecycleListener 来管理插件的生命周期,同时也触发 SandboxModule onActive() 事件,加载部分插件,加载插件对应的 ModelSpec。
// Agent 加载事件 public void onLoad() throws Throwable { ManagerFactory.getListenerManager().setPluginLifecycleListener(this); dispatchService.load(); ManagerFactory.load(); } // ChaosBlade 模块激活实现 public void onActive() throws Throwable { loadPlugins(); }
2)加载 Plugin
Plugin 加载时,创建事件监听器 SandboxEnhancerFactory.createAfterEventListener(plugin) ,监听器会监听感兴趣的事件,如 BeforeAdvice、AfterAdvice 等,具体实现如下:
// 加载插件 public void add(PluginBean plugin) { PointCut pointCut = plugin.getPointCut(); if (pointCut == null) { return; } String enhancerName = plugin.getEnhancer().getClass().getSimpleName(); // 创建filter PointCut匹配 Filter filter = SandboxEnhancerFactory.createFilter(enhancerName, pointCut); // 事件监听 int watcherId = moduleEventWatcher.watch(filter, SandboxEnhancerFactory.createBeforeEventListener(plugin), Event.Type.BEFORE); watchIds.put(PluginUtil.getIdentifier(plugin), watcherId); }
3)匹配 PointCut
SandboxModule onActive() 事件触发 Plugin 加载后,SandboxEnhancerFactory 创建 Filter,Filter 内部通过 PointCut 的 ClassMatcher 和 MethodMatcher 过滤。
public static Filter createFilter(final String enhancerClassName, final PointCut pointCut) { return new Filter() { @Override public boolean doClassFilter(int access, String javaClassName, String superClassTypeJavaClassName, String[] interfaceTypeJavaClassNameArray, String[] annotationTypeJavaClassNameArray ) { // ClassMatcher 匹配 ClassMatcher classMatcher = pointCut.getClassMatcher(); ... } @Override public boolean doMethodFilter(int access, String javaMethodName, String[] parameterTypeJavaClassNameArray, String[] throwsTypeJavaClassNameArray, String[] annotationTypeJavaClassNameArray) { // MethodMatcher 匹配 MethodMatcher methodMatcher = pointCut.getMethodMatcher(); ... }; }
4)触发 Enhancer
如果已经加载插件,此时目标应用匹配能匹配到 Filter 后,EventListener 已经可以被触发,但是 chaosblade-exec-jvm 内部通过 StatusManager 管理状态,所以故障能力不会被触发。
例如 BeforeEventListener 触发调用 BeforeEnhancer 的 beforeAdvice() 方法,在ManagerFactory.getStatusManager().expExists(targetName) 判断时候被中断,具体的实现如下:
public void beforeAdvice(String targetName, ClassLoader classLoader, String className, Object object, Method method, Object[] methodArguments) throws Exception { // 判断实验的状态 if (!ManagerFactory.getStatusManager().expExists(targetName)) { return; } EnhancerModel model = doBeforeAdvice(classLoader, className, object, method, methodArguments); if (model == null) { return; } ... // 注入阶段 Injector.inject(model); }
5)创建混沌实验
blade create servlet --requestpath=/topic delay --time=3000
该命令下发后,触发 SandboxModule @Http("/create") 注解标记的方法,将事件分发给 com.alibaba.chaosblade.exec.service.handler.CreateHandler
处理
在判断必要的 uid、target、action、model 参数后调用 handleInjection,handleInjection 通过状态管理器注册本次实验,如果插件类型是 PreCreateInjectionModelHandler 类型,将预处理一些东西。同是如果 Action 类型是 DirectlyInjectionAction,那么将直接进行故障能力注入,且不需要走 Enhancer,如 JVM OOM 故障能力等。
public Response handle(Request request) { if (unloaded) { return Response.ofFailure(Code.ILLEGAL_STATE, "the agent is uninstalling"); } // 检查 suid,suid 是一次实验的上下文ID String suid = request.getParam("suid"); ... return handleInjection(suid, model, modelSpec); } private Response handleInjection(String suid, Model model, ModelSpec modelSpec) { RegisterResult result = this.statusManager.registerExp(suid, model); if (result.isSuccess()) { // 判断是否预创建 applyPreInjectionModelHandler(suid, modelSpec, model); } }
ModelSpec
-
com.alibaba.chaosblade.exec.common.model.handler.PreCreateInjectionModelHandler
预创建 -
com.alibaba.chaosblade.exec.common.model.handler.PreDestroyInjectionModelHandler
预销毁
private void applyPreInjectionModelHandler(String suid, ModelSpec modelSpec, Model model) throws ExperimentException { if (modelSpec instanceof PreCreateInjectionModelHandler) { ((PreCreateInjectionModelHandler)modelSpec).preCreate(suid, model); } } ...
DirectlyInjectionAction
如果 ModelSpec 是 PreCreateInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction 类型,将直接进行故障能力注入,比如 JvmOom 故障能力,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将加载插件。
private Response handleInjection(String suid, Model model, ModelSpec modelSpec) { // 注册 RegisterResult result = this.statusManager.registerExp(suid, model); if (result.isSuccess()) { // handle injection try { applyPreInjectionModelHandler(suid, modelSpec, model); } catch (ExperimentException ex) { this.statusManager.removeExp(suid); return Response.ofFailure(Response.Code.SERVER_ERROR, ex.getMessage()); } return Response.ofSuccess(model.toString()); } return Response.ofFailure(Response.Code.DUPLICATE_INJECTION, "the experiment exists"); }
注册成功后返回 uid,如果本阶段直接进行故障能力注入了,或者自定义 Enhancer advice 返回 null,那么后不通过Inject 类触发故障。
6)注入故障能力
故障能力注入的方式,最终都是调用 ActionExecutor 执行故障能力。
- 通过 Injector 注入;
- DirectlyInjectionAction 直接注入,直接注入不进过 Inject 类调用阶段,如果 JVM OOM 故障能力等。
DirectlyInjectionAction 直接注入不经过Enhancer参数包装匹配直接到故障触发 ActionExecutor 执行阶段,如果是Injector 注入此时因为 StatusManager 已经注册了实验,当事件再次出发后ManagerFactory.getStatusManager().expExists(targetName) 的判断不会被中断,继续往下走,到了自定义的 Enhancer ,在自定义的 Enhancer 里面可以拿到原方法的参数、类型等,甚至可以反射调原类型的其他方法,这样做风险较大,一般在这里往往是取一些成员变量或者 get 方法等,用于 Inject 阶段参数匹配。
7)包装匹配参数
自定义的 Enhancer,如 ServletEnhancer,把一些需要与命令行匹配的参数 包装在 MatcherMode 里面,然后包装 EnhancerModel 返回,比如 --requestpath = /index ,那么requestpath 等于 requestURI;--querystring="name=xx" 做自定义匹配。参数包装好后,在 Injector.inject(model) 阶段判断。
public EnhancerModel doBeforeAdvice(ClassLoader classLoader, String className, Object object, Method method, Object[] methodArguments) throws Exception { Object request = methodArguments[0]; String requestURI = ReflectUtil.invokeMethod(request, ServletConstant.GET_REQUEST_URI, new Object[]{}, false); String requestMethod = ReflectUtil.invokeMethod(request, ServletConstant.GET_METHOD, new Object[]{}, false); MatcherModel matcherModel = new MatcherModel(); matcherModel.add(ServletConstant.METHOD_KEY, requestMethod); matcherModel.add(ServletConstant.REQUEST_PATH_KEY, requestURI); Map<String, Object> queryString = getQueryString(requestMethod, request); EnhancerModel enhancerModel = new EnhancerModel(classLoader, matcherModel); // 自定义参数匹配 enhancerModel.addCustomMatcher(ServletConstant.QUERY_STRING_KEY, queryString, ServletParamsMatcher.getInstance()); return enhancerModel; }
8)判断前置条件
Inject 阶段首先获取 StatusManage 注册的实验,compare(model, enhancerModel) 做参数比对,比对失败返回,limitAndIncrease(statusMetric) 判断 --effect-count --effect-percent 来控制影响的次数和百分比
public static void inject(EnhancerModel enhancerModel) throws InterruptProcessException { String target = enhancerModel.getTarget(); List<StatusMetric> statusMetrics = ManagerFactory.getStatusManager().getExpByTarget( target); for (StatusMetric statusMetric : statusMetrics) { Model model = statusMetric.getModel(); // 匹配命令行输入参数 if (!compare(model, enhancerModel)) { continue; } // 累加攻击次数和判断攻击次数是否到达 effect count boolean pass = limitAndIncrease(statusMetric); if (!pass) { break; } enhancerModel.merge(model); ModelSpec modelSpec = ManagerFactory.getModelSpecManager().getModelSpec(target); ActionSpec actionSpec = modelSpec.getActionSpec(model.getActionName()); // ActionExecutor执行故障能力 actionSpec.getActionExecutor().run(enhancerModel); break; } }
9)触发故障能力
由 Inject 触发,或者由 DirectlyInjectionAction 直接触发,最后调用自定义的 ActionExecutor 生成故障,如 DefaultDelayExecutor ,此时故障能力已经生效了。
public void run(EnhancerModel enhancerModel) throws Exception { String time = enhancerModel.getActionFlag(timeFlagSpec.getName()); Integer sleepTimeInMillis = Integer.valueOf(time); // 触发延迟 TimeUnit.MILLISECONDS.sleep(sleepTimeInMillis); }
3. 销毁实验
blade destroy 52a27bafc252beee
该命令下发后,触发 SandboxModule @Http("/destory") 注解标记的方法,将事件分发给 com.alibaba.chaosblade.exec.service.handler.DestroyHandler 处理,注销本次故障的状态,此时再次触发 Enchaner 后,StatusManger判定实验状态已经销毁,不会在进行故障能力注入
// StatusManger 判断实验状态 if (!ManagerFactory.getStatusManager().expExists(targetName)) { return; }
如果插件的 ModelSpec 是 PreDestroyInjectionModelHandler 类型,且 ActionSpec 的类型是 DirectlyInjectionAction 类型,停止故障能力注入,ActionSpec 的类型不是 DirectlyInjectionAction 类型,将卸载插件。
// DestroyHandler 注销实验状态 public Response handle(Request request) { String uid = request.getParam("suid"); ... // 判断 uid if (StringUtil.isBlank(uid)) { if (StringUtil.isBlank(target) || StringUtil.isBlank(action)) { return false; } // 注销status return destroy(target, action); } return destroy(uid); }
4. 卸载 Agent
blade revoke 98e792c9a9a5dfea
该命令下发后,触发 SandboxModule unload() 事件,同时插件卸载,完全回收 Agent 创建的各种资源。
public void onUnload() throws Throwable { dispatchService.unload(); ManagerFactory.unload(); watchIds.clear(); }
总结
本文以 Servlet 场景为例,详细介绍了 chaosblade-exec-jvm 项目架构设计和实现原理,后续将通过模型篇、协议篇、字节码篇、插件篇以及实战篇深入介绍此项目,使读者达到可以快速扩展自己所需插件的目的。
ChaosBlade 项目作为一个混沌工程实验工具,不仅使用简洁,而且还支持丰富的实验场景且扩展场景简单,支持的场景领域如下:
- 基础资源:比如 CPU、内存、网络、磁盘、进程等实验场景;
- Java 应用:比如数据库、缓存、消息、JVM 本身、微服务等,还可以指定任意类方法注入各种复杂的实验场景;
- C++ 应用:比如指定任意方法或某行代码注入延迟、变量和返回值篡改等实验场景;
- Docker 容器:比如杀容器、容器内 CPU、内存、网络、磁盘、进程等实验场景;
- Kubernetes 平台:比如节点上 CPU、内存、网络、磁盘、进程实验场景,Pod 网络和 Pod 本身实验场景如杀 Pod,Pod IO 异常,容器的实验场景如上述的 Docker 容器实验场景;
- 云资源:比如阿里云 ECS 宕机等实验场景。
ChaosBlade 社区欢迎各位加入,我们一起讨论混沌工程领域实践或者在使用 ChaosBlade 过程中产生的任何想法和问题。
作者简介
叶飞:Github @tiny-x,开源社区爱好者,ChaosBlade Committer,参与推动 ChaosBlade 混沌工程生态建设。
穹谷:Github @xcaspar,ChaosBlade 项目负责人,混沌工程布道师。
“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
教你 4 步搭建弹性可扩展的 WebAPI
作者 | 萧起 阿里云云原生团队 本文整理自《Serverless 技术公开课》,关注“Serverless”公众号,回复“入门”,即可获取 Serverless 系列文章 PPT。 导读:本节课程主要分为三个部分,基本概念中介绍基于函数计算的 WebAPI 与普通的 WebAPI 的区别及优势;开发流程中介绍如何在函数计算的控制台进行 WebAPI 的开发;操作演示中会实例演示函数计算 WebAPI 的开发过程。 基本概念 常见的 WebAPI 架构如上图所示,主要包括客户端(浏览器)、服务器、数据库,WebAPI 由服务器提供,同时服务器要完成负载均衡、登录鉴权的相关操作。 当客户端流量快速增大时,服务器端只能通过水平扩展加机器的方式来增加提高服务能力。 这种常规模式主要有两点局限性: 技术同学除了开发业务代码,有大量的服务器运维成本,来保证服务的稳定性、可用性,技术同学要花费很多时间进行运维工作,占用开发时间,降低项目研发效率。 流量突然增加时,需要水平扩展加机器,弹性的响应能力差,扩容速度往往要数十分钟,无法实现秒级极速扩容,导致一段时间内的服务能力不足。同时当流量变少时,难以...
- 下一篇
CDN百科11 | 如何用CDN加速OSS源站资源
在传统网站架构下,动态资源和静态资源不分离,随着访问量的增长,性能会成为瓶颈,时常会面临用户响应慢、卡顿不流畅、成本高、拓展性差等问题。用户直接访问OSS资源,访问速度会受到OSS的下行带宽以及Bucket地域的限制。如果通过CDN来访问OSS资源,带宽上限更高,并且可以将OSS的资源缓存至就近的CDN节点,通过CDN节点进行分发,访问速度更快,且费用更低。 选择通过阿里云CDN来加速OSS域名,实现静态资源的访问加速。今天的CDN百科,为大家介绍CDN加速OSS的应用场景以及通过CDN控制台实现CDN加速OSS的操作方法。 一、限时套餐 为了满足大部分客户的需求,阿里云推出CDN+OSS组合产品套餐,享受专属优惠,帮助用户搭配省心,套餐价格低至5.5折。 二、使用的前提条件 1、执行本文操作之前,请确保您已经完成阿里云账号注册 和实名认证。2、确保已在OSS上创建存储空间,且已为其绑定自定义域名。具体操作方法请参见创建存储空间和绑定自定义域名。 三、CDN+OSS架构及优势 阿里云CDN配合精准的调度系统,将您对静态资源的请求分配至最近节点,使您以最快的速度读取到所需的资源,有效解决...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2全家桶,快速入门学习开发网站教程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Red5直播服务器,属于Java语言的直播服务器
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7安装Docker,走上虚拟化容器引擎之路