基于one java agent的可插拔java agent方案
背景
第⼀阶段:⾃研微服务
阿⾥巴巴的微服务拆分实践进⾏的很早,从 2008 年就开始了,当时的单体应⽤已经⽆法承载业务迭代的速度,由五彩⽯项⽬开始了微服务化的改造,在这个改造过程中,也逐步诞⽣了服务框架,消息队列,数据库分库分表等三⼤中间件。在这个阶段的服务治理能⼒是通过 SDK⽅式直接依赖在框架⾥⾯的。每个中间件都有⾃⼰独⽴的 SDK 依赖,服务治理能⼒的升级需要借助框架 SDK 的升级来解决,升级成本是很⾼的。
第⼆阶段:Fat-SDK
随着中间件接⼊数量的增加,业务升级成本不断攀升,从 2013 年起诞⽣了代号 “Pandora”的项⽬,主要有 2 个⽬标,⼀是解决中间件和业务依赖的冲突问题,⼆是解决服务治理升级效率的问题。同⼀个组件,业务和中间件的可能依赖不同的版本,最常⻅的例如⽇志,序列化组件等等,如果⼤家共享⼀个版本则会出现中间件的升级影响到业务,或者出现不兼容的情况。Pandora 提供了⼀个轻量的隔离容器,通过类加载器隔离的⽅式,将中间件和业务的依赖互相隔离,⽽中间件和中间件之间的依赖也能互相隔离。另外,通过 Fat-SDK 的⽅式,将所有中间件⼀次性打包交付给业务⽅升级。这⼀点和 Maven 引⼊的 bom 的思路类似,但是相⽐bom 来说每个 Pandora 的插件都可以享有独⽴的依赖。通过这种⽅式,业务不再需要单独升级某个中间件,⽽是⼀次性把所有的中间件完成升级,从⽽⼤幅提升了中间件升级的效率。
第三阶段:One Java Agent
随着业务的进⼀步发展,中间件的数量逐步增加,Pandora 的⽅式也遇到了相当多的问题,也就是如果要把⼀个 Pandora 的版本在全集团内全部推平,需要⻓达 1 年的时间才能完成。这是因为即使是 Pandora 的⽅式,也需要业务修改代码,升级,验证,发布,这些并⾮业务真正关⼼,业务更希望专注于⾃身业务的发展。通常借助双⼗⼀⼤促这样的机会,才有可能完成中间件的升级。这也给服务治理的形态带来新的挑战。2019 年,阿⾥推出了 One Java Agent 的形态,把服务治理的能⼒下沉到 Java Agent 的形式,通过⽆侵⼊的⽅式,实现了中间件的迭代升级,进⼀步提升了升级效率。
在One Java Agent中, 各个中间件的代码能够独⽴开发、部署,且尽可能做到互不影响,其有以下几种特性:
- 每个 plugin 可以由启动参数来单独控制是否开启。
- 各个 plugin 的启动是并⾏的,将 java agent 的启动速度由 O(n+m+…)提升⾄O(n)。
- 各个 plugin 的类,都由不同的类加载器加载,最⼤限度隔离了各个 plugin,解决了各个agent可能出现的依赖冲突问题。
- 每个 plugin 的状态都可以上报到服务端,可以通过监控来检测各个 plugin 是否有问题。
One Java Agent 的 开源地址:https://github.com/alibaba/one-java-agent
使用
下载源码
下载源码:https://github.com/alibaba/one-java-agent,下载后的源码如下图所示
其中核心包为one-java-agent、one-java-agent-plugin、one-java-agent-spy,其他为示例demo工程。
开发agent插件
由于one-java-agent需要统一维护和管理插件,因此需要将需管理的agent插件加入one-java-agent工程体系中,按照接入场景分,一般为几种场景:
- 未开发的agent接入
- 已开发的agent jar接入
- 已开发的agent源码接入
下面分别针对以上三中情况详细的开发说明:
未开发的agent接入
即将要开发agent可按照one-java-agent的开发规范接入one-java-agent体系,主要有以下步骤:
1**、新建子模块工程**
工程文件需包括以下几部分:
**打包配置类:**assembly文件夹以及assembly.xml
**插件配置类:**plugin.properties
**agent****启动类:**DemoAgent
**插件激活器:**PluginActivator
插件所需的类加载处理器:DemoPluginClassLoaderHandler
**pom****依赖文件:**pom.xml
2**、配置打包配置类**
将plugin配置文件以及当前代码打包,一般默认即可
Expand source
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>bin</id>
<formats>
<format>zip</format>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>false</unpack>
<outputFileNameMapping>${artifactId}.jar</outputFileNameMapping>
<includes>
<include>${artifact}</include>
</includes>
</dependencySet>
<dependencySet>
<outputDirectory>/lib</outputDirectory>
<unpack>false</unpack>
<excludes>
<exclude>${artifact}</exclude>
</excludes>
</dependencySet>
</dependencySets>
<files>
<file>
<source>src/main/plugin.properties</source>
<destName>plugin.properties</destName>
</file>
</files>
</assembly>
3**、引入pom.xml文件依赖**
Expand source
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.alibaba.oneagent</groupId>
<artifactId>one-java-agent-parent</artifactId>
<version>0.0.2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>demo-plugin</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.oneagent</groupId>
<artifactId>one-java-agent-plugin</artifactId>
<version>${project.version}</version>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>bytekit-core</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>demo-plugin</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF-8</encoding>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>bin</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>${project.artifactId}@${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/main/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>local</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<delete dir="${user.home}/oneagent/plugins/${project.artifactId}@${project.version}">
</delete>
<copy todir="${user.home}/oneagent/plugins/${project.artifactId}@${project.version}">
<fileset dir="${project.build.directory}/${project.artifactId}@${project.version}" />
</copy>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
4**、编写agent启动类**
根据需要在premain或agentmain写实现
Expand source
import java.lang.instrument.Instrumentation;
/**
*
* @author hengyunabc 2020-07-28
*
*/
public class DemoAgent {
public static void premain(String args, Instrumentation inst) {
init(true, args, inst);
}
public static void agentmain(String args, Instrumentation inst) {
init(false, args, inst);
}
public static synchronized void init(boolean premain, String args, Instrumentation inst) {
System.out.println("demo-plugin demo-agent started.");
}
}
5**、编写插件激活器PluginActivator**
PluginActivator激活器需实现enabled(控制是否启动),init(初始化),start(开始),stop(结束)接口,对插件实现控制以及生命周期的监测。
Expand source
import com.alibaba.bytekit.ByteKit;
import com.alibaba.fastjson.JSON;
import com.alibaba.oneagent.plugin.PluginActivator;
import com.alibaba.oneagent.plugin.PluginContext;
public class DemoActivator implements PluginActivator {
private String name = this.getClass().getSimpleName();
@Override
public boolean enabled(PluginContext context) {
System.out.println("enabled " + this.getClass().getName());
System.err.println(this.getClass().getSimpleName() + ": " + JSON.toJSONString(this));
System.err.println("bytekit url: " + ByteKit.class.getProtectionDomain().getCodeSource().getLocation());
return true;
}
@Override
public void init(PluginContext context) throws Exception {
// 注册自定义的ClassLoaderHandler,让被增强的类可以加载到指定的类
ClassLoaderHandlerManager loaderHandlerManager = context.getComponentManager().getComponent(ClassLoaderHandlerManager.class);
loaderHandlerManager.addHandler(new DemoPluginClassLoaderHandler());
System.out.println("init " + this.getClass().getName());
Instrumentation instrumentation = context.getInstrumentation();
String args = context.getProperty("args");
DemoAgent.init(true,args,instrumentation); }
@Override
public void start(PluginContext context) throws Exception {
System.out.println("start " + this.getClass().getName());
}
@Override
public void stop(PluginContext context) throws Exception {
System.out.println("stop " + this.getClass().getName());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
6**、编写DemoPluginClassLoaderHandler类加载处理类**:
Expand source
import com.alibaba.oneagent.service.ClassLoaderHandler;
/**
*
* @author hengyunabc 2021-08-26
*
*/
public class DemoPluginClassLoaderHandler implements ClassLoaderHandler {
@Override
public Class<?> loadClass(String name) {
if (name.startsWith("com.activator.test")) {
try {
Class<?> clazz = this.getClass().getClassLoader().loadClass(name);
return clazz;
} catch (Throwable e) {
e.printStackTrace();
}
}
return null;
}
}
7**、配置插件配置类**
Expand source
specification=1
name=demo-plugin
version=1.0.0
classpath=demo-plugin.jar:lib/
pluginActivator=com.activator.test.DemoActivator
importPackages=com.alibaba.fastjson,com.alibaba.bytekit
其中name、version、pluginActivator根据实际情况指定。
–实际例子参照代码工程中的demo-plugin子模块
已开发的agent jar接入
对于已开发好的agent jar包,同样需要新建一个插件管理,新建的配置参考上面的章节,有以下几点区别:
1**、不用写 agent启动类,因为xxx-agent.jar已经有相关代码了**
2**、需将agent.jar文件直接放入与plugin.properties同级别的目录中**
3**、需在plugin.properties中指定jar路径,这样one-java-agent才可以启动此jar包**
4**、需在assembly.xml中指定将jar打包**
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>bin</id>
<formats>
<format>zip</format>
<format>dir</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>false</unpack>
<outputFileNameMapping>${artifactId}.jar</outputFileNameMapping>
<includes>
<include>${artifact}</include>
</includes>
</dependencySet>
<dependencySet>
<outputDirectory>/lib</outputDirectory>
<unpack>false</unpack>
<excludes>
<exclude>${artifact}</exclude>
</excludes>
</dependencySet>
</dependencySets>
<!--
使用文件组进行打包,把plugin.properties文件和xxx.jar文件(对于已经做好的javaagent JAR文件需要放入插件的根目录下)
-->
<fileSets>
<fileSet>
<directory>src/main/</directory>
<includes>
<include>*.properties</include>
<include>*.jar</include>
</includes>
<outputDirectory></outputDirectory>
</fileSet>
</fileSets>
</assembly>
这样即可集成进one-java-agent体系中,实际例子参照代码工程中的opentelemetry-plugin子模块
已开发好的agent源码接入
对于已开发完毕的agnet源码工程,也需跟场景一一样新建子工程,只不过跳过新的agent启动类编写,而去改造已有的agent启动类以及修改新编写的插件激活器PluginActivator。改造如下:
一般来说,一个已开发好的Agent 源码,它会有一个包含 premain 函数的启动类,假设为MyAgent类:
public class MyAgent {
public static void premain(String args, Instrumentation inst) {
// do something
}
}
可以先把原来的初始化逻辑抽取为init
函数,把原来的初始化逻辑移到里面,例如:
public class MyAgent {
public static void premain(String args, Instrumentation inst) {
init(args, inst);
}
public static void init(String args, Instrumentation inst) {
// do something
}
}
然后按上面的文档,在插件激活器PluginActivator的init
函数里调用原来的MyAgent.init(args, instrumentation);
函数
public class MyActivator implements PluginActivator {
...
@Override
public void init(PluginContext context) throws Exception {
Instrumentation instrumentation = context.getInstrumentation();
String args = context.getProperty("args");
MyAgent.init(args, instrumentation);
}
...
}
参数传递方式
对于agent插件所需的参数,一般有如下几种传递方式:
1、配置到插件的plugin.properties中,通过PluginContext#getProperty("key1")来获取值
2、通过-D
参数配置,比如插件aaa
,则可以配置为-Doneagent.plugin.aaa.key1=value1,
然后可以通过PluginContext#getProperty("key1")
来获取值。
3、配置在环境变量中,使用System.getenv(”xxx“)获取,如opentelemetry通过在启动脚本中export设置环境变量,opentelemetry agent会从环境变量中获取。
4、通过-D参数传递,然后在代码中用System.getProperty(“xxxx”)获取,如elastic apm agent则是通过-D参数传递,
打包
编译后使用mvn clean package -P local -DskipTests
会打包后安装最新到本地 ~/oneoneagent
目录下如C:\Users\Administrator\oneagent:
其中core存放的是one-java-agent的jar包,在使用时直接使用此agent做java-agent
而plugins则是存放相关可插拔的插件:
运行
在打完包后,使用如下:
java -javaagent:C:\Users\Administrator\oneagent\core\oneagent@0.0.2\one-java-agent.jar -jar ./springboot-mybatis2-1.0-SNAPSHOT.jar
可以通过-D来指定参数,可以指定的参数如下:
- oneagent.verbose 打印
trace
级别的日志,打印日志到stdout
- oneagent.plugin.disabled 禁止指定插件启动,比如
oneagent.plugin.disabled=aaa,bbb,ccc
- oneagent.plugin.${pluginName}.enabled 指定是否启动某个插件,比如: oneagent.plugin.aaa.enabled
Q&A
1、其他团队管理的one-java-agent打包出来的插件文件夹直接扔到统一的plugns下能用吗 。回答: 能用。
2、elastic-apm agent 使用-D的方式参数是否能传递。 回答:能
3、opentelemetry agent使用export设置环境变量是否能传递 回答:能
4、插件存在多版本的情况是否会加载最大版本 。回答: 20220428目前不会,维护者已经列出issue,待解决。
5、两个agent如果代理了同一个类,会不会起冲突?不会,各自的增强代码会不受影响低执行
6、不同agent的类能否实现共享?回答:可以,通过exportPackages,importPackages实现类共享,然后通过反射来执行共享类的方法。