10、Skywalking的埋点-插件开发的基本技巧

1. 概述

做中间件的埋点插件,需要通过对源码梳理,找到关键拦截点,如什么类的什么方法,通过其参数、返回值、对象属性等获取构建Span的数据信息。这节内容记录一下开发插件过程中一些零碎的事项。
通过继承ClassInstanceMethodsEnhancePluginDefine,在子类的实现方法中完成以下逻辑:

  1. 指定待增强的类
    @Override
    protected ClassMatch enhanceClass() {
        IndirectMatch hierarchyMatch = byHierarchyMatch(new String[]{ENHANCE_CLASS});
        PrefixMatch prefixMatch = nameStartsWith(PAJK_PACKAGE_PREFIX);
        return LogicalMatchOperation.and(hierarchyMatch, prefixMatch);
    }
  1. 在构造函数中添加增强逻辑,可通过类名(构造方法同名),方法参数信息指定构造函数。
    //这种方式,不做构造函数增强
    @Override
    public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
        return new ConstructorInterceptPoint[0];
    }

    //指定构造方法
    @Override
    public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
        return new ConstructorInterceptPoint[]{
                new ConstructorInterceptPoint() {
                    @Override
                    public ElementMatcher getConstructorMatcher() {
                        //第二个参数必须是String
                        return takesArgumentWithType(2, "java.lang.String");
                    }

                    @Override
                    public String getConstructorInterceptor() {
                        return INTERCEPT_CLASS;//指定类名(构造方法名)
                    }
                }
        };
    }
  1. 增强实例方法,通过InstanceMethodsInterceptPoint匹配将增强的方法
    @Override
    public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodsInterceptPoint[]{
                new InstanceMethodsInterceptPoint() {
                    @Override
                    public ElementMatcher getMethodsMatcher() {
                        return named(ENHANCE_METHOD);
                    }

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

                    @Override
                    public boolean isOverrideArgs() {
                        return false;
                    }
                }
        };
    }
  1. 增强类(静态)方法,通过StaticMethodsInterceptPoint匹配将增强的类方法
    @Override
    public StaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {
        return new StaticMethodsInterceptPoint[0];
    }

2. xxxMatch匹配的技巧

在以上代码中可以看到许多xxxMatch,大致是2类:

  • ClassMatch :用于匹配类
  • ElementMatcher :用于匹配方法
2.1 ClassMatch

在源码中可看到ClassMatch有如下这些实现类:

ClassAnnotationMatch
EitherInterfaceMatch
FailedCallbackMatch
HierarchyMatch
IndirectMatch
ListenableFutureCallbackMatch
LogicalAndMatch
LogicalOrMatch
MethodAnnotationMatch
MultiClassNameMatch
NameMatch
PrefixMatch
RegexMatch
SuccessCallbackMatch

这些匹配有按照类名字匹配、前缀匹配、继承关系、实现接口、组合与匹配、组合或匹配等等;可通过在源码中查找其应用来进一步理解其作用;另外需要注意,除了默认提供的这些,还是可以根据自己的需求来额外定制(通过实现接口IndirectMatch

2.2 ElementMatcher
  1. 系统api
    ElementMatchers中已经定义了很多用于方法匹配的方法,因其方法太多,不在此列举,自行查看

  2. 自定义matcher

return new ElementMatcher() {
    @Override
    public boolean matches(MethodDescription target) {
       //自己编排逻辑
       if(xxx) { 
            return true;
       }
       return false;
   }

3. 上下文数据的传递

插件埋点所需要的信息,通常是需要再多个方法中分别捕获,我们需要通过某种上下文机制将这些分散的信息搜集整合起来;从线程角度说通常分为同步请求(相同线程内完成一次执行)和异步请求(不同的线程内完成一次请求)。

3.1 同步请求的上下文数据
  1. ThreadLocal方式,在Skywalking中已为我们提供了这种途径ContextManager.getRuntimeContext()
  • 存储数据
    ContextManager.getRuntimeContext().put(key, data)
  • 检查数据
    ContextManager.getRuntimeContext().get(key),判断是否存在
  • 清理数据
    需要留意做善后清理数据ContextManager.getRuntimeContext().remove(key)
  1. Skywalking的扩展字段,Skywalking会在被增强的类中添加一个sw专用的属性,同时这个类会被修改,实现了接口EnhancedInstance,通过此接口中的2个方法来读写这个扩展属性
public interface EnhancedInstance {
    Object getSkyWalkingDynamicField();

    void setSkyWalkingDynamicField(Object value);
}

这个sw专用字段,就是一个普通的Object,可根据自己的需求给其赋值。
如这样一些使用场景:

  • 拦截目标类的构造方法,在构造房中new 一个自定义类,通过setSkyWalkingDynamicField赋值给扩展字段
  • 在其他方法中,捕获不同的数据,填充到这个对象的扩展属性里。
  • 读取扩展属性中的数据,构造、填充Span
3.2 异步请求的上下文数据

异步请求的场景下,因为跨越了线程,所以上下文需要在多个线程中传递,那么常规的ThreadLocal方式就不可用了;通过SW扩展属性来承载数据,在多个线程中传递的方式非常方便了。

异步请求时 需要借助AsyncSpan#prepareForAsyncAsyncSpan#asyncFinish来完成span的异步关闭,这里一个问题,异步跨线程的情况下,另外的线程里如何拿到这个span实例呢? 寻找在多个线程中都存在的对象,如果这个对象本身有承载额外数据的能力最好,如果没有,则需要增强这个类,借助sw的扩展字段来承载这个span,进而在异步结果处理中完成span的关闭。

异步请求的上下文的实例,通过在源码中搜索prepareForAsync方法的使用来加深理解,这里用ExitSpan方式暂记一下关键逻辑和步骤:

  1. 发送请求的线程a,创建ExitSpan 实例 exitSpan,会对exitSpan做一些赋值
  2. 线程b中会获取当前请求的执行结果。
  3. 线程a 和线程b 之间一定会有一个对象,在两个线程之间都可访问,暂叫 externObj;
  4. 线程a,在发起异步请求之前,调用exitSpan的prepareForAsync方法,并把exitSpan装入externObj中;如果externObj没有合适的属性去承载exitSpan数据,就扩展这个externObj的类,比如使用skywalking的类增强或者继承externObj的类,达到增加属性字段的效果
  5. 线程b,在拿到响应结果后,从externObj的取出exitSpan,根据返回值 给 exitSpan做赋值,最后调用asyncFinish异步关闭span

4. SpanStack

1. EntrySpan 的SpanStack

服务接收请求时,创建EntrySpan;tomcat和SpringMVC的插件都是创建 EntrySpan,这样就重复创建了EntySpan,这样没有意义,在sw中,只要最外层的EntrySpan,通过一个计数器(stackDepth)和栈的结构来实现,大致流程如下:

  • 请求进入1处,创建EntrySpan 暂叫tomEntrySpan,stackDepth=1,记录开始时间
  • 请求进入2处,创建EntrySpan时,stackDepth+1=2,会复用上一步创建的tomEntrySpan对象,而不是new 一个新的;同时会清理此span上的layer、logs和tags;保留springMVC层的layer、logs和tags。
  • 请求进入3处,执行finish方法将stackDepth-- = 1;
  • 请求进入4处,执行finish方法将stackDepth--=0,关闭tomEntrySpan,记录结束时间
image.png
2. ExitSpan 的SpanStack

假设上图tomcat是作为调用外部请求的,创建ExitSpan(这里只是借图举例)其逻辑流程却是这样:

  • 请求进入1处,创建ExitSpan实例 tomExitSpan,stackDepth=1,记录开始时间;在这一层才可以记录layer、logs和tags。
  • 请求进入2处,创建ExitSpan时,stackDepth+1=2,会复用上一步创建的tomExitSpan对象,而不是new 一个新的;这里layer、logs和tags设置无效。
  • 请求进入3处,执行finish方法将stackDepth-- = 1;
  • 请求进入4处,执行finish方法将stackDepth--=0,关闭tomExitSpan,记录结束时间。
3. 方法的重入

比如在ExitSpan场景下中,A类的put1和put2方法都被拦截,并且put1方法 调用了put2方法,在上述的SpanStack的机制里,由于put2方法中stackDepth=2,其内所处理的layer、logs和tags是无效的;对于这种情况,大概有2中方法处理:

  1. 当stackDepth!=1 的时候,通过上下文记录layerlayer、logs和tags的信息,等stackDepth=1时,从上下文中取出layerlayer、logs和tags的信息赋值给span。
  2. 控制span的创建时机,自行在上下文中增加stackDepth的计数控制,当stackDepth>1时,不在创建ExitSpan。即put1中有一个ExitSpan,put2中不创建ExitSpan,那么stackDepth一直=1,layerlayer、logs和tags的信息可以随时赋值给span

你可能感兴趣的:(10、Skywalking的埋点-插件开发的基本技巧)