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

  • 利用 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 依赖的一些包,也要一同打包,否则在加载代理执行时会出现找不到类的问题。具体怎么做呢?可以在 pmo.xml 加上下面的配置:

            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-assembly-pluginartifactId>
                <configuration>
                    <archive>
                        
                        <manifestEntries>
                            <Premain-Class>AgentPremain-Class>
                            <Agent-Class>AgentAgent-Class>
                            <Can-Redefine-Classes>trueCan-Redefine-Classes>
                            <Can-Retransform-Classes>trueCan-Retransform-Classes>
                        manifestEntries>
                    archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependenciesdescriptorRef>
                    descriptorRefs>
                configuration>
                <executions>
                    <execution>
                        <id>make-assemblyid>
                        <phase>packagephase>
                        <goals>
                            <goal>singlegoal>
                        goals>
                    execution>
                executions>
            plugin>

ClassNotFound 问题

我们的系统基本都是用 Spring 框架开发的,在录制 HTTP 请求的时候,我们拦截 Spring 中分发请求的 DispatcherServlet.doDispatch 类,在该方法的入口处获取 HttpServletRequest 中的数据,并输出到请求数据的存储服务里。输出数据的时候,是在 DispatcherServlet 里调用了录制 Agent 中的一个 Recorder 类的 record 方法:

public class Recorder {
    private static final Logger logger = LoggerFactory.getLogger(Recorder.class);

    public static void record(HttpServletRequest request) {
        // 从 request 中获取 http body、parameterMap、cookie 并存储
    }
}

在实际运行过程中,我们发现在执行 Recorder.record 的方法时,总是找不到 HttpServletRequest 类。这个类应该是 tomcat 默认加载的,为什么会找不到呢?

经过一番研究后发现,问题出在 Java 的 ClassLoader 机制上。在 Java 中 ClassLoader 是多层次的父子结构,子 ClassLoader 可以使用父 ClassLoader 加载的类,但是反过来不行。具体可以看下图,Recorder 在 AppClassLoader 中,而 HttpServletRequest 在 URLClassLoader 中加载的,而 AppClassLoader 是 URLClassLoader 的 parent。所以 Recorder 看不到 HttpServletRequest,那怎么办呢?

利用 Java Agent 和 Instrument 技术录制线上流量_第1张图片

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

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

你可能感兴趣的:(Java,Java,进阶)