ChaosBlade源码(三)jvm-sandbox

645 阅读15分钟

前言

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下的单例EventListenerHandlerSpyHandler)注册到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类名包含EnhancerBySpringCGLIBEnhancerBySpringCGLIB

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。