SkyWalking 源码之插件开发
1. 基础概念
本节主要介绍 SkyWalking 的 Java 探针插件开发过程中,所涉及核心对象的基本概念,主要包括 Span、Trace Segment、ContextCarrier 和 ContextSnapshot。 掌握这些核心对象后,就可以很容易地学习第 2 节介绍的核心对象相关的API。
1.1 Span
Span 是分布式追踪系统中一个非常重要的概念,可以理解为一次方法调用、一个程序块的调用、一次RPC调用或者数据库访问。 如果想更加深入地了解 Span 概念,可以学习一下论文 “Google Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”(简称“Google Dapper”)和OpenTracing。
SkyWalking 的 Span 概念与 “Google Dapper” 论文和 OpenTracing 类似,但还进行了扩展,可依据是跨线程还是跨进程的链路,将Span粗略分为两类:LocalSpan 和 RemoteSpan。
LocalSpan 代表一次普通的 Java 方法调用,与跨进程无关,多用于当前进程中关键逻辑代码块的记录,或在跨线程后记录异步线程执行的链路信息。 RemoteSpan 可细分为 EntrySpan 和E xitSpan:
- EntrySpan 代表一个应用服务的提供端或服务端的入口端点,如 Web 容器的服务端的入口、RPC服务器的消费者、消息队列的消费者;
- ExitSpan(SkyWalking的早期版本中称其为LeafSpan),代表一个应用服务的客户端或消息队列的生产者,如 Redis 客户端的一次Redis调用、MySQL 客户端的一次数据库查询、RPC组件的一次请求、消息队列生产者的生产消息。
1.2 Trace Segment
Trace Segment 是 SkyWalking 中特有的概念,通常指在支持多线程的语言中,一个线程中归属于同一个操作的所有Span的聚合。这些Span具有相同的唯一标识 SegmentID。 Trace Segment 对应的实体类位于 org.apache.skywalking.apm.agent.core.context.trace.TraceSegment
,其中重要的属性如下。
- TraceSegmentId:此 Trace Segment 操作的唯一标识。使用雪花算法生成,保证全局唯一。
- Refs:此 Trace Segment 的上游引用。对于大多数上游是RPC调用的情况,Refs 只有一个元素,但如果是消息队列或批处理框架,上游可能会是多个应用服务,所以就会存在多个元素。
- Spans:用于存储,从属于此 Trace Segment 的 Span 的集合。
- RelatedGlobalTraces:此 Trace Segment 的 Trace Id。大多数时候它只包含一个元素,但如果是消息队列或批处理框架,上游是多个应用服务,会存在多个元素。
- Ignore:是否忽略。如果 Ignore 为true,则此 Trace Segment 不会上传到 SkyWalking 后端。
- IsSizeLimited:从属于此 Trace Segment 的 Span 数量限制,初始化大小可以通过
config.agent.span_limit_per_segment
参数来配置,默认长度为 300。若超过配置值,在创建新的 Span 的时候,会变成 NoopSpan。NoopSpan表 示没有任何实际操作的 Span 实现,用于保持内存和GC成本尽可能低。 - CreateTime:此 Trace Segment 的创建时间。
1.3 ContextCarrier
分布式追踪要解决的一个重要问题是跨进程调用链的连接,ContextCarrier 的就是为了解决这个问题。如客户端A、服务端B两个应用服务,当发生一次 A 调用 B 的时候,跨进程传播的步骤如下。
- 客户端 A 创建空的 ContextCarrier。
- 通过
ContextManager#createExitSpan
方法创建一个 ExitSpan,或者使用ContextManager#inject
在过程中传入并初始化 ContextCarrier。 - 使用
ContextCarrier.items()
将 ContextCarrier 所有元素放到调用过程中的请求信息中,如 HTTP HEAD、Dubbo RPC 框架的attachments、消息队列 Kafka 消息的 header 中。 - ContextCarrier 随请求传输到服务端。
- 服务端 B 接收具有 ContextCarrier 的请求,并提取 ContextCarrier 相关的所有信息。
- 通过
ContextManager#createEntrySpan
方法创建 EntrySpan,或者使用ContextManager#extract
建立分布式调用关联,即绑定服务端 B 和客户端 A。
1.4 ContextSnapshot
除了跨进程,跨线程也是需要支持的,例如异步线程(内存中的消息队列)在Java中很常见。跨线程和跨进程十分相似,都需要传播上下文,唯一的区别是,跨线程不需要序列化。以下是跨线程传播的步骤。
- 使用
ContextManager#capture
方法获取 ContextSnapshot 对象。 - 让子线程以任何方式,通过方法参数或由现有参数携带来访问 ContextSnapshot。
- 在子线程中使用
ContextManager#continued
。
2. 核心对象相关API的使用
本节主要介绍 SkyWalking 的 Java 探针插件开发过程中涉及的重要 API 的使用,使读者能够掌握每个 API 的用法,进而使用正确的 API 完成 Java 探针插件的开发。
2.1 ContextCarrier#items
在跨进程链路追踪的案例场景中,我们使用 ContextCarrier#items 完成两个进程的链路数据管理。以 HTTP 请求为例,我们需要处理以下两个场景。
场景一,将发送进程的链路信息绑定到 header 中并通过客户端发送出去,具体代码如下:
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
httpRequest.setHeader(next.getHeadKey(), next.getHeadValue());
}
场景二,接收服务器通过解析 header 将链路信息绑定到本次接收处理中,具体代码如下:
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
next.setHeadValue(request.getHeader(ne xt.getHeadKey()));
}
2.2 ContextManager#createEntrySpan
一个应用服务的提供端或服务端的接收端点,如 Web 容器的服务端入口、RPC 服务器或消息队列的消费者,在被调用时,都需要创建 EntrySpan, 这时需要使用 ContextManager#createEntrySpan
来完成,具体代码如下:
ContextManager.createEntrySpan(operationName, contextCarrier);
ContextManager#createEntrySpan
API有以下两个很关键的入参。
- operationName
定义此 EntrySpan 的操作方法名称,如 HTTP 接口的请求路径。注意,operationName
必须是有穷尽的,比如 RESTful 接口匹配 /path/{id}
, 一定不要将 id 的真实值记录进来,因为 SkyWalking 在数据上报的时候,出于减少 operationName
长度和链路消息传输性能的考虑, 会将 operationName
映射在本地映射字典表中,使用 operationName
的映射值进行传输。因此,要保证 operationName
是有穷尽的,否则会造成字典表过大。
语音质检之类的 URL 似乎是无穷尽的,要看看怎么解决一下了。
- contextCarrier
为了绑定跨进程的追踪,需要将上游链路的追踪信息通过 ContextCarrier#items
绑定到本链路中,具体 API 使用见 ContextCarrier#items
的使用。
2.3 ContextManager#extract
在消息队列或是批处理框架中,上游是多个应用服务,所以会存在多个元素,在这种场景下需要使用 ContextManager#extract
将多个上游应用的追踪信息绑定到当前链路中。
下面是消息队列 Kafka 框架在批处理情况下,将多个上游应用链路绑定到一起,具体代码如下:
for (ConsumerRecord<?, ?> record : consumerRecords) {
ContextCarrier contextCarrier = new ContextCarrier();
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
Iterator<Header> iterator = record.headers().headers(next.getHeadKey()).iterator();
if (iterator.hasNext()) {
next.setHeadValue(new String(iterator.next().value()));
}
}
ContextManager.extract(contextCarrier);
}
2.4 ContextManager#createExitSpan
在一个应用服务的客户端或消息队列生产者的发送端点,如 Redis 客户端的一次内存访问、MySQL 客户端的一次数据库查询或 RPC 组件的一次请求, 当发生请求时,客户端进程需要使用 ContextManager#createExitSpan
来创建 ExitSpan
。具体代码如下:
ContextManager.createExitSpan(operationName, contextCarrier, peer);
ContextManager#createExitSpan
API 有以下 3 个很关键的入参。
operationName
:定义此 ExitSpan 的操作方法名称。注意,operationName 一定是有穷尽的,详情与ContextManager#createEntrySpan
的入参operationName
一致。contextCarrier
:为了绑定跨进程的追踪,需要将本线程的链路的追踪信息绑定到header
中,具体 API 使用见ContextCarrier#items
的使用。peer
:下游的地址,具体格式为ip:port
。若下游系统无法下探针,如 Redis、MySQL 等资源,则需要将下游所有的地址写入 peer 参数中,具体格式为ip:port;ip:port
。
2.5 ContextManager#inject
ContextManager#inject
是个不太常用的 API,当需要自己控制创建 ExitSpan 方法中的 ContextManager#inject
的调用时机时,可以使用此 API 完成。
2.6 ContextManager#createLocalSpan
对于进程中关键的本地逻辑代码块的链路追踪,或在跨线程后,开启对异步线程执行的链路追踪,需要使用 ContextManager#createLocalSpan
来创建 LocalSpan。具体代码如下:
ContextManager.createLocalSpan(operationName)
ContextManager#createLocalSpan
API有一个很关键的入参:operationName
。operationName
定义此 LocalSpan 的操作方法名称。 注意,operationName
一定是有穷尽的,详情与 ContextManager#createEntrySpan
的入参 operationName
一致。
2.7 ContextManager#capture
在跨进程进行链路追踪的时候,我们需要传递父线程的链路快照,这时需要 ContextManager#capture
来获取快照。 快照的传递,通常是由修改参数的数据或方法传递到子线程的。获取当前线程的快照的具体代码如下:
ContextSnapshot snapshot = ContextManager.capture();
2.8 ContextManager#continued
在子线程中关联父进程快照需要使用 ContextManager#continued
,具体代码如下:
ContextManager.continued(contextSnapshot);
2.9 ContextManager#stopSpan
无论哪个类型的 Span,都需要通过调用 stop
方法来结束此 Span 的追踪,具体代码如下:
ContextManager.stopSpan(span);
2.10 ContextManager#isActive
在不确定当前线程中是否存在未结束的 Span 的情况下,贸然调用 stop
方法会导致探针的异常,所以使用此 API 判断当前线程中是否有活跃的 Span。具体代码如下:
ContextManager.isActive();
2.11 ContextManager#activeSpan
此 API 用来获取当前进程中的活跃 Span,具体代码如下:
AbstractSpan activeSpan = ContextManager.activeSpan();
2.12 AbstractSpan#setComponent
Component 可以理解为组件,是 Span 中的重要属性,SkyWalking 支持的探针组件都定义在文件 org.apache.skywalking.apm.network.trace.component.ComponentsDefine
和 /oap-server/server-core/src/test/resources/component-libraries.yml
中, 将 Component 赋予 Span 需要使用 AbstractSpan#setComponent
这个 API,例如将 Tomcat 组件赋予 Span 的代码如下:
span.setComponent(ComponentsDefine.TOMCAT);
2.13 AbstractSpan#setLayer
Layer 可以理解为 Span 的简单显示,对于页面展示有很重要的意义。目前在 SkyWalking 中 Layer 有 5 种类型,分别是:
- 代表数据库的
SpanLayer.Database
; - 代表 RPC 框架的
SpanLayer.RPCFramework
; - 代表 Web 服务器接收 HTTP 请求的
SpanLayer.Http
; - 代表消息队列的
SpanLayer.MQ
; - 代表缓存数据库的
SpanLayer.Cache
。
比如当前 Span 的组件是数据库类型的组件,我们需要使用如下 API,对当前 Span 进行标记。
span.setLayer(SpanLayer.Database);
SpanLayer.asDB(span);//推荐
2.14 AbstractSpan#tag
有时候,我们需要在当前 Span 中增加对应的 tag,使用 AbstractSpan#tag
可以进行 Span 的记录,下面这些 tag 的 key 是有特殊意义的。
Tags.URL
:key 为 url,记录传入请求的 URL。Tags.STATUS_CODE
:key 为 status_code,记录响应的HTTP状态代码,多用于记录状态码大于等于 400 的情况。Tags.DB_TYPE
:key 为 db.type,记录数据库类型,例如 SQL、Redis、Cassandra 等。Tags.DB_INSTANCE
:key 为 db.instance,记录数据库实例名称。Tags.DB_STATEMENT
:key 为 db.statement,记录数据库访问的 SQL 语句。Tags.DB_BIND_VARIABLES
:key 为 db.bind_vars,记录 SQL 语句的绑定变量。Tags.MQ_QUEUE
:key 为 mq.queue,记录消息中间件的队列名称。Tags.MQ_BROKER
:key 为 mq.broker,记录消息中间件的代理地址。Tags.MQ_TOPIC
:key 为 mq.topic,记录消息中间件的主题名称。Tags.HTTP.METHOD
:key 为 http.method,记录 HTTP 请求的方法。
下面是两种记录 tag 的方法:
Tags.URL.set(span, request.getURI().toString());//推荐
span.tag("key", "value");
2.15 AbstractSpan#log
log 通常是记录当前方法出现异常时,将堆栈信息存入此 Span,或者根据一个时间点记录具有多个字段的公共日志。API使用如下:
span.log(System.currentTimeMillis(), even)
ContextManager.activeSpan().errorOccurred().log(t);
2.16 AbstractSpan#errorOccurred
在此 Span 追踪上下文的范围内,在自动检测机制中发生错误几乎意味着抛出异常,我们需要使用此 API 来标记此 Span 出现异常。
ContextManager.activeSpan().errorOccurred();
2.17 AbstractSpan#setPeer
peer 表示对端资源,格式为 ip:port
。若下游系统无法下探针,如 Redis、MySQL 等资源。需要将下游所有的地址写入 peer 参数中,具体格式是 ip:port;ip:port
,具体API如下:
span.setPeer("ip:port");
2.18 AsyncSpan
在一些场景中,当 Span 的 tag、log、结束时间等属性要在另一个线程中设置时,需要使用此 API 来完成,具体步骤如下:
- 在原始上下文中调用
AsyncSpan#prepareForAsync
; - 将该 Span 传播到其他线程,并完成相应属性的记录;
- 在全部操作就绪之后,可在任意线程中调用
#asyncFinish
结束调用; - 当所有 Span 的
AsyncSpan#prepareForAsync
完成后, 追踪上下文会结束, 并一起被回传到后端服务(根据API执行次数判断)。
LTS 插件开发
LTS GitHub 地址 https://github.com/ltsopensource/light-task-scheduler
已知 LTS 整体执行流程,问题:
- LTS 任务执行(TaskTracker)的执行流程, 源代码分析。 (为了找出需要 SkyWalking 写增强的切入点,按照目前的理解可以直接增强 业务代码上的任务执行方法)
- LTS 支持 Quartz,可不可以直接使用 quartz 的插件 (他增强的是 org.quartz.core.JobRunShell 类的 run 方法)? 答:没有使用到
- LTS 监控要实现的效果是什么? 答:主要是在任务执行时开启链路端点