Java 无痕埋点实现思路分享-javaagent篇
本篇文章基于我的开源 slot 工程。
本文基于 jdk8 编写
埋点系统对比
一般的,埋点系统可分为两大类:侵入式埋点和无侵入式埋点。
侵入式埋点
一般通过 SDK 来提供埋点能力。
侵入式埋点优点
埋点粒度可以随意控制,想在哪埋点就在哪埋点
使用简单,开发人员根据需要在指定的业务代码处调用 API 即可
侵入式埋点缺点
需要对业务代码进行改造,可能会需要重新进行回归测试和重新发版
对业务人员不友好,所有的埋点操作都需要通过研发人员进行操作,对于业务的响应有滞后
无侵入埋点
一般通过 agent 来实现。
无侵入埋点优点
无需对原有的业务代码进行改造
对业务人员较友好,业务人员可以配置需要埋点的内容
无侵入埋点缺点
埋点粒度无法控制,只能依托于埋点 agent 的实现粒度。
埋点的时效性只能依托于埋点 agent 实现,如果是通过 javaagent 来实现,那么需要重启应用来刷新埋点配置,如果是 attach 来实现则无需重启应用。
javaagent
在这片文章中对于 javaagent 只做简单介绍,无痕埋点的思路是本篇文章的重点。
如果读者想深入了解 javaagent 可自行搜索相关资料。
jvm 允许我们通过 javaagent 在程序启动的过程中对加载的 class 进行修改,这给我们的无痕埋点提供了入口,javaagent是slot 实现无痕埋点的基石。
javaagent 主要包含三个部分,分别是:
- Manifest
- Agent Class
- ClassFileTransformer
Manifest
jvm 要求 javaagent 的 jar 包中必须含有 Manifest 说明文件。
结构
我们将 Manifest 定义的属性分成了三组:基础、能力和特殊情况。
┌─── Premain-Class |
主要参数说明:
Premain-Class
: When an agent is specified at JVM launch time this attribute specifies the agent class. That is, the class containing the premain method. When an agent is specified at JVM launch time this attribute is required. If the attribute is not present the JVM will abort. Note: this is a class name, not a file name or path.Agent-Class
: If an implementation supports a mechanism to start agents sometime after the VM has started then this attribute specifies the agent class. That is, the class containing the agentmain method. This attribute is required, if it is not present the agent will not be started. Note: this is a class name, not a file name or path.Can-Redefine-Classes
:Boolean
(true
orfalse
, case irrelevant). Is the ability to redefine classes needed by this agent. Values other than true are considered false. This attribute is optional, the default is false.Can-Retransform-Classes
:Boolean
(true
orfalse
, case irrelevant). Is the ability to retransform classes needed by this agent. Values other thantrue
are consideredfalse
. This attribute is optional, the default isfalse
.Can-Set-Native-Method-Prefix
:Boolean
(true
orfalse
, case irrelevant). Is the ability to set native method prefix needed by this agent. Values other thantrue
are consideredfalse
. This attribute is optional, the default isfalse
.Boot-Class-Path
: A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries (commonly referred to as JAR or zip libraries on many platforms). These paths are searched by the bootstrap class loader after the platform specific mechanisms of locating a class have failed. Paths are searched in the order listed. Paths in the list are separated by one or more spaces. A path takes the syntax of the path component of a hierarchical URI. The path is absolute if it begins with a slash character (/
), otherwise it is relative. A relative path is resolved against the absolute path of the agent JAR file. Malformed and non-existent paths are ignored. When an agent is started sometime after the VM has started then paths that do not represent a JAR file are ignored. This attribute is optional.Launcher-Agent-Class
: If an implementation supports a mechanism to start an application as an executable JAR then the main manifest may include this attribute to specify the class name of an agent to start before the applicationmain
method is invoked.- 此文件最后必须要有行样换行
slot
工程的 Manifest 文件如下所示:
Manifest-Version: 1.0 |
如何生成
maven 插件
可以通过 maven-jar-plugin
插件来帮助我们生成 Manifest 文件,相关配置如下:
<plugin> |
其中 manifestEntries
块就是 Manifest 的内容。在 maven 构建 jar 包的时候就会自动生成 Manifest 文件。
自己创建
可以在 resource 文件夹下手动创建一个名为 MANIFEST.MF
的文件,文件的最后一行必须为空行,内容形如:
Manifest-Version: 1.0 |
Agent Class
与以往的普通程序不同,javaagent的入口必须为 premain
函数,并且对函数签名也有要求,javaagent 仅接受两个签名的函数,分别为:
// 第一种,优先加载 |
jvm 优先加载含有 Instrumentation
签名的方法,如果没有第一种
方法则会加载第二种
方法。我们可以对 Instrumentation
进行操作以达到我们操作 class 文件的目的, Instrumentation
定义如下:
public interface Instrumentation { |
我们可以操作 premain
方法中的 Instrumentation
对象来添加相关的操作。我们主要会用到 addTransformer
来添加转换类的实现,具体的为 SlotTransformer
ClassFileTransformer
在 Instrumentation 接口中,定义了添加和移除 ClassFileTransformer 的方法:
public interface Instrumentation { |
在 ClassFileTransformer 接口中,定义了 transform 抽象方法:
public interface ClassFileTransformer { |
当我们想对 Class 进行 bytecode instrumentation 时,就要实现 ClassFileTransformer
接口,并重写它的 transform
方法。
使用
javaagent
主要以 jar
包的形式提供服务,并在使用 java 命令时以 -javaagent:jar_path[=option]
来启动服务,示例如下:
java -javaagent:./target/TheAgent.jar -jar Demo.jar |
埋点思路
如何确定埋点范围
通过外部配置文件来指定。我们支持读取特定格式的 properties
文件解析出哪些方法需要进行埋点,properties
文件的路径通过 -javaagent:/your/properties/file/path
传递给 premain
的 String
参数。读取 properties
文件的方法如下所示:
/** |
异常处理
方法自身没有抛出异常
我们将目标方法代码块使用统一的 try-cache
进行处理,不论原方法是否抛出异常,我们都假定其会抛出异常,进行异常捕获并抛出。
在一开始实现过程中并没有对所有方法都进行异常捕获,但是在 spring boot 程序测试中发现 spring boot 存在全局异常捕获机制导致埋点流程被打断,因为进行了异常抛出所以代码的后续被 spring boot 接管了,而不会沿着原本的业务进行下去,那么埋点逻辑处理的就不完整,埋点数据收集的不完整,所以需要全局捕获异常,强制代码块走到埋点逻辑的结束。
例如原方法如下所示:
public void hello() { |
经过全局异常处理之后变为:
public void hello() throws Throwable { |
方法自身抛出异常/处理异常
如果方法自身抛出了异常或者对异常进行了处理,那么我们会在源代码的 catch
代码块中记录异常类型、异常信息和异常堆栈,以供后续分析。例如:
public void calc() { |
经过全局处理之后
public void calc() { |
方法耗时计算、方法开始时间
我们在方法代码块的最开始插入获取当前系统时间的时间戳和 LocalDateTime
来确定方法的开始时机,在方法的结尾、每个 catch
代码块中插入获取方法的结束时间代码并使用代码开始的时间戳减去方法结束的时间戳以获取方法的执行时间。
埋点数据的处理
为了埋点给系统带来最低的性能影响,埋点系统采用 disruptor
消息中间件来处理埋点产生的埋点数据得到及时有效的处理。源码在这
刚开始埋点系统采用的是 Java 自带的阻塞队列,发现存在性能问题和 OOM 问题,后期调研到 disruptor
组件,性能有了质的提升。在百万数据量下使用的内存和处理速度都有提升。
字节码插桩
slot 使用 ASM 字节码操作工具来对 Java 源码进行修改。JDK 自带的字节码操作框架无需额外引入框架,降低了 javaagent jar 包的体积。
ASM 是较为低级别的 API,使用和学习都存在一定的门槛,需要对 ClassFile 的结构有较深的理解和认识。
slot 是如何使用 ASM 进行字节码插桩的,可以参见这里
埋点思路与 javaagent 的结合
以上所有的思路全都依赖 ClassFileTransformer
中的 transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
方法来实现。特别的,slot 中的实现为 SlotTransformer。
我们主要会用到 loader、className 和 classfileBuffer 参数。
loader 是当前被加载到 class 的类加载器,这个参数非常有用,我们需要使用该加载器来加载修改字节码之后的类,否则可能会出现 ClassNotFoundException
特别的 spring boot 的 jar 包被称为 fat_jar,它内部的类加载机制由 spring 来控制,我们需要使用 spring 的类加载器来对修改后的类进行加载,否则会报 ClassNotFoundException
className 用来与埋点配置文件中的配置项进行匹配,用来确定哪些类需要进行埋点,如果不似乎我们需要埋点的类那么可以直接返回
null
,jvm 会自动加载原本的 classclassfileBuffer 存放了当前加载的 class 的字节码信息,我们使用 ASM 组件来操作字节码,插入我们需要的埋点字节码
jvm 保证在 transform 的过程如果出现异常的话最终加载的是未经修改的 class。
展望
- 对于跨进程和跨机器的集群服务支持埋点服务
- 可以更换为 attach api 来热更新埋点配置
- 目前只能通过配置文件的方式来指定埋点的范围,可以考虑使用 jar 包探测,探测目标工程为 spring boot、netty、grpc或其他工程来确定埋点范围