AREX 启动过程

通用 Java Agent 的启动过程

Java Agent 是一种 Java 应用程序,它能够在 Java 应用程序启动时动静地注入到 JVM 中,并在利用程序运行时监督和批改应用程序的行为。Java Agent 通常用于性能剖析、代码覆盖率、安全检查等方面。

以下是 Java Agent 的启动过程:

  1. 编写 Java Agent 程序,实现 premain 办法。premain 办法是 Java Agent 的入口办法,它会在 Java 应用程序启动时被调用。在 premain 办法中,能够进行一些初始化操作,如设置代理、加载配置文件等。
  2. 将 Java Agent 打包成 jar 文件,并在 MANIFEST.MF 文件中指定 Premain-Class 属性,该属性指定了 Java Agent 的入口类。
  3. 在启动Java应用程序时,通过 -javaagent 参数指定 Java Agent 的 jar 文件门路。例如:

    java -javaagent:/path/to/agent.jar -jar myapp.jar

    在下面的命令中,/path/to/agent.jar 是 Java Agent 的 jar 文件门路,myapp.jar 是 Java 应用程序的 jar 文件门路。

  4. 当 Java 应用程序启动时,JVM 会加载 Java Agent 的 jar 文件,并调用 premain 办法。在 premain 办法中,Java Agent 能够应用 Java Instrumentation API 来批改 Java 应用程序的字节码,实现对应用程序的监督和批改。

AREX 源码视角的启动过程

步骤一

arex-agent module pom.xml文件中,通过配置manifestEntries,将Premain-Class属性设置为io.arex.agent.ArexJavaAgent。这意味着在构建arex-agent.jar时,将在manifest文件中指定ArexJavaAgent类作为 Agent 的入口点。

步骤二

ArexJavaAgent类中,实现了premain办法作为Agent的入口办法。在premain办法中,它调用了agentmain办法。在agentmain办法中,进一步调用了init(Instrumentation inst, String agentArgs)函数。这个函数承受一个Instrumentation对象和一个字符串参数agentArgs

步骤三

init函数中,有两个重要的操作:installBootstrapJar()AgentInitializer.initialize()

installBootstrapJar()

installBootstrapJar()函数依据AgentInitializer.class找到其所在的 jar 包,并通过调用inst.appendToBootstrapClassLoaderSearch(jar)将其增加到Bootstrap ClassLoader的搜寻门路中。Bootstrap ClassLoader是Java虚拟机中负责加载外围类库(如java.langjava.util等)的非凡类加载器。通过调用appendToBootstrapClassLoaderSearch办法,能够将自定义的类库增加到Bootstrap ClassLoader的搜寻门路中,从而使得Java应用程序可能应用这些自定义的类库。

如要依据 class 对象或者 jar 包的实现,获取一个类所在的 jar 包,能够依照以下步骤进行:

  1. 获取该类的Class对象。
  2. 调用Class对象的getProtectionDomain()办法获取该类的ProtectionDomain对象。
  3. 调用ProtectionDomain对象的getCodeSource()办法获取该类的CodeSource对象。
  4. 调用CodeSource对象的getLocation()办法获取该类所在的jar包的URL。
  5. 通过URL对象的getFile()办法获取该jar包的门路。
AgentInitializer.initialize()

AgentInitializer.initialize()函数是依据ArexJavaAgent.class找到其所在的jar包(AgentInitializer.java文件),而后设置arex.agent.jar.file.path变量,即代理jar包所在的目录。

接下来,它会在该目录下查找/extensions/子目录,并读取该目录下的所有jar包文件,这些文件是扩大包所在的地位。

而后,调用createAgentClassLoader(agent jar, 扩大包.jar)函数,创立一个AgentClassLoader对象,它是AREX自定义的类加载器。应用自定义的类加载器是为了隔离,避免应用程序可能拜访AREX Agent的代码。

接着,调用createAgentInstaller()函数,该函数应用后面生成的AgentClassLoader加载类io.arex.agent.instrumentation.InstrumentationInstaller,获取其构造函数并创立实例,而后返回指向AgentInstaller接口的对象。

AdviceClassesCollector收集代理jar文件和扩大jar文件。

应用指向AgentInstaller接口的installer(后面返回的对象)调用install()函数,实际上会调用BaseAgentInstaller类的install()函数,该函数调用init(String agentArgs)进行初始化。

BaseAgentInstaller类的install()函数中,调用init()函数执行以下操作:

  1. 初始化TraceContextManager,生成IDGenerator用于生成TransactionID
  2. 初始化installSerializer
  3. 初始化RecordLimiter,设置录制频率限度。
  4. ConfigService加载代理配置,包含设置调试模式、动静类配置、排除操作配置、Dubbo回放阈值、记录速率配置等。
  5. 初始化数据收集器,依据运行模式进行判断,并启动数据收集器。
  6. 再次从服务器获取代理配置,进行三次重试,而后解析配置并进行再次设置和更新(留神:存在一个BUG,第二次从服务器获取配置后,并没有看到Dubbo的回放阈值失去更新)。

BaseAgentInstaller类的install()函数中,调用了名为transform()的形象函数。实际上,这个形象函数的具体实现在InstrumentationInstaller类的transform()函数中。

通过这些配置和操作,ArexJavaAgent类将被作为Agent的入口点,在Java应用程序启动时被加载,并对Bootstrap ClassLoader进行扩大,使得应用程序可能应用自定义的类库。

步骤四

在 InstrumentationInstaller 类的 transform() 函数中实现了对指标应用程序的代码注入操作。

  1. 通过 getAgentBuilder() 获取了 ByteBuddy 的 AgentBuilder。
  2. 通过 SPI 函数获取了所有标识为 ModuleInstrumentation.class 的类的列表,这些类应用了 com.google.auto.service 注解 @AutoService(ModuleInstrumentation.class)。
  3. 依据上文获取的List, 一一调用 InstallModule(),即应用步骤 6.a 中获取的 AgentBuilder 和 ModuleInstrumentation 注册模块。
  4. 在 ModuleInstrumentation 类中获取了 TypeInstrumentation 的列表,针对每个 TypeInstrumentation,找到其对应的 MethodInstrumentation 列表。
  5. 对于每个 MethodInstrumentation,调用 AgentBuilder.Identified 的 transform() 函数进行代码注入。

简而言之,这一步过程是实现了模块化的插桩性能。通过实现 ModuleInstrumentation 接口,能够定义须要进行代码注入的模块。在每个模块中,通过实现 TypeInstrumentation 接口能够定义须要注入代码的具体类型。而在每个类型中,通过实现 MethodInstrumentation 接口能够定义须要注入代码的具体方法。这样,AREX Agent 就能够依据这些定义,将录制和回放的代码注入到相应的办法中,实现了对应性能的记录和重放。

步骤五

实现所有类的注入,实现 AREX 的启动后 AREX 开始运行。

AREX 录制回放

录制回放概述

AREX的录制性能不仅仅是独自录制申请报文,而是将申请、应答报文以及外部调用的申请和应答一并保留下来。外围指标是将申请、应答和外部调用的申请应答一一关联起来保留。AREX采纳相似 OpenTelemetry 的 Tracing 技术,实现全链路跟踪并保留关联的 Tracing ID。

录制

录制分为入口录制和外部调用录制两局部。入口申请中没有Tracing ID,须要生成惟一的Tracing ID,并记录下来。入口录制保留申请和Tracing ID。外部调用录制保留Tracing ID和外部调用的申请、应答。

入口申请的响应报文也须要记录,即入口调用的应答和Tracing ID(这里提到的 Tracing ID 在后文中称为 AREX Record ID)。

回放

回放过程中,入口申请中蕴含AREX-Replay-ID和Record ID的报文。依据Record ID从数据库中获取相应的应答,并返回给调用方。同时,关联Replay ID记录回放过程中的数据并保留到数据库。

在外部调用过程中,如果检测到以后处于回放状态,则依据Record ID从数据库中获取数据返回(模仿应答),并记录外部调用的申请,关联Replay ID并保留到数据库。

依据Replay ID,找到入口调用的应答报文以及外部调用的申请报文,并进行录制场景和回放场景的差别比对。

最初输入差别后果,完结回放过程。

AREX Servlet 的入口录制和回放

代码所在目录 arex-agent-java\arex-instrumentation\servlet

AREX 注入代码三要素

  1. ModuleInstrumentation: FilterModuleInstrumentationV3
  2. TypeInstrumentation: FilterInstrumentationV3
  3. MethodInstrumentation:
     @Override    public List<MethodInstrumentation> methodAdvices() {        ElementMatcher<MethodDescription> matcher = named("doFilter")                .and(takesArgument(0, named("javax.servlet.ServletRequest")))                .and(takesArgument(1, named("javax.servlet.ServletResponse")));        return Collections.singletonList(new MethodInstrumentation(matcher, FilterAdvice.class.getName()));    } 

录制回放的步骤

  1. 改写 javax.servlet.Filter 类的 doFilter(request, response) 函数。
  2. 在函数入口处(OnMethodEnter)进行改写,并获取两个参数,0 位是 request,1 位是 response。

    a. 调用 ServletAdviceHelper.onServiceEnter(),传入申请和应答。

    b. 调用 CaseEventDispatcher.onEvent(CaseEvent.ofEnterEvent()),其中包含调用了 TimeCache.remove()、TraceContextManager.remove() 和 ContextManager.overdueCleanUp()。

    c. 调用 CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent()),其中包含调用了 initContext(source) 和 initClock()。

    initContext() 函数调用设置 ArexContext,入口处会生成 TraceID。ContextManager.currentContext(true, source.getCaseId()) 中的 createIfAbsent 参数传入 True,会调用 TRACE_CONTEXT.set(messageId)。

    initClock() 函数判断以后是否处于回放状态,如果是则解析工夫并调用TimeCache.put(millis)。如果以后是录制状态(即ArexContext不为空且不处于回放状态ContextManager.needRecord()),则调用RecordMocker。

  3. 在函数出口处(OnMethodExit)进行改写,调用ServletAdviceHelper.onServiceExit()。

    调用 new ServletExtractor<>(adapter, httpServletRequest, httpServletResponse).execute() 函数。

    而后调用 doExecute(),构建 Mocker 对象,并为 Mocker 对象设置申请头、Body 和属性。同时为 Mocker 对象设置响应对象、Body 和 Type。

    如果以后处于回放状态,则回放 Mocker 数据。如果以后处于录制状态,则保留 Mocker 数据。

相似的实现形式也实用于入口录制和回放,原理相似,不再赘述。

  • 对于 Dubbo,能够在 DubboProviderExtractor 类的 onServiceEnter() 中实现。
  • 对于 Netty,能够在io.netty.channel.DefaultChannelPipeline 类中的 add 前缀函数和 replace 函数中实现。

AREX 外部调用的录制回放

代码所在目录 arex-agent-java\arex-instrumentation\netty\arex-netty-v4

AREX 注入代码三要素:

  1. ModuleInstrumentation: NettyModuleInstrumentation
  2. TypeInstrumentation: ChannelPipelineInstrumentation
  3. MethodInstrumentation:
     @Override    public List<MethodInstrumentation> methodAdvices() {        return singletonList(new MethodInstrumentation(                isMethod().and(nameStartsWith("add").or(named("replace")))                        .and(takesArgument(1, String.class))                        .and(takesArgument(2, named("io.netty.channel.ChannelHandler"))),                AddHandlerAdvice.class.getName()));    } 

录制回放的步骤

在 Java Netty 中,ChannelPipeline 是一个事件处理机制,用于解决入站和出站事件。它是Netty的外围组件之一,用于治理 ChannelHandler 的解决流程。当一个事件被触发时,它会被传递给ChannelPipeline,而后由Pipeline中的每个 ChannelHandler 顺次解决。每个 ChannelHandler 都能够对事件进行解决或者转发给下一个 ChannelHandler。addAfter办法是用于向ChannelPipeline中增加一个新的ChannelHandler,并将其插入到指定的 ChannelHandler 之后。这个办法能够用于动静地批改 ChannelPipeline 中的解决流程,以便在运行时依据须要增加或删除处理器。

在改写 io.netty.channel.DefaultChannelPipeline 类的 add 前缀函数或者 replace 函数时,咱们能够在函数 OnMethodExit 时获取以后对象的 ChannelPipeline,以及参数 1 handleNamer 和参数 2 handler。

咱们能够进行以下判断和解决:

  • 如果 handler 是HttpRequestDecoder实例,则调用RequestTracingHandler()来解决回放的数据。
  • 如果handler是HttpResponseEncoder实例,则调用ResponseTracingHandler()来解决录制的数据。
  • 如果handler是HttpServerCodec实例,则调用ServerCodecTracingHandler()来解决。HttpServerCodec是Java Netty中的一个ChannelHandler,用于将HTTP申请和响应音讯编码和解码为HTTP音讯。它实现了HTTP协定的编解码,能够将HTTP申请和响应音讯转换为字节流,以便在网络中传输。

异步拜访的解决

在Java生态系统中存在多种异步框架和类库,如Reactor、RxJava等,同时还有一些类库提供了异步拜访的实现,例如 lettuce 提供了同步和异步拜访 Redis 的形式。不同的场景通常须要不同的解决方案。

以 ApacheAsyncClient 为例,它是通过在固定运行的线程中监听响应并发动回调(Callback)来实现异步解决。在整个调用、监听和回调的过程中,须要确保多个跨线程的 Trace 传递。

在注入代码中,须要应用 FutureCallbackWrapper 中的 TraceTransmitter 来传递 Trace。具体的注入地位如下:

  • ModuleInstrumentation: SyncClientModuleInstrumentation
  • TypeInstrumentation: InternalHttpAsyncClientInstrumentation(用于异步状况)、InternalHttpClientInstrumentation
  • MethodInstrumentation: 注入到org.apache.http.impl.nio.client.InternalHttpAsyncClient类的execute函数,应用named("execute")办法进行辨认。

录制回放的步骤

在注入函数中,咱们针对org.apache.http.impl.nio.client.InternalHttpAsyncClient类的execute函数进行操作,应用函数名named("execute")来标识该函数。

首先,咱们获取execute函数的第三个参数FutureCallback,并将其赋值给AREX实现的封装类FutureCallbackWrapper的callback参数。FutureCallback接口定义了两个办法:onSuccess和onFailure。当异步操作胜利实现时,onSuccess办法将被调用,并传递异步操作的后果作为参数。当异步操作失败时,onFailure办法将被调用,并传递异样作为参数。

而后,咱们进行以下判断:

  • 如果须要进行录制,则 FutureCallbackWrapper 的封装类重写了 completed(T) 函数,在 completed 函数中保留响应数据,而后调用原始的 FutureCallback 的 completed 办法。同样地,FutureCallbackWrapper 的封装类也重写了 failed() 函数,在 failed 函数中记录响应数据,并调用原始的 FutureCallback 的 failed 办法。
  • 如果须要进行回放,则获取回放数据并将其保留在本地的 mockResult 变量中。

最初,在注入函数的出口处,如果 mockResult 变量的数据不为空,并且 callback 是 AREX 封装类的实例,那么调用封装类的 replay 函数进行回放操作。

通过以上操作,咱们在 execute 函数的入口和出口处对跨线程的 Trace 传递进行了解决,包含录制和回放性能的实现。

AREX 录制频率设置

在 ServletAdviceHelper 类的 onServiceEnter 函数中(就是 Servlet 进入的函数中),实现了 AREX 的录制频率设置。

CaseEventDispatcher.onEvent(CaseEvent.ofEnterEvent());if (shouldSkip(adapter, httpServletRequest)) {            return null;}

首先,依据报文头和配置进行录制判断:

  • 如果申请报文头中有caseID字段,查问配置项arex.disable.replay,如果该配置项的值为true,则间接跳过录制。
  • 如果申请报文头中存在arex-force-record字段,并且该字段的值为true,则不能跳过录制。
  • 如果申请报文头中存在arex-replay-warm-up字段,并且该字段的值为true,则跳过录制。

接下来,会解析申请报文:

  • 如果申请URL为空,则跳过录制。
  • 如果申请URL在配置中的录制疏忽列表中,则跳过录制。

接着,会调用Config类的invalidRecord办法进行录制有效性的查看:

  • 如果配置处于debug状态,则不能跳过录制,间接返回false。
  • 如果配置的录制速率小于0,则跳过录制。

最初,依据申请的门路和录制速率判断是否须要跳过录制,这里应用 com.google.common.util.concurrent.RateLimiter 类的 acquire 函数。RateLimiter 是 Google Guava 库中的一个类,用于限度操作的频率。它能够用于管制某个操作在肯定工夫内最多能执行多少次,或者在肯定工夫内最多能执行多少次操作。应用RateLimiter类,须要先创立一个RateLimiter对象,并指定它的速率限度。而后,咱们能够应用 acquire() 办法获取一个许可证,示意能够执行一个操作。

  • 如果以后速率曾经达到了限度,acquire 函数会阻塞期待,直到可能获取到许可证为止。
  • 如果可能获取到许可证,则不跳过录制。

AREX 代码隔离

在Java虚拟机中,判断两个类是否相等时,不仅会比拟它们的全限定名,还会比拟它们的加载器。如果两个类的全限定名雷同,但加载它们的ClassLoader不同,Java虚构机会认为这是两个不同的类。

这种设计有助于保障Java虚拟机的安全性和隔离性。不同的ClassLoader能够加载同一个类,但它们加载的类是互相独立的,相互不可见的。这样能够防止不同应用程序或模块之间的类抵触和烦扰。

在 AREX 中,波及的 ClassLoader 有以下几种:

  • arex-agent:由 AppClassLoader 加载,用于加载 AREX Agent 的外围组件。
  • arex-agent-bootstrap:由疏导类加载器(Bootstrap ClassLoader)加载,用于加载 AREX Agent 的疏导类。
  • arex-agent-core:由 AgentClassLoader 加载,这是 AREX 自定义的 ClassLoader,负责加载 arex-agent-core 等 jar。
  • arex-instrumentation:由 UserClassLoader 加载,用于加载 AREX 的 Instrumentation、Module 和 Advice 等组件。

    • XXX Instrumentation & Module & Advice:由 AgentClassLoader 加载,用于加载具体的 Instrumentation、Module 和 Advice 等实现。
  • arex-instrumentation-api:由 AgentClassLoader 加载,包含 API 和 Runtime 两局部。

    • api:由 AgentClassLoader 加载,提供给用户应用的 API。
    • runtime:由 AppClassLoader 加载,用于 AREX 运行时的一些性能。
  • arex-instrumentation-foundation`:由 AgentClassLoader 加载,用于加载 AREX 的根底性能,如后端实现等。

这些不同的 ClassLoader 之间具备隔离性,确保了各个组件的独立性和安全性。

其中:

  • AgentClassLoader:AREX 自定义的 ClassLoader。
  • Bootstrap ClassLoader: Java Instrumentation API 是 Java SE 5 中引入的一个功能强大的工具,它容许在运行时批改Java类的行为。

    其中,Instrumentation类是Java Instrumentation API的外围类之一,它提供了一些办法来监测和批改Java应用程序的运行状态。 其中,appendToBootstrapClassLoaderSearch办法是Instrumentation类中的一个办法,它的作用是将指定的jar文件增加到Bootstrap ClassLoader的搜寻门路中。

    Bootstrap ClassLoader是Java虚拟机中的一个非凡的类加载器,它负责加载Java运行时环境中的外围类库,如java.lang和java.util等。

    通过调用appendToBootstrapClassLoaderSearch办法,能够将自定义的类库增加到Bootstrap ClassLoader的搜寻门路中,从而使得Java应用程序能够应用这些自定义的类库。 须要留神的是,因为appendToBootstrapClassLoaderSearch 办法会批改 Java 虚拟机的运行状态,因而只有具备足够权限的用户能力调用该办法。

  • AppClassLoader是Java应用程序默认的ClassLoader,它负责加载应用程序的类。AppClassLoader会从CLASSPATH环境变量或者零碎属性java.class.path指定的门路中加载类文件。

    如果须要加载的类不在AppClassLoader的搜寻门路中,它会委托给父ClassLoader进行加载,直到BootstrapClassLoader为止。

  • UserClassLoader 用户自定义ClassLoader,如SPIUtil类中Load办法如下获取ClassLoader加载

    ClassLoader cl = Thread.currentThread().getContextClassLoader();