前言
chaosblade-exec-jvm是基于jvm-sandbox开发的,本章先分析一下jvm-sandbox。
jvm-sandbox理解为一个javaagent框架,基于它可以快速开发javaagent程序。
和之前聊过的Sermant差不多,只是Sermant内置了很多服务治理插件,而sandbox只有框架。
本章主要分析:
1)Agent挂载与sandbox卸载;
2)sandbox类加载结构;
3)模块生命周期;
4)sandbox增强后的运行时逻辑;
注:
1)基于1.3.3版本;
2)忽略重置reset、刷新flush、单模块卸载unload等边缘逻辑;
3)本文主要分析sandbox运行时agentmain挂载,sandbox也支持启动premain挂载,逻辑类似;
一、使用案例
pom
pom通过provided引入sandbox-api,其中包含sandbox和servlet的api。
<dependencies>
<dependency>
<groupId>com.alibaba.jvm.sandbox</groupId>
<artifactId>sandbox-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
Module
创建一个Module实现类,通过Information注解设置模块id,暴露3个http端点:
1)create:创建实验;
2)destroy:销毁实验;
3)list:查看所有正在执行的实验,返回内存map;
@Information(id = "mychaos")
public class MyChaosModule implements Module {
private final Map<String, String> cache = new ConcurrentHashMap<String, String>();
@Command("/create")
public void create(HttpServletRequest req,
HttpServletResponse resp,
Map<String, String> param, // parameterMap的value数组用逗号分割
String queryString, // javax.servlet.http.HttpServletRequest.getQueryString
PrintWriter printWriter// javax.servlet.ServletResponse.getWriter
) {
// ...
}
@Command("/destroy")
public void destroy(HttpServletRequest req, HttpServletResponse resp, Map<String, String> param, PrintWriter printWriter) {
// ...
}
@Command("/list")
public void list(PrintWriter printWriter) {
printWriter.println(cache);
}
}
Module通过SPI方式发现,在META-INF/services/com.alibaba.jvm.sandbox.api.Module中写入Module完全限定类名。
功能1:创建混沌实验
1)根据className和methodName,创建Filter,用于过滤需要增强的class和method;
2)根据exp实验类型,创建EventListener,用于在增强点执行增强逻辑;
3)调用sandbox-api提供的ModuleEventWatcher,注册增强点监听,最后传入监听点(BEFORE-方法调用前、RETURN-方法执行后、THROWS-方法异常后),返回watchId
4)将watchId缓存,用于后续销毁实验;
private final Map<String, String> cache = new ConcurrentHashMap<String, String>();
@Resource
private ModuleEventWatcher moduleEventWatcher;
@Command("/create")
public void create(Map<String, String> param, PrintWriter printWriter) {
boolean isAfter = param.get("after") != null;
String className = param.get("className");
String methodName = param.get("methodName");
String exp = param.get("exp");
int id = moduleEventWatcher.watch(
new MyChaosFilter(className, methodName),
new MyChaosEventListener(exp),
isAfter ? Event.Type.RETURN : Event.Type.BEFORE
);
cache.put(id + "", className + "," + methodName + "," + exp);
printWriter.println("ok");
}
Filter精确匹配class和method。
public class MyChaosFilter implements Filter {
private final String className;
private final String methodName;
public MyChaosFilter(String className, String methodName) {
this.className = className;
this.methodName = methodName;
}
@Override
public boolean doClassFilter(int access, String javaClassName, String superClassTypeJavaClassName, String[] interfaceTypeJavaClassNameArray, String[] annotationTypeJavaClassNameArray) {
return javaClassName.equals(className);
}
@Override
public boolean doMethodFilter(int access, String javaMethodName, String[] parameterTypeJavaClassNameArray, String[] throwsTypeJavaClassNameArray, String[] annotationTypeJavaClassNameArray) {
return javaMethodName.equals(methodName);
}
}
EventListener#onEvent:当到达增强点触发,这里支持延迟和异常两种实验。通过抛出ProcessControlException异常,可以控制用户代码执行逻辑,比如抛出自定义异常或立即返回指定对象。
注:入参Event可以根据增强点转换为BeforeEvent/ThrowsEvent等,以获取更多信息。
public class MyChaosEventListener implements EventListener {
private final String exp;
public MyChaosEventListener(String exp) {
this.exp = exp;
}
@Override
public void onEvent(Event event) throws Throwable {
if ("delay".equals(exp)) {
Thread.sleep(5000);
} else if ("throw".equals(exp)) {
ProcessControlException.throwThrowsImmediately(new RuntimeException("chaos"));
}
}
}
功能2:销毁混沌实验
销毁实验,调用ModuleEventWatcher#delete传入watchId,清理缓存。
@Command("/destroy")
public void destroy(HttpServletRequest req, HttpServletResponse resp, Map<String, String> param, PrintWriter printWriter) {
String id = param.get("id");
moduleEventWatcher.delete(Integer.parseInt(id));
String remove = cache.remove(id);
printWriter.println(remove);
}
构建打包
将chaos模块放入sandbox-module路径下。
注:也可以放在家目录的.sandbox-module下,这些module属于用户模块。而sandbox内置的模块都在module目录下,称为系统模块。
验证
sandbox.sh封装了所有对sandbox的操作。
-p指定进程号,-n指定命名空间,-l查询模块列表。
sandbox内置3个系统模块,提供模块管理功能,比如查询模块列表就是由sandbox-module-mgr提供。
-d指定 {模块}/{端点}?{参数} ,执行创建实验、查询实验、销毁实验。
注:/configs端点是业务代码,这里通过自定义module实现异常注入。
最后通过-S卸载sandbox。
二、Agent挂载
1、挂载主流程
一般通过sandbox.sh脚本执行sandbox指令,sandbox.sh分两步走:
1)一阶段:调用sandbox-core.jar的CoreLauncher,根据pid挂载agent,触发agent的agentmain。
2)二阶段:一阶段会开启一个httpserver,这里调用对应http端点执行实际命令,如-v会调用sandbox-info/version端点(由sandbox-module-mgr模块提供);
注意,无论执行什么实际命令,只要非CONNECT_ONLY(通过-C开启,指定已经挂载的sandbox的ip和port),都需要走一次挂载逻辑。
为了在二阶段执行能够拿到ip和port,sandbox.sh需要将attach和http调用关联。
1)sandbox.sh:生成一个token,调用在执行agentmain时传入。
2)agentmain:将token-ip和port的映射关系,写入home下的.sandbox.token文件中。
3)sandbox.sh:根据token匹配到ip和port,进行后续http调用。
AgentLauncher#agentmain:挂载,执行install并记录结果。
AgentLauncher#writeAttachResult:将挂载结果写入home下的.sandbox.token文件中。
所以每次执行sandbox.sh都会向.sandbox.token文件中写入一条新数据:ns;token;ip;port。
2、install
处理类加载器
AgentLauncher#install:Step1,处理类加载器。
1)将spy.jar放入bootstrap类加载器(和sermant的god.jar差不多),全局管理namespace级别的单例对象。
2)每个namespace对应一个SandboxClassLoader负责加载sandbox-core.jar,是sandbox的核心实现。
AgentLauncher#loadOrDefineClassLoader:创建SandboxClassLoader。
SandboxClassLoader#loadClass:sandbox类加载器优先走core.jar加载,然后才走双亲委派。
至此,sandbox的类加载器结构如下。
sandbox-agent侧只有两个类,后续大部分逻辑都在core中,需要指定classloader通过反射执行。
创建Http服务
AgentLauncher#install:Step2,将入参和配置文件转换为CoreConfigure对象,开启CoreServer,返回server地址信息。
因为SandboxClassLoader是ns级别的,所以http服务也是每个ns对应一个。
前面说到,每次执行sandbox.sh都会走agentmain挂载逻辑,所以需要判断当前ns下server是否已经开启(isBind),没开启才走bind开启http服务。
3、启动Http服务
JettyCoreServer#bind:http服务启动分为四步
1)日志系统初始化;(忽略)
2)创建JvmSandbox用于后续业务处理;
3)开启HttpServer;
4)加载所有模块;(包括系统模块和用户模块)
创建JvmSandBox
创建CoreModuleManager
JvmSandBox创建DefaultCoreModuleManager管理所有Module。
DefaultCoreModuleManager:
1)CoreLoadedClassDataSource:操作Instrumentation,可获取已经加载的所有class;
2)ProviderManager:加载provider目录下的jar,主要用于通过SPI扩展模块加载的一些钩子方法,可以忽略;
3)moduleLibDirArray:模块文件和目录集合,比如包含sandbox-module和module;
4)loadedModuleBOMap:已经加载的模块,key=Information注解的id;
初始化Spy
JvmSandbox#init:对于部分class提前触发类加载(不重要),初始化spy。
SpyUtils#init:将当前ns下的单例EventListenerHandler(SpyHandler)注册到Bootstrap类加载器的全局Spy中。
Spy#init:spy的作用是管理不同ns(SandboxClassLoader)下的单例SpyHandler。
Spy#spyMethodOnCallBefore:运行时,用户代码被增强后会走这里,可以通过ns找到对应SpyHandler。
启动HttpServer
JettyCoreServer#initHttpServer:调用Jetty api创建Server。
JettyCoreServer#initJettyContextHandler:设置contextPath,创建ModuleHttpServlet用于接收http请求。所以对于-n ns001 -d mychaos/create,实际调用http端点如 /sandbox/ns001/module/http/mychaos/create。
加载所有模块
DefaultCoreModuleManager#reset:先卸载所有模块(忽略,这里没有加载过),再循环每个module.jar重新加载。
ModuleJarLoader#load:每个module.jar都采用独立的ModuleJarClassLoader通过SPI加载Module。
ModuleJarClassLoader。
ModuleJarClassLoader的loadClass类加载实现如下:
1)对于sandbox-api包中的类,包括servlet和Resource注解,优先走所在ns的SandboxClassLoader;
2)从自己module.jar中加载;
3)降级走被增强class的classloader加载;(所以模块中provided引入宿主依赖jar可以加载到对应class)
4)最终才走parent的classloader(AppClassLoader)加载;
DefaultCoreModuleManager#load:每个Module的加载流程如下,uniqueId是Information注解的id属性。
1)创建CoreModule包装Module;
2)识别Resource注解Field做依赖注入;
3)如果Module实现ModuleLifecycle,调用onLoad钩子;
4)标记CoreModule已经加载;
5)如果Information注解中isActiveOnLoad=true(默认就是true),自动触发激活;(如chaosblade的module是非自动激活的,需要通过sandbox.sh -a 模块名激活)
6)DefaultCoreModuleManager缓存id和CoreModule;
7)如果Module实现LoadCompleted,调用loadCompleted钩子;(比如通过premain挂载,module设置为自动激活,在这里可以直接通过watch对class做增强)
DefaultCoreModuleManager#injectResourceOnLoadIfNecessary:Resource能自动注入以下成员变量。
三、处理http请求
每个ns有一个ModuleHttpServlet处理http请求。
ModuleHttpServlet#doMethod:从请求路径中找到模块id,通过模块id找到CoreModule,只要Module加载完毕,就可以接收http请求,和激活/冻结无关。
ModuleHttpServlet#doMethod:
1)根据请求path匹配Http/Command注解方法;
2)构造方法入参数组;
3)反射调用目标方法;
ModuleHttpServlet#generateParameterObjectArray:可以自动注入的方法入参包含:
1)HttpServletRequest;
2)HttpServletResponse;
3)Map(String,String):ServletRequest#getParameterMap返回的map,将value通过逗号join;
4)Map(String,String数组):ServletRequest#getParameterMap返回的map;
5)String:HttpServletRequest#getQueryString;
6)PrintWriter:ServletResponse#getWriter;
四、watch
DefaultCoreModuleManager#injectResourceOnLoadIfNecessary:前面说到,对于每个Module可以通过Resource自动注入ModuleEventWatcher。
在agent挂载阶段,只是加载了sandbox基础服务和所有模块,所有的字节码增强需要用户按需实现。
调用ModuleEventWatcher#watch对目标字节码执行增强,Filter指定增强点,EventListener是增强逻辑,EventType指定增强位置(RETURN-方法返回前,BEFORE-方法执行前,THROWS-方法异常)。
DefaultModuleEventWatcher#watch:每次watch会生成顺序递增的watchId
1)构建SandboxClassFileTransformer,缓存到当前ns对应的CoreModule,并安装至Instrumentation;
2)classDataSource根据入参自定义Filter过滤得到已经被加载的需要retransfrom的class,对于这些class执行retransform;
3)如果Module已经被激活,则调用当前ns的EventListenerHandler(SpyHandler),注册用户的EventListener;
1、SandboxClassFileTransformer
SandboxClassFileTransformer#_transform:sandbox用asm做字节码增强
1)获取类结构,如果classBeingRedefined为空,代表class首次加载,使用asm读取原始字节数组,获取类结构;反之,代表class已经被加载过,被retransform触发,直接通过jdk反射获取类结构;
2)判断class是否需要增强,一方面使用用户的Filter匹配,另一方面UnsupportedMatcher过滤不能增强的class,如动态代理的class类名包含;
3)对目标class做增强,使用asm返回新的字节数组;
EventEnhancer#toByteCodeArray:这里会将被增强的class的classloader缓存下来。
EventWeaver#visitMethod:以EventType=before为例,最终携带namespace、listenerId(用户Listener的唯一编号)等临时变量,调用Spy#spyMethodOnBefore静态方法。
增强后如下:
如果对于同一个方法增强两次,则按照Transformer的顺序,先增强的在内层,后被调用;后增强的在外层,先被调用。比如案例中,先注入delay后注入throw,则无延迟直接异常;先注入throw后注入delay,则延迟后抛出异常。
2、retransform
对于已经加载的class,需要执行retransform让增强生效。
DefaultCoreLoadedClassDataSource#iteratorForLoadedClasses:操作Instrumentation获取所有已经被加载的class。
DefaultCoreLoadedClassDataSource#find:使用用户指定Filter过滤后得到需要retransform的已经被加载的class。
DefaultModuleEventWatcher#reTransformClasses:循环这些class执行retransform。
3、运行时
因为Spy在Bootstrap类加载器里,所以用户class能加载到。
Spy#spyMethodOnCallBefore:以before为例,根据ns找到ns下的单例EventListenerHandler。
EventListenerHandler#handleOnCallBefore:EventListenerHandler根据listener的id找到EventListener。如果模块被冻结,则会无法找到,从而不会触发增强逻辑。
EventListenerHandler#handleOnCallBefore:构造Event对象。
EventListenerHandler#handleEvent:确认EventListener关注当前EventType后执行onEvent。
EventListenerHandler#handleEvent:用户EventListener可以通过抛出ProcessControlException改变方法执行结果。
RETURN_IMMEDIATELY,方法直接返回指定对象,Spy执行结果为1。
THROWS_IMMEDIATELY,方法抛出指定异常,Spy执行结果为2。
其他情况,Spy执行结果为0,正常执行业务逻辑。
五、激活与冻结
如果设置模块的isActiveOnLoad=false,则模块在加载后处于冻结状态,如果此时通过watch对增强点插桩,Listener无法收到Event。
需要执行-a指令,将指定模块激活。
实际调用sandbox-module-mgr系统模块的active端点,根据id找到Module。
DefaultCoreModuleManager#active:
1)如果Module实现ModuleLifecycle,触发onActive回调;
2)将CoreModule中已经安装Transformer关联的Listener注册到EventListenerHandler;
watch插桩后通过激活将插桩和用户增强逻辑产生关联。
对于已经激活的模块,可以执行-A冻结,执行反向操作。
冻结是解除Listener与SpyHandler的关联,不会影响插桩。
-A调用sandbox-module-mgr系统模块的frozen端点,根据id找到Module。
DefaultCoreModuleManager#frozen:
1)如果Module实现ModuleLifecycle,调用onFrozen方法;
2)将Module下所有EventListener与SpyHandler(EventListenerHandler)解绑;
EventListenerHandler#frozen:将指定listener移除。
六、delete
调用ModuleEventWatcher传入缓存的watchId,移除插桩。
DefaultModuleEventWatcher#delete:
1)循环当前module中所有的Transformer,根据watchId匹配得到目标Transformer;
2)执行冻结,解绑Listener和SpyHandler;
3)调用Instrumentation#removeTransformer移除Transformer;
4)被增强的class可能已经被加载,需要再触发retransform还原class;
七、sandbox卸载
ControlModule#shutdown:通过-S指令执行sandbox卸载,实际调用
sandbox-control系统模块的shutdown端点。
ControlModule#uninstall:使用sandbox-control模块的类加载器加载AgentLauncher(agent启动类),反射调用uninstall方法,卸载当前ns。
注:sandbox-control模块虽然不依赖sandbox-agent.jar,模块类加载器ModuleJarClassLoader支持从AppClassLoader加载class,见上面加载所有模块。
AgentLauncher#uninstall:通过ns下的SandboxClassLoader反射调用Http服务销毁,关闭SandboxClassLoader。
JettyCoreServer#destroy:关闭JvmSandbox、关闭JettyServer、关闭logback。
重点在于JvmSandbox的销毁。
JvmSandbox#destroy:卸载所有Module,将ns下的单例SpyHandler(EventListenerHandler)从Spy中注销。
DefaultCoreModuleManager#unload:对于每个Module
1)冻结模块,将所有EventListener与SpyHandler(EventListenerHandler)解除关联;
2)触发ModuleLifeCycle#onUnload;
2)释放资源;
CoreModule#releaseAll:在Module中缓存了一些需要释放的资源,在结束后需要释放。
DefaultCoreModuleManager#injectResourceOnLoadIfNecessary:对于Module自动注入了ModuleEventWatcher的情况,会在这里执行所有Transformer的delete方法,移除Transformer,并恢复class。
至此用户class被还原,不会调用Spy。Spy取消关联被销毁ns的SpyHandler。解冻解除Listener与SpyHandler关联。
总结
1、类加载
BootstrapClassLoader:加载sandbox-spy.jar。
1)SpyHandler接口:定义before/return/throw的回调方法,每个ns会有一个SpyHandler实现(EventListenerHandler);
2)Spy:静态map维护每个ns的SpyHandler实现,后续用户代码可以通过Spy进入插桩逻辑;
AppClassLoader:加载sandbox-agent.jar。
1)AgentLuancher:agent启动类,维护n个ns下的SandboxClassLoader;
2)SandboxClassLoader:每个ns会创建一个SandboxClassLoader;
SandboxClassLoader:加载sandbox-core.jar,包括所有sandbox的核心实现。
1)JettyCoreServer:每个ns对应一个http服务,挂载后会启动;
2)DefaultCoreModuleManager:管理ns下的所有Module;
3)EventListenerHandler:每个ns有一个SpyHandler实现,维护用户通过watch加入的EventListener;
ModuleJarClassLoader:每个ns下的一个模块jar对应一个ModuleJarClassLoader,sandbox默认提供sandbox-mgr-module模块实现模块管理功能。
ModuleJarClassLoader的类加载机制如下:
1)对于sandbox-api包中的类,包括servlet和Resource注解,优先走所在ns的SandboxClassLoader;
2)从自己module.jar中加载;
3)降级走被增强class的classloader加载;(所以模块中provided引入宿主依赖jar可以加载到对应class)
4)最终才走parent的classloader(AppClassLoader)加载;
2、Agent挂载和sandbox卸载
Agent挂载一般通过sandbox脚本执行,通过-p指定java进程,-n指定namespace。
注:除了connect only场景(-C指定sandbox的ip和port),每次执行sandbox.sh都会触发挂载逻辑。
sandbox脚本执行sandbox-core.jar中的CoreLauncher,根据进程号挂载agent。
agentmain:
1)开启http服务;
2)初始化基础服务,SPI加载所有模块;
sandbox提供了sandbox-mgr-module系统模块,提供3个模块:
1)sandbox-info:实现-v指令;
2)sandbox-control:实现-S指令,卸载agent;
3)sandbox-module-mgr:实现-a激活、-A冻结、-l查询、-d自定义模块端点调用;
这些指令都对应一个http端点,格式如/sandbox/{namespace}/module/http/{模块id}/{端点},如-l实际对应/sandbox/{namespace}/module/http/sandbox-module-mgr/list。
每次执行挂载,sandbox脚本会生成唯一token传入agentmain。
agentmain将token、namespace、http服务ip和port写入家目录的.sandbox.token文件中。
sandbox脚本后续可以通过token匹配到http服务信息,进行后续实际命令调用。
sandbox脚本通过-p pid -n namespace -S卸载sandbox。
卸载底层:
1)关闭http服务;
2)循环所有Module,冻结并delete,移除Transformer并还原用户class;
3、模块生命周期
watch
用户在Module中通过Resource注入ModuleEventWatcher,调用ModuleEventWatcher#watch:
1)Filter:过滤需要增强的class和method;
2)EventListener:处理增强逻辑;
3)事件类型:可多选,如Return、Before、Throws;
watch底层逻辑:
1)构建SandboxClassFileTransformer,缓存到当前ns对应的CoreModule,安装至Instrumentation;
2)classDataSource根据入参自定义Filter过滤得到已经被加载的需要retransfrom的class,对于这些class执行retransform;
3)如果Module已经被激活,则调用当前ns的SpyHandler,注册EventListener;
每个Transformer对应一个watchId编号,watch方法会返回watchId,用户需要缓存,用于后续delete。
delete
用户调用ModuleEventWatcher#delete,传入缓存的watchId,移除增强:
1)循环当前module中所有的Transformer,根据watchId匹配得到目标Transformer;
2)执行冻结,将Transformer中的EventListener与SpyHandler解绑;
3)调用Instrumentation#removeTransformer移除Transformer;
4)被增强的class可能已经被加载,需要再触发retransform还原class;
激活
默认Information注解的isActiveOnLoad=true,代表模块自动激活,在agent挂载后,模块被加载后会自动激活。
-a指令指定模块名,调用sandbox-module-mgr系统模块的active端点,根据模块名找到模块。将模块中已经安装Transformer关联的EventListener注册到SpyHandler。
注意:
1)watch是安装Transformer增强字节码;
2)激活是将增强字节码与用户EventListener关联;
冻结
-A指令指定模块名,调用sandbox-module-mgr系统模块的active端点,根据模块名找到模块。将模块中Transformer关联的EventListener从SpyHandler注销。
注意:
1)delete是移除Transformer并恢复字节码,且包含冻结;
2)冻结仅仅将EventListener移除,但是字节码增强还在,只是在运行时不会执行用户的EventListener;
4、增强后的运行逻辑
asm增强后的执行逻辑如下:
1)在方法前后和异常时,调用Spy的静态方法执行EventListener;
2)用户EventListener可以通过抛出ProcessControlException异常改变方法执行结果:
RETURN_IMMEDIATELY,Spy执行结果为1,方法直接返回指定对象;
THROWS_IMMEDIATELY,Spy执行结果为2,方法抛出指定异常;
其他,Spy执行结果为0,正常执行业务逻辑。
如果同一个增强点增强多次,后增强的会在先增强的外层(Transformer有序)。
通过watch增强后,用户类可以通过以下路径触发EventListener。