JVM-Sandbox 流量回放
type
status
date
slug
summary
tags
category
difficulty
icon
password
项目介绍
JVM-SandBox流量回放平台是我在货拉拉参与开发与设计的项目。该平台具备无侵入式录制、回放、快速扩展API、mock数据和code diff等功能,可应用于业务回归测试、问题排查、压力测试、业务监控以及全链路追踪等多种场景。
项目架构

该系统采用三层架构设计:平台产品层、服务层和基础能力层。平台产品层承载核心业务功能,包含录制服务、回放服务及对比服务等关键组件;服务层负责插件管理和流量中心的运维管理;基础能力层则由JVM-Sandbox和JVM-Sandbox-Repeater提供技术支撑,实现JVM字节码增强以及流量录制回放等底层功能。
录制流程

流量回放平台采用以下系统化流程进行录制操作:
- 启动实时录制会话
- 评估并获取接口采样比例
- 执行流量录制与数据过滤
- 需要过滤噪音:
- 将流量定向至指定版本目标服务
- 系统化处理回放数据
- 执行噪音数据分析
- 持久化存储噪音信息
- 不需要过滤噪音,系统将生成录制报告
- 完成录制流程
录制流程的具体实现
在整个录制流程中,主要是由
DefaultEventListener
类实现的。DefaultEventListener
实现了sandbox中EventListener
类的接口onEvent
,是用来处理触发事件的类。在repeater中,不同的插件可以自定义自己的EventListener
来进行各自特殊的事件处理。
- 在事件处理中首先要经过四个事件过滤:
- event过滤;针对单个listener,只处理top的事件。
- 进行基础过滤,主要根据系统熔断、降级进行判断。
- 执行采样计算(只有 entrance 插件负责计算采样,子调用插件不计算)
- 判断是否是插件调用处理器设置为忽略的事件
- 初始化追踪器,当经过event过滤后,就会进行追踪起初始化
- Before事件处理
- 判断当前流量是否为回放流量,若为回放流量则调用
processor.doMock
方法执行 Mock,不执行后续操作。
- 若当前流量非回放流量,则基于当前获取到的信息拼接
Invocation
实例。其中会调用插件调用处理器的assembleRequest、assembleResponse、assembleThrowable、assembleIdentity
方法进行请求参数、返回结果、抛出异常、调用标识的拼接。
- 根据插件调用处理器的设定,判断是否需要进行
Invocation
中的request、response、throwable
参数的序列化。
- 将当前事件的
Invocation
信息存放到录制缓存中。
- Return 事件处理/ Throw 事件处理
return 事件与 throw 事件的处理逻辑基本一致。
- 判断当前流量是否为回放流量,若为回放流量则不执行后续操作。
- 从录制缓存中获取对应的
Invocation
实例,如果获取失败则打印失败日志,不执行后续操作
Invocation
实例获取成功后,调用插件调用处理器的assembleResponse
或者assembleThrowable
方法将reponse
或者throwable
信息设置到Invocation
实例中。并设定Invocation
的结束时间。
- 回调调用监听器
InvocationListener
的onInvocation
方法,判断Invocation
是否是一个入口调用,如果是则调用消息投递器的broadcastRecord
将录制记录序列化后上传给repeater-console
。如果不是则当做子调用保存到录制缓存中。
回放流程

回放流程的具体执行步骤如下所述:
- 系统初始化回放任务并检索相关流量数据
- 依据获取的流量数据执行回放操作
- 进行噪音评估分析:
- 需要降噪:
- 将流量定向至目标服务环境
- 对回放结果进行系统化处理
- 根据流量查询噪音
- 执行噪音数据分析与对比验证
- 不需要降噪:
- 直接将流量转发至目标服务
- 系统化处理回放结果数据
- 生成规范化的回放报告
- 完成流量数据更新,结束回放任务
回放流程具体实现
回放流程,是从用户通过请求 repeater-console 的回放接口开始的,然后 repeater-console 通过调用 repeater 提供的回放任务接收接口下发回放任务。repeater 在执行回放任务的过程中,会通过根据录制记录的信息,构造相同的请求,对被挂载的任务进行请求,并跟踪回放请求的处理流程,以便记录回放结果以及执行 mock 动作。
- 回放请求的处理与回放任务的分发
- 请求处理:在
RepeaterModule#repeat
方法中接收外部回放请求,校验参数后将其拼接成RepeatEvent
发布到内部EventBus
。RepeatSubscribeSupporter
捕获该事件,反序列化参数,获取录制记录信息并从 repeater-console 拉取详情。获取成功后用DefaultFlowDispatcher
分发,失败则打印日志结束任务。
- 任务分发:
DefaultFlowDispatcher#dispatch
方法校验回放任务信息,通过校验的任务初始化回放上下文并存入回放缓存,再根据入口调用类型获取对应的Repeater
进行回放处理。
- 回放器的回放触发与回放结果记录
- 请求触发:不同类型的
Repeater
(如JavaRepeater
和HttpRepeater
)实现executeRepeat
方法触发回放请求。JavaRepeater
从回放上下文获取入口调用记录,通过反射调用方法触发回放;HttpRepeater
获取入口调用记录并转成HttpInvication
类型,组装请求后发起http
请求。
- 结果记录:在
AbstractRepeater#repeat
方法中,执行回放任务时初始化RepeaterModel
实例,跟踪线程并触发请求获取结果,停止跟踪后将结果和状态信息保存到实例,最后通过消息投递器将结果序列化上传到 repeater-console 保存。
- 回放过程中的事件处理
- throw/return 事件不做处理
- before 事件直接交给插件调用处理器执行 mock,只对子调用事件执行 mock。
如何进行降噪?
降噪模型

在流量数据录制完成后,系统需要建立三个独立的服务环境:目标测试版本、基准版本(用于对比分析)以及基准版本的镜像环境。通过对这三个环境进行系统化的流量对比分析,首先通过基准版本与其镜像环境的对比确定系统固有噪声,随后将目标测试版本与基准版本进行比对以识别差异点,最后通过剔除已识别的系统噪声来实现精确的降噪处理。此流程设计确保了数据分析的精确性,减少无关因素的干扰,显著提升了回放结果的可靠度。
什么是JVM-Sandbox?
JVM-Sandbox是一款JVM沙箱容器工具,可以在不重启,不侵入目标 jvm 的前提下对目标方法进行代码增强。它主要用来拦截、监控和修改方法调用,帮助开发人员进行性能检测、问题诊断和安全检查等工作。
JVM-Sandbox架构设计
JVM-SANDBOX 分为多个子项目,其中 agent,core,spy 是主程序库。agent:沙箱启动的代理,core:沙箱内核,spy:沙箱间谍类(代码增强的埋点类)。

- 模块控制管理:负责管理 sandbox 自身模块以及使用者自定义模块,例如模块的加载,激活,冻结,卸载
- 事件监听处理:用户自定义模块实现 Event 接口对增强的事件进行自定义处理,等待事件分发处理器的触发。
- 沙箱事件分发处理器:对目标方法增强后会对目标方法追加三个环节,分别为方法调用前 BEFORE、调用后 RETURN、调用抛异常 THROWS、当代码执行到这三个环节时则会由分发器分配到对应的事件监听执行器中执行。
- 代码编织框架:通过 ASM 框架依托于 JVMTI 对目标方法进行字节码修改,从而完成代码增强的能力。
- 检索过滤器:当用户对目标方法创建增强事件时,沙箱会对目标 jvm 中的目标类和方法进行匹配以及过滤。匹配到用户设定目标类和方法进行增强,过滤掉 sandbox 内部的类以及 jvm 认为不可修改的类。
- 加载类检索: 获取到需要增强的类集合依赖检索过滤器模块
- HTTP 服务器:通过 http 协议与客户端进行通信(sandbox.sh 即可理解为客户端)本质是通过 curl 命令下发指令到沙箱的 http 服务器。例如模块的加载,激活,冻结,卸载等指令
如何启动JVM-SandBox?
JVM-Sandbox的启动方式有两种,都是通过java agent实现的。
- 在启动脚本中增加 -javaagent 参数这种方式会伴随着 JVM 一起启动,这种启动方式需要实现 premain 方法。优点是如果 agent 启动需要加载大量的类,随着 jvm 启动时直接加载不会导致 JVM 在运行时卡顿或者 CPU 抖动,缺点是不够灵活。
- 利用 Attach API 在 JVM 运行时不需要重启的情况下即可完成挂载,这种启动方式 agent 需要实现 agentmain 方法,优点是即插即用,不需要重启服务,缺点是如果 agent 启动需要加载大量的类可能会导致目标 jvm 出现卡顿,cpu 抖动等情况。
什么是java agent?如何实现?
java agent 是 JVM TI 接口的一种实现。
实现 java agent 需要实现 premain 或者 agentmain方法,这两个方法的参数都包含String字符串 featureString 和 Instrumentation 的实例对象。
- String featureString 是挂载时传递过来的参数 如 sandbox 路径地址,token,namspace(租户)等
- Instrumentation 是 JVM 提供的可以在运行时动态修改已加载类的基础库,获取 Instrumentation 实例只能通过 premain 或者 agentmian 方法参数中获取。
- 将两个参数传入install方法中,无论是哪种方式挂载 agent ,核心都在 install 方法中,install 方法是开始加载 agent 的业务逻辑
描述一下加载agent的业务逻辑

在 install 方法中完成对 agent 的初始化,在初始化的过程中主要是利用了双亲委派模型,使用了spy间谍类和自定义的 SandboxClassLoader 沙箱类加载器进行加载,实现沙箱内部类与业务类隔离。
在sandbox 的世界观中,任何一个java方法的调用都可以被分解为before、return、throws三个环节。由此在三个环节上引申出对应环节的事件探测和流程控制机制。
1. Spy 间谍类
在 install 中首先会利用 Instrumentation 实例将 sandbox-spy.jar 添加到 BootstrapClassLoader 的搜索范围内。
Spy 间谍类就是实现了 before,return,throws 等钩子函数。当将 Spy 间谍类埋点到业务代码中时,触发类加载机制时利用双亲委派模型则可以层层向上查找,在BootstrapClassLoader 中就可以将 Spy 间谍类正确加载,而在 Spy 的间谍类中内置了沙箱的 SpyHandler。这样就完成了目标类和沙箱内核的通讯。
这里将sandbox-spy.jar 注入 BootstrapClassLoader 的目的是为了利用双亲委派机制,使得spy间谍类一定可以被正确加载。
2. SandBoxClassLoader
在将 Spy 间谍类追加到 BootstrapClassLoader 中后创建 SandBoxClassLoader,目的是使用自定义的 classLoader 可以尽量减少对目标 JVM 的侵入。
在自定义的 SandboxClassLoader 中会破坏双亲委派模型,首先让自身加载,如果自身加载失败后再向上委托加载。这样做的目的是因为 SandboxClassLoader 只会加载沙箱 jar 文件的类,而这些 jia 文件路径并不在目标的 JVM 的 ClasssLoader 可搜索的路径上,所以向上委托加载无任何意义,破坏掉双亲委派模型,优先自身加载性能更好。
在创建完 SandboxClassLoader 后则会利用 SandboxClassLoader 加载 core.jar 中的代理类,来初始化 HTTP 服务以及加载所有模块。
总结:
首先将 spy 间谍类追加到 BootstrapClassLoader 中为后面代码增强做准备,最终目的是让目标 JVM 业务类可以和沙箱进行通讯,然后创建自定义的 SandboxClassLoader 用来加载沙箱内部类从而实现类隔离,最后启动 HttpServer 并初始化对应的 Servlet,启动完成后加载并初始化所有 module

什么是JVM字节码增强?
字节码文件是由十六进制值组成,包括:魔数、版本号、常量池、访问标志、当前类索引、父类索引、接口索引、字段表、方法表、附加属性。
JVM字节码增强主要是通过ASM框架依托于JVMTI对目标方法进行字节码修改,从而完成代码增强对能力。
如何实现JVM字节码增强?
- 实现JVMTI接口:
- JVMTI是JVM提供的工具接口,用于对JVM进行操作
- 通过接口注册各种事件钩子,在JVM事件触发时同时触发预定义的钩子
- 类文件加载流程:
- 当JVM加载类文件时,会触发ClassFileLoadHook事件
- 这会触发Instrumentation类库中的ClassFileTransformer的transform方法进行字节码转换
- 字节码转换过程:
- 在ClassFileTransformer的transform方法中返回转换后的字节码数组
- 字节码动态生成和修改
- 使用ASM框架对字节码进行动态生成和修改
这种技术在JVM-Sandbox中得到了应用,使其能够在不重启和不侵入目标JVM的前提下,实现对目标方法的代码增强。
什么是Instrumentation?
Instrumentation 是 JVM 提供的可以在运行时动态修改已加载类的基础库。java agent 是 JVM TI 接口的一种实现,Instrumentation 实例只能通过 agent 的 premain 或者 agentmian 方法的参数中获取。
在 Instrumentation 接口中提供了多个 api 用来管理和操作字节码:
- addTransformer:注册字节码转换器,当注册一个字节码转换器后,所有的类加载都会经过字节码转换器进行处理。
- retransformClasses 重新对 JVM 已加载的类进行字节码转换
- removeTransformer 删除已注册的字节码转换器,删除后新加载的类不会再经过字节码转换器处理,但是已经“增强”过的类还是会继续保留
ASM框架是什么?如何进行字节码生成?
ASM 是一个通用的 Java 字节码操作和分析框架。它能够以二进制形式修改已有的类或是动态生成类。
ASM 提供了一些常见的字节码转换和分析算法,从中可以构建定制的复杂转换和代码分析工具;几个核心的类:
- ClassReader:此类主要功能是读取字节码文件,然后把读取的数据通知ClassVisitor;
- ClassVisitor:用于生成和转换编译的抽象类,接收ClassReader 发出对 method 的访问请求,并且替换为另一个自定义的MethodVisitor
- ClassWriter:其继承于 ClassVisitor,主要用来生成类;
执行过程遵循一个结构化的流程:首先载入原始Class文件,随后通过访问者模式系统地遍历所有组件元素,在遍历过程中实施必要的元素转换,最终输出经过转换的字节码byte数组。

- 作者:Episkey
- 链接:https://episkey.top/article/JVM-Sandbox
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。