玩转Skywalking-Agent之高级用法及技术剖析

写在前面:

Skywalking-Agent作为集成到目标系统中的代理或SDK库,是Skywalking主要用来收集应用程序的各个维度的数据及应用程序链路跟踪,然后上报到SkyWalking-OAP(也是Skywalking的一部分)。不知道Skywalking的,先去了解了解Skywalking再来看哈。

虽然Skywalking-Agent提供了丰富的插件来跟踪不同服务组件的链路,不过有一些中间件还没法支持,为了支持对我们使用中间件的链路跟踪,我们不得不对SkyWalking-Agent底层源码进行二次开发来满足业务链路跟踪的需求以及熟悉Agent的一些高级用法。借此也了解一下 SkyWalking的Agent的设计和核心技术。


Skywalking-Agent简介

Skywalking-Agent采用插件化 + javaagent的形式来实现了数据的自动采集,这样可以做到对代码的无侵入性,插件化意味着可插拔,扩展性好(后文会介绍如何定义自己的插件)。其实很多主流框架都采用框架+插件的模式开发。如DataX、FlinkX通过插件支持众多异构数据源。

名词解释:

  • SPI:Skywalking暴露的插件规范接口,支持构造方法拦截、实例方法拦截及静态方法拦截。
  • Core:负责加载插件并且利用Byte Buddy提供的字节码增强逻辑对应用中指定类和方法的字节码进行增强。
  • Plugin:自定的插件,依赖Skywalking暴露的插件规范接口及利用Byte Buddy实现字节码增强。开发者根据这些接口实现自定义插件。


Skywalking-Agent高级用法

如下图所示,Skywalking-Agent完成了插件的定义和实现及管理,其主要组成部分也就是定义插件的要素:增强类、拦截器、Interceptor,下面会详细介绍。


自定义插件

根据OpenTracing规范(什么是OpenTracing规范,后面有介绍)自研Oceanbase(华为研发的企业级分布式数据库, 后面见链接)插件。


插件如何实现呢,可以看到它主要由三个部分组成:

  1. 插件定义类: 指定插件的定义类,最终会根据这里的定义类打包生成plugin(在skywalking-plugin.def中定义了插件类)
  2. Instrumentation: 增强器:指定切面,切点,要对哪个类的哪个方法进行增强
  3. Interceptor: 拦截器指定步骤2中要在方法的前置,后置还是异常中写增强逻辑

下面进入创建环节:

增强器:
org.apache.skywalking.apm.plugin.oceanbase.v4.define.OceanBaseStatementInstrumentation

拦截器:
org.apache.skywalking.apm.plugin.oceanbase.v4.StatementExecuteMethodsInterceptor

  1. 创建Instrumentation


  1. 创建Interceptor

首先 beforeMethod代表在执行PreparedStatement的execute、executeQuery、executeUpdate方法之前会调用这里的方法,与之对应的是afterMethod,代表执行切点方法后做增强处理。

其次 在beforeMethod中定义创建Span(每一个组件对应一个Span,也就是当执行被增强类的前置处理时创建一个Span,用于记录本次增强的信息),往Span中添加tag,上图中添加了db_type(数据库类型:例如:Mysql、OceanBase等等)、db_instance(数据库ip+端口)、db_statement(所致行的sql)


  1. 创建skywalking-plugin.def

最后创建def文件,文件中通过k=v方式定义该插件所包含的增强类(agent启动时首先会扫描该def文件,然后初始化插件的增强类)


oceanbase-4.x=org.apache.skywalking.apm.plugin.oceanbase.v4.define.OceanBaseStatementInstrumentation

自定义tag

Skywalking-Agent为调用的增强类准备了多个tag属性,这些属性在
org.apache.skywalking.apm.agent.core.context.tag.Tags中,绝大部分属性能满足我们的业务场景中的使用,但我们可以自定义tag来满足我们个性化的业务场景,例如:Tags并没有一个tag用来记录本次增强处理的时间,就让我们来定义一个tag来记录它。

添加tag


org.apache.skywalking.apm.agent.core.context.tag.Tags中添加tag key,如下所示添加tag:
timestamp

注:Tags中本身有很多个tag属性,此处为了较少code,省略了大部分tag

public final class Tags {
    private static final Map TAG_PROTOTYPES = new ConcurrentHashMap<>();
    private Tags() {
    }
    public static final StringTag URL = new StringTag(1, "url");
    public static final IntegerTag HTTP_RESPONSE_STATUS_CODE = new IntegerTag(2, "http.status_code", true);
    // add timestamp tag
    public static final StringTag TIMESTAMP = new StringTag(3, "timestamp");
}

使用tag

我们只需要在beforeMethod中,向Span中添加tag key/value即可

public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes,
                         MethodInterceptResult result) throws Throwable {
    Object request = ContextManager.getRuntimeContext().get(REQUEST_KEY_IN_RUNTIME_CONTEXT);
    ......
    if (request != null) {
        if (stackDepth == null) {
            ......
            if (IN_SERVLET_CONTAINER && IS_JAVAX && HttpServletRequest.class.isAssignableFrom(request.getClass())) {
                ......
                AbstractSpan span = ContextManager.createEntrySpan(operationName, contextCarrier);
                Tags.TIMESTAMP.set(span,String.valueOf(System.currentTimeMillis()));
                ......
            }
            ......
            stackDepth = new StackDepth();
            ContextManager.getRuntimeContext().put(CONTROLLER_METHOD_STACK_DEPTH, stackDepth);
        }
        ......
    }
}


插件运行起来的效果:

为了方便理解Tag在Skywalking中的角色,请看如下图及图中的Span部分:



首先说一下OpenTracing的规范,Skywalking按照OpenTracing将请求按照三个维度划分为Trace、Segment、Span三种模型,重点看一下Trace、Segment、Span这三种模型到底是什么。

  • Trace: 表示一整条调用链,包括跨进程、跨线程的所有Segment的集合。Segment: 表示一个进程(JVM)或线程内的所有操作的集合,即包含若干个Span。Span: 表示一个具体的操作。Span在不同的实现里可能有不同的划分方式,这里介绍一个比较容易理解的定义方式:
  • Tag:表示一个Span的具体操作的属性信息,例如:一次RPC查询请求是一个Span操作,这次查询请求包含的属性有:method(GET/POST等)、url(如: api/vi/report/search)、instance(127.0.0.1:8080)等等这些信息需要绑定到Span中,随着Span出栈上报到Skywalking-OAP。

自定义Endpoint

在使用Skywalking中很常见的一个问题是,Agent插件会对Controller的method进行增强,对于method中的XXXMapping书写方式会有value="detail/{id}"、value="/**"、value=""等等,对于这种restfull书写方式。Skywalking默认上报的Endpoint名称为detail/1、/**,我们无法定位到具体的请求url或者有太多的url,所以针对这种情况我们可以自定义Endpoint名称。如下所示。

Controller的method的增强器为:
org.apache.skywalking.apm.plugin.spring.mvc.v4.define.RestControllerInstrumentation

过滤器:
org.apache.skywalking.apm.plugin.spring.mvc.commons.interceptor.AbstractMethodInterceptor

如图中,operationName为Endpoint名称, 很明显是个不完整的url。我们可以通过以下code获取到完整的url,对于detail/{id}的处理也是一样有用

public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class[] argumentsTypes,
                         MethodInterceptResult result) throws Throwable {
    Object request = ContextManager.getRuntimeContext().get(REQUEST_KEY_IN_RUNTIME_CONTEXT);
    ......
    if (request != null) {
        if (stackDepth == null) {
            ......
            if (IN_SERVLET_CONTAINER && IS_JAVAX && HttpServletRequest.class.isAssignableFrom(request.getClass())) {
                ......
                //operationName: endpoint name,在此处更新自定上报的endpoint
                String operationName = this.buildOperationName(method, httpServletRequest.getMethod(),
                                                               (EnhanceRequireObjectCache) objInst.getSkyWalkingDynamicField());
                String operationName =  ((RequestFacade) request).getRequestURI().toString();
                AbstractSpan span = ContextManager.createEntrySpan(operationName, contextCarrier);
                ......
            }
            ......
            stackDepth = new StackDepth();
            ContextManager.getRuntimeContext().put(CONTROLLER_METHOD_STACK_DEPTH, stackDepth);
        }
        ......
    }
}

忽略特定Endpoint

我们使用Skywalking过程中发现Endpoint列表有一些没有实际作用的Endpoint,例如:

我们需要在agent\config下面新建一个配置文件
apm-trace-ignore-plugin.config,如下所示,trace.ignore_path为所需要忽略的Path路径。

trace.ignore_path=httpAsyncClient/**,httpasyncclient/**,SpringAsync

trace.ignore_path路径配置规则支持匹配风格,比如: /path/*, /path/**, /path/?。匹配的Path路径将不会被记录。

注:

由于是可选插件,因此,默认情况下并没有激活,我们需要将如下图中的jar包拷贝或剪切到上面的plugins目录下并添加相关配置来激活该插件,即:


Skywalking-Agent启动流程

启动及加载插件流程

Skywalking-Agent启动流程主要分5步骤:


Skywalking-Agent原理剖析

Trace Segment Span 详解


首先说一下OpenTracing的规范,Skywalking按照OpenTracing将请求按照三个维度划分为Trace、Segment、Span三种模型,重点看一下Trace、Segment、Span这三种模型到底是什么。

  • Trace: 表示一整条调用链,包括跨进程、跨线程的所有Segment的集合。
  • Segment: 表示一个进程(JVM)或线程内的所有操作的集合,即包含若干个Span。
  • Span: 表示一个具体的操作。Span在不同的实现里可能有不同的划分方式,这里介绍一个比较容易理解的定义方式:Entry Span:入栈Span。Segment的入口,一个Segment有且仅有一个Entry Span,比如HTTP或者RPC的入口,或者MQ消费端的入口等。Local Span:通常用于记录一个本地方法的调用。
  • Exit Span:出栈Span。Segment的出口,一个Segment可以有若干个Exit Span,比如HTTP或者RPC的出口,MQ生产端,或者DB、Cache的调用等。
  • Tag:表示一个Span的具体操作的属性信息,例如:一次RPC查询请求是一个Span操作,这次查询请求包含的属性有:method(GET/POST等)、url(如: api/vi/report/search)、instance(127.0.0.1:8080)等等这些信息需要绑定到Span中,随着Span出栈上报到Skywalking-OAP。

插件机制

Skywalking Agent提供了多种插件,实现不同框架的透明接入SkyWalking。Skywalking Agent插件体系主要设计三个流程:插件加载、插件匹配、插件拦截

插件体系的管理主要在
org.apache.skywalking.apm.agent.core.plugin中完成

插件的加载

插件加载的整体流程如下图 :

  • PluginBootstrap#loadPlugins() 方法开始加载插件
  • AgentClassLoader:作为类加载器对plugin下的所有jar进行加载
  • PluginResourcesResolver:插件资源解析器,读取所有插件的定义文件(skywalking-plugin.def)
  • PluginCfg: 插件定义配置,读取 skywalking-plugin.def 文件,生成插件定义(PluginDefine)

插件的匹配

我们提到SkyWalking Agent基于JavaAgent机制,实现应用透明接入SkyWalking

通过 JavaAgent 机制,我们可以在 #premain(String, Instrumentation) 方法里,调用Instrumentation#addTransformer(ClassFileTransformer) 方法,向 Instrumentation 注册ClassFileTransformer 对象,可以修改 Java 类的二进制,从而动态修改 Java 类的代码实现。

// org.apache.skywalking.apm.agent.SkyWalkingAgent
//以下为部分premain中的代码

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()));    
 agentBuilder.type(pluginFinder.buildMatch()) //设置需要拦截的类
            .transform(new Transformer(pluginFinder)) //设置Java类的修改逻辑。
            .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
            .with(new RedefinitionListener())
            .with(new Listener())
            .installOn(instrumentation);
PluginFinder.pluginInitCompleted();    

以上代码中agentBuilder在agentBuilder.type方法设置了需要拦截的类,transform方法设置了Java类的修改逻辑,及通过byte-buddy动态修改Java类的二进制。因为代码量大,逻辑复杂,我们跳过中间逻辑,直接到具体的匹配逻辑

Skywalking-Agent实现了不同方式的匹配模式,具体的插件匹配在ClassMatch的实现类中完成

  • NameMatch :基于完整的类名进行匹配,例如:"com.alibaba.dubbo.monitor.support.MonitorFilter" 。
  • IndirectMatch :间接匹配接口。相比 NameMatch 来说,确实比较 “委婉” 。
  • ClassAnnotationMatch :基于类注解进行匹配,可设置同时匹配多个。例如:"@RequestMapping"。
  • HierarchyMatch :基于父类 / 接口进行匹配,可设置同时匹配多个。
  • MethodAnnotationMatch :基于方法注解进行匹配,可设置同时匹配多个。目前项目里主要用于匹配方法上的org.skywalking.apm.toolkit.trace.@Trace 注解。


插件的拦截

在上文代码中我们已经设置需要拦截的类的方法以及类的修改逻辑,此部分要进行方法切面拦截。

拦截会涉及到以下元素 :

  • 拦截切面 InterceptPoint
  • 拦截器 Interceptor
  • 拦截类的定义Define :一个类有哪些拦截切面及对应的拦截器

如下图是插件拦截涉及到的类。如图所示:


根据方法类型的不同,使用不同 ClassEnhancePluginDefine 的实现类。其中,构造方法和静态方法使用相同的实现类。

  • AbstractClassEnhancePluginDefine :SkyWalking 类增强插件定义抽象基类
  • ClassEnhancePluginDefine :SkyWalking 类增强插件定义抽象类
  • AbstractClassEnhancePluginDefine 注重在定义( Define ),ClassEnhancePluginDefine 注重在增强( Enhance)。


因为代码量大,逻辑复杂,我们跳过中间逻辑,直接到具体的拦截器及逻辑

插件拦截最终会到以下四个拦截器实现类中

  • InstanceConstructorInterceptor ,构造方法拦截器接口
  • AroundInterceptor before-after拦截器接口
  • StaticMethodsAroundInterceptor ,静态方法拦截器接口
  • InstanceMethodsAroundInterceptor ,实例方法拦截器接口

执行步骤:

  • InterceptorInstanceLoader#load(String, classLoader) 方法,加载实例方法拦截器。
  • intercept(...) 方法,
  • Before-After 方式拦截实例方法,代码如下 :
  • 调用nstanceMethodsAroundInterceptor#beforeMethod(...) 方法,执行在实例方法之前的逻辑。
  • org.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult ,方法拦截器执行结果。当调用MethodInterceptResult.defineReturnValue(Object) 方法,设置执行结果,并标记不再继续执行。
  • 调用 Callable#call() 方法,执行原有实例方法。
  • 调用 InstanceMethodsAroundInterceptor#handleMethodException(...) 方法,处理异常。
  • 调用 InstanceMethodsAroundInterceptor#afterMethod(...) 方法,执行后置逻辑。


写在后面:

Skwyalking作为Apache旗下的优秀开源项目,其社区活跃,在国内一枝独秀,且中文文档丰富。Skywalking-Agent基于主流的Byte Buddy提供的字节码增强技术实现的自定义插件具有高性能,易维护,操作简单的特性。关于Skywalking,我会持续输出相关文章帮朋友深入了解Skwyalking,谢谢大家的关注


附件

Oceanbase(华为研发的企业级分布式数据库):
https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000000639550

Skywalking入门文档:
https://skywalking.apache.org/zh/2020-04-19-skywalking-quick-start/#