-
Notifications
You must be signed in to change notification settings - Fork 122
Home
[TOC]
Easeagent 是一个基于 Java Agent 实现的 APM[^apm] 探针。
[^apm]: Application Performance Management
node "Host Process" <<JVM>> {
rectangle easeagent <<java agent>> {
component "servlet" <<Plugin>>
component "jdbc" <<Plugin>>
}
}
通过对某些类方法的字节码进行修改,easeagent 能够收集程序运行时的一些信息,包括但不限于:
- 调用计数
- 调用耗时
- 调用堆栈
- 链路跟踪
- 更多^more
java agent 是一种特殊的 JAR 文件, 可以被 JVM 装载并实现对 Java 程序信息采集, 其装载方式有两种:^inst
- 使用命令行选项
-javaagent:xxx.jar
, 或者 - 使用 Attach API
com.sun.tools.attach.VirtualMachine#loadAgent
^attach.
public class JavaAgent {
public static void premain(String agentArgs, Instrumentation inst) {
// invoke this method by -javaagent option
}
public static void agentmain(String agentArgs, Instrumentation inst) {
// invoke this method by attach API
}
}
easeagent 是通过
-javaagent
选项来装载, 相反 stagemonitor 通过ByteBuddyAgent
去调用 Attach API完成装载的 .^bba
以一个 HelloWorld 程序为例:
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello world!");
System.in.read(); // wait until any key press.
}
}
尝试用两种方法装载时,其回调的时序图如下:
box "java -javaagent:xxx.jar HelloWorld"
participant JVM
participant JavaAgent
participant HelloWorld
end box
box "Attachment process"
participant VirtualMachine as VM
end box
JVM -> JavaAgent : premain(...)
JVM -> HelloWorld : main(...)
VM --> JVM : loadAgent(...)
JVM -> JavaAgent : agentmain(...)
如果有一个类 Service
:
class Service {
public void handle(...) {
...
}
}
我们可以通过 Java Agent API Instrumentation
^inst 修改 Service#handle
方法的字节码, 以实现它在每次被调用时都打印出调用耗时,改动后的字节码其等价的源码如下:
class Service {
public void handle(...) {
long begin = System.currentTimeMillis();
try {
... // origin code of this method
} finally {
long end = System.currentTimeMillis();
System.out.println("takes" + (end - begin) + " ms");
}
}
}
实际上, 我们会借助 ByteBuddy^bb 来简化这一实现过程。
JVM 通过 ClassLoader 来装载运行时需要的字节码, 其中 SystemClassLoader 是所有 java 程序中用来装载启动时classpath
指定的那些类的字节码. 举个例子,还是运行 HelloWorld :
node "java HelloWorld" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
[java.**.class]
rectangle System <<ClassLoader>> {
[HelloWorld.class]
}
}
}
HelloWorld 的字节码由 SystemClassLoader 来装载。 而 Java 标准库的字节码则是由一个特殊的 BootstrapClassLoader 来装载的。
ClassLoader 之间是 父与子 的关联关系, 当一个 ClassLoader 需要装载一个类时, 会先让其 父 尝试加载。成功后则父子共用, 反之 子 再尝试加载, 成功后 父 是不可见的。
现实中的 Java 程序中 ClassLoader 层次关系要复杂许多, 以运行在 Tomcat 中的两个 Web 应用为例:^tomcat
node "Tomcat" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
rectangle System <<ClassLoader>> {
rectangle Common <<ClassLoader>> {
rectangle WebApp2 <<ClassLoader>>
rectangle WebApp1 <<ClassLoader>>
}
}
}
}
注意 由 WebApp1 装载的类对于 WebApp2 是不可见的, 即使它们都装载了同样的类,也是彼此独立的。
The agent class will be loaded by the system class loader (see ClassLoader.getSystemClassLoader). This is the class loader which typically loads the class containing the application main method.^inst
假设我们是一个运行 Web 应用的 Tomcat 进程装载一个 Java Agent,其中 Web 应用 和 Java Agent 都分别依赖了不同版本的 Log4j v1.1
和 v1.2
。此时问题来了:
JVM 中哪一个版本的 Log4j 会被装载呢?
提示 Java Agent 不同的装载回调时机,加上各式各样的 ClassLoader 层次关系, 会导致结果千奇百怪
node "Tomcat" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
rectangle System <<ClassLoader>> {
["log4j-v1.2"] <<java agent>>
rectangle Common <<ClassLoader>> {
rectangle WebApp <<ClassLoader>> {
["log4j-v1.1"] <<webapp>>
}
}
}
}
}
对于这个问题理解与处理,直接决定了 easeagent 的核心设计思路。
上文抛出的问题已经暗示了不能使用 Java Agent 默认的 SystemClassLoader 装载其依赖的字节码, 更不能像 stagemonitor 那样与宿主程序公用 ClassLoader,那么要怎么做呢?
node "Tomcat" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
rectangle System <<ClassLoader>> {
rectangle JavaAgent <<ClassLoader>> {
["log4j-v1.2"]
}
rectangle Common <<ClassLoader>> {
rectangle WebApp <<ClassLoader>> {
["log4j-v1.1"]
}
}
}
}
}
完美解决了!(其实并没有😫)
当宿主程序的情况变成这样的时候:
node "App" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
rectangle System <<ClassLoader>> {
["log4j-v1.1"]
rectangle JavaAgent <<ClassLoader>> {
["log4j-v1.2"]
}
}
}
}
由于 JavaAgentClassLoader 的准备装载 Log4j 时, 会先让其 父 也就是 SystemClassLoader 尝试装载, 于是装载了 v1.1
而不是我们期望中的 v1.2
。
实际中是借用 spring-boot-loader^sbl将 easeagent 所有的依赖打包到一个 JAR 文件中,并自定义独立的 ClassLoader 实现装载。
node "App" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
rectangle System <<ClassLoader>> {
["log4j-v1.1"]
}
rectangle JavaAgent <<ClassLoader>> {
["log4j-v1.2"]
}
}
}
若将 JavaAgentClassLoader 的 父 设为 BootstrapClassLoader 后, 任何除标准库以外的依赖会因 父 无法装载,而只能由 JavaAgentClassLoader 完成装载,则上述问题也得到了解决。 实现方法很简单, 类似:
URL[] urls = ... // Java Agent Dependencies
ClassLoader loader = new URLClassLoader(urls, null);
BootstrapClassLoader 是不能被引用的, 所以将 URLClassLoader 的 父 设置为
null
即可。
完美解决了!(其实又并没有😫😫)
现实世界里的 Java Agent 并不会像修改 Service#handle
方法字节码那么简单,比如我们希望借助 Metrics ^metrics 记录调用状态,势必引入对相关字节码:
class Service {
public void handle(...) {
try {
... // origin code of this method
} finally {
MetricRegistry mr = SharedMetricRegistries.getOrCreate("easestack");
mr.meter("Service#handler").mark();
}
}
}
一旦这么做了之后, 每当 Service#handle
被调用则会出现 ClassNotFoundException
提示 MetricRegistry
找不到, 因为 SystemClassLoader 并不知道去哪里找到 MetricRegistry
。
node "App" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
rectangle System <<ClassLoader>> {
[Service]
}
rectangle JavaAgent <<ClassLoader>> {
[MetricRegistry]
}
}
}
难道要重回来路子, 使用相同的 ClassLoader 吗?
当然不能走老路子, Instrumentation#appendToBootstrapClassLoaderSearch
提供了办法,为其额外增加查找字节码途径。
node "App" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
[MetricRegistry]
rectangle System <<ClassLoader>> {
[Service]
}
rectangle JavaAgent <<ClassLoader>> {
[Other]
}
}
}
完美解决了!(其实仍然没有😫😫😫)
我们不能一股脑儿把 JavaAgent 依赖的 JAR 全部往 BootstrapClassLoader 里加, 这会和老路子没什么区别。
似乎遇到进退两难的局面, 但要解决这个问题还得看细节。
要做到限制极少的外部字节码引入, 同时还要避免与其它 ClassLoader 中字节码的冲突。 解决方案是 EventBus :
public class EventBus {
public static final BlockQueue<Object> queue = ...;
}
public class MarkMeter {
final String name;
public MarkMeter(String name) { this.name = name;}
}
将 EventBus
和 MarkMeter
注入到 BootstrapClassLoader 中,然后
class Service {
public void handle(...) {
try {
... // origin code of this method
} finally {
EventBus.queue.offer(new MarkMeter("Service#handle"));
}
}
}
而后另起一个线程拉取 EventBus 的事件对象并处理:
class EventPolling implements Runnable {
final MetricRegistry mr = SharedMetricRegistries.getOrCreate("easestack");
public void run() {
while(true) {
try {
MarkMeter event = (MarkMeter)EventBus.queue.poll(1, TimeUnit.MILLISECOND);
if (event == null) continue;
mr.meter(event.name).mark();
} catch (InterruptedException ignore) {
return ;
}
}
}
}
这样一来,我们 ClassLoader 状况就是:
node "App" <<JVM>> {
rectangle Bootstrap <<ClassLoader>> {
[EventBus]
[MarkMeter]
rectangle System <<ClassLoader>> {
[Service]
}
rectangle JavaAgent <<ClassLoader>> {
[MetricRegistry]
[EventPolling]
}
}
}
并不完美, 但是从细节中取得了平衡。
ByteBuddy 提供了
ClassInjector.UsingInstrumentation
来简化注入代码的实现。
借助 Java Reflection 机制实现 YAML 配置文件内容到需要配置的对象属性上的。
参考 stagemonitor 的实现。
不同于 stagemonitor 通过 javax.servlet.Filter
的机制实现, 而是通过修改所有 javax.servlet.Servlet
实现类的 service
方法的字节码。
TODO