开发实战
本篇介绍 chaosblade-exec-jvm 如何从零开始如何开发一个组件故障场景。
构思步骤
- 新建工程
- 定义模型
- 定义合适的切点
- 定义
Enhancer增强类 - 定义
ActionExecutor - 定义
plugin
Lettuce 故障场景
Lettuce是一个基于netty异步通信的redis客户端,本篇开发lettuce plugin,对自定义输入匹配的 key 实现:
string值类型的值篡改延迟
自定义异常抛出
新建工程
按插件的规范,在chaosblade-exec-plugin模块下新建工程chaosblade-exec-plugin-lettuce。
定义模型
这里继承了FrameworkModelSpec类,FrameworkModelSpec类默认包含了延迟和自定义异常的故障能力,下面介绍如何篡改值。
getTarget()实现的靶点。getShortDesc()描述getLongDesc()描述getExample()例子
public class LettuceModeSpec extends FrameworkModelSpec {
@Override
protected List<MatcherSpec> createNewMatcherSpecs() {
// todo
return null;
}
@Override
public String getTarget() {
return "lettuce";
}
@Override
public String getShortDesc() {
return "redis client lettuce experiment";
}
@Override
public String getLongDesc() {
return "redis client lettuce experiment";
}
@Override
public String getExample() {
return "lettuce --key=name update --value=meepo";
}
}
lettuce --key=name update --value=meepo
lettuce作为实验的靶点,update是实验的动作,具体可参数模型篇。--key模型中的 matchers,实验规则匹配器,KeyMatcherSpec继承BasePredicateMatcherSpec,会required()判断是是否是必输字段。--value模型中 action 参数,指update需要更新成这个值。
public class KeyMatcherSpec extends BasePredicateMatcherSpec {
@Override
public String getName() {
// --key
return "key";
}
@Override
public String getDesc() {
return "key matcher";
}
@Override
public boolean noArgs() {
return false;
}
@Override
public boolean required() {
// 是否必输
return true;
}
}
public class ValueFlagSpec implements FlagSpec {
@Override
public String getName() {
return "value";
}
@Override
public String getDesc() {
return "value set";
}
@Override
public boolean noArgs() {
return false;
}
@Override
public boolean required() {
return false;
}
}
在UpdateAction里面添加ValueFlagSpec 和LettuceActionExecutor,LettuceActionExecutor是实验upadte的执行阶段,后面介绍。
public class UpdateActionSpec extends BaseActionSpec {
private static ValueFlagSpec valueFlagSpec = new ValueFlagSpec();
public UpdateActionSpec() {
super(new LettuceActionExecutor(valueFlagSpec));
}
@Override
public String getName() {
return "update";
}
@Override
public String[] getAliases() {
return new String[0];
}
@Override
public String getShortDesc() {
return "update action spec";
}
@Override
public String getLongDesc() {
return "update action spec";
}
@Override
public List<FlagSpec> getActionFlags() {
List<FlagSpec> flagSpecs = new ArrayList<FlagSpec>();
flagSpecs.add(valueFlagSpec);
return flagSpecs;
}
@Override
public PredicateResult predicate(ActionModel actionModel) {
if (StringUtil.isBlank(actionModel.getFlag(valueFlagSpec.getName()))) {
return PredicateResult.fail("less value argument");
}
return PredicateResult.success();
}
}
然后在LettuceModeSpec中的createNewMatcherSpecs()方法中添加UpdateActionSpec和ValueMatcherSpec即可。
@Override
public List<MatcherSpec> createNewMatcherSpecs() {
addActionSpec(new UpdateActionSpec());
List<MatcherSpec> matchers = new ArrayList<MatcherSpec>();
matchers.add(new KeyMatcherSpec());
return matchers;
}
定义切点
定义切点需要找到合适的切点,Lettuce基于netty异步通信,所有的命令都在io.lettuce.core.protocol.CommandHandler#write
方法中发出。ClassMatcher和MethodMatcher还有更多的匹配方式可参考插件篇
public class LettucePointCut implements PointCut {
@Override
public ClassMatcher getClassMatcher() {
NameClassMatcher nameClassMatcher = new NameClassMatcher("io.lettuce.core.protocol.CommandHandler");
return nameClassMatcher;
}
@Override
public MethodMatcher getMethodMatcher() {
NameMethodMatcher nameMethodMatcher = new NameMethodMatcher("write");
return nameMethodMatcher;
}
}
定义Enhancer
Enhancer获取io.lettuce.core.protocol.CommandHandler#write()方法第二个参数的值,这里为了演示,先只支持几个简单命令,先获取命令的类型,如果不是SET、SETNX、HSET类型return null故障能力就中断了,然后获取key在Inject阶段匹配。
public class LettuceEnhancer extends BeforeEnhancer {
private static final Logger logger = LoggerFactory.getLogger(LettuceEnhancer.class);
private static final List<String> SUPPORTS_COMMANDS = new ArrayList<String>();
static {
SUPPORTS_COMMANDS.add("SET");
SUPPORTS_COMMANDS.add("SETNX");
SUPPORTS_COMMANDS.add("HSET");
}
@Override
public EnhancerModel doBeforeAdvice(ClassLoader classLoader,
String className,
Object object,
Method method,
Object[] methodArguments) throws Exception {
Object command = methodArguments[1];
Object args = ReflectUtil.getFieldValue(command, "command", false);
Object commandType = ReflectUtil.getFieldValue(args, "type", false);
if (!SUPPORTS_COMMANDS.contains(String.valueOf(commandType))) {
return null;
}
Object commandArgs = ReflectUtil.getFieldValue(args, "args", false);
List singularArguments = ReflectUtil.getFieldValue(commandArgs, "singularArguments", false);
Object keyArgument = singularArguments.get(0);
MatcherModel matcherModel = new MatcherModel();
if (keyArgument == null) {
return null;
}
Object key = ReflectUtil.getFieldValue(keyArgument, "key", false);
matcherModel.add("key", key);
logger.debug("lettuce matchers: {}", JSON.toJSONString(matcherModel));
return new EnhancerModel(classLoader, matcherModel);
}
}
定义ActionExecutor
LettuceActionExecutor执行更新值。
public class LettuceActionExecutor implements ActionExecutor {
private static final Logger logger = LoggerFactory.getLogger(LettuceEnhancer.class);
private ValueFlagSpec valueFlagSpec;
public LettuceActionExecutor(ValueFlagSpec valueFlagSpec) {
this.valueFlagSpec = valueFlagSpec;
}
@Override
public void run(EnhancerModel enhancerModel) throws Exception {
Object command = enhancerModel.getMethodArguments()[1];
Object args = ReflectUtil.getFieldValue(command, "command", false);
Object commandArgs = ReflectUtil.getFieldValue(args, "args", false);
List singularArguments = ReflectUtil.getFieldValue(commandArgs, "singularArguments", false);
Object valArgument = singularArguments.get(1);
Object originVal = ReflectUtil.getFieldValue(valArgument, "val", false);
if (!(originVal instanceof String)) {
logger.info("not support value, value type: {}", originVal.getClass());
return;
}
Object codec = ReflectUtil.getFieldValue(valArgument, "codec", false);
String value = enhancerModel.getActionFlag(valueFlagSpec.getName());
Object[] arguments = new Object[]{value, codec};
Object valueArgument = ReflectUtil.invokeStaticMethod(valArgument.getClass(), "of", arguments, false);
if (valueArgument != null) {
logger.info("update value success. origin value: {}, update value: {}", originVal, value);
// 更新原来的值
singularArguments.set(1, valueArgument);
}
}
}
定义plugin
定义plugin添加模型、PointCut、Enhancer,并且在当前工程resources新建文件resources/META-INF/services/com.alibaba.chaosblade.exec.common.aop.Plugin,把自定义plugin的全限定名复制到文件里面com.alibaba.chaosblade.exec.plugin.lettuce.LettucePlugin即可,此时一个插件就开发完成了。
public class LettucePlugin implements Plugin {
@Override
public String getName() {
return "lettuce plugin";
}
@Override
public ModelSpec getModelSpec() {
return new LettuceModeSpec();
}
@Override
public PointCut getPointCut() {
return new LettucePointCut();
}
@Override
public Enhancer getEnhancer() {
return new LettuceEnhancer();
}
}
注意: Lettuce command支持很多不同的类型codec,本次开发实战为了演示,只支持RedisCommands<String, String>,否则不支持篡改。也不支持batch command模式。
打包和执行
准备
方式一
首先提交代码 push 到自己的仓库、需要go、java_home 、maven
- clone
git clone https://github.com/chaosblade-io/chaosblade
- 修改 Makefile
cd chaosblade
vi Makefile
把Makefile里面的BLADE_EXEC_JVM_PROJECT改成修改成你 fork 的仓库地址,保存退出

- 编译
// linux
make build_linux
// macos
make build_darwin
方式二
如果已经下载了chaosbladerelase 包可以使用此方式,在chaosblade-exec-jvm目录下编译打包,需要java_home 、maven
make build
编译成功后,在当前目录生成如下yml和jar
plugins/chaosblade-jvm-spec-0.6.0.yamlbuild-target/chaosblade-0.6.0/lib/sandbox/module/chaosblade-java-agent-0.6.0.jar
分别将yml和jar替换到如下图的chaosblade目录下:

混沌实验
- 挂载
agent:--pid 3356是被攻击应用的 jvm 进程号,每次挂载对应一个 uid,卸载 agent 的时候需要 uid
./blade prepare jvm --pid 3356
- 创建混沌实验
./blade c lettuce --key=name update --value=tiny

- 销毁
./blade create destroy 863c8c5a2c2c3deb
卸载 agent
./blade destroy 6a0863a4f0da8a38