Skywalking原理篇(一):Agent 启动流程解析

Java Agent简介

什么是Java Agent

Java Agent本质上可以理解为一个插件,该插件就是一个特制的Jar包。这个Jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改

如何实现一个Java Agent

实现Agent启动方法

Java Agent 支持目标JVM启动时加载以及JVM运行时加载,这两种不同的加载模式会使用不同的入口函数。
如果需要在目标JVM启动的同时加载 Agent,那么可以选择实现下面的方法

  1. public static void premain(String agentArgs, Instrumentation inst)
  2. public static void premain(String agentArgs)

如果需要在目标JVM运行时加载 Agent,那么可以选择实现下面的方法

  1. public static void agentmain(String agentArgs, Instrumentation inst)
  2. public static void agentmain(String agentArgs)

JVM 将首先寻找1,如果没有发现1,再寻找2。代码中有两个参数需要注意
agentArgs:-javaagent 命令携带的参数。agent.service_name 这个配置项的默认值有三种覆盖方式,其中,使用探针配置进行覆盖,探针配置的值就是通过该参数传入的
instjava.lang.instrumen.Instrumentation 是 Instrumention 包中定义的一个接口,它提供了操作类定义的相关方法,核心方法如下

  • addTransformer()/removeTransformer():注册/注销一个 ClassFileTransformer 类的实例,该 Transformer 会在类加载的时候被调用,可用于修改类定义
  • redefineClasses():该方法针对的是已经加载的类,它会对传入的类进行重新定义
  • getAllLoadedClasses():返回当前 JVM 已加载的所有类
  • getInitiatedClasses():返回当前 JVM 已经初始化的类
  • getObjectSize():获取参数指定的对象的大小

指定Main-Class

定义一个 MANIFEST.MF 文件,在其中添加 premain-classAgent-Class 配置项

Manifest-Version: 1.0
PreMain-Class: com.test.AgentClass
Agent-Class: com.test.AgentClass

Agent加载

  • 启动时加载:启动参数增加-javaagent:[path],其中path为对应的agent的jar包路径
  • 运行中加载:使用com.sun.tools.attach.VirtualMachine加载

SkyWalking Agent启动流程

核心流程

skywalking的agent入口函数在org.apache.skywalking.apm.agent.SkywalkingAgent#premain
方法具体实现如下(省略了异常代码块及日志打印)

final PluginFinder pluginFinder;
// 初始化配置信息
SnifferConfigInitializer.initializeCoreConfig(agentArgs);
// 加载agent插件, 并使用PluginFinder为插件进行分类
pluginFinder = new PluginFinder(new PluginBootstrap().loadPlugins());
// 创建一个ByteBuddy对象用于修改字节码
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
// 创建一个AgentBuilder对象,详细代码省略...
AgentBuilder agentBuilder = new AgentBuilder(byteBuddy)...
// 使用JDK SPI加载并启动BootService
ServiceManager.INSTANCE.boot();
// 添加一个JVM勾子函数, 在JVM退出时关闭所有BootService服务
Runtime.getRuntime()
    .addShutdownHook(new Thread(ServiceManager.INSTANCE::shutdown, "skywalking service shutdown thread"));

核心启动流程如下图所示
Skywalking原理篇(一):Agent 启动流程解析_第1张图片

初始化配置

配置初始化的入口方法在
org.apache.skywalking.apm.agent.core.conf.SnifferConfigInitializer
#initializeCoreConfig
方法的具体实现如下(省略了异常代码块及日志打印)

AGENT_SETTINGS = new Properties();
// 1.加载agent.config配置文件
final InputStreamReader configFileStream = loadConfig();
AGENT_SETTINGS.load(configFileStream);
for (String key : AGENT_SETTINGS.stringPropertyNames()) {
    String value = (String) AGENT_SETTINGS.get(key);
    // 2.解析agent.config文件中的配置
    AGENT_SETTINGS.put(key, PropertyPlaceholderHelper.INSTANCE.replacePlaceholders(value, AGENT_SETTINGS));
}
// 3.解析skywalking.开头的系统参数,截取后写入配置类
overrideConfigBySystemProp();
agentOptions = StringUtil.trim(agentOptions, ',');
if (!StringUtil.isEmpty(agentOptions)) {
	agentOptions = agentOptions.trim();
    // 4.解析Java Agent参数, 写入配置类
    overrideConfigByAgentOptions(agentOptions);
}
// 5.将配置类中的信息填充到Config类对应的静态字段中
initializeConfig(Config.class);
IS_INIT_COMPLETED = true;

可以看到配置信息会依次从三个地方进行获取并覆盖

  • agent.config 文件
  • 系统参数
  • javaagent options

所以,配置参数的优先级为 javaagent options > 系统参数 > agent.config文件
下面通过在3个地方同时配置 agent.service_name 参数来验证一下配置加载流程
Live-Demo + skywalking源码 项目为例
修改项目 ProjectB 的JVM启动参数,同时配置 javaagent options、系统参数

-javaagent:/Users/wangbo/Documents/Study_Workspace/skywalking/skywalking-agent/skywalking-agent.jar=agent.service_name=Project_Options
-Dskywalking.agent.service_name=Project_Env

覆盖 agent.config 中指定参数的的环境变量

SW_AGENT_NAME=Project_Config

Skywalking原理篇(一):Agent 启动流程解析_第2张图片

然后debug启动 projectB 进行源码调试

加载agent.config配置文件

private static final String SPECIFIED_CONFIG_PATH = "skywalking_config";
private static final String DEFAULT_CONFIG_FILE_NAME = "/config/agent.config";
String specifiedConfigPath = System.getProperty(SPECIFIED_CONFIG_PATH);
File configFile = StringUtil.isEmpty(specifiedConfigPath) ? new File(
    AgentPackagePath.getPath(), DEFAULT_CONFIG_FILE_NAME) : new File(specifiedConfigPath);

源码中第一步为加载 agent.config 配置文件,可以看出agent.config 文件加载的优先级为
环境变量 skywalking_config 指定的路径 > skywalking-agent.jar 同级的config目录

解析agent.config配置

Skywalking原理篇(一):Agent 启动流程解析_第3张图片
第一步agent.config文件加载到properties后,默认的配置格式都是 **配置项 = ${环境变量:配置默认值}**
第二步将对具体的值进行解析,首先判断环境变量中是否包含该配置,有则替换,无则使用默认值,所以解析后的配置格式变为 **配置项 = 配置值** 。可以看到 agent.service_name 对应的值为Project_Config

解析系统参数配置Skywalking原理篇(一):Agent 启动流程解析_第4张图片

第三步为解析系统参数,可以看到 agent.service_name 被覆盖为 Project_Env

解析javaagent options配置

Skywalking原理篇(一):Agent 启动流程解析_第5张图片
第四步为解析javaagent options参数,可以看到agent.service_name被覆盖为Project_Options

将解析出来的配置全部填充到Conig静态配置类中

Skywalking原理篇(一):Agent 启动流程解析_第6张图片
配置加载的最终目的就是将配置项全部填充到Config配置类的各个静态字段中,这样后续使用配置信息时直接通过Config类进行获取即可,可以看到之前配置的 agent.service_name 在最后一步被确定为 Project_Options

插件加载

加载插件的入口方法在
org.apache.skywalking.apm.agent.core.plugin.PluginBootstrap#loadPlugins
方法的具体实现如下

// 自定义类加载器初始化
AgentClassLoader.initDefaultLoader();
// 创建插件定义解析类
PluginResourcesResolver resolver = new PluginResourcesResolver();
// 通过调用AgentClassLoader的资源加载方法解析出所有插件定义文件的路径
List<URL> resources = resolver.getResources();
if (resources == null || resources.size() == 0) {
	return new ArrayList<AbstractClassEnhancePluginDefine>();
}
// 将插件定义文件解析为插件配置类 
for (URL pluginUrl : resources) {
	PluginCfg.INSTANCE.load(pluginUrl.openStream());
}
List<PluginDefine> pluginClassList = PluginCfg.INSTANCE.getPluginClassList();
List<AbstractClassEnhancePluginDefine> plugins = new ArrayList<AbstractClassEnhancePluginDefine>();
for (PluginDefine pluginDefine : pluginClassList) {
    // 通过反射将所有插件定义类实例化
	AbstractClassEnhancePluginDefine plugin = (AbstractClassEnhancePluginDefine) Class.forName(pluginDefine.getDefineClass(), true, AgentClassLoader
    	.getDefault()).newInstance();
    plugins.add(plugin);
}
// 通过SPI的方式发现并实例化其他插件并添加到plugins中
plugins.addAll(DynamicPluginLoader.INSTANCE.load(AgentClassLoader.getDefault()));

自定义类加载器 AgentClassLoader

由于应用默认的类加载器只能加载JVM相关的类库及 Classpath 下的类库,skywalking 为了实现不修改应用结构,无侵入加载插件jar包,自定义了一个 AgentClassLoader
AgentClassLoader 的静态代码块会调用 registerAsParallelCapable() 方法开启并行类加载功能,一方面能优化类加载的速度,另一方面是为了解决JVM7以上版本的类加载器死锁问题

https://github.com/apache/skywalking/pull/2016

旧版的 AgentClassLoader 未开启并行机制,默认的loadClass方法会使用 synchronized 锁住当前的类加载器对象,当 WebAppClassLoaderAgentClassLoader 加载的类有依赖关系时就会产生死锁。而开启了并行机制后,loadClass 会将锁的级别降低到被加载的类的级别上,不再对类加载器进行加锁
PS:当委派模型没有严格分层的环境中(如出现闭环委托),类加载器需要具有并行能力,否则会导致死锁

我们知道agent的插件目录有四种类型

  • 引导插件:在agent的 bootstrap-plugins 目录下
  • 内置插件:在agent的 plugins 目录下
  • 可选插件:在agent的 optional-plugins 目录下
  • 激活插件:在agent的 activations 目录下

再通过查看 AgentClassLoader 的构造函数可以发现默认只加载pluginsactivations 路径下的插件

// Config.Plugin.MOUNT = Arrays.asList("plugins", "activations");
public AgentClassLoader(ClassLoader parent) throws AgentPackageNotFoundException {
    super(parent);
    File agentDictionary = AgentPackagePath.getPath();
    classpath = new LinkedList<>();
    Config.Plugin.MOUNT.forEach(mountFolder -> classpath.add(new File(agentDictionary, mountFolder)));
}

解析插件定义

PluginResourcesResolver 是Agent插件的资源解析器,会通过 AgentClassLoader 中的
findResource() 方法读取所有Agent插件中的 skywalking-plugin.def 文件
以activemq-5.x-plugin插件为例

activemq-5.x=org.apache.skywalking.apm.plugin.activemq.define.ActiveMQProducerInstrumentation
activemq-5.x=org.apache.skywalking.apm.plugin.activemq.define.ActiveMQConsumerInstrumentation

拿到所有 def 文件后,会使用 PluginCfg 类将def中的定义转换为 PluginDefine 对象,该对象有两个字段

public class PluginDefine {
    // 插件名称:activemq-5.x
    private String name;
    // 插件定义类的路径:org.apache.skywalking.apm.plugin.activemq.define.ActiveMQProducerInstrumentation
    private String defineClass;
}

然后遍历所有 PluginDefine 对象,通过反射 defineClass 字段将每一个插件实例化为
AbstractClassEnhancePluginDefine 对象,添加到plugins列表中,最后将SPI发现并实例化其他插件也添加到plugins列表中返回

为什么实例化的类为 AbstractClassEnhancePluginDefine ?

该类是所有 Agent 插件类的顶级父类,通过官方的 插件开发指南 可以得知,SkyWalking 提供两类通用的定义去拦截构造方法, 实例方法和类方法

  • ClassInstanceMethodsEnhancePluginDefine 定义了构造方法 Contructor 拦截点和 instance method 实例方法拦截点.

  • ClassStaticMethodsEnhancePluginDefine 定义了类方法 class method 拦截点

  • 也可以继承 ClassEnhancePluginDefine 去设置所有的拦截点, 但这不常用

类图关系如下
Skywalking原理篇(一):Agent 启动流程解析_第7张图片

可以看到这里是一个典型的模版模式的使用场景

AbstractClassEnhancePluginDefine 抽象出了所有实现类增强时需要调用的方法
ClassEnhancePluginDefine 定义了所有抽象方法的具体执行步骤
然后真正的增强逻辑全部通过实现 ClassInstanceMethodsEnhancePluginDefine
ClassStaticMethodsEnhancePluginDefine 中的抽象方法延迟到子类中

其中的核心方法定义

  • define() :插件类增强逻辑的入口,定义了如何拦截需要修改的 Java 类
  • enhance() :真正调用byte buddy执行增强逻辑的地方
  • enhanceClass() :返回 ClassMatch,用于匹配当前插件要增强的目标类
  • witnessClass() :一个开源组件可能有多个版本,插件会通过该方法识别组件的不同版本,防止对不兼容的版本进行增强
  • getConstructorsInterceptPoints() :获取 Contructor 构造器拦截点列表
  • getInstanceMethodsInterceptPoints() :获取 instance method 实例方法拦截点列表
  • getStaticMethodsInterceptPoints() :获取 class method 类方法拦截点列表

插件分类

插件加载方法 loadPlugins() 执行结束后返回了已经实例化的 AbstractClassEnhancePluginDefine 插件定义类列表,然后传入了 PluginFinder 的构造函数中
org.apache.skywalking.apm.agent.core.plugin.PluginFinder#PluginFinder()

// 使用NameMatch的插件定义实例列表
private final Map<String, LinkedList<AbstractClassEnhancePluginDefine>> nameMatchDefine = new HashMap<String, LinkedList<AbstractClassEnhancePluginDefine>>();
// 使用IndirectMatch的插件定义实例列表
private final List<AbstractClassEnhancePluginDefine> signatureMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();
// 引导类插件定义实例列表
private final List<AbstractClassEnhancePluginDefine> bootstrapClassMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();

public PluginFinder(List<AbstractClassEnhancePluginDefine> plugins) {
    for (AbstractClassEnhancePluginDefine plugin : plugins) {
        // 获取当前插件定义类的ClassMatch
        ClassMatch match = plugin.enhanceClass();

        if (match == null) {
            continue;
        }

        // 如果匹配规则为名称匹配
        if (match instanceof NameMatch) {
            NameMatch nameMatch = (NameMatch) match;
            LinkedList<AbstractClassEnhancePluginDefine> pluginDefines = nameMatchDefine.get(nameMatch.getClassName());
            if (pluginDefines == null) {
                pluginDefines = new LinkedList<AbstractClassEnhancePluginDefine>();
                // 以类名为key, 类的实例对象为value写入nameMatchDefine集合中
                nameMatchDefine.put(nameMatch.getClassName(), pluginDefines);
            }
            pluginDefines.add(plugin);
        } else {
            // 如果不是名称匹配, 写入signatureMatchDefine集合中
            signatureMatchDefine.add(plugin);
        }

        // 引导类插件写入bootstrapClassMatchDefine集合中
        if (plugin.isBootstrapInstrumentation()) {
            bootstrapClassMatchDefine.add(plugin);
        }
    }
}

enhanceClass() 方法根据返回的 ClassMatch 类型进行分类,写入不同的集合中,并在后面 Byte Buddy 匹配需要增强的类时传入匹配规则
根据官方的 插件开发指南 得知,常见的类匹配有四种方式

  • byName 通过类的全限定名(Fully Qualified Class Name, 即 包名 + . + 类名).
  • byClassAnnotationMatch 根据目标类是否存在某些注解.
  • byMethodAnnotationMatch 根据目标类的方法是否存在某些注解.
  • byHierarchyMatch 根据目标类的父类或接口

对应的实现类如下

  • NameMatch :直接使用完整类名进行匹配
  • IndirectMatch :间接规则匹配接口
    • ClassAnnotationMatch :基于类注解进行匹配,可设置同时匹配多个。例如:@RequestMapping
    • HierarchyMatch :基于父类 / 接口进行匹配,可设置同时匹配多个。
    • MethodAnnotationMatch :基于方法注解进行匹配,可设置同时匹配多个。目前项目里主要用于匹配方法上的 @Trace 注解

Skywalking原理篇(一):Agent 启动流程解析_第8张图片

综上,PluginFinder 是持有了所有插件定义对象的一个容器,并对不同插件进行分类存放在了不同的集合中

创建 AgentBuilder

AgentBuilderByte Buddy 库专门用来支持 Java Agent 的一个 API
AgentBuilder 相关的核心代码如下

// 创建一个ByteBuddy对象用于修改字节码
final ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(Config.Agent.IS_OPEN_DEBUGGING_CLASS));
// 创建一个AgentBuilder对象
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())
	)
    // 省略部分代码 ... ...

    // 指定需要通过插件增强的类
    .type(pluginFinder.buildMatch())
    // 设置transform(具体的增强逻辑)
    .transform(new Transformer(pluginFinder))
    // 设置已加载类的agent修改策略
    .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
    //回调接口,用来在debug模式下保存增强后的字节码文件
    .with(new Listener())
    .installOn(instrumentation);

匹配规则

再来看一下匹配规则的具体逻辑,方法入口
org.apache.skywalking.apm.agent.core.plugin.PluginFinder#buildMatch

// 使用NameMatch的插件定义实例列表
private final Map<String, LinkedList<AbstractClassEnhancePluginDefine>> nameMatchDefine = new HashMap<String, LinkedList<AbstractClassEnhancePluginDefine>>();
// 使用IndirectMatch的插件定义实例列表
private final List<AbstractClassEnhancePluginDefine> signatureMatchDefine = new ArrayList<AbstractClassEnhancePluginDefine>();
public ElementMatcher<? super TypeDescription> buildMatch() {
    // 建立匹配规则, 在内部类中添加第一个默认规则
    ElementMatcher.Junction judge = new AbstractJunction<NamedElement>() {
        @Override
        public boolean matches(NamedElement target) {
            // 名称匹配模式的插件全部使用类名进行匹配
            return nameMatchDefine.containsKey(target.getActualName());
        }
    };
    // 排除所有的接口
    judge = judge.and(not(isInterface()));
    // 遍历特殊匹配规则模式的插件列表
    for (AbstractClassEnhancePluginDefine define : signatureMatchDefine) {
        ClassMatch match = define.enhanceClass();
        if (match instanceof IndirectMatch) {
            // 获取特殊匹配规则
            judge = judge.or(((IndirectMatch) match).buildJunction());
        }
    }
    // 返回ByteBuddy元素匹配器
    return new ProtectiveShieldMatcher(judge);
}

增强逻辑创建

AgentBuildertransform() 方法需要传入一个 Transformer 类,该类需要再实现一个
transform() 方法,每当有一个类加载时,就会触发 transform 逻辑,对类进行匹配和修改,所以这个
Transformer 类就是真正触发 ByteBuddy 增强逻辑的入口,我们看一下它的实现

private static class Transformer implements AgentBuilder.Transformer {
    private PluginFinder pluginFinder;

    Transformer(PluginFinder pluginFinder) {
        this.pluginFinder = pluginFinder;
    }

    @Override
    public DynamicType.Builder<?> transform(final DynamicType.Builder<?> builder,
                                            final TypeDescription typeDescription,
                                            final ClassLoader classLoader,
                                            final JavaModule module) {
        // typeDescription 是要被修改类的完整描述,pluginFinder的find方法可以根据描述类的类名或其他信息取出插件定义类实例
        List<AbstractClassEnhancePluginDefine> pluginDefines = pluginFinder.find(typeDescription);
        if (pluginDefines.size() > 0) {
            DynamicType.Builder<?> newBuilder = builder;
            // EnhanceContext 是为了保证流程的一个记录器, 比如执行了某个步骤后就会记录一下, 防止重复操作
            EnhanceContext context = new EnhanceContext();
            for (AbstractClassEnhancePluginDefine define : pluginDefines) {
                // 调用插件定义类的 define 方法真正执行修改字节码的逻辑
                DynamicType.Builder<?> possibleNewBuilder = define.define(
                    typeDescription, newBuilder, classLoader, context);
                if (possibleNewBuilder != null) {
                    newBuilder = possibleNewBuilder;
                }
            }
            if (context.isEnhanced()) {
                LOGGER.debug("Finish the prepare stage for {}.", typeDescription.getName());
            }
			// 返回修改后的字节码, 进行替换
            return newBuilder;
        }

        LOGGER.debug("Matched class {}, but ignore by finding mechanism.", typeDescription.getTypeName());
        return builder;
    }
}

启动BootService

Skywalking Agent 采用了插件化架构(又称微内核架构),其中 apm-agent-core 对应的就是内核系统,而
apm-sdk-plugin 对应的就是各个插件模块。前面的内容我们讲解了插件是如何被加载的,那么插件又是如何与 OAP 通信的?配置又是如何拉取的?这些功能服务就是封装在各个 BootService 当中​

BootService定义体系

首先所有的插件服务都需要实现接口 org.apache.skywalking.apm.agent.core.boot.BootService
其中定义了4个生命周期的方法

public interface BootService {
    void prepare() throws Throwable;
    void boot() throws Throwable;
    void onComplete() throws Throwable;
    void shutdown() throws Throwable;
}

其次每个 BootService 实现还需要使用到两种注解

  • @DefaultImplementor 用于标识 BootService 接口的默认实现
  • @OverrideImplementor 用于覆盖默认 BootService 实现,通过其 value 字段指定要覆盖的默认实现

并且覆盖实现只能覆盖默认实现,不能再去覆盖另外的覆盖实现

常见的BootService服务

  • GRPCChannelManager:维护 Agent 与后端 OAP 集群通信时使用的网络连接
  • JVMService:收集并发送服务实例的 CPU、堆内存使用情况以及 GC 信息
  • ContextManager:负责管理一个 SkyWalking Agent 中所有的 Context 对象
  • ContextManagerExtendService:负责创建 Context 对象
  • TraceSegmentServiceClient:负责将 Trace 数据序列化并发送到 OAP 集群
  • SamplingService:负责实现 Trace 的采样
  • ConfigurationDiscoveryService:检测动态配置并更新

BootService加载流程

再来看看启动boot服务的核心方法入口在 org.apache.skywalking.apm.agent.core.boot.ServiceManager 它是一个使用枚举实现单例模式的工具类,存放所有 BootService 的实例并管理其生命周期

// BootService实例容器,使用Map结构方便服务间直接通过Class对象进行调用
private Map<Class, BootService> bootedServices = Collections.emptyMap();

public void boot() {
    // 加载所有 BootService 的实现并写入实例容器
    bootedServices = loadAllServices();
	// 循环调用所有boot服务的prepare方法
    prepare();
    // 循环调用所有boot服务的boot方法
    startup();
    // 循环调用所有boot服务的onComplete方法
    onComplete();
}

private Map<Class, BootService> loadAllServices() {
    Map<Class, BootService> bootedServices = new LinkedHashMap<>();
    List<BootService> allServices = new LinkedList<>();
    // 加载出所有 BootService 实现
    load(allServices);
    for (final BootService bootService : allServices) {
        Class<? extends BootService> bootServiceClass = bootService.getClass();
        // 首先判断这个 BootService 服务是否有 DefaultImplementor 注解
        boolean isDefaultImplementor = bootServiceClass.isAnnotationPresent(DefaultImplementor.class);
        if (isDefaultImplementor) {
            // 判断这个 BootService 是否被加载过
            if (!bootedServices.containsKey(bootServiceClass)) {
                // 如果没有加载过则进行加载
                bootedServices.put(bootServiceClass, bootService);
            } else {
                //ignore the default service
            }
        } else {
            // 判断这个 BootService是否有 OverrideImplementor 注解
            OverrideImplementor overrideImplementor = bootServiceClass.getAnnotation(OverrideImplementor.class);
            if (overrideImplementor == null) {
                // 如果这个 BootService 既没有 DefaultImplementor 注解,又没有 OverrideImplementor 注解
                // 判断这个 BootService 是否被加载过
                if (!bootedServices.containsKey(bootServiceClass)) {
                    // 没有的话就当作是默认实现进行加载
                    bootedServices.put(bootServiceClass, bootService);
                } else {
                    // 如果被加载过说明这个 BootService 包含多个不一致的定义,则抛出异常
                    throw new ServiceConflictException("Duplicate service define for :" + bootServiceClass);
                }
            } else {
        		// 取出覆盖实现需要覆盖的 BootService Class 类
                Class<? extends BootService> targetService = overrideImplementor.value();
                // 判断这个 BootService 是否被加载过
                if (bootedServices.containsKey(targetService)) {
                    // 如果被加载过,则判断这个 BootService 是否有 DefaultImplementor 注解
                    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;
}

void load(List<BootService> allServices) {
    // 通过SPI机制从 META-INF/services 文件下加载出所有bootservice接口的实现
    for (final BootService bootService : ServiceLoader.load(BootService.class, AgentClassLoader.getDefault())) {
        allServices.add(bootService);
    }
}

最后再附上一张 BootService 加载流程图
Skywalking原理篇(一):Agent 启动流程解析_第9张图片

加载流程结束后会依次调用所有 BootService 实例的 prepare()boot()onComplete() 方法
这些服务全部启动起来之后,Agent 才能真正的对外提供服务

以 trace-ignore-plugin 插件为案例体验 BootService 服务如何进行扩展

项目结构
trace-ignore-plugin/
├── apm-trace-ignore-plugin.config
├── apm-trace-ignore-plugin.iml
├── pom.xml
└── src
    └── main
        ├── java/org/apache/skywalking/apm/plugin/trace
        │   └── ignore
        │       ├── TraceIgnoreExtendService.java # BootService覆盖实现类, 覆盖SamplingService
        │       ├── TraceIgnorePatternWatcher.java
        │       └── conf
        │           ├── IgnoreConfig.java
        │           │   └── IgnoreConfigInitializer.java
        │           └── matcher
        │               ├── FastPathMatcher.java
        │               └── TracePathMatcher.java
        └── resources
            └── META-INF
                └── services
                    └── org.apache.skywalking.apm.agent.core.boot.BootService # SPI增加BootService实现 

该插件主要扩展了 Trace 的采样实现 SamplingService
我们先简单了解一下 Trace 收集和发送时涉及的服务

TracingContext 保存了 Trace 的上下文信息,ContextManagerExtendService 则负责创建
TracingContext,并且会调用 SamplingServicetrySampling() 方法进行采样,如果采样失败(例如配置了 slow Trace 采样时间阈值),这时 ContextManagerExtendService 就会生成
IgnoredTracerContext,它是个空 Context 实现,不会记录 Trace 信息
​而 TraceIgnoreExtendService 这个覆盖实现重写了 SamplingServicetrySampling() 方法
它通过匹配我们配置的 ignore_path ,直接忽略了 Trace 信息的生成

@Override
public boolean trySampling(final String operationName) {
    if (patterns.length > 0) {
        for (String pattern : patterns) {
            if (pathMatcher.match(pattern, operationName)) {
                LOGGER.debug("operationName : " + operationName + " Ignore tracking");
                return false;
            }
        }
    }
    return super.trySampling(operationName);
}
插件介绍
  • 这个插件的目的是过滤掉你希望被忽略的 endpoint
  • 你可以设置多个URL路径模式。匹配到的 endpoint 将会不会被追踪
  • 当前的匹配规则遵循Ant Path匹配样式,比如/path/*,/path/**,/path/?
使用项目

Live-Demo&Skywalking源码工程​

使用方法
  1. apm-trace-ignore-plugin.jar 文件从 /optional-plugins 目录copy到 /plugins 目录
  2. 启动服务配置系统环境变量 skywalking.trace.ignore_path 值是要忽略的路径​
验证 ServiceManager 持有的 SamplingService 实例

未替换该插件时
Skywalking原理篇(一):Agent 启动流程解析_第10张图片
替换后
Skywalking原理篇(一):Agent 启动流程解析_第11张图片

验证 Ignore Trace 结果

修改 ProjectB 项目的唯一接口,让其直接返回 Trace ID 然后启动 ProjectB项目 并配置系统环境变量 skywalking.trace.ignore_path

-Dskywalking.trace.ignore_path=/projectB/test

打开浏览器访问 localhost:8762/projectB/test 可以看到直接返回了 Ignore Trace
Skywalking原理篇(一):Agent 启动流程解析_第12张图片
而访问其他path则正常显示 Trace ID
Skywalking原理篇(一):Agent 启动流程解析_第13张图片

添加JVM钩子

启动流程的最后,会添加一个JVM钩子函数,当JVM退出时执行,方法入口在 org.apache.skywalking.apm.agent.core.boot.ServiceManager#shutdown

public void shutdown() {
    for (BootService service : bootedServices.values()) {
        try {
            service.shutdown();
        } catch (Throwable e) {
            LOGGER.error(e, "ServiceManager try to shutdown [{}] fail.", service.getClass().getName());
        }
    }
}

它会遍历所有的 BootService 实例并执行 shutdown() 方法来停止所有服务

总结

本文通过对 Skywalking Agent 核心启动流程的介绍,让我们从大体上了解到探针在我们的应用启动之前究竟做了哪些准备工作。主要包括配置的加载、插件的加载、ByteBuddy 如何增强目标类以及 BootService 的加载。知道了这些之后,能够为我们接下来更深入的研究 Agent 的各种机制和原理时做好铺垫。

你可能感兴趣的:(Skywalking)