本文的主角是一款非常著名且使用的链路追踪工具:SkyWalking
SkyWalking是一个APM(application performance monitor)系统,专门为微服务、云原生和基于容器(Docker、Kubernetes、Mesos)的架构而设计,包含了云原生架构下的分布式系统的监控、跟踪、诊断功能。
通过agent的方式,可以做到对代码无侵入式的介入,实时实现整个服务链路的监控。本文的将要介绍的重点是SkyWalking如何通过agent实现如此全面的监控功能。这里提到的agent是在java中使用的agent技术,本文所讲的内容也是依托于java为基础的。
JDK从1.5开始,引入了agent机制,用户可以通过-javaagent
参数使用agent技术。agent技术可以使JVM在加载class文件之前先加载agent文件,通过修改JVM传入的字节码来实现自定义代码的注入。
为什么称之为静态agent
?因为使用此种方式,不需要通过指定VM参数的方式,所以想要修改agent必须要对服务进行重启。
下面我们动手实现一个简单的静态agent。
1.1.2.1 配置
实现静态agent需要配置agent的启动类,用来发现方法
premain
,这是实现静态agent的启动方法,因为是在jvm加载类之前,所以叫做pre-agent
静态agent通常有两种方式:
MANIFEST.MF 文件
需要在resources下创建META-INF文件夹,在内部创建MANIFEST.MF文件,其格式如下(注意最后一行要换行,否则idea或报错):
Manifest-Version: 1.0
Premain-Class: com.wjbgn.warriors.agent.StaticAgentTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true
除此之外,还需要引入maven-assembly-plugin
插件,否则MANIFEST.MF文件的内容会被maven打包后的内容覆盖掉。
org.apache.maven.plugins
maven-assembly-plugin
jar-with-dependencies
src/main/resources/META-INF/MANIFEST.MF
【推荐】引入编译插件 maven-assembly-plugin
直接使用maven-assembly-plugin
插件就能达到使用agent的效果,所以推荐此方式。
org.apache.maven.plugins
maven-assembly-plugin
jar-with-dependencies
com.wjbgn.warriors.agent.StaticAgentTest
true
true
1.1.2.2 测试
创建一个测试类:
package com.wjbgn.warriors.agent;
import java.lang.instrument.Instrumentation;
/**
* @description: 静态agent测试类
* @author:weirx
* @date:2022/6/30 15:13
* @version:3.0
*/
public class StaticAgentTest {
/**
* description: 静态agent启动类
* @param agentArgs
* @param inst
* @return: void
* @author: weirx
* @time: 2022/6/30 15:14
*/
public static void premain(String agentArgs, Instrumentation inst) {
// 在springboot启动前打印一下文字
System.out.println("this is static agent");
// 打印vm参数配置的agent参数
System.out.println(agentArgs);
}
}
使用assembly插件打包,在idea中:
当然也可以使用命令:mvn assembly:single
。
打包后文件在项目的target
下。
在idea添加启动参数:
蓝色部分是携带的参数,其余部分指定agent的jar包位置,完整命令如下:
-javaagent:E:\workspace\warriors\warriors-agent\target\warriors-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar=[testAgnet]
启动项目
如上所示,成功输出我们预期内容。
JDK在1.6版本开始,又引入了
attach
方式,对于运行中的应用程序,可以对其附加agent,这一操作让我们可以动态的修改已经加载的类。这也是称之为动态agent
的原因。
通过VirtualMachine的
attach(pid)
可以获得VirtualMachine实例,之后通过loadAgent(agent path)
方法将指定的agent加载到正在运行的JVM当中,实现动态加载。
1.2.2.1 配置
想要使用VirtualMachine,需要引入对应的依赖:
com.sun
tools
1.8
system
${java.home}/../lib/tools.jar
修改配置文件:
org.apache.maven.plugins
maven-assembly-plugin
jar-with-dependencies
com.wjbgn.warriors.agent.DynamicAgentTest
true
true
1.2.2.2 准备测试环境
我们静态agent工程启动,保持在运行中:
查看其进程id:
E:\workspace\warriors>jps
15248 Launcher
6480 Jps
13764 RemoteMavenServer36
4532 WarriorsAgentApplication
9188
WarriorsAgentApplication的进程id是4532
。
1.2.2.3 测试
创建测试类
package com.wjbgn.warriors.agent;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
/**
* @description: 动态agent测试
* @author:weirx
* @date:2022/6/30 15:53
* @version:3.0
*/
public class DynamicAgentTest {
/**
* description: 动态agent,通过attach和loadAgent进行探针载入
*
* @param agentArgs
* @param inst
* @return: void
* @author: weirx
* @time: 2022/6/30 15:54
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
// 打印以下日志
System.out.println("this is static agent");
// 打印参数
System.out.println(agentArgs);
}
public static void main(String[] args) {
VirtualMachine virtualMachine = null;
try {
// 指定进程号
virtualMachine = VirtualMachine.attach("4532");
// 指定agent jar包路径和参数
virtualMachine.loadAgent("E:\workspace\warriors\warriors-agent\target\warriors-agent-0.0.1-SNAPSHOT-jar-with-dependencies.jar", "agentTest");
} catch (AttachNotSupportedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}
}
}
如上所示,包含两部分:
使用assembly插件打包,在idea中:
当然也可以使用命令:mvn assembly:single
。
打包后文件在项目的target
下。
启动程序
运行上面的main
方法,查看正在运行的4532
项目的控制台:
如上所示,已经输出了动态注入的内容。
无论是静态agent,还是动态,我们发现在其对应的方法当中,都有一个Instrumentation
的入参,那么它是做什么的呢?
使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
更多介绍参考:Java Instrumentation
关于静态agent
和动态agent
的简单介绍以及使用方式就介绍这么多,当然还有很多细节和有用的方面没有涉及,但是本节的重点意义在于让大家了解agent的工作方式,方便我们后面学习skywalking的agent实现。源码阅读过程中会针对涉及到的agent技术进行讲解。
前面花费大量的篇章来解释什么是agent,从本节开始,正式进入源码阅读阶段。
需要单独说明的:
如果你是用的是8.7.0之前的版本,skywalking的服务端和agent的代码是放在同一个项目工程skywalking下的。
如果使用8.7.0之后的版本,agent的相关代码被抽离出skywalking当中,不同的语言会对应不同的agent,需要根据需要去自行选择,比如java使用skywalking-java
在开始学习源码前,建议按照上面的说明提供的地址拷贝一份源码到本地,有利于对源码的学习。
skywalking是采用静态agent的方式,所以我们首先要找到它的agent启动类SkyWalkingAgent.java
的位置:
我们从上至下分析下premain的逻辑,只看关键代码位置:
2.1.1.1 定义插件寻找器
// 定义一个插件寻找器
final PluginFinder pluginFinder;
2.1.1.2 加载配置
// 初始化指定参数,我们启动项目时候,可以指定服务的名称等参数,内部还加载配置文件agent.config的内容
SnifferConfigInitializer.initializeCoreConfig(agentArgs);
使用过skywalking的都知道,我们有两种配置被监控你服务的方式:
不论何种方式,都会通过此行代码进行加载。
2.1.1.3 加载插件
//初始化插件寻找器
//使用PluginResourcesResolver加载并定义所有插件
pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
new PluginBootstrap().loadPlugins()
方法内部使用PluginResourcesResolver
加载所有的组件,并且要求组件必须定义在文件skywalking-plugin.def
当中:
如上图所示,我们常用的组件都会有其对应的插件定义在这个工程中,需要加载的类都会通过skywalking-plugin.def
进行配置,该文件格式如下所示:
new PluginBootstrap().loadPlugins()
加载插件的流程如下:
上图流程简介如下:
loadPlugins()
PluginResourcesResolver
,并调用其getResources()
AgentClassLoader
的getResources("skywalking-plugin.def")
方法获取所有插件下的skywalking-plugin.def
文件。ClassLoader
,使用它的根据文件名称获取文件方法getResources(String name)
,得到Enumeration
集合,通过遍历获取List
List
,通过PluginCfg.INSTANCE.load
加载插件。
pluginDefine = reader.readLine()
逐行读取内容PluginDefine.build(pluginDefine)
构建读取到的内容=
进行截取,分别获取插件名称pluginName
和类定义defineClass
new PluginDefine(pluginName, defineClass)
List
List
得到所有的插件集合后,new PluginFinder(List
的主要作用是为已加载的插件做缓存,并且提供快速查找已加载插件的能力。
ByteBuddy:Byte Buddy是一个字节码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。
2.1.2.1 实例化ByteBuddy
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
其中IS_OPEN_DEBUGGING_CLASS
如果开启,skywalking会相对于agent根目录的位置创建文件夹,记录这些被插桩的类。用来和skywalking开发者解决兼容性问题。
2.1.2.2 创建AgentBulider
用来定义ByteBuddy的一些行为。如下忽略无关的类:
AgentBuilder agentBuilder = new AgentBuilder.Default(byteBuddy).ignore(
nameStartsWith("net.bytebuddy.")
.or(nameStartsWith("org.slf4j."))
.or(nameStartsWith("org.groovy."))
.or(nameContains("javassist"))
.or(nameContains(".asm."))
.or(nameContains(".reflectasm."))
.or(nameStartsWith("sun.reflect"))
.or(allSkyWalkingAgentExcludeToolkit())
.or(ElementMatchers.isSynthetic()));
2.1.2.3 定义边缘类集合:EdgeClasses
JDK9ModuleExporter.EdgeClasses edgeClasses = new JDK9ModuleExporter.EdgeClasses();
其实EdgeClasses就是一个List,其中会包含ByteBuddy的核心类。
public class ByteBuddyCoreClasses {
private static final String SHADE_PACKAGE = "org.apache.skywalking.apm.dependencies.";
public static final String[] CLASSES = {
SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.RuntimeType",
SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.This",
SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.AllArguments",
SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.AllArguments$Assignment",
SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.SuperCall",
SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.Origin",
SHADE_PACKAGE + "net.bytebuddy.implementation.bind.annotation.Morph",
};
}
通过这些报名,我们发现其实它们是一些annotation(注解)
。
2.1.2.4 将EdgeClasses注入BoostrapClassLoader
BootstrapInstrumentBoost.inject(pluginFinder, instrumentation, agentBuilder, edgeClasses);
这个位置涉及到后面要讲解的东西,暂时先不讲解。
但是关于类加载器的内容要简单描述一下,我们了解类加载器的同学应该知道java的类加载关系:
为什么要注入到BoostrapClassLoader?
自定义的ClassLoader只能在最下层,而AgentClassLoader通过字节码修改的类,是不能够被BootStrapClassLoader直接使用的,所以需要注入进去。
2.1.2.5 打开读边界
这行主要为了解决jdk9中模块系统的跨模块类访问问题。与本文重点无关,略过。
JDK9ModuleExporter.openReadEdge(instrumentation, agentBuilder, edgeClasses);
2.1.2.6 AgentBuilder属性设置
agentBuilder
// 指定ByteBuddy修改的符合条件的类
.type(pluginFinder.buildMatch())
// 指定字节码增强工具
.transform(new Transformer(pluginFinder))
//指定字节码增强模式,REDEFINITION覆盖修改内容,RETRANSFORMATION保留修改内容(修改名称),
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
// 注册监听器-监听异常情况,输出日子
.with(new RedefinitionListener())
// 对transform和异常情况监听,输出日志
.with(new Listener())
// 将定义好的agent注册到instrumentation
.installOn(instrumentation);
ServiceManager.INSTANCE.boot();
如上所示ServiceManager
基于ServerLoader
实现,是JDK提供的一种SPI机制。
boot()方法:
public void boot() {
// 加载所有的服务到bootServices
bootedServices = loadAllServices();
// 准备
prepare();
// 启动
startup();
//完成
onComplete();
}
2.1.3.1 bootedServices是什么?
private Map bootedServices = Collections.emptyMap();
其中的BootService
是一个接口,其包含了服务的生命周期,插件开始工作时,需要被启动,如下所示:
public interface BootService {
/**
* 准备
* @throws Throwable
*/
void prepare() throws Throwable;
/**
* 启动
* @throws Throwable
*/
void boot() throws Throwable;
/**
* 完成
* @throws Throwable
*/
void onComplete() throws Throwable;
/**
* 停止
* @throws Throwable
*/
void shutdown() throws Throwable;
/**
* 优先级,优先级高的BootService会优先启动
*/
default int priority() {
return 0;
}
}
所有实现了BootService接口的服务都会在此被加载。
2.1.3.2 loadAllServices()
这个方法通过jdk提供的SPI机制,ServiceLoader
将需要的类加载进来,而这些类被定义在指定的配置文件当中:
配置文件内容如下:
org.apache.skywalking.apm.agent.core.remote.TraceSegmentServiceClient
org.apache.skywalking.apm.agent.core.context.ContextManager
org.apache.skywalking.apm.agent.core.sampling.SamplingService
org.apache.skywalking.apm.agent.core.remote.GRPCChannelManager
org.apache.skywalking.apm.agent.core.jvm.JVMMetricsSender
org.apache.skywalking.apm.agent.core.jvm.JVMService
org.apache.skywalking.apm.agent.core.remote.ServiceManagementClient
org.apache.skywalking.apm.agent.core.context.ContextManagerExtendService
org.apache.skywalking.apm.agent.core.commands.CommandService
org.apache.skywalking.apm.agent.core.commands.CommandExecutorService
org.apache.skywalking.apm.agent.core.profile.ProfileTaskChannelService
org.apache.skywalking.apm.agent.core.profile.ProfileSnapshotSender
org.apache.skywalking.apm.agent.core.profile.ProfileTaskExecutionService
org.apache.skywalking.apm.agent.core.meter.MeterService
org.apache.skywalking.apm.agent.core.meter.MeterSender
org.apache.skywalking.apm.agent.core.context.status.StatusCheckService
org.apache.skywalking.apm.agent.core.remote.LogReportServiceClient
org.apache.skywalking.apm.agent.core.conf.dynamic.ConfigurationDiscoveryService
org.apache.skywalking.apm.agent.core.remote.EventReportServiceClient
org.apache.skywalking.apm.agent.core.ServiceInstanceGenerator
通过下面的方法将上面配置的所有实现了bootService
的类加载到allServices当中。
void load(List allServices) {
for (final BootService bootService : ServiceLoader.load(BootService.class, AgentClassLoader.getDefault())) {
allServices.add(bootService);
}
}
在上一步加载完全部的类之后,需要去遍历这些类,得到一个bootedServices的集合。在看代码逻辑之前,需要看下skywalking定义的两个注解:
@DefaultImplementor
默认实现@OverrideImplementor
覆盖实现带有@DefaultImplementor
注解的类,表示它会有类去继承它,继承它的类需要带有 @OverrideImplementor
注解,并指定继承的类的名称,例如:
默认实现类:
@DefaultImplementor
public class JVMMetricsSender implements BootService, Runnable, GRPCChannelListener
继承它的类:
@OverrideImplementor(JVMMetricsSender.class)
public class KafkaJVMMetricsSender extends JVMMetricsSender implements KafkaConnectionStatusListener
在了解了skywalking的默认类和继承类的机制后,有代码逻辑如下:
private Map loadAllServices() {
Map bootedServices = new LinkedHashMap<>();
List allServices = new LinkedList<>();
// SPI加载
load(allServices);
// 遍历
for (final BootService bootService : allServices) {
Class extends BootService> bootServiceClass = bootService.getClass();
// 是否带有默认实现
boolean isDefaultImplementor = bootServiceClass.isAnnotationPresent(DefaultImplementor.class);
if (isDefaultImplementor) {// 是默认实现
// 是默认实现,没有添加到bootedServices
if (!bootedServices.containsKey(bootServiceClass)) {
//加入bootedServices
bootedServices.put(bootServiceClass, bootService);
} else {
//ignore the default service
}
} else {// 不是默认实现
// 是否是覆盖实现
OverrideImplementor overrideImplementor = bootServiceClass.getAnnotation(OverrideImplementor.class);
// 不是覆盖
if (overrideImplementor == null) {
// bootedServices没有
if (!bootedServices.containsKey(bootServiceClass)) {
//加入bootedServices
bootedServices.put(bootServiceClass, bootService);
} else {
throw new ServiceConflictException("Duplicate service define for :" + bootServiceClass);
}
} else {
// 是覆盖,value获取的是其继承的类targetService
Class extends BootService> targetService = overrideImplementor.value();
// 如果bootedServices已经包含targetService
if (bootedServices.containsKey(targetService)) {
// 判断targetServices是否是默认实现
boolean presentDefault = bootedServices.get(targetService)
.getClass()
.isAnnotationPresent(DefaultImplementor.class);
// 是默认实现
if (presentDefault) {
// 添加进去
bootedServices.put(targetService, bootService);
} else {
// 没有默认实现,不能覆盖,抛出异常
throw new ServiceConflictException(
"Service " + bootServiceClass + " overrides conflict, " + "exist more than one service want to override :" + targetService);
}
} else {
// 是覆盖实现,它覆盖的默认实现还没有被加载进来
bootedServices.put(targetService, bootService);
}
}
}
}
return bootedServices;
}
2.1.3.3 prepare(),startup(),onComplete()
在加载完全部的类之后,还有准备,启动和完成等分操作,它们的代码实现相同,如下所示:
private void prepare() {
// 获取所有的类
bootedServices.values().stream()
// 根据优先级排序,BootService的priority
.sorted(Comparator.comparingInt(BootService::priority))
// 遍历
.forEach(service -> {
try {
// 执行每一个BootService的实现类的prepare()方法
service.prepare();
} catch (Throwable e) {
LOGGER.error(e, "ServiceManager try to pre-start [{}] fail.", service.getClass().getName());
}
});
}
private void startup() {
bootedServices.values().stream()
// 根据优先级排序,BootService的priority
.sorted(Comparator.comparingInt(BootService::priority))
// 遍历
.forEach(service -> {
try {
// 执行每一个BootService的实现类的boot()方法
service.boot();
} catch (Throwable e) {
LOGGER.error(e, "ServiceManager try to start [{}] fail.", service.getClass().getName());
}
});
}
private void onComplete() {
// 遍历
for (BootService service : bootedServices.values()) {
try {
// 执行每一个BootService的实现类的onComplete()方法
service.onComplete();
} catch (Throwable e) {
LOGGER.error(e, "Service [{}] AfterBoot process fails.", service.getClass().getName());
}
}
}
为skywalking的运行服务添加一个shutdown的钩子。
Runtime.getRuntime()
.addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));
shutdown方法,与准备和启动方法不同之处在于,shutdown的排序方式是按照优先级的倒序排序,为了优雅的停机,后启动的服务,先停机:
public void shutdown() {
bootedServices.values().stream().
// 排序,按照优先级的倒序
sorted(Comparator.comparingInt(BootService::priority).reversed())
.forEach(service -> {
try {
// 执行每个服务的shutdown
service.shutdown();
} catch (Throwable e) {
LOGGER.error(e, "ServiceManager try to shutdown [{}] fail.", service.getClass().getName());
}
});
}
本章节用了不小的篇幅讲解premain方法的源码启动过程,主要包括以下的方面:
本章主要在于讲解启动流程,代码量较大,涉及到字节码增强的关键暂时未讲解。
到此为止,关于skywalking的相关启动流程就基本介绍完成了。之所以说是基本完成,是因为内部还以些关于插件启动流程的部分没有具体讲解,比如:
限于篇幅原因,后续文章继续讲解。
通过本篇文章,相信您一定也有了不少的收获:
def
文件定义插件的方式,即插件的加载过程ByteBuddy
实现字节码增强默认实现
,覆盖实现
的注解定义方式。除了以上具体的,还会看到诸如策略模式、建造者模式、观察者等设计模式。
阅读源码,虽然很多时候会让你感到晦涩难懂,但是当你坚持下来,收获绝对是意想不到的。养成阅读源码的好习惯,将会在编码工作中起到很大的帮助。
本文到此结束,感谢阅读!