ChaosBlade源码(四)chaosblade-exec-jvm

359 阅读20分钟

前言

chaosblade-exec-jvm是chaosblade下的一个子项目,基于jvm-sandbox,实现jvm层面的故障注入。

上一章的案例中基于sandbox实现了一个简单的chaos模块,但和chaosblade-exec-jvm还是有所不同的。

考虑几个问题:

1)对于同一个切点,根据业务参数不同,注入相同的故障action,会有几个Transformer?比如对kafka消费者topic=A注入异常,再对kafka消费者topic=B注入异常。

blade create kafka throwCustomException --exception java.lang.Exception --consumer --process app.jar --topic a
blade create kafka throwCustomException --exception java.lang.Exception --consumer --process app.jar --topic b

如果有多个Transformer,那么字节码增强后就像下面这样。

image.png

2)如果对同一个切点,注入不同行为action的故障生效顺序是怎样的?还是只有一个生效?比如jvm实验针对一个method即抛出异常也执行延迟。

blade create jvm throwCustomException --exception java.lang.Exception --classname a --methodname b --process app.jar
blade create jvm delay --time 3000 --classname a --methodname b --process app.jar

本章基于上述背景分析chaosblade-exec-jvm的实现:

1)chaosblade模块激活;

2)创建实验;

3)实验执行;

4)销毁实验;

注:1.7.4版本。

一、案例

sandbox提供了ModuleSPI加载不同模块。

chaosblade-exec-jvm在sandbox基础上又提供了PluginSPI加载不同插件。

image.png

每个Plugin需要实现4个方法:

1)getName:插件名;

2)getModelSpec:实验模型定义,比如:是否需要执行增强逻辑(MatcherSpec)、根据参数(FlagSpec)如何执行(ActionExecutor);

3)getPointCut:插件增强点;

4)getEnhancer:增强逻辑;

image.png

1、jvm

jvm插件特点是PointCut为空,用户在创建实验时指定classname和methodname后才能决定增强位置。

public class JvmPlugin implements Plugin {
    @Override
    public String getName() {
        return ModelConstant.JVM_TARGET;
    }
    @Override
    public ModelSpec getModelSpec() {
        return new JvmModelSpec();
    }
    @Override
    public PointCut getPointCut() {
        return null;
    }
    @Override
    public Enhancer getEnhancer() {
        return new MethodEnhancer();
    }
}

ModelSpec

JvmModelSpec。

public class JvmModelSpec extends MethodModelSpec implements PreCreateInjectionModelHandler,
        PreDestroyInjectionModelHandler {

    public JvmModelSpec() {
        super();
        addActionSpec(new JvmOomActionSpec());
        addActionSpec(new JvmCpuFullLoadActionSpec());
        addActionSpec(new JvmDynamicActionSpec());
        addActionSpec(new CodeCacheFillingActionSpec());
        addActionSpec(new JvmThreadFullActionSpec());
        addActionSpec(new FullGCActionSpec());
    }
    @Override
    public String getTarget() {
        return "jvm";
    }
    // ...
}
public class MethodModelSpec extends BaseModelSpec {
    public MethodModelSpec() {
        addThrowExceptionActionDef();
        addReturnValueAction();
        addDelayAction();
        addMethodMatcherDef();
    }
}

JvmModelSpec包含target=jvm下的9个action(ActionSpec)。

image.png

如注入自定义异常:

blade create jvm throwCustomException --exception java.lang.Exception --classname com.Controller --methodname sayHello

Enhancer

jvm实验的Enhancer同时实现了before和after。

注:只有jvm实验实现了after增强

image.png

MethodEnhancerForBefore:以before为例,子类只需要提供运行时matcher参数,如jvm实验从className和Method中提取className和methodName加入MatcherModel。

image.png

所有Enhancer都继承BeforeEnhancer,插件子类提供EnhancerModel包含运行时切点的信息,父类BeforeEnhancer调用Injector实际做注入逻辑。

image.png

2、kafka

kafka插件的特点是同一个target=kafka有两个Plugin实现

consumer消费者,producer生产者。

image.png

如为消费者注入异常。

blade create kafka throwCustomException --exception java.lang.Exception --consumer --process KafkaConsumerApp

如为生产者指定topic=test注入异常。

blade create kafka throwCustomException --exception java.lang.Exception --producer --process KafkaProducerApp --topic test

KafkaPlugin基类声明消费者和生产者插件的ModelSpec是一样的,即target/action/flag/matcher都一样,不同插件运行时自行区分生产和消费逻辑。

注:target一致会导致,即使用户通过--consumer指定为消费者实验,会同时修改producer和consumer的字节码,只是运行时producer不会执行增强逻辑,后面会看到这个逻辑

public abstract class KafkaPlugin implements Plugin, KafkaConstant {
    @Override
    public ModelSpec getModelSpec() {
        return new KafkaModelSpec();
    }
}

producer

KafkaProducerPlugin声明kafka生产者插件。

image.png

KafkaProducerPointCut拦截KafkaProducer#send。

image.png

KafkaProducerEnhancer从入参ProducerRecord中提取topic,填充EnhancerModel。

image.png

consumer

KafkaConsumerPointCut拦截KafkaConsumer#poll方法。

public class KafkaConsumerPointCut implements PointCut, KafkaConstant {

    @Override
    public ClassMatcher getClassMatcher() {
        return new NameClassMatcher("org.apache.kafka.clients.consumer.KafkaConsumer");
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        return new MethodMatcher() {
            @Override
            public boolean isMatched(String methodName, MethodInfo methodInfo) {
                return methodName.equals("poll") && methodInfo.getParameterTypes().length == 1;
            }
        };
    }
}

KafkaConsumerEnhancer在目前版本中兼容性较差,容易导致注入失败,笔者在分析时提了PR(340)修复。

主要就是通过反射从目标对象中获取topic和groupId填充EnhanceerModel,用于后续Injector做compre匹配。

public class KafkaConsumerEnhancer extends BeforeEnhancer implements KafkaConstant {

    @Override
    public EnhancerModel doBeforeAdvice(ClassLoader classLoader, String className, Object object, Method method, Object[] methodArguments) throws Exception {
        MatcherModel matcherModel = new MatcherModel();
        matcherModel.add(CONSUMER_KEY, "true");
        // groupId
        String groupId = this.findGroupId(object);
        matcherModel.add(GROUP_ID_KEY, groupId);
        // topic
        Set<String> topicKeySet = this.findTopic(object);
        EnhancerModel enhancerModel = new EnhancerModel(classLoader, matcherModel);
        enhancerModel.addCustomMatcher(TOPIC_KEY, topicKeySet, ConsumerTopicMatcher.getInstance());
        return enhancerModel;
    }
}

3、dubbo

dubbo和kafka类似,分为provider和consumer两个plugin。

比如为provider侧暴露的rpc方法注入异常。

blade create dubbo throwCustomException --exception java.lang.Exception --service com.x.DemoService --methodname sayHello --provider --process App

dubbo的特点是支持provider线程池满载实验

blade c dubbo threadpoolfull --provider --process App

注:目前对provider线程池隔离场景不太兼容

比如DemoService2使用公共线程池,DemoService使用独立线程池,实际在故障注入阶段,只有某个线程池会生效。(启动阶段第一个暴露的Service对应线程池会生效)

@DubboService
public class DemoService2Impl implements DemoService2 {
}
@DubboService(executor = "myExecutor1")
public class DemoServiceImpl implements DemoService {
}

这里主要说明的是另一个问题,如果一个plugin需要对多个class+method做增强,只能写在一个PointCut和Enhancer中。(当然你也可以分成n个plugin,比如拆分为consumer、provider、threadpool)

比如DubboProviderPlugin同时要支持异常、延迟、线程池满载。

PointCut需要通过or匹配:

1)WrappedChannelHandler#received:对应线程池处理入口;

2)AbstractProxyInvoker#invoke:对应服务调用入口;

public class DubboProviderPointCut implements PointCut {

    @Override
    public ClassMatcher getClassMatcher() {
        return new OrClassMatcher()
            .or(new NameClassMatcher("org.apache.dubbo.rpc.proxy.AbstractProxyInvoker"))
            .or(new SuperClassMatcher("org.apache.dubbo.remoting.transport.dispatcher.WrappedChannelHandler"));
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        AndMethodMatcher methodMatcherThan2700 = new AndMethodMatcher();
        ParameterMethodMatcher parameterMethodMatcherThan2700 = new ParameterMethodMatcher(new String[] {
            "org.apache.dubbo.rpc.Invocation"}, 0,
            ParameterMethodMatcher.GREAT_THAN);
        methodMatcherThan2700.and(new NameMethodMatcher("invoke")).and(parameterMethodMatcherThan2700);
        OrMethodMatcher orMethodMatcher = new OrMethodMatcher();
        orMethodMatcher.or(methodMatcherThan2700).or(new NameMethodMatcher("received"));
        return orMethodMatcher;
    }
}

DubboEnhancer:运行阶段,只能根据方法名区分走不同逻辑。

public abstract class DubboEnhancer extends BeforeEnhancer {
    @Override
    public EnhancerModel doBeforeAdvice(ClassLoader classLoader, String className, Object object,
                                        Method method, Object[]
                                                methodArguments)
            throws Exception {
        // 线程池处理逻辑
        if (method.getName().equals("received")) {
            DubboThreadPoolFullExecutor.INSTANCE.setWrappedChannelHandler(object);
            return null;
        }
        // 延迟/异常处理
    }
}

另外说说线程池满载的实现方式。

DubboThreadPoolFullExecutor#setWrappedChannelHandler:在服务端receive收到调用,将WrappedChannelHandler缓存。注意DubboThreadPoolFullExecutor是个单例对象,所以received增强只会触发一次

image.png

WaitingTriggerThreadPoolFullExecutor#triggerThreadPoolFull:获取线程池,执行full方法,这里也只会执行一次。

image.png

DubboThreadPoolFullExecutor#getThreadPoolExecutor:dubbo反射获取目标线程池。因为每次调用dubbo url不同,但是这个方法只会执行一次,所以无法实现线程池隔离的实验

image.png

AbstractThreadPoolFullExecutor#full:满载方式是,chaosblade线程池(executorService)定时按照最大线程数n向目标线程池(threadPoolExecutor)填充n个无限sleep任务。

image.png

image.png

AbstractThreadPoolFullExecutor#revoke:在实验销毁阶段,将这些无限sleep任务取消。

image.png

二、激活

根据前面第三章sandbox的铺垫,chaosblade-exec-jvm基于sandbox提供了chaosblade模块。

chaosblade模块统一管理所有实验,每个实验需要通过实现Plugin接口,由chaosblade模块通过SPI加载。

sandbox模块加载onLoad和卸载onUnload钩子,只是内存准备和资源清理逻辑,可以忽略。

重点逻辑从模块被激活onActive开始。

注:第一章blade命令行提到过,blade jvm prepare第一步是挂载sandbox,第二步是激活chaosblade模块。

image.png

SandboxModule#loadPlugins:通过SPI加载Plugin封装为PluginBean,注册ModelSpec和PluginBean为单例管理。

image.png

1、Plugin加载

chaosblade-exec-jvm最终产物是一个模块jar,放在sandbox的module目录下,所以可以被sandbox加载。

image.png

这个jar中目录结构如下,plugins目录下是n个插件。

插件和chaosblade模块一起打在了jar包里,所以spi加载要从chaosblade-exec-jvm自己这个jar里面找。

image.png

PluginJarUtil#getPluginFiles:获取所有插件jar。

image.png

PluginLoader#load:这里为每个plugin的jar创建了一个URLClassLoader,这个URLClassLoader的parent正是chaosblade模块的ModuleJarClassLoader。

image.png

结合上一章,最终chaosblade-exec-jvm的类加载结构如下,所以插件也可以provided引入用户class,也可以自己引入三方依赖,虽然chaosblade一般不需要用。

image.png

2、ModelSpec和PluginBean

每个Plugin对应一个ModelSpectargetModelSpec的唯一标识。

image.png

对于一些插件,如dubbo,他有生产和消费两个Plugin,但是两个ModelSpec的target都是dubbo,即对应blade create dubbo。

所以在chaosblade的log里可以看到,模块被激活后,有很多公用target的Plugin,这点比较重要。

image.png

DefaultPluginBeanManager维护了target下的所有PluginBean,封装为PluginBeans。

image.png

三、创建实验

chaosblade模块提供了create端点,负责创建实验。

image.png

请求入参解析如下:

image.png

CreateHandler#handle:根据第一章blade命令行铺垫,suid是由blade命令生成的随机字符串,代表实验id,target即实验目标如kafka、jvm,action是实验类型如throwCustomException、delay。

image.png

最终根据用户输入参数和ModelSpec定义,填充为一个Model。

matcher用于匹配切点是否要具体执行,action用于定义如何执行。

image.png

CreateHandler#handleInjection:

1)注册实验;2)字节码增强;3)ModelSpec预注入

image.png

1、注册实验

DefaultStatusManager维护当前正在执行的实验。

1)models:target→实验Model唯一标识→实验Model;

2)experiments:实验uid→实验Model唯一标识

image.png

DefaultStatusManager#registerExp:生成实验Model唯一标识identifier,如果实验已经存在返回失败,否则experiments维护实验uid和identifier的关系。

注:这里Model封装为了StatusMetrics。

image.png

实验Model的唯一标识由三部分组成:target+action+matcher

比如dubbo实验如下:

./blade create dubbo throwCustomException \
--exception java.lang.Exception --service com.x.DemoService --methodname sayHello --provider --process Application

那么最终实验唯一标识如下:

dubbo|throwCustomException|methodname=sayHello|provider=true|service=com.x.DemoService

注意这里不包含flags,比如对同一个地方注入两个不同的exception。

如果实验重复,返回406,the experiment exists。

image.png

2、字节码增强

CreateHandler#lazyLoadPlugin:根据target找到n个Plugin,执行增强。

这里就是1个target对应n个Plugin关系的重要体现。

同1个target,比如dubbo,即使设置matcher是consumer,provider侧的字节码也会一起被增强,只是在执行过程中不会被matcher匹配

image.png

SandboxModule#add:调用sandbox的watch api注册监听,根据上一章分析,就是安装Transformer,如果目标class已经被加载,再触发retransform。

这说明了一个问题,Transformer数量=插件Plugin数量,比如针对dubbo consumer调用异常和延迟只会有一个Transformer。(但是jvm异常和延迟会有2个Transformer,本质原因是Plugin是两个,见下面预注入)

image.png

sandbox Filter的创建可以忽略,就是根据Plugin的Pointcut适配创建。

BeforeEventListener较为简单,正常实现EventListener即可,执行Plugin的Enhancer逻辑。

image.png

对于jvm实验如抛出自定义异常,支持--after,在方法执行后抛出异常。

sandbox的ReturnEvent没有维护方法调用的上下文,只能拿到方法返回值

AfterEventListener同时监听Before和Return事件,目的是在方法返回阶段能拿到BeforeEvent的信息,BeforeEvent信息通过ThreadLocal维护。

image.png

3、预注入

CreateHandler#applyPreInjectionModelHandler:对于部分ModelSpec定义,如果实现了PreCreateInjectionModelHandler,会被回调preCreate

image.png

目前有三个ModelSpec需要根据action做特殊处理。

druid是数据库连接池满载,和dubbo线程池满载类似,忽略。

image.png

dubbo线程池满载

DubboModelSpec#preCreate:如果当前action是dubbo线程池满载,dubbo需要在这里往ActionExecutor中塞入标识(ActionExecutor在ModelSpec中,所以action纬度下单例)。

这也是因为class增强是target维度统一埋入的,需要塞入标志位用于区分是否需要在provider的received方法处真实触发线程池满载。

image.png

DubboThreadPoolFullExecutor#setWrappedChannelHandler:

dubbo provider运行时收到请求,判断当前是否有线程池满载实验,如果有才会执行该逻辑。

image.png

jvm实验

jvm实验的特点在案例中说过,Plugin无法指定PointCut。

JvmModelSpec#preCreate:jvm实验action可以分为两类。

image.png

JvmOomActionSpec#createInjection:直接执行实验,如OOM实验。

image.png

MethodPreInjectHandler#preHandleInjection:自定义异常、自定义脚本、延迟实验,根据用户参数classname和methodname动态创建Plugin,调用SandboxModule#add安装字节码增强。

image.png

这个MethodPlugin的pointcut就是指定class和method。

image.png

四、实验执行

1、主流程

BeforeEventListener#handleEvent:以before为例。

通过反射获取补充一些sandbox BeforeEvent中未包含的信息如method,调用Plugin的Enhancer。

如果Enhancer抛出InterruptProcessException,转换为sandbox的ProcessControlException,可以控制流程走向,如立即抛出异常到宿主,方法立即返回指定Object。

如果Enhancer抛出未知异常,sandbox会正常执行目标方法。

注:具体流程控制,见上一章sandbox。

image.png

所有Enhancer都继承自BeforeEnhancer

1)插件从当前执行切点中获取matcher需要的参数,比如kafka需要从message中获取topic;

2)执行inject;

image.png

Injector#inject:循环target下的所有正在执行的实验Model(用户入参):

1)compare匹配用户Model和插件EnhancerModel中的Matcher,注意只匹配一个;

2)处理effect-count、effect-percent逻辑;

3)执行具体Action;

image.png

2、匹配compare

并非所有plugin都是create后就生效(如jvm oom),大部分plugin在切点的基础上,需要基于业务参数EnhancerModel和实验matcher匹配。

Injector#compare:按照插件填充的EnhancerModel(左)和实验Model(右),如果匹配成功,才执行后续逻辑。

image.png

匹配包含众多逻辑,简单理解就是map中的kv比对,忽略。

这里说明了一个事情:对于一个Transformer(一个插件对应一个Transformer)注入不同action的故障只有一个实验生效,实际生效action没有固定规则,因为按照底层存储map.table的顺序而定

比如target=kafka,消费者即抛出异常也执行延迟,只有其中一个生效。

但是对于target=jvm又有所不同,因为同一个方法又异常又延迟,会创建2个Plugin对应2个Transformer。

如果第一个Transformer中这里拿到异常action,则只会执行异常;如果第一个Transformer中拿到延迟action,则会执行2次延迟,这完全取决于底层map的顺序。

从我实验结果来看,只能拿到异常action,所以只会执行异常case。

3、effect-count/percent

Injector#limitAndIncrease:处理effect逻辑。

1)effect-count:限制触发action的次数。

2)effect-percent:按照百分比概率,触发action。

image.png

4、执行action

概览

根据target+action可以找到ActionExecutor执行故障注入逻辑。

在common包中给出了ActionExecutor的多种通用实现:

1)DefaultDelayExecutor:delay,延迟;

2)DefaultThrowExceptionExecutor:throwCustomException/throwDeclaredException,抛异常;

3)DefaultReturnValueExecutor:return,修改返回值;

4)AbstractThreadPoolFullExecutor:threadpoolfull,线程池满载抽象实现;

5)AbstractConnPoolFullExecutor:connectionpoolfull,连接池满载抽象实现;

image.png

对于大部分插件,ModelSpec通过继承FrameworkModelSpec就能得到通用Action能力。

image.png

如延迟和抛异常,大部分插件都支持。

image.png

image.png

部分插件有特殊Action。

如jvm插件,支持动态脚本、oom等Action。

image.png

如servlet插件,支持修改http响应码。

image.png

这里挑几个看看。

延迟

有许多插件都支持delay action。

比如jvm对指定方法延迟:

blade create jvm delay --process app.jar \ 
--classname com.x.ConsumerApplication \ 
--methodname doSayHello --time 5000

较为特殊的是一些远程调用client支持调用超时,比如dubbo consumer:

blade create dubbo delay --process app.jar \
--time 5000 --service com.x.DemoService2 \
--methodname sayHello --consumer

这些远程调用client的delay效果与普通delay不同,如果delay超过了rpc超时时间,会抛出网络异常

比如dubbo默认超时时间1s,如果delay 2s,则抛出网络异常,如果delay 500ms,则仅sleep 500ms,且这500ms不会计入1s的远程调用时间内

DubboEnhancer#doBeforeAdvice:Dubbo Consumer获取远程调用超时时间,在EnhancerModel中注入TimeoutExecutor

image.png

这个TimeoutExecutor的作用,就是抛出dubbo内部的rpc超时异常RpcException

image.png

DefaultDelayExecutor:所有延迟action都走这里,从flag参数中获取用户指定的延迟时间time。

1)如果插件Enhancer安插了TimeoutExecutor,执行网络超时逻辑,判断延迟时间是否超过rpc超时时间,如果超过,则执行TimeoutExecutor抛出异常,反之仅仅sleep;

2)如果插件Enhancer没有安插TimeoutExecutor,仅仅sleep,比如jvm插件;

注:offset参数用于在time参数上做随机偏移,比如--time 5000 --offset 1000,则最终睡眠时间在[4000,6000]。

image.png

异常

DefaultThrowExceptionExecutor#run:DefaultThrowExceptionExecutor同时支持两个action。

throwCustomException从flags中获取异常class。

throwDeclaredException从method反射获取第一个申明的异常。

反射创建exception,通过抛异常控制流程。

image.png

自定义脚本

jvm插件支持自定义脚本。

案例

假设用户代码如下:

@RestController
public class ScriptFaultInjectController {

    @Autowired
    private HttpbinClient httpbinClient; // http客户端


    @GetMapping("/script-fault")
    public UserDO scriptFault(@RequestParam String id) {
        return getUser(id);
    }

    private UserDO getUser(String id) {
        UserDO userDO = new UserDO();
        userDO.setId(id);
        userDO.setName("xxx");
        return userDO;
    }
}

提供一个自定义脚本如下,作用是在getUser方法返回后,执行自定义逻辑:

1)如果id=1,返回一个新用户,id=uuid,name不变;

2)如果id=2,抛出自定义异常;

3)其他,调用httpbin的web服务,对id做base64解码,修改用户的name为解码后的值;

import com.x.SysException;
import com.x.HttpbinClient;
import com.x.UserDO;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.UUID;

public class ChangeReturnObject {

    public Object run(Map<String, Object> params) throws Exception {
        UserDO returnObj = (UserDO) params.get("return");
        String id = (String) params.get("0");
        if ("1".equals(id)) {
            UserDO userDO = new UserDO();
            userDO.setId(UUID.randomUUID().toString());
            userDO.setName(returnObj.getName());
            return userDO;
        } else if ("2".equals(id)) {
            throw new SysException(2, "user not exist");
        } else {
            Object o = params.get("target");
            Field field = o.getClass().getDeclaredField("httpbinClient");
            field.setAccessible(true);
            HttpbinClient httpbinClient = (HttpbinClient) field.get(o);
            returnObj.setName(httpbinClient.base64(id));
            return returnObj;
        }
    }
}

执行blade命令如下。

注意:

1)如果通过--script-content指定脚本,需要对java代码做base64;

2)--after,指定方法执行后执行脚本,这样才能拿到返回对象;

blade c jvm script --after \
--classname com.x.ScriptFaultInjectController \
--methodname getUser --script-content base64后的脚本 --process ServiceE

接下来通过源码来分析,这个java脚本需要满足哪些条件。

比如方法名,入参为什么是map,入参map里的key对应value是什么,为什么可以加载用户class。

主流程

DynamicScriptExecutor#run:主流程大致如下

1)getScriptContent,从flags读取脚本,支持--script-file 从本地文件系统读取java文件,支持--script-content-decode 非base64脚本,支持--script-content base64脚本;

2)getClassLoader,创建脚本类需要用到的类加载器;

3)compile,编译得到脚本class,用目标classloader加载;

4)createParams,获取脚本方法入参;

5)run,执行脚本方法;

6)根据执行结果,支持抛出异常、修改返回值、修改入参。

image.png

类加载器

DynamicScriptExecutor#getClassLoader:

1)如果flags中指定了外部jar,如external-jar或external-jar-path,创建一个ClassLoaderForScript,支持加载文件系统中的外部jar

2)如果flags未使用外部jar,使用被增强class的classloader,在这里就是用户class,所以脚本中可以使用用户class

image.png

ClassLoaderForScript继承URLClassLoader:

1)优先走插件类加载器,见上面Plugin加载,其实在这里就能加载到用户class了;

2)其次走被增强class的classloader;

3)最终从外部jar加载;

image.png

编译并加载脚本类

AbstractScriptEngineService#compile:脚本一般只会编译一次,有一个lru缓存,key是被增强的class+method(没处理重载方法)。

image.png

JavaCodeScriptEngine#compileClass:使用javax.tools.JavaCompiler编译脚本类,对于生成的脚本class,需要再封装一个CompiledClassLoader来加载。

image.png

CompiledClassLoader的作用是将编译得到的脚本class放入类加载搜索路径,parent还是上面的类加载器(ClassLoaderForScript或用户classloader)。

image.png

获取方法入参

DynamicScriptExecutor#createParams:方法入参就三种:

1)target:被增强class的目标对象;

2)return:返回值;

3)0-n:入参;

image.png

执行脚本方法

JavaExecutableScript#run:最终反射调用脚本对象instance的run方法,入参params是上面得到的map。

注意:

1)脚本必须存在run方法;

2)run方法的入参是Map;

3)run方法权限可以是private的;

image.png

处理执行结果

DynamicScriptExecutor#run:

1)如果脚本抛出任何异常,向用户代码该异常;

2)如果run返回非空,则跳过用户方法,立即返回该对象,注意如果被增强方法返回void,想要跳过方法执行,也必须返回一个非空对象

3)不满足上述情况,支持修改入参;

image.png

DynamicScriptExecutor#checkAndChangeParameters:替换入参。

image.png

五、销毁实验

DestroyHandler#handle:blade支持根据uid删除,也支持根据target+action批量删除,如blade destroy jvm throwCustomException,本质批量删除也是拿到target+action下所有实验,循环uid删除。

image.png

DestroyHandler#destroy:根据uid从内存中移除实验Model。

image.png

DestroyHandler#applyPreDestroyInjectionModelHandler:只有部分实验会做预销毁。

image.png

比如下面三个。

image.png

DubboModelSpec#preDestroy:druid和dubbo判断是否是连接池或线程池满载实验,如果是的话,停止满载。

image.png

JvmModelSpec#preDestroy:jvm实验分为多种情况。

image.png

HeapJvmOomExecutor#innerStop:case1,如果是DirectlyInjectionAction,比如堆内存OOM实验,则释放内存。

image.png

DynamicScriptExecutor#stop:case2,如果action实现StoppableActionExecutor,调用stop方法。如自定义脚本,这里清理classname+methodname维度的缓存脚本类。

image.png

MethodPreInjectHandler#preHandleRecovery:case2,除了DirectlyInjectionAction,其他classname+methodname注入的故障,如延迟、异常、自定义脚本,都会调用SandboxModule移除监听。

image.png

SandboxModule#delete:根据插件唯一标识,获取watchId,调用sandbox的ModuleEventWatcher#delete,移除Transformer,恢复class。

image.png

注意:只有jvm实验,如延迟、异常、自定义脚本,才能在blade destroy uid阶段恢复class增强。其余实验,如dubbo、kafka这种,都需要blade revoke卸载sandbox,才能恢复原先的class

总结

1、插件模型

chaosblade-exec-jvm核心模块通过SPI加载Plugin接口。

每个Plugin代表一种实验插件,由各plugin子模块实现。

Plugin包含以下属性:

1)name:插件名;

2)ModelSpec:实验模型定义;

3)PointCut:插件增强点;

4)Enhancer:增强逻辑;

image.png

ModelSpec定义实验模型,与命令行有对应关系。

blade create kafka throwCustomException --topic a --producer --exception java.lang.Exception --process app.jar

ModelSpec.target:实验对象,对应kafka;

ActionSpec.MatcherSpec:在pointcut的基础上用于匹配需要注入的业务参数key,对应topic、producer;

ActionSpec.name:执行器名,对应throwCustomException;

ActionSpec.FlagSpec:执行器参数,对应exception;

ActionSpec.ActionExecutor:执行器实现;

2、chaosblade模块激活

chaosblade模块激活后,为每个plugin.jar创建一个URLClassLoader,使用这个classloader通过SPI加载每个插件。

类加载结构如下:

image.png

每个Plugin封装为PluginBean,相同target的插件组合为PluginBeans创建实验时相同target的Plugin会一并安装增强

如dubbo consumer和provider因为target都是dubbo,虽然仅创建consumer实验,provider的字节码也会被增强。

3、创建实验

创建实验参数包含:

1)uid:实验id,由blade命令行生成;

2)target:实验对象;

3)matcher:匹配参数;

4)action:执行器名称和参数;

image.png

image.png

Step1,注册实验

实验对象Model=target+action+matcher,实验对象id=target+action名+matcher,比如: dubbo|throwCustomException|methodname=sayHello|provider=true|service=com.x.DemoService。

如果实验对象id已存在,则返回406。

Step2,字节码增强

根据target找到PluginBeans,循环每个插件,获取PointCut和Enhancer调用sandbox的watch api注册监听,安装Transformer。

Transformer数量=Plugin数量

注意:相同target的插件被同时安装

Step3,预注入

对于连接池(druid)和线程池(dubbo provider)满载,需要在实验Model中埋入action参数,标记当前已经开启池满载实验。

对于jvm实验分为两种情况:

1)直接注入实验,如OOM实验,直接触发实验执行;

2)自定义异常、自定义脚本、延迟实验,根据用户参数classname和methodname动态创建Plugin(Jvm插件的PointCut是空,要在预注入阶段根据用户输入动态创建Plugin),调用SandboxModule#add安装字节码增强;

4、实验执行

image.png

Step1,插件Enhancer根据运行情况组装EnhancerModel,里面包含需要matcher匹配的数据,比如kafka的topic=a。

Step2,循环target下的所有实验Model,执行compare匹配。

compare对实验Model和插件EnhancerModel做kv匹配。

对于一个Transformer(一个插件对应一个Transformer)注入不同action故障只有一个action生效,实际生效action没有固定规则,因为按照底层存储map.table的顺序而定

image.png

Step3,处理effect逻辑。

1)effect-count:限制触发action的次数。

2)effect-percent:按照百分比概率,触发action。

Step4,根据实验action找到ActionExecutor执行实验。

具体不同action的逻辑见正文。

5、销毁实验

blade支持根据uid单个删除实验,也支持根据target+action批量删除实验,批量删除基于uid单个删除实现。

Step1,内存map移除实验Model。

Step2,部分实验需要预销毁。

针对dubbo线程池和druid连接池,取消满载。

针对jvm实验分两种case:

1)直接实验,如OOM,取消实验;

2)延迟、异常、自定义脚本,调用sandbox的delete api,恢复字节码;

注意:只有jvm实验,如延迟、异常、自定义脚本才能在blade destroy uid阶段恢复class增强。其余实验,如dubbo、kafka这种,都需要blade revoke卸载sandbox,才能恢复原先的class