
本文的主角是一款非常著名且使用的链路追踪工具: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打包后的内容覆盖掉。
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-assembly-plugin</artifactId>
- <configuration>
- <descriptorRefs>
- <descriptorRef>jar-with-dependencies</descriptorRef>
- </descriptorRefs>
- <archive>
- <manifestFile>
- src/main/resources/META-INF/MANIFEST.MF
- </manifestFile>
- </archive>
- </configuration>
- </plugin>
-
【推荐】引入编译插件 maven-assembly-plugin
直接使用maven-assembly-plugin插件就能达到使用agent的效果,所以推荐此方式。
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-assembly-plugin</artifactId>
- <configuration>
- <descriptorRefs>
- <descriptorRef>jar-with-dependencies</descriptorRef>
- </descriptorRefs>
- <archive>
- <manifestEntries>
- <Premain-Class>com.wjbgn.warriors.agent.StaticAgentTest</Premain-Class>
- <Can-Redefine-Classes>true</Can-Redefine-Classes>
- <Can-Retransform-Classes>true</Can-Retransform-Classes>
- </manifestEntries>
- </archive>
- </configuration>
- </plugin>
-
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,需要引入对应的依赖:
- <dependency>
- <groupId>com.sun</groupId>
- <artifactId>tools</artifactId>
- <version>1.8</version>
- <scope>system</scope>
- <systemPath>${java.home}/../lib/tools.jar</systemPath>
- </dependency>
-
修改配置文件:
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-assembly-plugin</artifactId>
- <configuration>
- <descriptorRefs>
- <descriptorRef>jar-with-dependencies</descriptorRef>
- </descriptorRefs>
- <archive>
- <manifestEntries>
- <Agent-Class>com.wjbgn.warriors.agent.DynamicAgentTest</Agent-Class>
- <Can-Redefine-Classes>true</Can-Redefine-Classes>
- <Can-Retransform-Classes>true</Can-Retransform-Classes>
- </manifestEntries>
- </archive>
- </configuration>
- </plugin>
-
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 的功能了。
关于静态agent和动态agent的简单介绍以及使用方式就介绍这么多,当然还有很多细节和有用的方面没有涉及,但是本节的重点意义在于让大家了解agent的工作方式,方便我们后面学习skywalking的agent实现。源码阅读过程中会针对涉及到的agent技术进行讲解。
前面花费大量的篇章来解释什么是agent,从本节开始,正式进入源码阅读阶段。
需要单独说明的:
如果你是用的是8.7.0之前的版本,skywalking的服务端和agent的代码是放在同一个项目工程skywalking下的。
如果使用8.7.0之后的版本,agent的相关代码被抽离出skywalkin当中,不同的语言会对应不同的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进行配置,该文件格式如下所示:

image.png
new PluginBootstrap().loadPlugins()加载插件的流程如下:

上图流程简介如下:
loadPlugins()PluginResourcesResolver,并调用其getResources()AgentClassLoader的getResources("skywalking-plugin.def")方法获取所有插件下的skywalking-plugin.def文件。ClassLoader,使用它的根据文件名称获取文件方法getResources(String name),得到Enumeration<URL>集合,通过遍历获取List<URL>List<URL>,通过PluginCfg.INSTANCE.load加载插件。
pluginDefine = reader.readLine()逐行读取内容PluginDefine.build(pluginDefine)构建读取到的内容=进行截取,分别获取插件名称pluginName和类定义defineClassnew PluginDefine(pluginName, defineClass)List<PluginDefine>List<AbstractClassEnhancePluginDefine>得到所有的插件集合后,new PluginFinder(List<AbstractClassEnhancePluginDefine>)的主要作用是为已加载的插件做缓存,并且提供快速查找已加载插件的能力。
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<Class, BootService> 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<BootService> 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<Class, BootService> loadAllServices() {
- Map<Class, BootService> bootedServices = new LinkedHashMap<>();
- List<BootService> 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实现字节码增强默认实现,覆盖实现的注解定义方式。除了以上具体的,还会看到诸如策略模式、建造者模式、观察者等设计模式。
阅读源码,虽然很多时候会让你感到晦涩难懂,但是当你坚持下来,收获绝对是意想不到的。养成阅读源码的好习惯,将会在编码工作中起到很大的帮助。