本人阅读了 Skywalking 的大部分核心代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 "调用链监控APM" 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。
作者已经将过程写成一部书籍,奈何没有钱发表,如果您知道渠道可以联系本人。一定重谢。
本书涉及到的核心技术与思想
JavaAgent , ByteBuddy,SPI服务,类加载器的命名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。
适用人群
自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;
版权
本书是作者呕心沥血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。
原版PDF+源码请见:
PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客
第1章 第一个Agent应用
JavaAgent其实就是jdk提供的一个工具,让我们有能力去修改字节码,然后加载到JVM使其生效。调用链监控本质就是对各框架的方法调用进行监控,在方法逻辑里面进行埋点,能做到这点的非JavaAgent莫属。
JavaAgent在修改字节码有两个时机,第一个就是在类加载前修改,这种就是静态绑定。第二种就是在类加载后JVM进程正在运行了,然后进行绑定修改,这种就是动态绑定,但是动态绑定对原始字节码修改作了很多限制,比如:新增方法,字段等。由于我们制作的调用链监控需要在原始字节码新增字段,所以采用了静态绑定,动态绑定就不在本书讨论范畴。
JavaAgent的静态绑定
JavaAgent技术的核心就是制作一个agent jar包,在启动JVM时,使用-javaagent参数就可以指定这个agent jar。JVM启动时在main方法之前会先加载这个agent jar里面类的premain方法,这个就是字节码修改的入口方法。
这个agent jar 打包时,需要在Manifest文件声明premain的路径等信息,具体实例如下图:
下面我们先来制作一个agent jar,顺便把我们的项目架构搭建起来。
1.1 创建项目结构
打开idea,创建一个名为hadluo-smart-apm的顶级maven聚合工程(聚合工程就是packaging为POM的模块项目),然后新建两个子模块jar项目:
- apm-agent-core: 核心逻辑实现。
- apm-commons: 工具类,公共接口和类。
hadluo-smart-apm的POM文件如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>https://zhida.zhihu.com/search?content_id=251888894&content_type=Article&match_order=1&q=spring-boot-starter-parent&zhida_source=entity</artifactId>
<version>2.4.10</version>
<relativePath />
</parent>
<groupId>com.hadluo.apm</groupId>
<artifactId>hadluo-smart-apm</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>apm-agent-core</module>
<module>apm-commons</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
注意:为了书面优美,简洁,后续代码只贴关键性代码,可能会省略非必要的代码。
apm-agent-core的POM文件如下:
<parent>
<groupId>com.hadluo.apm</groupId>
<artifactId>hadluo-smart-apm</artifactId>
<version>1.0</version>
</parent>
<artifactId>apm-agent-core</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.https://zhida.zhihu.com/search?content_id=251888894&content_type=Article&match_order=2&q=kafka&zhida_source=entity</groupId>
<artifactId>kafka_2.13</artifactId>
<version>3.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>https://zhida.zhihu.com/search?content_id=251888894&content_type=Article&match_order=1&q=kafka-clients&zhida_source=entity</artifactId>
<version>3.9.0</version>
</dependency>
<!-- 获取cpu,内存信息工具用-->
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>3.12.2</version>
</dependency>
<!-- 获取cpu,内存信息工具用-->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.2.0</version>
</dependency>
<!-- 获取cpu,内存信息工具用-->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>com.hadluo.apm</groupId>
<artifactId>apm-commons</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
apm-agent-core的POM依赖了kafka,oshi等都是我们后面要用到的,我们索性先添加,后面缺的在后面补充。注意还要依赖apm-commons工具项目。
apm-commons的POM如下:
<parent>
<groupId>com.hadluo.apm</groupId>
<artifactId>hadluo-smart-apm</artifactId>
<version>1.0</version>
</parent>
<artifactId>apm-commons</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.19</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
byte-buddy是修改字节码的一个好用的工具,我们也先添加上。后面还用到了spring的读取环境下文件工具,也先添加上。
到此我们创建的项目结构如下图所示:
1.2 Agent Jar的制作与打包配置
前面说到Agent Jar需要配置指定premain路径的manifest文件,这个需要在apm-agent-core的POM文件中添加以下内容:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>apm-agent-core</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。 -->
<Premain-Class>com.hadluo.apm.agentcore.AgentMain</Premain-Class>
<!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。 -->
<Agent-Class>com.hadluo.apm.agentcore.AgentMain</Agent-Class>
<!--Can-Redefine-Classes: 是否可进行类定义。 -->
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<!--Can-Retransform-Classes: 是否可进行类转换。 -->
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</transformer>
<!-- 多个同名配置文件会合并,而不是覆盖-->
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>hadluo-apm-plugin.def</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
上面pom中配置了一个很重要的打包插件:maven-shade-plugin ,这个插件可以指定mainfest清单文件的配置(比如:Agent-Class),这个JavaAgent技术所必须的。
打包插件的第二个重要功能就是可以将所有依赖的第三方jar的同名配置文件进行合并。
这里先引出了插桩插件 的概念:本书通过对mysql,redis,tomcat等组件进行字节码插桩(增强)都是通过插桩插件 来实现的,而且一个组件对应一个插桩插件,每个插桩插件 需要一份 hadluo-apm-plugin.def 文件进行声明,因此打包后需要将多个def文件内容进行合并,不然就会丢失def文件内容。
maven-shade-plugin配置的AppendingTransformer就可以进行同名文件的合并。
下面我们实现premain方法,在apm-agent-core项目下,新建类:
com.hadluo.apm.agentcore.AgentMain
public class AgentMain {
public static void premain(String args, Instrumentation instrumentation) {
Logs.debug(AgentMain.class, "premain开始执行,args=" + args);
}
}
Logs为apm-commoms的打印日志工具,本书针对所有工具类都不会贴代码,会指定统一的下载路径,后续只要是没贴出的都是工具代码,后面不在赘述。
当premain执行时就会打印出日志,Instrumentation 为字节码修改的口子。
1.3 测试 Agent Jar
通过maven的package或install我们可以得到打包后的apm-agent-core-1.0.jar,如下图所示:
然后自己新建一个springboot的测试项目,启动项目前,我们指定JVM启动参数:
-javaagent:F:\hadluo\mvn_res\res\com\hadluo\apm\apm-agent-core\1.0\apm-agent-core-1.0.jar
当然你的apm-agent-core-1.0.jar路径肯定跟我的不一样。启动发现我们的premain执行了打印,如图所示:
说明我们的第一个agent制作成功。
1.4 本章小结
本章内容较为简单,主要就是讲解了JAVA为开发者提供了一个修改字节码的途径,这个途径就是JavaAgent,然后我们实现了第一个agent应用,并且进行测试通过。
我们还搭建了项目的结构,后续都是围绕这个项目结构进行开发,我们在实现一个较为复杂的系统时,也都是这样一步一步循序渐进,就像砌房子一样需要一块砖一块砖累积。
本章涉及到的工具类:
com.hadluo.apm.commons.Logs
package com.hadluo.apm.commons;
public class Logs {
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static void debug(Class<?> clazz, String msg) {
System.err.println(msg);
}
public static void info(Class<?> clazz, String msg) {
System.out.println(msg);
}
public static void err(Class<?> clazz, String msg, Throwable e) {
e.printStackTrace();
System.err.println(msg);
}
public static void err(Class<?> clazz, String msg) {
System.err.println(msg);
}
private static String printExceptionInfo(Throwable causeException) {
return causeException.toString() + LINE_SEPARATOR;
}
public static String convert2String(Throwable throwable, final int maxLength) {
final StringBuilder stackMessage = new StringBuilder();
Throwable causeException = throwable;
int depth = 5;
while (causeException != null && depth != 0) {
stackMessage.append(printExceptionInfo(causeException));
boolean isLookDeeper = printStackElement(causeException.getStackTrace(), new AppendListener() {
@Override
public void append(String value) {
stackMessage.append(value);
}
@Override
public boolean overMaxLength() {
return stackMessage.length() > maxLength;
}
});
if (isLookDeeper) {
break;
}
causeException = causeException.getCause();
depth--;
}
return stackMessage.toString();
}
private static boolean printStackElement(StackTraceElement[] stackTrace, AppendListener printListener) {
if (stackTrace.length == 0) {
/**
* In some cases, people would fill empty stackTrace intentionally.
* This is a quick stop.
*/
return true;
}
for (StackTraceElement traceElement : stackTrace) {
printListener.append("at " + traceElement + LINE_SEPARATOR);
if (printListener.overMaxLength()) {
return true;
}
}
return false;
}
private interface AppendListener {
void append(String value);
boolean overMaxLength();
}
}