前言
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,那么字节码增强后就像下面这样。
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加载不同插件。
每个Plugin需要实现4个方法:
1)getName:插件名;
2)getModelSpec:实验模型定义,比如:是否需要执行增强逻辑(MatcherSpec)、根据参数(FlagSpec)如何执行(ActionExecutor);
3)getPointCut:插件增强点;
4)getEnhancer:增强逻辑;
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)。
如注入自定义异常:
blade create jvm throwCustomException --exception java.lang.Exception --classname com.Controller --methodname sayHello
Enhancer
jvm实验的Enhancer同时实现了before和after。
注:只有jvm实验实现了after增强。
MethodEnhancerForBefore:以before为例,子类只需要提供运行时matcher参数,如jvm实验从className和Method中提取className和methodName加入MatcherModel。
所有Enhancer都继承BeforeEnhancer,插件子类提供EnhancerModel包含运行时切点的信息,父类BeforeEnhancer调用Injector实际做注入逻辑。
2、kafka
kafka插件的特点是同一个target=kafka有两个Plugin实现。
consumer消费者,producer生产者。
如为消费者注入异常。
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生产者插件。
KafkaProducerPointCut拦截KafkaProducer#send。
KafkaProducerEnhancer从入参ProducerRecord中提取topic,填充EnhancerModel。
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增强只会触发一次。
WaitingTriggerThreadPoolFullExecutor#triggerThreadPoolFull:获取线程池,执行full方法,这里也只会执行一次。
DubboThreadPoolFullExecutor#getThreadPoolExecutor:dubbo反射获取目标线程池。因为每次调用dubbo url不同,但是这个方法只会执行一次,所以无法实现线程池隔离的实验。
AbstractThreadPoolFullExecutor#full:满载方式是,chaosblade线程池(executorService)定时按照最大线程数n向目标线程池(threadPoolExecutor)填充n个无限sleep任务。
AbstractThreadPoolFullExecutor#revoke:在实验销毁阶段,将这些无限sleep任务取消。
二、激活
根据前面第三章sandbox的铺垫,chaosblade-exec-jvm基于sandbox提供了chaosblade模块。
chaosblade模块统一管理所有实验,每个实验需要通过实现Plugin接口,由chaosblade模块通过SPI加载。
sandbox模块加载onLoad和卸载onUnload钩子,只是内存准备和资源清理逻辑,可以忽略。
重点逻辑从模块被激活onActive开始。
注:第一章blade命令行提到过,blade jvm prepare第一步是挂载sandbox,第二步是激活chaosblade模块。
SandboxModule#loadPlugins:通过SPI加载Plugin封装为PluginBean,注册ModelSpec和PluginBean为单例管理。
1、Plugin加载
chaosblade-exec-jvm最终产物是一个模块jar,放在sandbox的module目录下,所以可以被sandbox加载。
这个jar中目录结构如下,plugins目录下是n个插件。
插件和chaosblade模块一起打在了jar包里,所以spi加载要从chaosblade-exec-jvm自己这个jar里面找。
PluginJarUtil#getPluginFiles:获取所有插件jar。
PluginLoader#load:这里为每个plugin的jar创建了一个URLClassLoader,这个URLClassLoader的parent正是chaosblade模块的ModuleJarClassLoader。
结合上一章,最终chaosblade-exec-jvm的类加载结构如下,所以插件也可以provided引入用户class,也可以自己引入三方依赖,虽然chaosblade一般不需要用。
2、ModelSpec和PluginBean
每个Plugin对应一个ModelSpec,target是ModelSpec的唯一标识。
对于一些插件,如dubbo,他有生产和消费两个Plugin,但是两个ModelSpec的target都是dubbo,即对应blade create dubbo。
所以在chaosblade的log里可以看到,模块被激活后,有很多公用target的Plugin,这点比较重要。
DefaultPluginBeanManager维护了target下的所有PluginBean,封装为PluginBeans。
三、创建实验
chaosblade模块提供了create端点,负责创建实验。
请求入参解析如下:
CreateHandler#handle:根据第一章blade命令行铺垫,suid是由blade命令生成的随机字符串,代表实验id,target即实验目标如kafka、jvm,action是实验类型如throwCustomException、delay。
最终根据用户输入参数和ModelSpec定义,填充为一个Model。
matcher用于匹配切点是否要具体执行,action用于定义如何执行。
CreateHandler#handleInjection:
1)注册实验;2)字节码增强;3)ModelSpec预注入
1、注册实验
DefaultStatusManager维护当前正在执行的实验。
1)models:target→实验Model唯一标识→实验Model;
2)experiments:实验uid→实验Model唯一标识;
DefaultStatusManager#registerExp:生成实验Model唯一标识identifier,如果实验已经存在返回失败,否则experiments维护实验uid和identifier的关系。
注:这里Model封装为了StatusMetrics。
实验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。
2、字节码增强
CreateHandler#lazyLoadPlugin:根据target找到n个Plugin,执行增强。
这里就是1个target对应n个Plugin关系的重要体现。
同1个target,比如dubbo,即使设置matcher是consumer,provider侧的字节码也会一起被增强,只是在执行过程中不会被matcher匹配。
SandboxModule#add:调用sandbox的watch api注册监听,根据上一章分析,就是安装Transformer,如果目标class已经被加载,再触发retransform。
这说明了一个问题,Transformer数量=插件Plugin数量,比如针对dubbo consumer调用异常和延迟只会有一个Transformer。(但是jvm异常和延迟会有2个Transformer,本质原因是Plugin是两个,见下面预注入)
sandbox Filter的创建可以忽略,就是根据Plugin的Pointcut适配创建。
BeforeEventListener较为简单,正常实现EventListener即可,执行Plugin的Enhancer逻辑。
对于jvm实验如抛出自定义异常,支持--after,在方法执行后抛出异常。
sandbox的ReturnEvent没有维护方法调用的上下文,只能拿到方法返回值。
AfterEventListener同时监听Before和Return事件,目的是在方法返回阶段能拿到BeforeEvent的信息,BeforeEvent信息通过ThreadLocal维护。
3、预注入
CreateHandler#applyPreInjectionModelHandler:对于部分ModelSpec定义,如果实现了PreCreateInjectionModelHandler,会被回调preCreate。
目前有三个ModelSpec需要根据action做特殊处理。
druid是数据库连接池满载,和dubbo线程池满载类似,忽略。
dubbo线程池满载
DubboModelSpec#preCreate:如果当前action是dubbo线程池满载,dubbo需要在这里往ActionExecutor中塞入标识(ActionExecutor在ModelSpec中,所以action纬度下单例)。
这也是因为class增强是target维度统一埋入的,需要塞入标志位用于区分是否需要在provider的received方法处真实触发线程池满载。
DubboThreadPoolFullExecutor#setWrappedChannelHandler:
dubbo provider运行时收到请求,判断当前是否有线程池满载实验,如果有才会执行该逻辑。
jvm实验
jvm实验的特点在案例中说过,Plugin无法指定PointCut。
JvmModelSpec#preCreate:jvm实验action可以分为两类。
JvmOomActionSpec#createInjection:直接执行实验,如OOM实验。
MethodPreInjectHandler#preHandleInjection:自定义异常、自定义脚本、延迟实验,根据用户参数classname和methodname动态创建Plugin,调用SandboxModule#add安装字节码增强。
这个MethodPlugin的pointcut就是指定class和method。
四、实验执行
1、主流程
BeforeEventListener#handleEvent:以before为例。
通过反射获取补充一些sandbox BeforeEvent中未包含的信息如method,调用Plugin的Enhancer。
如果Enhancer抛出InterruptProcessException,转换为sandbox的ProcessControlException,可以控制流程走向,如立即抛出异常到宿主,方法立即返回指定Object。
如果Enhancer抛出未知异常,sandbox会正常执行目标方法。
注:具体流程控制,见上一章sandbox。
所有Enhancer都继承自BeforeEnhancer。
1)插件从当前执行切点中获取matcher需要的参数,比如kafka需要从message中获取topic;
2)执行inject;
Injector#inject:循环target下的所有正在执行的实验Model(用户入参):
1)compare匹配用户Model和插件EnhancerModel中的Matcher,注意只匹配一个;
2)处理effect-count、effect-percent逻辑;
3)执行具体Action;
2、匹配compare
并非所有plugin都是create后就生效(如jvm oom),大部分plugin在切点的基础上,需要基于业务参数EnhancerModel和实验matcher匹配。
Injector#compare:按照插件填充的EnhancerModel(左)和实验Model(右),如果匹配成功,才执行后续逻辑。
匹配包含众多逻辑,简单理解就是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。
4、执行action
概览
根据target+action可以找到ActionExecutor执行故障注入逻辑。
在common包中给出了ActionExecutor的多种通用实现:
1)DefaultDelayExecutor:delay,延迟;
2)DefaultThrowExceptionExecutor:throwCustomException/throwDeclaredException,抛异常;
3)DefaultReturnValueExecutor:return,修改返回值;
4)AbstractThreadPoolFullExecutor:threadpoolfull,线程池满载抽象实现;
5)AbstractConnPoolFullExecutor:connectionpoolfull,连接池满载抽象实现;
对于大部分插件,ModelSpec通过继承FrameworkModelSpec就能得到通用Action能力。
如延迟和抛异常,大部分插件都支持。
部分插件有特殊Action。
如jvm插件,支持动态脚本、oom等Action。
如servlet插件,支持修改http响应码。
这里挑几个看看。
延迟
有许多插件都支持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。
这个TimeoutExecutor的作用,就是抛出dubbo内部的rpc超时异常RpcException。
DefaultDelayExecutor:所有延迟action都走这里,从flag参数中获取用户指定的延迟时间time。
1)如果插件Enhancer安插了TimeoutExecutor,执行网络超时逻辑,判断延迟时间是否超过rpc超时时间,如果超过,则执行TimeoutExecutor抛出异常,反之仅仅sleep;
2)如果插件Enhancer没有安插TimeoutExecutor,仅仅sleep,比如jvm插件;
注:offset参数用于在time参数上做随机偏移,比如--time 5000 --offset 1000,则最终睡眠时间在[4000,6000]。
异常
DefaultThrowExceptionExecutor#run:DefaultThrowExceptionExecutor同时支持两个action。
throwCustomException从flags中获取异常class。
throwDeclaredException从method反射获取第一个申明的异常。
反射创建exception,通过抛异常控制流程。
自定义脚本
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)根据执行结果,支持抛出异常、修改返回值、修改入参。
类加载器
DynamicScriptExecutor#getClassLoader:
1)如果flags中指定了外部jar,如external-jar或external-jar-path,创建一个ClassLoaderForScript,支持加载文件系统中的外部jar;
2)如果flags未使用外部jar,使用被增强class的classloader,在这里就是用户class,所以脚本中可以使用用户class;
ClassLoaderForScript继承URLClassLoader:
1)优先走插件类加载器,见上面Plugin加载,其实在这里就能加载到用户class了;
2)其次走被增强class的classloader;
3)最终从外部jar加载;
编译并加载脚本类
AbstractScriptEngineService#compile:脚本一般只会编译一次,有一个lru缓存,key是被增强的class+method(没处理重载方法)。
JavaCodeScriptEngine#compileClass:使用javax.tools.JavaCompiler编译脚本类,对于生成的脚本class,需要再封装一个CompiledClassLoader来加载。
CompiledClassLoader的作用是将编译得到的脚本class放入类加载搜索路径,parent还是上面的类加载器(ClassLoaderForScript或用户classloader)。
获取方法入参
DynamicScriptExecutor#createParams:方法入参就三种:
1)target:被增强class的目标对象;
2)return:返回值;
3)0-n:入参;
执行脚本方法
JavaExecutableScript#run:最终反射调用脚本对象instance的run方法,入参params是上面得到的map。
注意:
1)脚本必须存在run方法;
2)run方法的入参是Map;
3)run方法权限可以是private的;
处理执行结果
DynamicScriptExecutor#run:
1)如果脚本抛出任何异常,向用户代码该异常;
2)如果run返回非空,则跳过用户方法,立即返回该对象,注意如果被增强方法返回void,想要跳过方法执行,也必须返回一个非空对象;
3)不满足上述情况,支持修改入参;
DynamicScriptExecutor#checkAndChangeParameters:替换入参。
五、销毁实验
DestroyHandler#handle:blade支持根据uid删除,也支持根据target+action批量删除,如blade destroy jvm throwCustomException,本质批量删除也是拿到target+action下所有实验,循环uid删除。
DestroyHandler#destroy:根据uid从内存中移除实验Model。
DestroyHandler#applyPreDestroyInjectionModelHandler:只有部分实验会做预销毁。
比如下面三个。
DubboModelSpec#preDestroy:druid和dubbo判断是否是连接池或线程池满载实验,如果是的话,停止满载。
JvmModelSpec#preDestroy:jvm实验分为多种情况。
HeapJvmOomExecutor#innerStop:case1,如果是DirectlyInjectionAction,比如堆内存OOM实验,则释放内存。
DynamicScriptExecutor#stop:case2,如果action实现StoppableActionExecutor,调用stop方法。如自定义脚本,这里清理classname+methodname维度的缓存脚本类。
MethodPreInjectHandler#preHandleRecovery:case2,除了DirectlyInjectionAction,其他classname+methodname注入的故障,如延迟、异常、自定义脚本,都会调用SandboxModule移除监听。
SandboxModule#delete:根据插件唯一标识,获取watchId,调用sandbox的ModuleEventWatcher#delete,移除Transformer,恢复class。
注意:只有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:增强逻辑;
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加载每个插件。
类加载结构如下:
每个Plugin封装为PluginBean,相同target的插件组合为PluginBeans,创建实验时相同target的Plugin会一并安装增强。
如dubbo consumer和provider因为target都是dubbo,虽然仅创建consumer实验,provider的字节码也会被增强。
3、创建实验
创建实验参数包含:
1)uid:实验id,由blade命令行生成;
2)target:实验对象;
3)matcher:匹配参数;
4)action:执行器名称和参数;
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、实验执行
Step1,插件Enhancer根据运行情况组装EnhancerModel,里面包含需要matcher匹配的数据,比如kafka的topic=a。
Step2,循环target下的所有实验Model,执行compare匹配。
compare对实验Model和插件EnhancerModel做kv匹配。
对于一个Transformer(一个插件对应一个Transformer)注入不同action故障,只有一个action生效,实际生效action没有固定规则,因为按照底层存储map.table的顺序而定。
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。