前言
定位:读者需要对 Java JVM、Java Agent 有一定了解,之前写过"Java 自动化探针技术的核心原理和实践"的文章。OpenTelemetry 源代码的核心实现正好基于它的知识。如果你对 Java Instrumentation、Byte Buddy 不太熟悉,建议你先看看这篇文章。
本文主要分三个部分:
OpenTelemetry Java 探针项目结构、编译部署的介绍;
OpenTelemetry Agent 启动原理和源代码具体的实现;
OpenTelemetry Java 上进行二次开发组件的完整实例和运行效果。
如果喜欢文章的内容和疑问,欢迎分享,公众号下留言。
文章涉及技术概念:
JVMTI、Java Agent、Class Loader、Bootstrap ClassLoader、 Java Instrumentation、Byte Buddy、Java Byte-Code、ServiceLoader、SPI
重温 Java Agent
JVMTI
JVM 在设计之初,就考虑到了虚拟机状态的监控、程序 Debug、线程和内存分析等功能。
JVMTI :Java Virtual Machine Tool Interface。它底层基于 C、C++实现。通过它可以探查 JVM 内部的一些运行状态,甚至控制 JVM 应用程序的执行。Sun 公司出了 Java Agent,一个用 Java 实现 JVMTI 的流行方案。
Java Agent 技术由来
Java Agent 直译为 Java 代理,中文圈也流行另外一个称呼 Java 探针 Probe
技术。
它在 JDK1.5 引入,是一种可以动态修改 Java 字节码的技术。Java 类编译后形成字节码被 JVM 执行,在 JVM 在执行这些字节码之前获取这些字节码的信息,通过字节码转换器对字节码进行修改,以此来完成一些额外的功能。
Java Agent 是一个不能独立运行 jar 包,它通过依附于目标程序 JVM 进程工作。
Agent 启动方式
Premain Agent 模式
Java 程序运行前:在Main
方法执行之前,通过一个叫 premain
方法来执行。
启动时需要在目标程序的启动参数中添加 -javaagent
参数,Java Agent 内部通过注册 ClassFileTransformer ,这个转化器在 Java 程序 Main
方法前加了一层拦截器。在类加载之前,完成对字节码修改。
Premain 完整工作流程图
JVM Attach Agent 模式
另一种是程序运行中修改,需通过 JVM 中 Attach 技术实现。
Agentmain 工作原理
和 Premain 模式相似,主要区别在进行字节码增强前,拦截入口不同。一个叫Premain
,一个叫Agentmain
。 运行时加载,当前 JVM 进程已经启动了。这时借助另一个 JVM 进程通信,调用 Attach API 再把 Agent 启动起来。后面的字节码修改和重加载的过程那就是一样的。
编写一个演示 Attach 通信的 JVM 程序,用于启动 目标 JVM 程序的 Agent。
Java Agent 的价值
Java Agent 成熟的技术架构,有着对字节码通用的重写能力。它应用场景是非常广泛。
IDE 的调试功能,例如 Eclipse、IntelliJ IDEA;
热部署功能,例如 JRebel、XRebel、spring-loaded;
各种线上诊断工具,例如 Btrace、Greys,国内阿里的 Arthas;
各种性能分析工具,例如 Visual VM、JConsole 等;
全链路性能检测工具,例如 OpenTelemetry、Skywalking、Pinpoint 等。
接下来,我们用案例实现性能检测工具的 Java Agent 探针。
OpenTelemetry 如何使用 Java Agent
我们打开 OpenTelemetry Java 探针源文件 Jar 包,同理,从
MANIFEST.MF
文件中,找到对应 Premain 和 Attach 启动 Agent 的实现类。
Opentelemetry Java 项目
OpenTelemetry 目前 repositories 项目已经有 60 个了。大体分为几类。
社区、文档内项目,比如
community
、opentelemetry.io
;各语言内探针项目,目前 Java、Go、Python、JS、Rust、Php 包括 eBPF;
OpenTelemetry Collector 相关项目,比如
opentelemetry-collector
opentelemetry-operator
。
OpenTelemetry 会把非官方贡献项目有一个类似XX-Contrib
第三方库,各种语言探针和 Collector 项目都会有。一方面,考虑是这些库有特定适用范围。另一方面,给官方项目一些必要的补充。
Java 探针核心项目opentelemetry-java-instrumentation
源代码地址。
https://github.com/open-telemetry/opentelemetry-java-instrumentation
鉴于项目名太长,暂且下面统称 OJI ,OJI 至少 Java 8+ 版本支持。
OJI 相关项目 SDK 和 Contrib
OpenTelemetry Java SDK 源代码地址:
OJI 实现非侵入式的 Java 探针,SDK 项目基于 OJI 上提供了manual
手动创建链路的能力,这种能力通过 OpenTelemetry API、SDK 方式实现的。
OpenTelemetry Java Contrib 源代码地址:
https://github.com/open-telemetry/opentelemetry-java-contrib
Contrib 给 Java 探针贡献了额外第三方库,目前 Contrib 主要提供一些库。
通过 OpenTelemetry Client 官方架构图,我们总结下 OJI、SDK、Contrib 项目的关系。
OJI 目录结构
我们把源项目通过 Gradle 方式导入 IDE 工具后,看到的 OJI 项目结构。
OJI 需要关注的几个项目模块:
docs
帮助介绍文档benchmark-overhead
压力测试工具custom-checks
源代码格式规范检查javaagent
javaagent 核心实现javaagent-bootstrap
Bootstrap classloader 核心实现instrumentation
这里开发自己组件的地方muzzle
涉及安全字节码增强和类加载的控制
OJI 如何部署编译、运行探针
初次编译
OJI 使用Gradle
来进行依赖管理,配置用的是 Kotlin DSL 脚本文件。
Groovy DSL 脚本文件使用.gradle 文件扩展名
Kotlin DSL 脚本文件使用.gradle.kts 文件扩展名
OJI 开发需要 JDK 9+ 的版本,查看本地 IDE JDK 版本是否符合要求。建议 IDE 用IntelliJ IDEA
,Eclipse
可能一堆环境问题。
导入项目后,探针生成是通过javaagent
模块的 Tasks 来编排完成。
我们执行gradle assemble
进行打包任务。初次打包需要花费很长时间,OJI 拉取很多依赖。我建议你最好开代理,大致执行半个小时。
如果没有代理,可能遇到依赖拉取失败问题,我看网上有人编译超过一个多小时,他给的方案在build.gradle.kts
文件添加其他的仓库地址,比如阿里云仓库。这个仅供参考。
编译打包
经过等待,最后显示如下类似信息,说明编译成功。
从上面看到,编译会打包探针成一个 Jar 包,存放在javaagent/build/libs/
。
下面可以来动手二次编写 OpenTelemetry 探针。
运行探针
我拉了 OJI 最新版本 1.22.0 重写了一个探针:
opentelemetry-javaagent-1.22.0-jiangzhiwei.jar
通过 java agent 方式启动探针,运行如下:
探针的核心实现
OJI 底层设计原理
OJI 基于 Javaagent 技术启动它的探针。我们通过 Gradle 编译生成的 Jar 包中的 MANIFEST.MF
文件中定义的 Agent 的启动类 OpenTelemetryAgent
。
MANIFEST.MF
这个文件源代码不存在的,它是用 Gradle 脚本文件 javaagent/build.gradle.kts
操作 manifest
生成的。
OpenTelemetryAgent 是整个探针入口,它通过startAgent
方法实现 Javaagent 的 premain
和 agentmain
。
OJI Agent 启动过程
startAgent
按顺序做的一些操作。
Classloader 先去加载 Bootstrap 下的类。
初始化我们 Instrument , 这里面包含我们开发第三方组件探针服务。
通过 AgentInitializer.initialize 去调用 ClassLoader 加载 AgentStarterImpl 类,它是启动 Agent 的核心实现。
从源代码可以看到:
1、AgentStarterImpl 用到 Bytebuddy
做字节码增强技术。
2、AgentStarterImpl 加载两个类库,分布是 instrumentation
、extensionClassLoader
到这,我们需要系统回顾下 Java Classloader 技术。了解下 OpenTelemetry Javaagent 类加载设计机制、structure
和hierarchy
的概念。最后着重讲讲 OpenTelemetry 基于 Classloader 加载我们开发组件服务核心实现以及开发我们自己的 Instrument 的过程。
OJI Agent structure
Java ClassLoader
Java 类加载器。虚拟机把描述类的数据从 class 字节码文件加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
JVM 中内置了几个重要的 ClassLoader:
Bootstrap ClassLoader
根加载器,负责加载 JVM 运行时核心类,这些类位于 $JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现。Extension ClassLoader
扩展类加载器,负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器等等,这些库名通常以 javax 开头,它们的 jar 包位于$JAVA_HOME/lib/ext/*.jar
中,有很多 jar 包。System ClassLoader
系统类加载器,Extension ClassLoader 的子类,用于加载应用级别的类到 JVM,即加载 classpath 目录下的 Java 类,通过ClassLoader.getSystemClassLoader()
可以获得。
这块需要重点说一下。网上还有另外一个概念App ClassLoader
,其实它们是一回事, 具体参考 Jdk9 中的 ClassLoader 的文档。
Customer ClassLoader
自定义的 ClassLoader 指开发者根据具体需求编写的类加载器,可以实现定制化加载。
URLClassLoader
位于网络上静态文件服务器提供的 jar 包和 class 文件,JDK 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。Extension ClassLoader 和 App ClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
OJI Class Loader
下面是 OpenTelemetry Java 项目服务对应 Java ClassLoader 加载的分布图。
System class loader
我们 Java Agent 实现类 io.opentelemetry.javaagent.OpenTelemetryAgent
通过这个 Classloader 在应用程序启动进行初始化。
它的唯一职责是将 Agent 相关的类推送到 JVM 的引导类加载器中,这个实现交给io.opentelemetry.javaagent.bootstrap.AgentInitializer
类。该类位于引导类加载器内,在 javaagent jar 中,这个类位于 io/opentemetry/javaagent/目录中。
比如之前提到 AgentInitializer
加载 javaagent-tools 相关 Agent 类。
Bootstrap class loader
根加载器加载了 OJI 主要的组件 modules
:
javaagent-bootstrap
: 它由OpenTelemetryAgent
启动,继续做 Agent
初始化过程
instrumentation-api
andinstrumentation-api-semconv
:
instrumentation-api-semconv
是 OpenTelemetry 提供链路操作的基本 API, instrumentation-api-semconv
提供链路核心组件的数据定义,比如 Metrics、组件 Attribute、Method 等等。
instrumentation-annotations-support
OJI 支持用注解 annotation 方式进行自动化注入。比如我们可以用@WithSpan
注解。
比如 OJI 支持在 Java spring-boot 下通过注解自动生成 Span 的源代码大致实现。
javaagent-extension-api
最重要里面提供开发 Agent Instrument 最基本服务模块,包含开发核心组件的基类InstrumentationModule
、组件接口TypeInstrumentation
、字节码增强的接口TypeTransformer
我们后面开发组件基于这个 modules 封装。依赖
otel.javaagent-bootstrap
Gradle plugin 的所有 modules : 这些 modules 定义了基础功能类库在 bootstrap 类加载器里全局使用. 比如用来保存 Servlet 上下文路径功能库,用来给 Kafka 消费者的本地线程切换机。这些 modules 命名规则类似::instrumentation:...:bootstrap
。
Agent class loader
javaagent-tooling
这个模块由javaagent-bootstrap
的 OpenTelemetryAgent 初始化,它调用 Java Instrument API 实现了 Agent 具体安装过程。采用是 ByteBuddy 字节码增强框架去构建 Java Agent 的类转化器ClassFileTransformer
。这个模块还实现了 OpenTelemetry SDK 的初始化和启动。muzzle
这个模块主要为了解决我们开发的 Instrument 增强应用程序字节码时,如何和应用程序的代码出现冲突或者不匹配时做的相关处理。
javaagent-extension-api
这个模块的io.opentelemetry.javaagent.extension
这个 package 实现了 Java Instrument API 封装的 Instrument Module ,我们开发 Instrument 就是继承这些 Module。Instrument 的发布通过 SPI 技术注册服务实现。依赖
otel.javaagent-instrumentation
Gradle plugin 的所有 modules : 我们开发 Instrument 就是写成这样一个个 module。
它们所有都会实现InstrumentationModule
, 有一些中间件比较复杂,module 下还会有一个library instrumentation
这些 modules 命名规则似:instrumentation:...:javaagent
。
Agent ClassLoader 加载字节码特殊处理
在编译后的探针 Jar 文件中, 由 AgentClassLoader
加载的类和资源文件会被存放在 inst/
目录中. 其中所有的 .class
都会替换成.classdata
,脚本函数代码。
这样设计的目的是针对加载了应用程序类的一些通用类加载器,规避它们再去加载这些 Agent 类,可能导致的类冲突。这样,从 Javaagent 内部与应用程序的代码完全隔离。比如 Instrument 包含库依赖,可能应用程序也会用到这个库依赖,但是它们用的是不同版本!这种情况是常见的场景,没有办法避免有冲突。下面是编译探针包inst/
对应.classdata
。
Extension class loader
OJI 除了核心组件放在otel.javaagent-instrumentation
module 外,还设计了一个 Extensions
框架,它主要支持扩展和增强 Agent 功能,后面单独介绍。
The extension class loader(s) 就是被用来加载这些定制化的 Extension, 具体用法就是启动 OpenTelemetry Agent 配置otel.javaagent.extensions
, 或者嵌入相应的 Extension Jar 包 到 extensions/
目录下。比如用参数方式启动 OpenTelemetry extensions。
opentelemetry-extension-demo-1.0.jar
里面就是我们写的 Extension。 为了避免类冲突,每个 Extension 都是独立编译打包。
这里单独提一下,Extension 、Instrument 执行顺序,OJI 在相应基类提供 Order
方法设置他们在 ClassLoader 中加载顺序权重。
Agent 组件 Instrument 加载过程
介绍完 OJI 的 Class Loader 的设计原理和运行机制,现在基于这些知识我们能更好理解 OJI 启动 Agent 的过程:
AgentStarterImpl 通过
createExtensionClassLoader
创建了一个 URLClassLoader 对象叫ExtensionClassLoader
,同时AgentInstaller.installBytebuddyAgent
通过 Bytebuddy 框架初始化 Agent 对象AgentBuilder
对象,底层调用 Java Instrument API。AgentInstaller.installBytebuddyAgent
通过loadOrdered
静态方法从ExtensionClassLoader
类加载器把发布的 InstrumentationLoader 对象获取出来。
对于的 InstrumentationLoader 类结构,调用它的extend
方法通过 loadOrdered
静态方法获取发布的 InstrumentationModule
,然后通过InstrumentationModuleInstaller
对象的install
方法来实现 Instrument 的加载过程。
这里面特别说明一下:之前给大家介绍 Java Agent 启动原理提到,我们自己写的 Module 在 Premain 或者 Agentmain 方法中,会显式调用 Instrumentation 对象写入我们的 Module。
那 OJI 怎么写入Transformer
呢?OJI 并没有手动硬编码方式写入 Module。考虑到我们探针支持组件非常多,每一个硬编码 Module 管理复杂而且耦合性太紧。OJI 利用了 Java SPI 技术,每个 Module 按照一定固定配置,动态的发布到 ClassLoader 中,需要用到时候通过 Java 的ServiceLoader
类获取。下面章节我会详细介绍 SPI 技术和ServiceLoader
。
InstrumentationModuleInstaller.install
方法构建 Instrument Module 基本服务 Module 注册探针相关作用范围:比如通过classLoaderMatcher
设置只对指的的 ClassLoader 下所有应用类做字节码增强。比如通过typeMatcher
设置对指的 Java package 下所有类做字节码增强。
Module
contextProvider
保留业务类相关类、方法、变量的上下文信息。比如我们字节码增强时,想要获取一些监控有用的 Metric ,可以从上下文获取。
也可看到 typeMatcher
通过调用 Module 自身的typeInstrumentation.typeMatcher()
来实现的,下面一个例子就是我写的一个 Instrument Module,通过 Debug OJI,我们可以看到上面提到整个 OJI 启动 Agent ,特别是如何加载 Instrument Module 过程。
Java SPI 技术
SPI 全称 Service Provider Interface,即服务提供接口,基于服务的注册与发现机制,服务提供者向系统注册服务,服务使用者通过查找发现服务,可以达到服务的提供与使用的分离,甚至完成对服务的管理。通过解耦服务具体实现和使用,使得程序的扩展性大大增强,甚至可插拔。
Java 基于 SPI 机制提供了一套用来被第三方实现的 API,主要用于框架的开发。
通过 Java SPI 我们可以动态加载组件和动态服务发现。比如数据库驱动 java.sql.Driver 接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL 和 PostgreSQL 都有不同的实现提供给用户,而 Java 的 SPI 机制可以为某个接口寻找服务实现。
Java SPI 是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
Java ServiceLoader
ServiceLoader 是 JDK1.6 基于 SPI(Service Provider Interfaces)思想引入的一个实现类。
使用场景
基于 SPI 动态替换、发现服务机制, ServiceLoader 在许多框架有着应用, 常见的例子。
数据库驱动 JDBC 加载不同数据库的连接配置,比如 Mysql、Oracle、SQLServer,
日志框架 SLF4J 加载不同提供商的日志实现类,
Spring 通过 spring.handlers 和 spring.factories 两种方式实现 SPI 机制,可以在不修改 Spring 源码的前提下,做到对 Spring 框架的扩展开发。
ServiceLoader 原理
ServiceLoader 数据结构。
ServiceLoader 在实现方式上采用的是迭代器模式。在迭代器实现中采用的是懒加载方式,即用到时才加载(这里加载指的是解析 SPI 接口资源文件)。ServiceLoader 首先从类加载路径下读取 SPI 接口配置文件,将所有的配置文件地址解析到一个 Enumeration 的集合对象中,然后逐个解析配置文件,将配置文件中的每一行解析为一个类的全限定名存入一个 Iterator 中,通过对 Iterator 逐个迭代读取类名,并将解析的类名通过 Class.forName()得到每个类的 Class 对象,最后通过 ClassLoader 反射的方式创建 SPI 实现类并缓存到一个 Map 集合中。
OpenTelemetry SafeServiceLoader 调用 ServiceLoader 的基本实现。
使用 ServiceLoader 几个标准约定步骤:
创建一个接口文件,声明需要实现的方法;
在 resources 资源目录下创建 META-INF/services 文件夹,resources 其实是和类的根路径在同一级目录,示例中 resources 目录仅仅是为了易于区分资源文件和 Java 类文件,其实也可以将 META-INF/services 建立在 src 目录下,运行效果也是一样的。如果使用的是 IDEA 开发工具,META-INF/services 可以放在与 java 目录同级的某个资源文件夹下。
在 services 文件夹中创建文件,以接口全限定名命名,文件必须使用 UTF-8 编码,可以使用"#"作为注释符。
创建接口实现类,并将该实现类的全限定名注册到接口文件中。
核心代码解析
应用程序调用 ServiceLoader.load 方法。
先创建一个新的 ServiceLoader 对象,实例化该类中的成员变量。
应用程序通过迭代器访问对象实例。
ServiceLoader 先判断成员变量 providers 对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。
如果没有缓存,执行类的装载,实现如下:
(1) 读取 META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader 可以跨越 jar 包获取 META-INF 下的配置文件,具体加载配置的实现代码如下:
(2) 通过反射方法 Class.forName()加载类对象,并用 instance()方法将类实例化。
(3) 把实例化后的类缓存到 providers 对象中,(LinkedHashMap 类型)然后返回实例对象。
ServiceLoader 加载 OJI Instrument 过程
上面我们提到:InstrumentationLoader 调用extend
方法通过 loadOrdered
静态方法加载 InstrumentationModule
,这个静态方法来自 SafeServiceLoader
,可以看到,该方法里面就是调用了 ServiceLoader 类的load
方法实例化我们的 Instrument Module 类。
动手如何写组件 Instrument 模块
OJI InstrumentationModule 模块
InstrumentationModule
是 OpenTelemetry Javaagent 核心组件. OpenTelemetry 为了给 Java 核心的中间件和语言框架的数采支持,OJI 提供非常丰富和兼容不同版本的组件 Module 实现。既有 Jetty、Kafka、Mongo、JDBC 这样流行的中间件,也有 Spring、Struct、RMI、Servlet 语言框架支持,而且最大化兼容不同版本。
如果想支持自定义的组件,我们可以自己来实现一个新的InstrumentationModule
。
一个InstrumentationModule
必须定义一个组件名称。OJI 用 Gradle 脚本编译时,需要通过在配置文件opentelemetry-java-instrumentation/settings.gradle.kts
中通过hideFromDependabot
引入这个 Module ,这样打包时 Module 自动打入 Agent Jar 中。
TypeInstrumentation 的介绍
InstrumentationModule
包含了一组TypeInstrumentation
,一个TypeInstrumentation
对应组件下具体的一个字节码增强实现,在 Module 中通过链表可以有多个实现。这样针对业务类不同场景,采取不同的字节码增强策略。
上面介绍 SPI 技术已经提到,InstrumentationModule
通过配置 META-INF/services/
文件找到对应的 Module 接口服务。
OJI 提供了注解 ,用@AutoService
标签自动生成资源文件。
OJI Extensions 组件
OJI 为了支持新特性、扩展功能单独设计一个独立扩展组件。比如它提供了IdGenerator
接口,你可以不用系统自动生成,自己实现 Trace、Span 的 Id 生成规则。
限于篇幅,后面单独做一个技术分享介绍 Extensions
,有兴趣的可以参考。
字节码增强框架
Agent 本质是通过操作字节码,动态修改运行时 Java 对象。
我们把一类对现有字节码进行修改或者动态生成全新字节码文件的技术叫做字节码增强技术。
字节码增强技术的实现有很多方式,简单整理下目前比较成熟的一些操作字节码的框架。
JDK动态代理
运行期动态的创建代理类,只支持接口;
ASM
一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。不过 ASM 在创建 class 字节码的过程中,操纵的级别是底层 JVM 的汇编指令级别,这要求 ASM 使用者要对 class 组织结构和 JVM 汇编指令有一定的了解;
Javassist
一个开源的分析、编辑和创建 Java 字节码的类库(源码级别的类库)。Javassist 是 Jboss 的一个子项目,其主要的优点,在于简单,而且快速。直接使用 Java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
Byte Buddy
是一个较高层级的抽象的字节码操作工具,相较于 ASM 而言。Byte Buddy 本身也是基于 ASM API 实现的。Byte Buddy 以出色的性能,被著名的框架和工具(例如 Mockito,Hibernate,Jackson,Google 的 Bazel 构建系统等)使用。
Byte Buddy
Byte Buddy 是致力于解决字节码操作和 instrumentation API 的复杂性的开源框架。
使用 Byte Buddy,任何熟悉 Java 编程语言的人都容易地进行字节码操作。
官网的示例展现了如何生成一个简单的类,这个类是 Object 的子类,并且重写了 toString 方法,用来返回“Hello World!”
Demo 来源 bytebuddy.net
ByteBuddy Advice 技术
限于篇幅,后面文章单独介绍 ByteBuddy 的东西。
一个完整的探针开发、部署、运行的实例
目标
基于 OJI 开发新的 Java 探针,功能是:
开发一个探针组件,该组件统计指定 Java 程序下所有方法的执行时间。
这个指定 Java 程序:基于 OpenTelemetry + Jagger 链路演示系统, 探针打印相关链路生成 Trace、Span 等相关信息
新建 Java Instrument 项目
我们按照 OpenTelemetry 开发组件规范, 在instrumentation
目录下新建一个目录作为组件的目录,命名为laziobird
。
在此目录中新建
javaagent
目录,目录下创建 Gradle 配置文件build.gradle.kts
,我们需要引入otel.javaagent-instrumentation
依赖开发组件。
2、在 OJI 全局的 Gradle 配置文件settings.gradle.kts
中添加 Instrument 引入。
和其它通用 Java 框架组件平级,我们声明新的 Instrument 叫 laziobird
。
在javaagent
目录下构建我们的项目结构,大致如下:
开发 InstrumentationModule 组件
我们组件定义的一个 InstrumentationModule 叫 MyModule
,代码如下:
这个 InstrumentationModule 在我们打成探针 Jar 包后,可以从 SPI 的services
找到它。
接下来,我们实现TypeInstrumentation
接口核心方式,写我们自己的 Instrument 组件。
首先,我们通过typeMatcher()
方法,声明 Instrument 对那些类起作用。为了方便演示,我们探针限定测试 Java 程序里的 Spring MVC Controller 类,效果就是 Instrument 对所有 API 方法做字节码增强。
transform()
方法可以看到,com.observable.trace.otel.controller
下所有方法,都会被 AOP 方式拦拦截进 LaziobirdAdvice
这个 Advice。
LaziobirdAdvice
具体实现:我们通过 ByteBuddy Advice 在所有方法进入前,创建一个内部变量laziobirdStartTime
,记录开始时间。然后调用 OJI Span API ,将改变量保存到 Span 中。同时打印一些链路,TraceId、SpanId 等信息。 在方法执行完退出前,获取局部变量laziobirdStartTime
,最后和当前时间差值,就统计出方法的执行时间。
探针编译部署
运行 OJI Gradle 脚本 maven assembly
,进行编译打包 。第一次默认打包需要 30 分钟上,后面下载完依赖包后,编译速度快很多了。
运气不错,我们只花了不到两分钟。对于探针叫:
opentelemetry-java-instrumentation/javaagent/build/libs/opentelemetry-javaagent-1.22.0-jiangzhiwei.jar
测试用例部署探针
我们写了一个完整链路测试用例程序。
OpenTelemetry+Jaeger 的分布式链路追踪演示案例
项目大概架构长这样:
项目包含一个 Java 程序、我们写的 OpenTelemetry Java 探针、最后是 OpenTelemetry 上报 Trace 给 Jeager UI 程序。
我们启动 Java 程序、同时通过参数形式启动 Java Agent。
其中otel.jar
是我写的的这个应用程序,我们可以从启动日志看到探针自动注入的信息。另外我们指定 OpenTelemetry Trace 通过设置Dotel.traces.exporter=jaeger
上报到 Jaeger。
测试用例展示探针链路追踪的效果
运行程序的步骤:
1、访问一个后端请求,叫做loadBalancer
,代码这样。
从代码看到,请求一个 Controller,这个方法里面又跳转请求三个 Controller,下面页面效果。
探针二次开发组件的拦截效果
我们通过探针打印的日志信息,看到业务程序每个方法完整调用过程。
最后上报给 Jeager 生成链路图效果,可以看到探针打印的链路 TraceId 和 SpanId 一一对应上。
OJI 探针上报 Jeager 链路图
Github 案例地址
为了方便大家上手实践,我贡献案例到 Github,其实基于 Java Agent 性能诊断工具、链路分析的 Java 探针基本都是类似实现,大部分区别在于字节码增强实现的差异。
当然,要求更高的性能和底层功能,可以直接编写 C、C++的 JVMT 动态链接库。
探针实现
https://github.com/laziobird/opentelemetry-java-instrumentation
分布式链路演示程序
https://github.com/laziobird/opentelemetry-jaeger
建议使用非容器简单部署方式
作者介绍
蒋志伟,爱好技术的架构师,曾就职于阿里、Qunar、美团,前 pmcaffCTO,目前 Opentelemetry 中国社区发起人。
评论 1 条评论