SkyWalking8源码(二)客户端启动与类加载

前言

本章基于SkyWalking8.6.0分析javaagent客户端:

  1. 梳理agent启动流程;
  2. 关注类加载设计;

一、主流程

SkyWalkingAgent#premain:agent启动主流程。

主流程分为五步:

  1. 加载配置注入Config
  2. 加载skywalking-plugin.def 对应class注入PluginFinder
  3. 使用ByteBuddy 定义class增强逻辑,安装到Instrumentation
  4. ServiceManager 加载并启动所有BootService
  5. 注册ShutdownHook

二、加载配置

SnifferConfigInitializer#initializeCoreConfig:加载配置。

配置取自于三个地方,优先级从低到高依次是:agent.config文件-D参数agent参数

agent.config配置,如agent.service_name。

  1. 支持占位符替换,如SW_AGENT_NAME,取值来源:-D参数>环境变量>agent.config;
  2. 支持默认值;
ini 复制代码
agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}

-D参数配置,需要加上skywalking前缀,如:

ini 复制代码
java -Dskywalking.agent.service_name=servicexxx -javaagent:agent.jar -jar app.jar

agent参数配置,逗号分割配置项,等号分割kv对:

ini 复制代码
java -javaagent:agent.jar=agent.service_name=a -jar app.jar

所有的配置项都会注入全局配置类Config

如agent.service_name注入Config$Agent.SERVICE_NAME。

三、加载skywalking-plugin.def

1、什么是skywalking-plugin.def

对于每个skywalking插件,都会在skywalking-plugin.def资源文件中配置插件定义

所有插件都继承自AbstractClassEnhancePluginDefine类。

AbstractClassEnhancePluginDefine是个抽象类。

AbstractClassEnhancePluginDefine#define方法是个模板方法,是class增强定义的主流程。

对于插件的AbstractClassEnhancePluginDefine的实现类,只需要声明:增强哪里、如何增强。

比如RocketMQ4.x并发消费者增强ConsumeMessageConcurrentlyInstrumentation

  1. 增强class:org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently接口的实现类;
  2. 增强method:consumeMessage;
  3. 增强实现:org.apache.skywalking.apm.plugin.rocketMQ.v4.MessageConcurrentlyConsumeInterceptor;
typescript 复制代码
public class ConsumeMessageConcurrentlyInstrumentation extends ClassInstanceMethodsEnhancePluginDefine {
    private static final String ENHANCE_CLASS = "org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently";
    private static final String CONSUMER_MESSAGE_METHOD = "consumeMessage";
    private static final String INTERCEPTOR_CLASS = "org.apache.skywalking.apm.plugin.rocketMQ.v4.MessageConcurrentlyConsumeInterceptor";
    @Override
    public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[] {
            new InstanceMethodsInterceptPoint() {
                @Override
                public ElementMatcher<MethodDescription> getMethodsMatcher() {
                    return named(CONSUMER_MESSAGE_METHOD);
                }

                @Override
                public String getMethodsInterceptor() {
                    return INTERCEPTOR_CLASS;
                }

                @Override
                public boolean isOverrideArgs() {
                    return false;
                }
            }
        };
    }
    @Override
    protected ClassMatch enhanceClass() {
        return HierarchyMatch.byHierarchyMatch(new String[] {ENHANCE_CLASS});
    }
}

2、AbstractClassEnhancePluginDefine

AbstractClassEnhancePluginDefine的子类可以通过各种方式描述增强哪些类。

增强哪个class

实现AbstractClassEnhancePluginDefine#enhanceClass:返回一个ClassMatch

ClassMatch 是一个标记接口没有任何方法,有两大类实现:

  1. NameMatch:根据class完全限定类名匹配;
  1. IndirectMatch:非直接匹配;

除了NameMatch都是非直接匹配,比如ClassAnnotationMatch根据class上的annotation匹配。

根据版本增强

部分场景下,对于不同版本的同一个class,需要提供不同的增强实现。

AbstractClassEnhancePluginDefine#witnessClasses/witnessMethods:

通过定义witnessClass和Method,根据class或method是否存在,区分版本是否增强当前class。

比如SpringMVC4增强定义:

SpringMVC5增强定义:

是否增强BootstrapClassLoader类

AbstractClassEnhancePluginDefine#isBootstrapInstrumentation:

一般情况下,不需要增强BootstrapClassLoader下的类。

RunnableInstrumentation#isBootstrapInstrumentation:

RunnableInstrumentation需要增强java.util.Runnable。

定义增强点

增强点支持三种:

  1. ConstructorInterceptPoint:增强构造方法;

getConstructorMatcher:拦截哪些构造方法;

getConstructorInterceptor:增强拦截器完全限定类名;

  1. InstanceMethodsInterceptPoint/InstanceMethodsInterceptV2Point:增强实例方法;

getMethodsMatcher:拦截那些实例方法;

getMethodsInterceptor :V1增强拦截器完全限定类名,InstanceMethodsAroundInterceptor实现类;

isOverrideArgs:是否重写入参;

getMethodsInterceptorV2 :V2增强拦截器完全限定类名,InstanceMethodsAroundInterceptorV2实现类;

  1. StaticMethodsInterceptPoint/StaticMethodsInterceptV2Point:增强静态方法,和实例方法类似,不再赘述;

V1V2拦截器

在V1拦截器中,handleMethodException方法异常,afterMethod方法执行后,无法拿到beforeMethod中的临时变量。(V1可以通过RuntimeContext这样的ThreadLocal处理)

这类数据其实就是上下文。在V2拦截器中,可以通过MethodInvocationContext在当前拦截器中传递。

3、加载AbstractClassEnhancePluginDefine

PluginBootstrap#loadPlugins:

将所有skywalking-plugin.def加载为AbstractClassEnhancePluginDefine。

创建默认ClassLoader

AgentClassLoader#initDefaultLoader:

skywalking中大部分类都是由一个全局AgentClassLoader 加载的,后文称这个ClassLoader为默认ClassLoader

默认ClassLoader的父类加载器就是agent的类加载器 ,即AppClassLoader

AgentClassLoader可以加载plugins和activations目录下的插件。

PluginDefine

使用默认ClassLoader加载classpath下所有skywalking-plugin.def。

每行skywalking-plugin.def都会成为一个PluginDefine对象。

如tomcat要加载TomcatInstrumentation和ApplicationDispatcherInstrumentation。

ini 复制代码
tomcat-7.x/8.x=org.apache.skywalking.apm.plugin.tomcat78x.define.TomcatInstrumentation
tomcat-7.x/8.x=org.apache.skywalking.apm.plugin.tomcat78x.define.ApplicationDispatcherInstrumentation

加载AbstractClassEnhancePluginDefine

最终使用默认ClassLoader加载AbstractClassEnhancePluginDefine

scss 复制代码
AbstractClassEnhancePluginDefine plugin = (AbstractClassEnhancePluginDefine)
           Class.forName(pluginDefine.getDefineClass(), true, 
                         AgentClassLoader.getDefault()).newInstance();

所以AbstractClassEnhancePluginDefine实现类只能加载这些插件:

  1. AppClassLoader:skywalking-agent.jar里的class,classpath里的class;
  2. 默认ClassLoader:plugins和activations目录下的class;

注入PluginFinder

PluginFinder构造,将AbstractClassEnhancePluginDefine划分为三个集合:

  1. nameMatchDefine :通过className精确匹配的插件定义,即enhanceClass返回的ClassMatch类型为NameMatch
  2. signatureMatchDefine :enhanceClass返回的ClassMatch类型为IndirectMatch的插件;
  3. bootstrapClassMatchDefine:需要在BootStrapClassLoader中增强的插件;

四、安装增强

注意这里只是对class增强的定义实际对class增强是在类加载阶段

1、主流程

SkyWalkingAgent#premain:

先处理PluginFinder中bootstrapClassMatchDefine集合中需要BootstrapClassLoader增强的插件。

这部分插件在bootstrap-plugins 目录下,默认不会启用,这段逻辑忽略

注:如上面创建默认ClassLoader所述,默认ClassLoader只能加载plugins和activations目录下的class。

SkyWalkingAgent#premain:接下来操作ByteBuddy的api安装增强

  1. type:定义增强哪些class;
  2. Transformer:定义如何增强;
  3. Listener:debug相关;

2、buildMatch定义增强哪些类

PluginFinder#buildMatch:匹配条件通过or关联,组合所有插件需要增强的class

  1. nameMatchDefine,NameMatch,插件通过className精确匹配;
  2. signatureMatchDefine,IndirectMatch,插件自己构造匹配条件Junction;

ProtectiveShieldMatcher代理匹配条件Junction,发生任何插件的匹配逻辑异常,都会降级返回false不匹配。

3、Transformer定义如何增强(重要)

主流程

Transformer 实现AgentBuilder.Transformer(bytebuddy)的transform方法定义如何增强一个类。

入参:

  1. builder:增强构造builder;
  2. typeDescription:被增强的类描述;
  3. classLoader被增强的类的类加载器 ,比如springboot应用中的LaunchedURLClassLoader

SkyWalkingAgent.Transformer#transform:

  1. 根据类描述TypeDescription找到类增强定义AbstractClassEnhancePluginDefine;
  2. 执行AbstractClassEnhancePluginDefine#define方法构建增强;

AbstractClassEnhancePluginDefine#define:

如果witnessClasses /witnessMethods返回class或method在增强类的ClassLoader下不存在,则不增强;

AbstractClassEnhancePluginDefine#enhance:分别增强静态方法和实例方法。

实例方法增强为例,这里V1和V2的区别仅仅在于Interceptor的实现不同,看V1。

实现EnhancedInstance

ClassEnhancePluginDefine#enhanceInstance:

对于实例方法增强,所有class都会实现EnhancedInstance接口。

EnhancedInstance

对于这些类新增一个Object类型_$EnhancedClassField_ws属性,实现EnhancedInstance

构造增强

构造增强的拦截逻辑都委派到ConstructorInter

ConstructorInter构造,传入拦截器完全限定类名增强类的ClassLoader

通过InterceptorInstanceLoader加载插件拦截器。

InterceptorInstanceLoader 非常重要,是agent自己ClassLoader到应用ClassLoader的桥梁

如果拦截器也用默认ClassLoader加载(见上面默认AgentClassLoader),那么拦截器里就无法加载应用的class,比如SpringBoot的LaunchedURLClassLoader加载的class。

所以拦截器的类加载器需要特殊定制

  1. INSTANCE_CACHE:插件拦截器instance缓存;
  2. EXTEND_PLUGIN_CLASSLOADERS:被增强的ClassLoader到Agent的ClassLoader的映射;

InterceptorInstanceLoaderload:假设当前是个springboot应用,传入targetClassLoader就是LaunchedURLClassLoader

ini 复制代码
public static <T> T load(String className,
                         ClassLoader targetClassLoader) throws IllegalAccessException, InstantiationException, ClassNotFoundException, AgentPackageNotFoundException {
    if (targetClassLoader == null) {
        targetClassLoader = InterceptorInstanceLoader.class.getClassLoader();
    }
    String instanceKey = className + "_OF_" + targetClassLoader.getClass()
    .getName() + "@" + Integer.toHexString(targetClassLoader
                                           .hashCode());
    Object inst = INSTANCE_CACHE.get(instanceKey);
    if (inst == null) {
        INSTANCE_LOAD_LOCK.lock();
        ClassLoader pluginLoader;
        try {
            pluginLoader = EXTEND_PLUGIN_CLASSLOADERS.get(targetClassLoader);
            if (pluginLoader == null) {
                pluginLoader = new AgentClassLoader(targetClassLoader);
                EXTEND_PLUGIN_CLASSLOADERS.put(targetClassLoader, pluginLoader);
            }
        } finally {
            INSTANCE_LOAD_LOCK.unlock();
        }
        inst = Class.forName(className, true, pluginLoader).newInstance();
        if (inst != null) {
            INSTANCE_CACHE.put(instanceKey, inst);
        }
    }
    return (T) inst;
}

对于每个目标ClassLoader会构造一个新的AgentClassLoader并缓存这个新的AgentClassLoader的parent是目标ClassLoader

通过这种方式,插件拦截器即可以加载agent中的class,也可以加载应用的class。

ConstructorInter#intercept:运行阶段,构造拦截可以拿到被增强的对象obj和构造参数allArguments。

注:对于增强异常会try-catch。

实例方法增强

ClassEnhancePluginDefine#enhanceInstance:实例方法增强分为两种:

  1. 重写入参,使用InstMethodsInterWithOverrideArgs
  2. 不重写入参,使用InstMethodsInter

以InstMethodsInter为例。

InstMethodsInter 构造还是会用插件类加载器(见上面构造拦截)加载拦截器。

InstMethodsInter#intercept:运行阶段

  1. 可以拿到目标对象、方法入参、原始方法调用函数(zsuper)、目标方法;
  2. 所有拦截器发生的异常都会try-catch;
  3. 通过MethodInterceptResult可以在before拦截返回值;

五、BootService

1、什么是BootService

BootService是apm-agent-core模块(skywalking-agent.jar)下的一个接口。

  1. prepare、boot、onComplete:启动阶段调用;
  2. shutdown:进程关闭阶段调用;
  3. priority:优先级;
csharp 复制代码
public interface BootService {
    void prepare() throws Throwable;
    void boot() throws Throwable;
    void onComplete() throws Throwable;
    void shutdown() throws Throwable;
    default int priority() {
        return 0;
    }
}

SkyWalkingAgent#premain:ServiceManager管理所有BootService实现。

ServiceManager#boot:ServiceManager在agent启动最后阶段,加载所有BootService并依次调用BootService的prepare、boot、onComplete方法。

ServiceManager#findService:BootService之间可以通过ServiceManager做依赖发现。

2、加载BootService

ServiceManager#loadAllServices:

  1. 加载BootService实现类;
  2. 处理DefaultImplementorOverrideImplementor逻辑,注入Map,key是BootService的实现类,value是对应实例;

ServiceManager#load:BootServiceJDK的SPI加载采用默认ClassLoader不能加载应用的class

注:并不是BootService都是由默认AgentClassLoader加载的,大部分BootService实现就在核心skywalking-agent.jar中,默认AgentClassLoader会委派到AppClassLoader加载。

DefaultImplementor 注解用于修饰一个默认BootService实现。

OverrideImplementor 注解用于修饰重写一个默认BootService

比如上面这两个例子,agent有两种方式发送segment到OAP:

  1. TraceSegmentServiceClient :agent直连OAP,走gRPC发送,这个实现在skywalking-agent.jar中,所以是AgentClassLoader委派给AppClassLoader加载
  2. KafkaTraceSegmentServiceClient :agent发送到Kafka,OAP从Kafka消费,这个实现在optional-reporter-plugins/kafka-reporter-plugin.jar 中,不会被默认AgentClassLoader加载到,默认不生效,如果将kafka-reporter-plugin.jar 放入plugins目录下,则可以由默认AgentClassLoader加载

3、启动BootService

ServiceManager#prepare/startup/onComplete:

ServiceManager会依次调用BootService的prepare、boot、onComplete方法。

  1. priority越小越优先调用
  2. onComplete方法不受优先级影响
  3. 异常被try-catch;

ServiceManager#shutdown:进程停止反向调用所有BootService的shutdown方法。

六、其他

1、插件自定义配置

agent可以配置众多plugin配置。

在agent核心配置类Config中,Plugin只关联少部分配置。

其余plugin配置都在各自插件中维护,通过PluginConfig注解注入。

AgentClassLoader#findClass:当class被AgentClassLoader加载,都会触发AgentClassLoader#processLoadedClass

AgentClassLoader#processLoadedClass:

这里会利用启动阶段拿到的所有kv配置,反射注入PluginConfig注解的类。

2、SkyWalkingAgent.Listener

安装增强阶段加入了SkyWalkingAgent.Listener。

SkyWalkingAgent.Listener#onTransformation:

InstrumentDebuggingClass#log:通过开启agent.is_open_debugging_class ,可以将增强的class保存到skywalking-agent.jar的debugging目录下

反编译可以追溯增强情况。

总结

启动流程

  1. 加载配置到全局Config类,优先级agent.config<-D参数<agent参数
    1. agent.config,支持占位符,占位符取值来源:-D参数>环境变量>agent.config;
    2. -D参数,在原始配置前要加skywalking前缀,如-Dskywalking.agent.service_name;
    3. agent参数,逗号分割配置项,等号分割kv,如agent.jar=k1=v1,k2=v2;
  1. 加载skywalking-plugin.def
    1. 全局默认AgentClassLoader 加载插件,能加载plugins和activations下的jar里的资源和class,其parent为AppClassLoader;
    2. skywalking-plugin.def是插件定义文件,文件每行都会成为一个PluginDefine
    3. 根据PluginDefine 中的className,全局默认AgentClassLoader 加载插件AbstractClassEnhancePluginDefine定义;
  1. 定义class增强
    1. 操作ByteBuddy api定义Instrument增强逻辑;
    2. 增强哪些类,组合插件定义的ClassMatch 构建bytebuddy的ElementMatcher
    3. 如何增强,SkyWalkingAgent.Transformer 实现bytebuddy的AgentBuilder.Transformer,在class加载时执行增强逻辑;
  1. 启动BootService
    1. 通过JDK SPI加载;
    2. 全局默认AgentClassLoader加载BootService;
    3. 依次调用BootService的prepare、boot、onComplete方法;
    4. prepare、boot可以通过优先级控制;
  1. 注册ShutdownHook,执行BootService#shutdown;

增强类加载

SkyWalkingAgent.Transformer在运行时类加载阶段增强class。

循环class的每个增强插件定义AbstractClassEnhancePluginDefine

  1. 如果witnessClasseswitnessMethods返回的class或method无法找到,跳过该插件,解决组件多版本问题;
  2. 依次增强静态方法实例方法
  3. 对于实例方法增强 的class,都会实现EnhancedInstance 接口,新增一个成员变量_$EnhancedClassField_ws
  1. 构造拦截器

    1. 插件定义会声明增强拦截器实现,比如InstanceMethodsAroundInterceptor拦截实例方法;
    2. 插件拦截器会被包装一层,比如InstMethodsInter 包装InstanceMethodsAroundInterceptor,控制实例方法执行的流程;
    3. InstMethodsInter 构造会用InterceptorInstanceLoader#load加载插件拦截器,这是agent类加载器到用户classLoader的桥梁
    4. InstMethodsInter在实例方法执行阶段,会try-catch插件拦截器的所有执行逻辑;

类加载器

以最常见的springboot应用来说,skywalking-agent的类加载情况如下。

  1. skywalking-agent.jar中的class都由AppClassLoader加载;
  2. 插件定义BootService 都由全局默认AgentClassLoader加载;
  3. 插件拦截器独立AgentClassLoader 加载,这个AgentClassLoader的parent是用户增强的类的原始加载器,如springboot的LaunchedURLClassLoader
  4. AgentClassLoader 自己是满足双亲委派的,即全局默认AgentClassLoader 会先委派AppClassLoader,插件拦截器的独立AgentClassLoader 会先委派LaunchedURLClassLoader

欢迎大家评论或私信讨论问题。

本文原创,未经许可不得转载。

欢迎关注公众号【程序猿阿越】。

相关推荐
伏颜.28 分钟前
Spring懒加载Bean机制
java·spring
细心的莽夫28 分钟前
集合复习(java)
java·开发语言·笔记·学习·java-ee
yours_Gabriel34 分钟前
java基础:面向对象(二)
java·开发语言·笔记·学习
Enaium38 分钟前
Rust入门实战 编写Minecraft启动器#3解析资源配置
java·开发语言·rust
虫小宝1 小时前
在Spring Boot中实现多线程任务调度
java·spring boot·spring
虫小宝2 小时前
如何在Java中实现PDF生成
java·开发语言·pdf
Java4ye2 小时前
Netty 是如何解析 Redis RESP 协议的——请求篇
后端
菜鸡且互啄693 小时前
在线教育平台,easyexcel使用案例
java·开发语言
八月林城3 小时前
JAVA导出数据库字典到Excel
java·数据库·excel
浅念同学5 小时前
算法-常见数据结构设计
java·数据结构·算法