简介

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

沙箱的特性

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

沙箱常见应用场景

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

JVM-SANDBOX还能帮助你做很多很多,取决于你的脑洞有多大了。

实时无侵入AOP框架

在常见的AOP框架实现方案中,有静态编织和动态编织两种。

  1. 静态编织:静态编织发生在字节码生成时根据一定框架的规则提前将AOP字节码插入到目标类和方法中,实现AOP;
  2. 动态编织:动态编织则允许在JVM运行过程中完成指定方法的AOP字节码增强.常见的动态编织方案大多采用重命名原有方法,再新建一个同签名的方法来做代理的工作模式来完成AOP的功能(常见的实现方案如CgLib),但这种方式存在一些应用边界:
  • 侵入性:对被代理的目标类需要进行侵入式改造。比如:在Spring中必须是托管于Spring容器中的Bean。
  • 固化性:目标代理方法在启动之后即固化,无法重新对一个已有方法进行AOP增强。

要解决​​无侵入​​的特性需要AOP框架具备 在运行时完成目标方法的增强和替换。在JDK的规范中运行期重定义一个类必须准循以下原则:

  1. 不允许新增、修改和删除成员变量
  2. 不允许新增和删除方法
  3. 不允许修改方法签名

JVM-SANDBOX属于基于​​Instrumentation​​的动态编织类的AOP框架,通过精心构造了字节码增强逻辑,使得沙箱的模块能在不违反JDK约束情况下实现对目标应用方法的无侵入运行时AOP拦截

源码

源码下载-一定要切换到稳定版本:
​​​https://github.com/alibaba/jvm-sandbox​​​ 也可以直接下载打包好的版本
​https://github.com/alibaba/jvm-sandbox/releases/tag/1.3.3​

安装

源码包导入开发工具后切换到bin目录:

Sandbox 入门(打包、安装、启动、调试、日志)_sandbox

  1. 注释掉不需要的插件:
<!-- 为了发布到MAVEN中央仓库而用的插件 -->
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>luanjia-ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
  1. 打包
    打包前先将​​​sandbox-packages.sh​​脚本中执行单测的代码调过,修改如下:
# maven package the sandbox
mvn clean cobertura:cobertura package -Dmaven.test.skip=true -f ../pom.xml \
|| exit_on_err 1 "package sandbox failed."

修改完成后执行脚本​​./sandbox-packages.sh ​​​,脚本执行后会在工程​​target​​目录下生成安装好的文件,如图:

Sandbox 入门(打包、安装、启动、调试、日志)_sandbox_02

​sandbox-stable-bin.zip​​就是生成好的安装文件,如果需要在哪里安装直接将这个包在那个服务器解压即可。

脚本会将打包后的文件拷贝到安装目录:

# copy jar to TARGET_DIR
cp ../sandbox-core/target/sandbox-core-*-jar-with-dependencies.jar ${SANDBOX_TARGET_DIR}/lib/sandbox-core.jar \
&& cp ../sandbox-agent/target/sandbox-agent-*-jar-with-dependencies.jar ${SANDBOX_TARGET_DIR}/lib/sandbox-agent.jar \
&& cp ../sandbox-spy/target/sandbox-spy-*-jar-with-dependencies.jar ${SANDBOX_TARGET_DIR}/lib/sandbox-spy.jar \
&& cp sandbox-logback.xml ${SANDBOX_TARGET_DIR}/cfg/sandbox-logback.xml \
&& cp sandbox.properties ${SANDBOX_TARGET_DIR}/cfg/sandbox.properties \
&& cp sandbox.sh ${SANDBOX_TARGET_DIR}/bin/sandbox.sh \
&& cp install-local.sh ${SANDBOX_TARGET_DIR}/install-local.sh

目录结构

./sandbox/
+--bin/
| +--sandbox.sh
|
+--cfg/
| +--sandbox-logback.xml
| +--sandbox.properties
| `--version
|
+--lib/
| +--sandbox-agent.jar
| +--sandbox-core.jar
| `--sandbox-spy.jar
|
+--provider/
|. `--sandbox-mgr-provider.jar
|
`--module/
`--sandbox-mgr-module.jar

详细说明可以参考官网:https://github.com/alibaba/jvm-sandbox/wiki/CONFIG

安装

方式一直接拷贝打包好的安装文件:

直接将上面生成的包​​sandbox-stable-bin.zip​​拷贝到安装目录,直接解压即可。

方式二直接网上下载release包:

# 下载最新版本的JVM-SANDBOX
wget http://ompc.oss-cn-hangzhou.aliyuncs.com/jvm-sandbox/release/sandbox-stable-bin.zip

# 解压
unzip sandbox-stable-bin.zip

方式三执行install-local脚本。

不能直接执行工程​​bin​​​目录下的​​install-local.sh​​​脚本,需要执行我们上面打包好的​​target​​中的脚本,如图:

Sandbox 入门(打包、安装、启动、调试、日志)_jvm_03

执行安装脚本(本地调试用这种方式):

cd ../target/sandbox
./install-local.sh -p /Users/admin/Documents/

-p:表示安装目录,默认安装在​​${HOME}/.opt​​目录下。

启动

sandbox启动有两种方式:​​ATTACH​​​和​​AGENT​​。

ATTACH方式启动

即插即用的启动模式,可以在不重启目标JVM的情况下完成沙箱的植入。原理和GREYS、BTrace类似,利用了JVM的Attach机制实现。

假设目标JVM进程号为’33342’,进入沙箱执行脚本

cd {安装目录}/sandbox/bin

挂载目标应用

./sandbox.sh -p 33342

挂载成功后会提示

./sandbox.sh -p 33342
NAMESPACE : default
VERSION : 1.2.0
MODE : ATTACH
SERVER_ADDR : 0.0.0.0
SERVER_PORT : 55756
UNSAFE_SUPPORT : ENABLE
SANDBOX_HOME : /Users/vlinux/opt/sandbox
SYSTEM_MODULE_LIB : /Users/vlinux/opt/sandbox/module
USER_MODULE_LIB : ~/.sandbox-module;
SYSTEM_PROVIDER_LIB : /Users/vlinux/opt/sandbox/provider
EVENT_POOL_SUPPORT : DISABLE

卸载沙箱

./sandbox.sh -p 33342 -S
jvm-sandbox[default] shutdown finished.

​./sandbox.sh​​命令参数详解: https://github.com/alibaba/jvm-sandbox/wiki/CONFIG

AGENT方式启动

有些时候我们需要沙箱工作在应用代码加载之前,或者一次性渲染大量的类、加载大量的模块,此时如果用ATTACH方式加载,可能会引起目标JVM的卡顿或停顿(GC),这就需要启用到AGENT的启动方式。

假设SANDBOX被安装在了/Users/luanjia/pe/sandbox,需要在JVM启动参数中增加上

-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -javaagent:/Users/luanjia/pe/sandbox/lib/sandbox-agent.jar

这样沙箱将会伴随着JVM启动而主动启动并加载对应的沙箱模块。

对比Agent和ATTACH,Agent(premain)是JDK5就提供的,主要提供JVM启动时的代理加载功能,灵活性和易用性欠佳;ATTACH(agentmain)则是JDK6提出的,能够支持JVM运行时的代理启动,灵活性和易用性好,给线上调试等提供了底层能力支持。

调试

​sandbox​​的调试需要使用远程调试,在目标应用启动时加上远程调试参数(目标应用必须以非DEBUG模式启动):

-Xdebug
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000

Sandbox 入门(打包、安装、启动、调试、日志)_安装_04

使用IDEA的远程调试功能,并加上如下配置:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5050

Sandbox 入门(打包、安装、启动、调试、日志)_安装_05

​sandbox​​​以​​ATTACH​​方式启动即可:

./sandbox.sh -p `ps -ef | grep java | grep 'com.alibaba.repeater.console.start.Application' | grep -v grep | awk '{print $2}'

Sandbox 入门(打包、安装、启动、调试、日志)_sandbox_06

日志

Sandbox的日志

​sandbox​​​的日志配置是在​​cfg​​​下的​​sandbox-logback.xml​​​文件。这个文件会在沙箱启动时被加载,通过配置我们可以找到日志文件位置为​​${user.home}/logs/sandbox/sandbox.log.%d{yyyy-MM-dd}​​。

<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10000">

<appender name="SANDBOX-FILE-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/sandbox.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/sandbox.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %SANDBOX_NAMESPACE %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<root level="info">
<appender-ref ref="SANDBOX-FILE-APPENDER"/>
</root>

</configuration>

module 日志

​module​​​本身是一个单独项目(jar),所以​​module​​​的日志是由项目本身控制,以源码中的​​sandbox-debug-module​​​为例,他其实是一个module编写的是示例工程,他的日志可以直接查看项目中的​​logback.xml​​文件。

<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="10000">

<!-- DEBUG模块:Servlet监控日志 -->
<logger name="DEBUG-SERVLET-ACCESS" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/servlet-access.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/servlet-access.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>

<!-- DEBUG模块:Exception监控日志 -->
<logger name="DEBUG-EXCEPTION-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/exception-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/exception-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>

<!-- DEBUG模块:JDBC监控日志 -->
<logger name="DEBUG-JDBC-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/jdbc-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/jdbc-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>

<!-- DEBUG模块:JDBC监控日志 -->
<logger name="DEBUG-SPRING-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/spring-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/spring-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>

<!-- DEBUG模块默认日志 -->
<root level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/debug.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</root>

<!-- DEBUG模块:LifeCycle监控日志 -->
<logger name="DEBUG-LIFECYCLE-LOGGER" level="INFO">
<appender class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/logs/sandbox/debug/lifecycle-monitor.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${user.home}/logs/sandbox/debug/lifecycle-monitor.log.%d{yyyy-MM-dd}</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
</logger>
</configuration>