likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 28

前言

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

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

一、主流程

SkyWalkingAgent#premain:agent启动主流程。

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

主流程分为五步:

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

二、加载配置

SnifferConfigInitializer#initializeCoreConfig:加载配置。

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

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

agent.config配置,如agent.service_name。

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

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

java -Dskywalking.agent.service_name=servicexxx -javaagent:agent.jar -jar app.jar

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

java -javaagent:agent.jar=agent.service_name=a -jar app.jar

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

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

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

三、加载skywalking-plugin.def

1、什么是skywalking-plugin.def

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

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

所有插件都继承自AbstractClassEnhancePluginDefine类。

AbstractClassEnhancePluginDefine是个抽象类。

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

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

对于插件的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;
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

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

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

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

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

  1. IndirectMatch:非直接匹配;

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

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

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

根据版本增强

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

AbstractClassEnhancePluginDefine#witnessClasses/witnessMethods:

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

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

比如SpringMVC4增强定义:

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

SpringMVC5增强定义:

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

是否增强BootstrapClassLoader类

AbstractClassEnhancePluginDefine#isBootstrapInstrumentation:

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

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

RunnableInstrumentation#isBootstrapInstrumentation:

RunnableInstrumentation需要增强java.util.Runnable。

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

定义增强点

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

增强点支持三种:

  1. ConstructorInterceptPoint:增强构造方法;

getConstructorMatcher:拦截哪些构造方法;

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

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

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

getMethodsMatcher:拦截那些实例方法;

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

isOverrideArgs:是否重写入参;

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

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

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

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

V1V2拦截器

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

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

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

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

3、加载AbstractClassEnhancePluginDefine

PluginBootstrap#loadPlugins:

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

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

创建默认ClassLoader

AgentClassLoader#initDefaultLoader:

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

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

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

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

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

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

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

PluginDefine

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

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

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

如tomcat要加载TomcatInstrumentation和ApplicationDispatcherInstrumentation。

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

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中增强的插件;

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

四、安装增强

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

1、主流程

SkyWalkingAgent#premain:

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

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

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

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

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

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

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

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

2、buildMatch定义增强哪些类

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

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

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

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

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

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

主流程

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

入参:

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

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

SkyWalkingAgent.Transformer#transform:

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

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

AbstractClassEnhancePluginDefine#define:

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

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

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

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

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

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

实现EnhancedInstance

ClassEnhancePluginDefine#enhanceInstance:

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

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

EnhancedInstance

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

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

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

构造增强

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

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

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

通过InterceptorInstanceLoader加载插件拦截器。

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

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

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

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

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

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

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

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。

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

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

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

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

实例方法增强

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

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

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

以InstMethodsInter为例。

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

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

InstMethodsInter#intercept:运行阶段

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

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

五、BootService

1、什么是BootService

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

  1. prepare、boot、onComplete:启动阶段调用;
  2. shutdown:进程关闭阶段调用;
  3. priority:优先级;
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实现。

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

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

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

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

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

2、加载BootService

ServiceManager#loadAllServices:

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

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

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

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

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

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

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

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

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

比如上面这两个例子,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加载

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

3、启动BootService

ServiceManager#prepare/startup/onComplete:

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

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

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

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

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

六、其他

1、插件自定义配置

agent可以配置众多plugin配置。

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

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

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

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

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

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

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

AgentClassLoader#processLoadedClass:

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

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

2、SkyWalkingAgent.Listener

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

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

SkyWalkingAgent.Listener#onTransformation:

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

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

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

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

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

总结

启动流程

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

  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

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

  1. 构造拦截器

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

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

类加载器

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

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

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

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

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

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

转载自:https://juejin.cn/post/7382891974955417637
评论
请登录