1.1 java agent原理
我们知道,要使用Skywalking去监控服务,需要在其 VM 参数中添加 "-
javaagent:/usr/local/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar"。这里就 使用到了java agent技术。
Java agent 是什么?
Java agent是java命令的一个参数。参数 javaagent 可以用于指定一个 jar 包。
- 这个 jar 包的 MANIFEST.MF 文件必须指定 Premain-Class 项。
- Premain-Class 指定的那个类必须实现 premain() 方法。
当Java 虚拟机启动时,在执行 main 函数之前,JVM 会先运行 -javaagent所指定 jar 包内 Premain- Class 这个类的 premain 方法 。
如何使用java agent?
使用 java agent 需要几个步骤:
- 定义一个 MANIFEST.MF 文件,必须包含 Premain-Class 选项,通常也会加入Can-Redefine- Classes 和 Can-Retransform-Classes 选项。
- 创建一个Premain-Class 指定的类,类中包含 premain 方法,方法逻辑由用户自己确定。
- 将 premain 的类和 MANIFEST.MF 文件打成 jar 包。
- 使用参数 -javaagent: jar包路径 启动要代理的方法。
1.1.1 搭建java agent工程
使用maven创建java_agent_demo工程
在java文件夹下新建PreMainAgent类
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import java.lang.instrument.Instrumentation;
public class PreMainAgent {
/**
* 在这个 premain 函数中,开发者可以进行对类的各种操作。
* 1、agentArgs 是 premain 函数得到的程序参数,随同 "-- javaagent"一起传入。与 main 函数不同的是,
* 这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
* 2、Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。*
* java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,
* 集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=========premain方法执行1========");
System.out.println(agentArgs);
}
/**
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 则会执行 premain(String agentArgs)
* @param agentArgs
*/
public static void premain(String agentArgs) {
System.out.println("=========premain方法执行2========");
System.out.println(agentArgs);
}
}
类中提供两个静态方法,方法名均为premain,不能拼错
在pom文件中添加打包插件
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>PreMainAgent</Premain-Class>
<Agent-Class>PreMainAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
该插件会在自动生成META-INF/MANIFEST.MF文件时,帮我们添加agent相关的配置信息。 使用maven的package命令进行打包:
打包成功之后,复制打包出来的jar包地址
1.1.2 搭建主工程
使用maven创建java_agent_user工程
Main
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
先运行一次,然后点击编辑MAIN启动类
在VM options中添加代码
代码为
-javaagent:路径\java-agent-demo-1.0-SNAPSHOT.jar=HELLOAGENT
启动时加载javaagent,指向上一节中编译出来的java agent工程jar包地址,同时在最后追加参数 HELLOAGENT。
运行MAIN方法,查看结果:
可以看到java agent的代码优先于MAIN函数的方法运行,证明java agent运行正常
1.1.3 统计方法调用时间
Skywalking中对每个调用的时长都进行了统计,这一小节中我们会使用ByteBuddy和Java agent技术来 统计方法的调用时长。
Byte Buddy是开源的、基于Apache 2.0许可证的库,它致力于解决字节码操作和instrumentation API 的复杂性。Byte Buddy所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背 后。通过使用Byte Buddy,任何熟悉Java编程语言的人都有望非常容易地进行字节码操作。Byte Buddy提供了额外的API来生成Java agent,可以轻松的增强我们已有的代码。
添加依赖:
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.9.2</version>
</dependency>
</dependencies>
修改PreMainAgent代码
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;
import java.lang.instrument.Instrumentation;
public class PreMainAgent {
public static void premain(String agentArgs, Instrumentation inst) {
//创建一个转换器,转换器可以修改类的实现
//ByteBuddy对java agent提供了转换器的实现,直接使用即可
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
return builder
// 拦截任意方法
.method(ElementMatchers.<MethodDescription>any())
// 拦截到的方法委托给TimeInterceptor
.intercept(MethodDelegation.to(MyInterceptor.class));
}
};
new AgentBuilder // Byte Buddy专门有个AgentBuilder来处理Java Agent的场景
.Default()
// 根据包名前缀拦截类
.type(ElementMatchers.nameStartsWith("com.agent"))
// 拦截到的类由transformer处理
.transform(transformer)
.installOn(inst);
}
}
先生成一个转换器,ByteBuddy提供了java agent专用的转换器。通过实现Transformer接口利用 builder对象来创建一个转换器。转换器可以配置拦截方法的格式,比如用名称,本例中拦截所有方 法,并定义一个拦截器类
MyInterceptor。
创建完拦截器之后可以通过Byte Buddy的AgentBuilder建造者来构建一个agent对象。AgentBuilder可 以对指定的包名前缀来生效,同时需要指定转换器对象。
import net.bytebuddy.implementation.bind.annotation.Origin;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
public class MyInterceptor {
@RuntimeType
public static Object intercept(@Origin Method method,
@SuperCall Callable<?> callable)
throws Exception {
long start = System.currentTimeMillis();
try {
//执行原方法
return callable.call();
} finally {
//打印调用时长
System.out.println(method.getName() + ":" + (System.currentTimeMillis() - start) + "ms");
}
}
}
MyInterceptor就是一个拦截器的实现,统计的调用的时长。参数中的method是反射出的方法对象,而 callable就是调用对象,可以通过callable.call()方法来执行原方法。
重新打包,执行maven package命令。接下来修改主工程代码。主工程将Main类放置到 com.agent包 下。修改代码内容为:
Main
public class Main {
public static void main(String[] args) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello World");
}
}
休眠1秒,使统计时长的演示效果更好一些。执行main方法之后显示结果
我们在没有修改代码的情况下,利用java agent和Byte Buddy统计出了方法的时长,Skywalking的 agent也是基于这些技术来实现统计调用时长。
1.2 Open Tracing介绍
OpenTracing通过提供平台无关、厂商无关 的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。OpenTracing中最核心的概念就是 Trace。
1.2.1 Trace的概念
在广义上,一个trace代表了一个事务或者流程在(分布式)系统中的执行过程。在OpenTracing标准 中,trace是多个span组成的一个有向无环图(DAG),每一个span代表trace中被命名并计时的连续性 的执行片段。
例如客户端发起的一次请求,就可以认为是一个Trace。将上面的图通过Open Tracing的语义修改完之 后做可视化,得到下面的图
图中每一个色块其实就是一个span
1.2.2 Span的概念
一个Span代表系统中具有开始时间和执行时长的逻辑运行单元。span之间通过嵌套或者顺序排列建立 逻辑因果关系。
Span里面的信息包括:操作的名字,开始时间和结束时间,可以附带多个 key:value 构成的 Tags(key 必须是String,value可以是 String, bool 或者数字),还可以附带 Logs 信息(不一定所有的实现都支持) 也是 key:value形式。
下面例子是一个 Trace,里面有8个 Span:
一个span可以和一个或者多个span间存在因果关系。OpenTracing定义了两种关系: ChildOf 和 FollowsFrom。这两种引用类型代表了子节点和父节点间的直接因果关系。未来,OpenTracing将支 持非因果关系的span引用关系。(例如:多个span被批量处理,span在同一个队列中,等等)
ChildOf 很好理解,就是父亲 Span 依赖另一个孩子 Span。比如函数调用,被调者是调用者的孩子,比 如说 RPC 调用,服务端那边的Span,就是 ChildOf 客户端的。很多并发的调用,然后将结果聚合起来 的操作,就构成了 ChildOf 关系。
如果父亲 Span 并不依赖于孩子 Span 的返回结果,这时可以说它他构成 FollowsFrom 关系。
如图所示,左边的每一条追踪代表一个Trace,而右边时序图中每一个节点就是一个Span。
1.2.3 Log的概念
每个span可以进行多次Logs操作,每一次Logs操作,都需要一个带时间戳的时间名称,以及可选的任 意大小的存储结构。
如下图是一个异常的Log:
如下图是两个正常信息的Log,它们都带有时间戳和对应的事件名称、消息内容
1.2.4 Tags的概念
每个span可以有多个键值对(key:value)形式的Tags,Tags是没有时间戳的,支持简单的对span进行 注解和补充。
如下图就是一个Tags的详细信息,其中记录了数据库访问的SQL语句等内容。