AREX 启动过程
通用 Java Agent 的启动过程
Java Agent 是一种 Java 应用程序,它能够在 Java 应用程序启动时动静地注入到 JVM 中,并在利用程序运行时监督和批改应用程序的行为。Java Agent 通常用于性能剖析、代码覆盖率、安全检查等方面。
以下是 Java Agent 的启动过程:
- 编写 Java Agent 程序,实现
premain
办法。premain
办法是 Java Agent 的入口办法,它会在 Java 应用程序启动时被调用。在premain
办法中,能够进行一些初始化操作,如设置代理、加载配置文件等。 - 将 Java Agent 打包成 jar 文件,并在 MANIFEST.MF 文件中指定
Premain-Class
属性,该属性指定了 Java Agent 的入口类。 在启动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 文件门路。
- 当 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.lang
和java.util
等)的非凡类加载器。通过调用appendToBootstrapClassLoaderSearch
办法,能够将自定义的类库增加到Bootstrap ClassLoader的搜寻门路中,从而使得Java应用程序可能应用这些自定义的类库。
如要依据 class 对象或者 jar 包的实现,获取一个类所在的 jar 包,能够依照以下步骤进行:
- 获取该类的
Class
对象。 - 调用
Class
对象的getProtectionDomain()
办法获取该类的ProtectionDomain
对象。 - 调用
ProtectionDomain
对象的getCodeSource()
办法获取该类的CodeSource
对象。 - 调用
CodeSource
对象的getLocation()
办法获取该类所在的jar包的URL。 - 通过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()
函数执行以下操作:
- 初始化
TraceContextManager
,生成IDGenerator
用于生成TransactionID
。 - 初始化
installSerializer
。 - 初始化
RecordLimiter
,设置录制频率限度。 ConfigService
加载代理配置,包含设置调试模式、动静类配置、排除操作配置、Dubbo回放阈值、记录速率配置等。- 初始化数据收集器,依据运行模式进行判断,并启动数据收集器。
- 再次从服务器获取代理配置,进行三次重试,而后解析配置并进行再次设置和更新(留神:存在一个BUG,第二次从服务器获取配置后,并没有看到Dubbo的回放阈值失去更新)。
在BaseAgentInstaller
类的install()
函数中,调用了名为transform()
的形象函数。实际上,这个形象函数的具体实现在InstrumentationInstaller
类的transform()
函数中。
通过这些配置和操作,ArexJavaAgent
类将被作为Agent的入口点,在Java应用程序启动时被加载,并对Bootstrap ClassLoader进行扩大,使得应用程序可能应用自定义的类库。
步骤四
在 InstrumentationInstaller 类的 transform() 函数中实现了对指标应用程序的代码注入操作。
- 通过 getAgentBuilder() 获取了 ByteBuddy 的 AgentBuilder。
- 通过 SPI 函数获取了所有标识为 ModuleInstrumentation.class 的类的列表,这些类应用了 com.google.auto.service 注解 @AutoService(ModuleInstrumentation.class)。
- 依据上文获取的List, 一一调用 InstallModule(),即应用步骤 6.a 中获取的 AgentBuilder 和 ModuleInstrumentation 注册模块。
- 在 ModuleInstrumentation 类中获取了 TypeInstrumentation 的列表,针对每个 TypeInstrumentation,找到其对应的 MethodInstrumentation 列表。
- 对于每个 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 注入代码三要素
- ModuleInstrumentation: FilterModuleInstrumentationV3
- TypeInstrumentation: FilterInstrumentationV3
- 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())); }
录制回放的步骤
- 改写 javax.servlet.Filter 类的 doFilter(request, response) 函数。
在函数入口处(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。
在函数出口处(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 注入代码三要素:
- ModuleInstrumentation: NettyModuleInstrumentation
- TypeInstrumentation: ChannelPipelineInstrumentation
- 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();