-
利用 Java Agent 和 Instrument 技术录制线上流量
- Java Instrument 技术
-
遇到的难题
- 自动打包依赖
- ClassNotFound 问题
- HttpServletRequest body 只能 get 一次
利用 Java Agent 和 Instrument 技术录制线上流量
在做性能压测的时候,需要先准备好压测请求数据,可以采用人工制造的方式,也可以在线上录制流量,线下回放。这里,我们使用 Java Agent 和 Instrument 技术,做了一个代理 Agent 实现了不修改代码即可录制线上请求数据的功能。
Java Instrument 技术
Java Instrument 技术怎么用这里就不重复了,网上文章很多,可以看看这篇:Java SE 6 新特性 Instrumentation 新功能。
遇到的难题
下面我们讲一下在开发 Agent 过程中遇到的难题。
自动打包依赖
录制 Agent 依赖的一些包,也要一同打包,否则在加载代理执行时会出现找不到类的问题。具体怎么做呢?可以在 加上下面的配置:
<plugin>
<groupId></groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<!-- manifestEntries 用于配置打包生成 文件所需的描述信息,如果不配置下面的内容,JVM 会找不到 Agent 类入口-->
<manifestEntries>
<Premain-Class>Agent</Premain-Class>
<Agent-Class>Agent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
ClassNotFound 问题
我们的系统基本都是用 Spring 框架开发的,在录制 HTTP 请求的时候,我们拦截 Spring 中分发请求的 类,在该方法的入口处获取 HttpServletRequest 中的数据,并输出到请求数据的存储服务里。输出数据的时候,是在 DispatcherServlet 里调用了录制 Agent 中的一个 Recorder 类的 record 方法:
public class Recorder {
private static final Logger logger = ();
public static void record(HttpServletRequest request) {
// 从 request 中获取 http body、parameterMap、cookie 并存储
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
在实际运行过程中,我们发现在执行 的方法时,总是找不到 HttpServletRequest 类。这个类应该是 tomcat 默认加载的,为什么会找不到呢?
经过一番研究后发现,问题出在 Java 的 ClassLoader 机制上。在 Java 中 ClassLoader 是多层次的父子结构,子 ClassLoader 可以使用父 ClassLoader 加载的类,但是反过来不行。具体可以看下图,Recorder 在 AppClassLoader 中,而 HttpServletRequest 在 URLClassLoader 中加载的,而 AppClassLoader 是 URLClassLoader 的 parent。所以 Recorder 看不到 HttpServletRequest,那怎么办呢?
最初想到,可以在 Agent 里带上 servlet.api,让 AppClassLoader 也加载一遍 HttpServletRequest,这样做有两个问题:
- 重复加载类,导致系统臃肿不合理。
- 是 tomcat 自带的 api,在 Agent 里带的版本往往不适合对应的 tomcat,可能导致 tomcat 启动异常。
那还能怎么办呢?这时候,我们想到了 URLClassLoader 有一个 addURL 的接口,可以添加新的类库。那么如果我们让 URLClassLoader 去加载 Recorder 类的化,也一样能够做到让 Recorder 访问 HttpServletRequest。
具体做法是,将原来的流量录制 Agent 拆成两个包,新录制 Agent 只包含 agentmain 和 instrument transform 相关类,将 Recorder 数据收集类放到另一个包 client 中。在新录制 Agent 中添加代码找到 URLClassLoader 调用 addURL 方法加载 client 包。(用 WebappClassLoader 加载 client 包也能达到目的)
HttpServletRequest body 只能 get 一次
调用 HttpServletRequest 的 getInputStream 方法读取数据后,会导致在 Controller 端再次读取时啥也读不到,这个怎么办呢?需要在读取 http body 后,再伪造一个 HttpServletRequest 向后传递,具体办法可以看看:解决在Filter中读取Request中的流后, 然后在Controller中读取不到的做法。