写点什么

源码系列 | 阿里 JVM-Sandbox 核心源码剖析

用户头像
九叔
关注
发布于: 2021 年 04 月 11 日
源码系列 | 阿里JVM-Sandbox核心源码剖析

前言

实际上,无论是 async-profiler 还是 Arthas,都遵循了 JVMTI 规范,使之能够使用 Instrumentation 来构建一个独立于应用程序的 Agent 程序,以便监测和协助运行在 JVM 上的程序,并且微乎其微的性能开销,对目标程序带来的损耗极低。


之所以我会对 JVM-Sandbox 的核心源码进行剖析,其主要原因是在于 JVM-Sandbox 的开源社区似乎并不活跃,相关资料极其匮乏,许多对 JVM-Sandbox 设计原理和实现细节感兴趣的同学只能望而却步(比如:避免类污染、冲突等问题的类隔离策略,以及类隔离后的“通讯”操作等)。因此,相信大家仔细阅读本文后,能够有所裨益。

核心原理

JVM-Sandbox 将目标方法分解为 BEFORE、RETURN、THROWS 等三个环节,由此在对应环节上引申出相应的事件探测和流程控制机制。也就是说,通过将这三个环节的事件进行分离,我们可以完成如下 3 点 AOP 操作:

  • 动态感知、改变方法的入参;

  • 动态感知、改变方法的出参和抛出异常;

  • 改变方法的执行流程。


这里对改变方法的执行流程做一个简单的介绍。假设我们对目标方法植入了 BEFORE、RETURN、THROWS 等三个环节的事件增强,那么当方法逻辑执行之前,首先会触发 BEFORE 事件,待 BEFORE 事件结束后,通过返回的状态信息(包括状态码、自定义响应结果)来判断方法的后续走向,如果返回的状态码并不是直接返回自定义结果和抛出异常(以限流为例,如果触发限流阈值则直接返回异常信息),则继续执行方法的正常流程(RETURN 和 THROWS 事件同样也具备和 BEFORE 一样的流程控制机制)。RETURN 环节对应的事件在方法执行结束返回前触发。而 THROWS 事件在方法执行过程中抛出异常时触发。JVM-Sandbox 的增强策略,如图 1 所示。

图1 sandbox的增强策略

实现细节剖析

在 sandbox-core 包下,CoreLauncher 类作为 JVM-Sandbox 的启动器,由它的 attachAgent()方法负责 attach 到目标进程上,如下所示:

private void attachAgent(final String targetJvmPid,                         final String agentJarPath,                         final String cfg) throws Exception {    VirtualMachine vmObj = null;    try {        vmObj = VirtualMachine.attach(targetJvmPid);        if (vmObj != null) {            vmObj.loadAgent(agentJarPath, cfg);        }    } finally {        if (null != vmObj) {            vmObj.detach();        }    }}
复制代码

当成功 attache 到目标进程上后,会触发 Agent-Class 的 agentmain()方法,这里的 Agent-Class 对应的就是 sandbox-agent 包下的 AgentLauncher 类。agentmain()方法会调用 install()方法,该方法内包含了诸多操作,比如:将 sandbox-spy 包下的 Spy 类注册到 BootstrapClassLoader 中创建 SandboxClassLoader 加载 sandbox-core 包下的所有类(包括依赖的 sandbox-api 等),以及反射调用 sandbox-core 包下的 JettyCoreServer.bind()方法启动 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 {         // 将Spy注入到BootstrapClassLoader         inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(                getSandboxSpyJarPath(getSandboxHome(featureMap))                // SANDBOX_SPY_JAR_PATH        )));        // 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀         final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(                namespace,                getSandboxCoreJarPath(getSandboxHome(featureMap))                // 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);     } } 
复制代码

当调用 JettyCoreServer.bind()的方法成功启动 HTTP 服务后,接下来就会触发一系列的调用,直至调用到 sandbox-core 包的 ModuleJarLoader 类,由它触发回调函数 onLoad()(其实现为 DefaultCoreModuleManager 的静态内部类 InnerModuleLoadCallback)。在 InnerModuleLoadCallback.onLoad()方法内部会调用 DefaultCoreModuleManager.load()方法触发对 Module 的加载和注册,如下所示:

private synchronized void load(final String uniqueId,                               final Module module,                               final File moduleJarFile,                               final ModuleJarClassLoader moduleClassLoader) throws ModuleException {    if (loadedModuleBOMap.containsKey(uniqueId)) {        logger.debug("module already loaded. module={};", uniqueId);        return;    }    logger.info("loading module, module={};class={};module-jar={};",            uniqueId,            module.getClass().getName(),            moduleJarFile    );    // 初始化模块信息    final CoreModule coreModule = new CoreModule(uniqueId, moduleJarFile, moduleClassLoader, module);    // 注入@Resource资源    injectResourceOnLoadIfNecessary(coreModule);    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD);    // 设置为已经加载    coreModule.markLoaded(true);    // 如果模块标记了加载时自动激活,则需要在加载完成之后激活模块    markActiveOnLoadIfNecessary(coreModule);    // 注册到模块列表中    loadedModuleBOMap.put(uniqueId, coreModule);    // 通知生命周期,模块加载完成    callAndFireModuleLifeCycle(coreModule, MODULE_LOAD_COMPLETED);}
复制代码

当成功创建 ModuleClassLoader(父类加载器为 SandboxClassLoader)并加载好 Module 相关的类后,会由 markActiveOnLoadIfNecessary()方法触发 DefaultCoreModuleManager 的 active()方法。这里非常关键,会调用单例类 EventListenerHandlers 的 active()方法将事件监听器(也就是被 ModuleClassLoader 加载的用户的 Module 模块的 AdviceListener 实现)和监听器 ID(每个监听器都有一个唯一的监听器 ID)绑定到由 SandboxClassLoader 加载的 EventListenerHandlers 类的一个全局 Map 上(mappingOfEventProcessor)。至此为止,可以理解为 SandboxClassLoader 到 ModuleClassLoader 的引用关系就构建好了,如下所示:

@Overridepublic synchronized void active(final CoreModule coreModule) throws ModuleException {    // 如果模块已经被激活,则直接幂等返回    if (coreModule.isActivated()) {        logger.debug("module already activated. module={};", coreModule.getUniqueId());        return;    }    logger.info("active module, module={};class={};module-jar={};",            coreModule.getUniqueId(),            coreModule.getModule().getClass().getName(),            coreModule.getJarFile()    );    // 通知生命周期    callAndFireModuleLifeCycle(coreModule, MODULE_ACTIVE);    // 激活所有监听器    for (final SandboxClassFileTransformer sandboxClassFileTransformer : coreModule.getSandboxClassFileTransformers()) {        EventListenerHandlers.getSingleton().active(                sandboxClassFileTransformer.getListenerId(),                sandboxClassFileTransformer.getEventListener(),                sandboxClassFileTransformer.getEventTypeArray()        );    }    // 标记模块为:已激活    coreModule.markActivated(true);}
复制代码

当触发模块并且 JVM-Sandbox 成功对目标类进行增强后,JVM-Sandbox 会在目标类的方法中进行插装。以 BEFORE 事件为例,通过反编译代码,我们可以发现在方法逻辑执行之前会优先触发 Spy 的静态方法 spyMethodOnBefore()的调用,如下所示:

public static Ret spyMethodOnBefore(final Object[] argumentArray,                                    final String namespace,                                    final int listenerId,                                    final int targetClassLoaderObjectID,                                    final String javaClassName,                                    final String javaMethodName,                                    final String javaMethodDesc,                                    final Object target) throws Throwable {    final Thread thread = Thread.currentThread();    if (selfCallBarrier.isEnter(thread)) {        return Ret.RET_NONE;    }    final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);    try {        final MethodHook hook = namespaceMethodHookMap.get(namespace);        if (null == hook) {            return Ret.RET_NONE;        }        return (Ret) hook.ON_BEFORE_METHOD.invoke(null,                listenerId, targetClassLoaderObjectID, SPY_RET_CLASS, javaClassName, javaMethodName, javaMethodDesc, target, argumentArray);    } catch (Throwable cause) {        handleException(cause);        return Ret.RET_NONE;    } finally {        selfCallBarrier.exit(thread, node);    }}
复制代码

Spy.spyMethodOnBefore()方法最终会通过反射调用 EventListenerHandlers 的静态方法 onBefore()(在 sandbox-core 包被加载后,会触发调用 SpyUtils 的静态方法 init()将 EventListenerHandlers 类的静态方法 onBefore()注册到由 BootstrapClassLoader 加载的 Spy 类的静态内部类 MethodHook 的钩子上。这样一来,等于就通过 Spy 类打通了 AppClassLoader、SandboxClassLoader,以及 ModuleClassLoader 三者之间的“通讯”),由该方法内部触发对 handleOnBefore()方法的调用。


在 EventListenerHandlers.handleOnBefore()方法中,会根据目标类至 Spy.spyMethodOnBefore()传递过来的事件 ID 从全局的 mappingOfEventProcessor 中获取出对应的事件处理器,然后调用 handleEvent()方法执行事件,如下所示:

private Spy.Ret handleOnBefore(final int listenerId,                               final int targetClassLoaderObjectID,                               final String javaClassName,                               final String javaMethodName,                               final String javaMethodDesc,                               final Object target,                               final Object[] argumentArray) throws Throwable {    // 获取事件处理器    final com.alibaba.jvm.sandbox.core.enhance.weaver.EventProcessor wrap = mappingOfEventProcessor.get(listenerId);    // 如果尚未注册,则直接返回,不做任何处理    if (null == wrap) {        logger.debug("listener={} is not activated, ignore processing before-event.", listenerId);        return newInstanceForNone();    }    // 获取调用跟踪信息    final com.alibaba.jvm.sandbox.core.enhance.weaver.EventProcessor.Process process = wrap.processRef.get();    // 调用ID    final int invokeId = invokeIdSequencer.getAndIncrement();    process.pushInvokeId(invokeId);    // 调用过程ID    final int processId = process.getProcessId();    // 如果当前处理ID被忽略,则立即返回    if (process.touchIsIgnoreProcess(processId)) {        process.popInvokeId();        return newInstanceForNone();    }    final ClassLoader javaClassLoader = ObjectIDs.instance.getObject(targetClassLoaderObjectID);    final BeforeEvent event = process.getEventFactory().makeBeforeEvent(            processId,            invokeId,            javaClassLoader,            javaClassName,            javaMethodName,            javaMethodDesc,            target,            argumentArray    );    try {        return handleEvent(listenerId, processId, invokeId, event, wrap);    } finally {        process.getEventFactory().returnEvent(event);    }}
复制代码

在 handleEvent()方法内部,最终会调用 sandbox-api 包下的 EventListener 类的 onEvent()—>switchEvent()方法(这里的实现为 AdviceAdapterListener)执行事件调用(也就是调用我们的 AdviceListener 实现),如下所示:

private void switchEvent(final OpStack opStack,                         final Event event) throws Throwable {    switch (event.type) {        case BEFORE: {            final BeforeEvent bEvent = (BeforeEvent) event;            final ClassLoader loader = toClassLoader(bEvent.javaClassLoader);            final Class<?> targetClass = toClass(loader, bEvent.javaClassName);            final Advice advice = new Advice(                    bEvent.processId,                    bEvent.invokeId,                    toBehavior(                            targetClass,                            bEvent.javaMethodName,                            bEvent.javaMethodDesc                    ),                    bEvent.argumentArray,                    bEvent.target            );            final Advice top;            final Advice parent;            // 顶层调用            if (opStack.isEmpty()) {                top = parent = advice;            }            // 非顶层            else {                parent = opStack.peek().advice;                top = parent.getProcessTop();            }            advice.applyBefore(top, parent);            opStackRef.get().pushForBegin(advice);            adviceListener.before(advice);            break;        }        //省略部分代码}
复制代码

最后再总结一下多个类加载器之间的类调用的过程:

  • 首先在 BootstrapClassLoader 中注册一个 Spy 类;

  • 在 Spy 类中绑定由 SandboxClassLoader 加载的 EventListenerHandlers 类的方法引用;

  • 在 EventListenerHandlers 中绑定由 ModuleClassLoader 加载的用户 Module 模块的 AdviceListenerImpl 引用。

====== END ======



至此,本文内容全部结束。如果在阅读过程中有任何疑问,欢迎在评论区留言参与讨论。



推荐文章:

码字不易,欢迎转发

发布于: 2021 年 04 月 11 日阅读数: 294
用户头像

九叔

关注

阿里巴巴 | 高级技术专家 2020.03.25 加入

GIAC全球互联网架构大会讲师,《超大流量分布式系统架构解决方案》、《人人都是架构师》、《Java虚拟机精讲》书籍作者

评论 (7 条评论)

发布
用户头像
九叔,赞

读本文后,能够有所裨益。 核心原理

2021 年 06 月 15 日 18:28
回复
用户头像
赞一个!!!
2021 年 04 月 13 日 17:29
回复
用户头像
九叔niubility
2021 年 04 月 12 日 15:13
回复
用户头像
牛人👍
2021 年 04 月 12 日 06:58
回复
用户头像
九叔牛逼
2021 年 04 月 11 日 21:53
回复
用户头像
学习了,感谢分享
2021 年 04 月 11 日 21:42
回复
用户头像
跟九叔学源码👍 👍 👍
2021 年 04 月 11 日 21:30
回复
没有更多了
源码系列 | 阿里JVM-Sandbox核心源码剖析