前言
在解析SkyWalking的源码之前,我们先来了解下SkyWalking中的插件机制,那它的作用是什么?为什么先要去了解它呢?我们知道SkyWalking是用来监控应用程序的系统,那它必然需要收集我们应用程序中需要监控的相关数据,那问题就来了,它怎么知道我们系统中需要收集哪些数据?所以插架的作用就在于此,由于对监控的应用程序的不确定性,进而提供了可扩展的插件机制来满足实际的业务监控需求。
为什么先要去了解它?因为对监控数据的来源收集是应用监控的基础,也是第一步,也就是SkyWalking中的探针部分,同时它也是我们解析Agent启动源码中重要的一环。
在SkyWalking的文档及源文件中,相信你能看到官方已经提供了很多常见框架的不同版本的通用插件,这里面一些由官方提供,一些由社区开发者Pull;但是如果你想在项目中重度使用SkyWalking来做APM,仅仅使用这些插件还是不够的,你必然需要封装符合自己业务监控需求的插件,相应的,你必须的知道插件的机制以及如何开发。
基础概念
工欲善其事必先利其器,开发插件前,我们先得具备SkyWalking的插件中的基础概念以及开发规范的要点。
ContextManager
它是上下文管理器,是SkyWalking中的核心API,负责提供所有主要的API,像下文中Span和Context的一些列操作都是由它封装并提供方法,同时负责维护Context上下文,内部是通过ThreadLocal
来进行多线程的隔离和维护的,在后面插件开发的过程中会经常使用到这个类的。
Span
跨度,它是系统中完成的单个工作单元,通俗的说一条完整的调用链路就是由多个Span单元组成的,通过下图相信你会更加直观的了解它,
这是/storage/waste
的一条调用链路,下面的每一行就代表一个Span,上面的test
是自定义的插件拦截定义的Span。
OpenTracing的规范中定义了一个Span需要包含操作名称、开始和结束时间、跨度标签、跨度日志、SpanContext
,其中SpanContext的作用是携带跨进程的边界数据,由traceId
、spanId
、键值对数据组成。
而SkyWalking的Span概念与之类似,同时提供了一些扩展,我们先来看下Span的类图结构,
首先AsyncSpan
定义了一个顶层接口,
public interface AsyncSpan {
AbstractSpan prepareForAsync();
AbstractSpan asyncFinish();
}
能看到里面定义了两个方法,它们是在异步场景下使用的API,当Span中的组成信息需要在另一个线程中进行设置时,像标签、日志、属性等,就可以使用它们。使用的方式也很简单,比如在A线程的Span里面调用prepareForAsync()
方法,然后再B线程中拿到Span进行设置,完成后调用asyncFinish()
方法来结束调用即可。
再来看它的子类接口AbstractSpan
,它里面就定义了Span的属性组成,像操作名称OperateName、标签Tag、日志Log、组件Component、类别Layer等等。其中
- Component在项目的
ComponentsDefine
中已经定义了很多,也可以进行扩展; - Layer有5种值:UNKNOWN(默认值)、DB、RPC_FRAMEWORK、HTTP、MQ,根据情况选择即可。
而下面的实现子类中,其中NoopSpan
是一个空实现,没有任何实际操作,用来存放IgnoredTracerContext
的,我们主要看AbstractTracingSpan
下三个重要的实现子类:
LocalSpan
:表示本地的跨度,像实例方法、静态方法。EntrySpan
:表示服务提供者和MQ的消费者。ExitSpan
:表示服务客户端和MQ的生产者。
我们再定义Span的时候只需要根据上面的描述创建相应的Span即可,其中EntrySpan
和ExitSpan
注意要设置Component和Layer值。这里创建Span的方式也很简单,利用ContextManager
提供的API来完成,
ContextManager.createLocalSpan("");
ContextManager.createEntrySpan("");
ContextManager.createExitSpan("");
Context
Context通常代表着上下文,相信看过一些框架源码,像Spring等,一定对它很熟悉;那我们来看看SkyWalking中的Context是怎么定义的,先看类图,
这里AbstractTracerContext
是一个顶层接口,代表着跟踪上下文的管理器,IgnoredTracerContext
刚才我们提到过,源码注释中对其说明是表示一个被忽略的上下文,主要作用是用一个字段int stackDepth
来维护堆栈深度,它通常在Span中的OperateName定义为空时会被创建;而TracingContext
才是核心跟踪逻辑控制器,它保存着一些基本的信息个状态以外,还存放着上下文中传播的Span
和跨度深度等重要数据。
我们在创建Span
的时候方法内部会自动帮我创建AbstractTracerContext
并初始化,所以不用担心,主要在下面两种情况下会需要我们进行传递处理:
- 跨线程:当我们在调用方法栈中包含异步处理调用时,我们需要跟踪异步线程的数据并将其记录在原栈的上下文的跟踪信息中,这时需要利用
ContextSnapshot
来完成TracerContext
的传播。使用步骤如下(官方已经写的很详细了,拿来即用):- 使用
ContextManager#capture
方法获取 ContextSnapshot 对象. - 让子线程以任何方式, 通过方法参数或由现有参数携带来访问 ContextSnapshot
- 在子线程中使用
ContextManager#continued
.
- 使用
- 跨进程:相信实际项目中,更多的是分布式系统,一个完成的调用链路通常是服务A -> 服务B -> 服务C这样,这时候为了实现分布式的追踪,就属于跨进程了,我们需要将上下文在整个过程中进行传播,这就需要
ContextCarrier
来完成了。使用步骤如下:- 在客户端, 创建一个新的空的
ContextCarrier
. - 通过
ContextManager#createExitSpan
创建一个 ExitSpan 或者使用ContextManager#inject
来初始化ContextCarrier
. - 将
ContextCarrier
所有信息放到请求头 (如 HTTP HEAD), 附件(如 Dubbo RPC 框架), 或者消息 (如 Kafka) 中 - 通过服务调用, 将
ContextCarrier
传递到服务端. - 在服务端, 在对应组件的头部, 附件或消息中获取
ContextCarrier
所有内容. - 通过
ContextManager#createEntrySpan
创建 EntrySpan 或者使用ContextManager#extract
来绑定服务端和客户端.
- 在客户端, 创建一个新的空的
结尾
通过上面我们知道了SkyWalking中插件的作用、自定义插件中需要定义的哪些基本信息和规范,以及如何在不同的场景下进行上下文的传播,那接下来就是如何进行插件开发了,下一篇我们继续。
身未动,心已远。
把一件事做到极致就是天分!