简介: 总以为混沌工程离你很远?但产生故障的那一刻不是由你来抉择的,而是那一刻来抉择你,你能做的就是为之做好筹备。混沌工程在阿里外部曾经利用多年,而 ChaosBlade 这个开源我的项目是阿里多年来通过注入故障来反抗故障的教训结晶。为使大家更深刻的理解其实现原理以及如何扩大本人所须要的组件故障注入,咱们筹备了一个系列对其做具体技术分析:架构篇、模型篇、协定篇、字节码篇、插件篇以及实战篇。
作者 | 叶飞、穹谷
导读:总以为混沌工程离你很远?但产生故障的那一刻不是由你来抉择的,而是那一刻来抉择你,你能做的就是为之做好筹备。混沌工程在阿里外部曾经利用多年,而 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
- 执行过程
以下通过 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
。 - 代码分析
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);
}
- 销毁试验
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);
}
- 卸载 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 我的项目负责人,混沌工程布道师。