背景
目前转转的辅助测试平台有JVM注入、流量回放,前者通过修改指定服务、类、方法返回值,Mock超时、异常等难以测试覆盖场景,提高测试效率;后者通过在稳定环境采集流量,动态环境自动回放用例,根据diff同一用例稳态和动态环境返回结果来检验程序是否存在问题,降低代码变动对整体系统带来的风险,这些平台均用到了jvm-sandbox技术,最近学习了相关知识,记录下初次体验心得。
jvm-sandbox是在目标jvm中安装一个沙箱jvm,通过自定义classloader加载代理类和目标jvm进行隔离,实现在不影响正常业务的前提下做到功能增强。官方解释jvm-sandbox实现了一种在不重启、不侵入目标JVM应用的AOP解决方案。具有无侵入、类隔离、可插拔、多租户、高兼容的特性,应用场景有线上故障定位、线上系统流控、线上故障模拟、方法请求录制和结果回放、动态日志打印、安全信息监测和脱敏等等。
接下来由注入一个异常为例来熟悉下整个过程,假设我们需要测试文件生成的异常场景,在业务服务调用文件生成的方法注入异常,在执行到该方法时无论什么条件文件生成均失败,这里目标测试服务我们称它A,进行异常注入功能的部分封装为模块chaosblade称它为B,配置服务注入异常服务我们成为C,在C服务中配置异常如下图:
第一步 安装沙箱
A服务启动的时候添加-javaagent配置挂载JVM-SANDBOX
java -javaagent:/sandbox/lib/sandbox-agent.jar
通过javaagent方式启动会调用AgentLauncher类中的premain方法
public static void premain(String featureString, Instrumentation inst) {
// 避免对服务启动有影响
try{
LAUNCH_MODE = LAUNCH_MODE_AGENT;
install(toFeatureMap(featureString), inst);
}catch (Exception e) {
e.printStackTrace();
}
}
然后执行install方法在服务A的jvm中安装jvm-sandbox
将sandbox-spy.jar包路径添加到BootstrapClassloader的加载路径下, SpyHandler间谍处理器追加到 BootstrapClassLoader 中,业务jvm和沙箱jvm通过间谍类进行通信
构造自定义的SandboxClassloader,反射加载Sandbox核心对象,实现沙箱类与业务类隔离
实例化sandbox-core.jar中的CoreConfigure内核启动配置类,包含沙箱启动方式、挂载服务ip、启动端口号、模块路径等信息
调用JettyCoreServer bind方法创建沙箱对象、初始化Spy类、启动jetty服务器,通过此服务可以通过http请求方式动态操作沙箱以及模块。
private static synchronized InetSocketAddress install(final Map<String, String> featureMap,
final Instrumentation inst) {
final String namespace = getNamespace(featureMap);
final String propertiesFilePath = getPropertiesFilePath(featureMap);
final String coreFeatureString = toFeatureString(featureMap);
try {
final String home = getSandboxHome(featureMap);
// 将Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
getSandboxSpyJarPath(home)
// SANDBOX_SPY_JAR_PATH
)));
// 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀
final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
namespace,
getSandboxCoreJarPath(home)
// SANDBOX_CORE_JAR_PATH
);
// CoreConfigure类定义
final Class<?> classOfConfigure = sandboxClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);
// 反序列化成CoreConfigure类实例
final Object objectOfCoreConfigure = classOfConfigure.getMethod("toConfigure", String.class, String.class)
.invoke(null, coreFeatureString, propertiesFilePath);
// CoreServer类定义
final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);
// 获取CoreServer单例
final Object objectOfProxyServer = classOfProxyServer
.getMethod("getInstance")
.invoke(null);
// CoreServer.isBind()
final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);
// 如果未绑定,则需要绑定一个地址
if (!isBind) {
try {
classOfProxyServer
.getMethod("bind", classOfConfigure, Instrumentation.class)
.invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
} catch (Throwable t) {
classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
throw t;
}
}
// 返回服务器绑定的地址
return (InetSocketAddress) classOfProxyServer
.getMethod("getLocal")
.invoke(objectOfProxyServer);
} catch (Throwable cause) {
throw new RuntimeException("sandbox attach failed.", cause);
}
}
第二步 加载模块B
jvm-sandbox是基于插件化的设计思想,其中有一个重要的角色叫模块(module),通过它实现了功能增强。模块的生命周期包括加载、激活、冻结、刷新、卸载。类型分系统、自定义两种,系统模块放在/sandbox/bin/../module 路径下用于负责管理管理模块的生命周期,沙箱及模块的随时加载、卸载且对目标jvm无侵入体现了其”可插拔“特性;自定义模块放在/.sandbox-module路径下,因其”多租户“的特性可以有多个自定义模块,在自定义模块中需要实现 Module 接口,声明 @Information 注解指定唯一 id,定义 ModuleEventWatcher 事件观察者(织入增强代码的入口),声明 @Command 指令的处理方法(比如注入异常、解除异常等)。在我们这个例子中,异常注入功能封装在自定义模块B。通过ModuleJarClassLoader模块加载器遍历模块存储路径下所有的jar。
void load(final ModuleJarLoadCallback mjCb,
final ModuleJarLoader.ModuleLoadCallback mCb) {
// 开始逐条加载
for (final File moduleJarFile : listModuleJarFileInLib()) {
try {
mjCb.onLoad(moduleJarFile);
new ModuleJarLoader(moduleJarFile, mode).load(mCb);
} catch (Throwable cause) {
logger.warn("loading module-jar occur error! module-jar={};", moduleJarFile, cause);
}
}
}
第三步 代码增强
模块加载完成后调用watch方法进行代码增强,核心通过Instrumentation类库提供的Api来实现
给对应的模块创建SandboxClassFileTransformer沙箱类形变器,注册到沙箱模块内核封装对象中
Instrumentation对象通过addTransformer注册字节码转换器后,接下来所有的类加载都会经过sandClassFileTransformer
匹配查找需要增强的类
通过retransformClasses 重新对 JVM 已加载的类进行字节码转换
EventListenerHandler事件处理器激活增强类,激活成功后可以监听沙箱事件,接收沙箱指令
下图为反编译得到的未进行异常注入class
下图为注入异常后的class,Spy间谍类将增强代码已织入到方法的before、return、throws环节
第四步 测试业务服务A
此时异常注入生效运行程序,文件生成报错
以上就是对jvm-sandbox一次简单的体验,先将需要动态织入到业务服务的功能以自定义模块的格式封装好,然后放到模块安装路径下,在业务服务上安装jvm-sandbox,内置jetty服务器启动后,可以通过http请求方式动态操作沙箱以及模块,从而实现对业务代码的功能增强,整个过程见下图,可见jvm-sandbox是一个灵活、易上手的代码增强工具,感兴趣的来一起体验吧https://github.com/alibaba/jvm-sandbox。