JVM SandBox简要介绍

JVM-SANDBOX(沙箱)实现了一种在不重启、不侵入目标JVM应用的AOP解决方案。

沙箱的特性

  1. 无侵入:目标应用无需重启也无需感知沙箱的存在
  2. 类隔离:沙箱以及沙箱的模块不会和目标应用的类相互干扰
  3. 可插拔:沙箱以及沙箱的模块可以随时加载和卸载,不会在目标应用留下痕迹
  4. 多租户:目标应用可以同时挂载不同租户下的沙箱并独立控制
  5. 高兼容:支持JDK[6,11]

沙箱常见应用场景

  • 线上故障定位
  • 线上系统流控
  • 线上故障模拟
  • 方法请求录制和结果回放
  • 动态日志打印
  • 安全信息监测和脱敏

 

1.1 AOP

在介绍 JVM SandBox 之前,我们先来回顾一下 AOP 技术。

AOP(面向切面编程,Aspect Oriented Programming)技术已被业界广泛应用,其思想是面向业务处理过程的某个步骤或阶段进行编程,这个步骤或阶段被称为切面,其目的是降低业务逻辑的各部分之间的耦合,常见的 AOP 实现基本原理有两种:代理和行为注入。

1)代理模式
在代理模式下,我们会创建一个代理对象来代理原对象的行为,代理对象拥有原对象行为执行的控制权,在这种模式下,我们基于代理对象在原对象行为执行的前后插入代码来实现 AOP。

https://static001.infoq.cn/resource/image/74/55/74910532ba61c7844469c738b62f5455.png

2-1 代理模式

2)行为注入模式
在行为注入模式下,我们不会创建一个新的对象,而是修改原对象,在原对象行为的执行前后注入代码来实现 AOP。

https://static001.infoq.cn/resource/image/41/c6/41325ce612071123b7d298ff8e3e73c6.png

2-2 注入模式

1.2 JVM SandBox

   JVM SandBox 是阿里开源的一款 JVM 平台非侵入式运行期 AOP 解决方案,本质上是一种 AOP 落地形式。

   为什么不采用 Spring AOP 方案呢?Spring AOP 方案的痛点在于不是所有业务代码都托管在 Spring 容器中,而且更底层的中间件代码、三方包代码无法纳入到回归测试范围,更糟糕的是测试框架会引入自身所依赖的类库,经常与业务代码的类库产生冲突,因此,JVM SandBox 应运而生。

   JVM SandBox 本身是基于插件化的设计思想,允许用于以“模块”的方式基于 JVM SandBox 提供的 AOP 能力开发新的功能。基于 JVM SandBox,我们不需要关心如何在 JVM 层实现 AOP 的技术细节,只需要通过 JVM SandBox 提供的编程结构告诉“沙箱”,我们希望对哪些类哪些方法进行 AOP,在切面点做什么即可,JVM SandBox 模块功能编写起来非常简单。下面是一个示例模块代码:

@MetaInfServices(Module.class)  
@Information(id = "my-sandbox-module")// 模块名  
public class MySandBoxModule implements Module {  
    private Logger LOG = Logger.getLogger(MySandBoxModule.class.getName());  
    @Resource  
    private ModuleEventWatcher moduleEventWatcher;  
  
    @Command("addLog")// 模块命令名  
    public void addLog() {  
        new EventWatchBuilder(moduleEventWatcher)  
                .onClass("com.float.lu.DealGroupService")// 想要对 DealGroupService 这个类进行切面  
                .onBehavior("loadDealGroup")// 想要对上面类的 loadDealGroup 方法进行切面  
                .onWatch(new AdviceListener() {  
                    @Override  
                    protected void before(Advice advice) throws Throwable {  
                        LOG.info(" 方法名: " + advice.getBehavior().getName());// 在方法执行前打印方法的名字  
                    }  
                });  
    }  
}  

 

 

如上面代码所示,通过简单常规的编码即可实现对某个类的某个方法进行切面,不需要有了解即可上手。上面的模 JVM SandBox 和初始化之后便可以被使用了。比如,只需要告 JVM SandBox my-sandbox-module 这个模块的 addLog 这个方法,我们编写的功能的调用就会被注入到目标地方

 

我们知道Java对象的行为(函数,方法)是存储在方法区的,从下图可以看到,方法区的数据是由类加载器把编译好的class文件加载到jvm方法区的。所以我们可以得出简单思路是:
1. 在对应类Java代码中新增日志代码,并重新编译得到新的class文件。
2. 让jvm重新加载这个类的class文件到方法区

第一步倒是挺好实现,但是第二步,如何让jvm加载一个已经加载过的类?
答案是“java.lang.instrument.Instrumentation”

instrument JVM 提供的一个可以修改已加载类的类库,专门为 Java 语言编 写的插桩服务提供支持。Instrumentation中有两个方法都可以实现重新替换已经存在的class文件,它们是:redefineClasses retransformClasses。区别是redefineClasses 是自己提供字节码文件替换 掉已存在的 class 文件,retransformClasses 是在已存在的字节码文件上修改后再替换之。它需要依赖 JVMTI Attach API 机制实现。JVM TI(JVM TOOL INTERFACEJVM 工具接口) JVM 提供的一套对 JVM 进行操作的工具接口。通过JVMTI,可以实现对 JVM 的多种操作,它通过接口注册 各种事件勾子,在 JVM 事件触发时,同时触发预定义的勾子,以实现对各个 JVM 事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退 出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等 待、VM 启动与退出等等。Instrument 就是一个基于 JVMTI 接口的,以代理方式连接和访问 JVM 的一个 Agent

 

JVM SandBox 容器的启动依赖 Java Agent,Java Agent(Java 代理)是 JDK 1.5 之后引入的技术。Agent 就是 JVMTI 的一种实现,Agent 有两种启动方式,一是随 Java 进 程启动而启动;二是运行时载入,通过 attach API,将模块(jar 包)动态地 Attach 到指定进程 id 的 Java 进程内。开发一个 Java Agent 有两种方式,一种是实现一个 premain 方法,但是这种方式实现的 Java Agent 只能在 JVM 启动的时候被加载;另一种是实现一个 agentmain 方法,这种方式实现的 Java Agent 可以在 JVM 启动之后被加载。JVM SandBox Agent 对于这两种方式都有实现,用户可以自行选择使用,因为在 JVM 层这两种方式底层的实现原理大同小异,下面先通过两行代码,来看看基于 agentmain 方式实现的 Java Agent 是如何被加载的:

VirtualMachine vmObj = VirtualMachine.attach(targetJvmPid);//targetJvmPid 为目标 JVM 的进程 ID 

vmObj.loadAgent(agentJarPath, cfg);  // agentJarPath agent jar 包的路径,cfg 为传递给 agent 的参数 

1.3可插拔

本文理解的 JVM SandBox 可插拔至少有两层含义:一层是 JVM 沙箱本身是可以被插拔的,可被动态地挂载到指定 JVM 进程上和可以被动态地卸载;另一层是 JVM 沙箱内部的模块是可以被插拔的,在沙箱启动期间,被加载的模块可以被动态地启用和卸载。
一个典型的沙箱使用流程如下:

$./sandbox.sh -p 33342 #将沙箱挂载到进程号为 33342 的 JVM 进程上 

$./sandbox.sh -p 33342 -d 'my-sandbox-module/addLog' #运行指定模块, 模块功能生效 

$./sandbox.sh -p 33342 -S #卸载沙箱 

JVM 沙箱可以被动态地挂载到某个正在运行的目标 JVM 进程之上(前提是目标 JVM 没有禁止 attach 功能),沙箱工作完之后还可以被动态地从目标 JVM 进程卸载掉,沙箱被卸载之后,沙箱对对目标 JVM 进程产生的影响会随即消失(这是沙箱的一个重要特性),沙箱工作示意图如下:

https://static001.infoq.cn/resource/image/73/b2/738b7c6f9fab7b6b408e51199db8f5b2.png

4-1 沙箱工作示意

客户端通过 Attach 将沙箱挂载到目标 JVM 进程上,沙箱的启动实际上是依赖 Java Agent,上文已经介绍过,启动之后沙箱会一直维护着 Instrument 对象引用,在沙箱中 Instrument 对象是一个非常重要的角色,它是沙箱访问和操作 JVM 的唯一通道,后续修改字节码和重定义类都要经过 Instrument。另外,沙箱启动之后同时会启动一个内部的 Jetty 服务器,这个服务器用于外部进程和沙箱进行通信,上面看到的./sandbox.sh -p 33342 -d ‘my-sandbox-module/addLog’ 这行代码,实际上就是通过 HTTP 协议来告诉沙箱执行 my-sanbox-module 这个模块的 addLog 这个功能的。

 

1.4 

sandbox的代主要分几个程:启、模强实现

启动

上面我们提到,使用Instrumentation进行字节码增强有2种模式(attach模式和java-agent模式),sandbox-jvm的启动有这2种方式,入口都在AgentLauncher中,分别对应着agentmain和premain,它们都调用了install方法,以agentmain为例

public static void agentmain(String featureString, Instrumentation inst) {
        LAUNCH_MODE = LAUNCH_MODE_ATTACH;
        final Map<String, String> featureMap = toFeatureMap(featureString);
        writeAttachResult(
                getNamespace(featureMap),
                getToken(featureMap),
                install(featureMap, inst)
        );
    }

install函数的作用是在目标jvm上安装sandbox,创建独立的classloader,通过classloader加载JettyCoreServer.class,并且反射生成实例,建立httpserver监听请求

// 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;
	}
}

启动jetty server,监听http请求,并且调用coreModuleManager.reset进行模块的加载,在下面一节介绍。

public synchronized void bind(final CoreConfigure cfg, final Instrumentation inst) throws IOException {
        this.cfg = cfg;
        try {
            initializer.initProcess(()->{
                    logger.info("initializing server. cfg={}", cfg);
                    jvmSandbox = new JvmSandbox(cfg, inst);
                    initHttpServer();
                    initJettyContextHandler();
                    httpServer.start();
                }
            });
            // 初始化加载所有的模块
            try {
                jvmSandbox.getCoreModuleManager().reset();
            } catch (Throwable cause) {
                logger.warn("reset occur error when initializing.", cause);
            }
            final InetSocketAddress local = getLocal();
            logger.info("initialized server. actual bind to {}:{}",
                    local.getHostName(),
                    local.getPort()
            );
        } catch (Throwable cause) {
                     // 对外抛出到目标应用中
            throw new IOException("server bind failed.", cause);
        }
    }

模块加载

模块是什么?sandbox将不同的业务进行模块划分,不同的模块使用不同的classloader进行加载,例如如果我们想实现流量录制,我们可以自定义一个模块通过字节码增强实现流量入口的监听并进行录制,这就是我们后面会介绍的repeater。
先来看下CoreModuleManager.reset() 的工作:
加载过程是先卸载再加载,首先根据cfg(配置存储对象)中的的module包路径配置得到moduleLibDirArray(需要加载的模块路径:系统模块+用户模块), 每个模块独立加载。

for (final File moduleLibDir : moduleLibDirArray) {
	// 用户模块加载目录,加载用户模块目录下的所有模块
	// 对模块访问权限进行校验
	if (moduleLibDir.exists() && moduleLibDir.canRead()) {
		new ModuleLibLoader(moduleLibDir, cfg.getLaunchMode())
				.load(new InnerModuleJarLoadCallback(), new InnerModuleLoadCallback());
	} 
}  

强实现

参考

https://blog.csdn.net/weixin_37512224/article/details/108226345

猜你喜欢

转载自blog.csdn.net/weixin_36996888/article/details/109098779
今日推荐