利用 Java Agent 和 Instrument 技术录制线上流量

时间:2024-10-08 09:32:12

  • 利用 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,那怎么办呢?

ClassLoader 关系图

最初想到,可以在 Agent 里带上 servlet.api,让 AppClassLoader 也加载一遍 HttpServletRequest,这样做有两个问题:

  1. 重复加载类,导致系统臃肿不合理。
  2. 是 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中读取不到的做法