混沌工程 chaosblade java 故障注入原理

唐潇唐 · 2023年10月02日 · 3910 次阅读

由于最近在基于chaosblade开发公司自定义的插件,我将 chaosblade-exec-jvm 加载流程以及执行机制梳理了一遍,本篇文章也基于此,作为源码阅读笔记

加载流程

chaosblade 在执行 Java 应用故障注入时,分为两部分,首先通过prepare命令加载插件,以监听故障注入命令;其次是create命令创建具体的故障注入命令。

框架入口位置是在com.alibaba.chaosblade.exec.bootstrap.jvmsandbox.SandboxModule,在调用 prepare 命令时,首先会调用onLoad()方法,会做三件事

  1. 将自己设置为 plugin 生命周期的监听器,用于监听管理 plugin 的生命周期
  2. 注册createstatusdestroy三个 handler,用于处理创建故障注入,查询故障注入状态,销毁故障注入 3 个具体的命令
  3. 设置当前标志位,表示当前已经是加载了插件

如果返回成功后,sleep 5s,等待初始化完成

然后再通过chaosblade调用active()方法,而chaosblade-exec-jvm通过接受到请求后,调用onActive()方法,用于加载所有的 plugin,具体逻辑如下:

  • 获取plugins目录下所有的 plugin,然后通过 spi 加载所有的 plugin
  • 然后初始化每一个 plugin 为一个 pluginBean,并且将所有的 modelSpec 和 plugin 注册到内存中,以为了在后续接受具体故障注入命令时能够加载对应的 plugin,以及判断命令格式是否正确

如果上述两步都执行成功,就返回prepare执行成功

执行流程

create 流程

从注册的 handler 中获取到 createHandler

  • 判断是否加载了插件以及一些参数校验

  • 根据targetactionFlagparams初始化 model 对象,根据 model 对象执行对应的 predict 方法,即可以在 plugin 中实现predicate方法,可以执行一些自定义的action参数逻辑校验,如果成功,则继续,反之,返回参数校验失败。例如,在delay故障的时候,需要判断--time为必填项

public PredicateResult predicate(ActionModel actionModel) {
        if (StringUtil.isBlank(actionModel.getFlag(timeFlag.getName()))) {
            return PredicateResult.fail("less time argument");
        }
        return PredicateResult.success();
    }
  • 执行preMatcherPredict(),可以做一些自定义的matcher参数逻辑校验,或者做一些参数变形。比如在我们自定义的逻辑中,我们需要根据我们的应用唯一标识,获取到该应用下所有可用的 ip 列表,根据如下逻辑进行参数变形
protected PredicateResult preMatcherPredicate(Model model) {
        if (model == null) {
            return PredicateResult.fail("matcher not found for dsf");
        }
        MatcherModel matcher = model.getMatcher();
        Set<String> keySet = matcher.getMatchers().keySet();
        if (keySet.contains(DsfConstant.CONSUMER_KEY)) {
            if (keySet.contains(DsfConstant.SERVICE_GROUP)) {
                return PredicateResult.success();
            } else {
                return PredicateResult.fail("less sgName option of consumer for dsf experiment");
            }
        }
        if (keySet.contains(DsfConstant.PROVIDER_KEY)) {
            if (keySet.contains(DsfConstant.SERVICE_GROUP) &&
                    keySet.contains(DsfConstant.ENV_TYPE)) {
                if (!keySet.contains(DsfConstant.IP_LIST)) {
                    String env = (String) matcher.getMatchers().get(DsfConstant.ENV_TYPE);
                    String sgName = (String) matcher.getMatchers().get(DsfConstant.SERVICE_GROUP);
                    String kernelUrl = getKernelUrl(env, sgName);
                    String endpoints = null;
                    if (kernelUrl != null) {
                        try {
                            endpoints = getEndpointInSgGroup(kernelUrl);
                        } catch (IOException ignored) {
                        }
                    }
                    if (endpoints != null) {
                        matcher.remove(DsfConstant.SERVICE_GROUP);
                        matcher.remove(DsfConstant.ENV_TYPE);
                        matcher.add(DsfConstant.IP_LIST, endpoints);
                        LOGGER.info("add ip list {} for dsf", endpoints);
                    }
                } else {
                    matcher.remove(DsfConstant.SERVICE_GROUP);
                    matcher.remove(DsfConstant.ENV_TYPE);
                }
            }
            return PredicateResult.success();
        }
        return PredicateResult.fail("less necessary matcher is consumer or provider for dsf");
    }
  • 执行 matcher 的predict参数校验逻辑,可以对每个参数进行具体的逻辑校验

  • 将实验 id 注册进去,如果 id 已存在,则返回实验 id 存在的报错

  • 懒加载 plugin,根据 target 获取到 pluginBeans,先判断 pluginBeans 是否已经加载 (即标志位是否为 true,其实 plugin 已经通过 spi 加载过了),如果没有加载则通过对应 target 下的 plugin 标志位设置为 true,并加入到 listener 中,加入的具体逻辑就是开始 watch pointcut 中定义的 class 和 method。当然可以通过设置 matcher 来控制是 watch before 还是 after 事件

  • 执行具体的preCreate逻辑,如果 plugin 的 modelSpec 实现了PreCreateInjectionModelHandlerpreCreate方法,则会执行具体的 preCreate 逻辑,即可以通过该方法,进行一些直接的故障注入逻辑,比如:我的需求如果是调用 create 命令时,需要将内存占满,则可以通过实现 preCreate 方法,来实现内存占用逻辑,因为不需要进行时间监听,则不需要实现对应的 pointcut 监听

  • 如果全部操作成功就返回成功,反正,返回 server error 的报错

而 watch 事件是怎么通知到插件逻辑的呢?具体逻辑是在com.alibaba.chaosblade.exec.bootstrap.jvmsandbox.BeforeEventListenercom.alibaba.chaosblade.exec.bootstrap.jvmsandbox.AfterEventListeneronEvent()中,我们以 beforeEvent 为示例,具体步骤为:

  • 如果 plugin 的实现类的getPointCut()不为空,或者切面的类不是以java开头,或者sun开头,或者[开头,或者com.alibaba.chaosblade开头,并且匹配到了classMatcher.isMatched(),则会将 class 注册到监听类中,用户可以定义不同的 class match 逻辑来进行筛选,具体的匹配逻辑位于com.alibaba.chaosblade.exec.common.aop.matcher.clazz
  • 方法匹配则是通过methodMatcher.isMatched()方法来判断是否匹配到了需要监听的方法,用户可以通过定义不同的 method match 逻辑来进行筛选,具体的逻辑位于com.alibaba.chaosblade.exec.common.aop.matcher.method

  • 如果 event 是对应的事件,比如是 beforeEvent,通过 classloader 获取 class,通过反射获取对应的方法,这里获取到的 class 和 method 就是通过 pointcut 配置的监听切面

  • 获取到该 plugin 的 enhancer 实现类,做对应的切面增强逻辑。首先看这个 target 实验是否存在,如果存在直接返回;如果不存在,则调用业务方实现类的doBeforeAdvice逻辑,如果需要做具体的切面增强,需要返回一个EnhancerModel对象,这个对象是业务方从切面获取到的参数配置,即需要对哪些参数做切面增强,如果该对象为 null,那直接返回,不进行增强;如果不是 null,那么从内存中根据 target 查询 create 的参数配置,如果参数不匹配切面的参数,说明不匹配,反正才进行增强;然后会判断是否配置了effect-count,即需要控制故障注入增强的次数,或者effect-percent,故障注入的影响百分比;如果没有触发上述的限流,则根据 actionExecutor 进行具体的故障注入。

那么怎么实现 executor 逻辑呢?chaosblade 提供了一些默认的 executor 实现,比如,如果 modelSpec 继承FrameworkModelSpec,则默认实现了延迟和抛异常两种故障,则只需在doBeforeAdvice实现具体的延迟和抛异常切面。如果默认的 executor 满足不了需求,可以实现ActionExecutor的 run 方法,实现自定义的故障注入

destroy 流程

整体来看,destroy 流程还是比较简单的,整体分为 3 个部分

  1. 从实验列表中移除掉当前实验
  2. 从监控列表中移除掉当前实验
  3. 执行PreDestroyInjectionModelHandler实验类的preDestroy方法,可以自定义一些销毁前钩子函数

整体来看,chaosblade-exec-jvm可扩展性还是非常高的,可以非常方便实现自定义的 plugin 逻辑。不过问题是,当前的插件过多,会导致业务逻辑过于分散了;好处是扩展插件时会比较方便,并且不会担心项目后续迭代造成与当前插件的代码冲突

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册