本文主要基于 SkyWalking 3.2.6 正式版
分布式链路追踪系统,链路的追踪大体流程如下:
本文主要分享【第一部分】 SkyWalking Agent 收集 Trace 数据。文章的内容顺序如下:
不包括插件对 Context 收集的方法的调用,后续单独文章专门分享,胖友也可以阅读完本文后,自己去看 apm-sdk-plugin
的实现代码。
本文涉及到的代码如下图:
org.skywalking.apm.agent.core.context.trace.TraceSegment
,是一次分布式链路追踪( Distributed Trace ) 的一段。
TraceSegment 属性,如下:
traceSegmentId
属性,TraceSegment 的编号,全局唯一。在 「2.1 ID」 详细解析。refs
属性,TraceSegmentRef 数组,指向的父 TraceSegment 数组。relatedGlobalTraces
属性,关联的 DistributedTraceId 数组。spans
属性,包含的 Span 数组。在 「2.2 AbstractSpan」 详细解析。这是 TraceSegment 的主体,总的来说,TraceSegment 是 Span 数组的封装。ignore
属性,是否忽略该条 TraceSegment 。在一些情况下,我们会忽略 TraceSegment ,即不收集链路追踪,在下面 「3. Context」 部分内容,我们将会看到这些情况。isSizeLimited
属性,Span 是否超过上限( `Config.Agent.SPAN_LIMIT_PER_SEGMENT` )。超过上限,不在记录 Span 。为什么会有多个爸爸?
友情提示:多个爸爸的故事,可能比较难懂,等胖友读完全文,在回过头想想。或者拿起来代码调试调试。
下面,我们来具体看看 TraceSegment 的每个元素,最后,我们会回过头,在 「2.4 TraceSegment」 详细解析它。
org.skywalking.apm.agent.core.context.ids.ID
,编号。从类的定义上,这是一个通用的编号,由三段整数组成。
目前使用 GlobalIdGenerator 生成,作为全局唯一编号。属性如下:
part1
属性,应用实例编号。part2
属性,线程编号。part3
属性,时间戳串,生成方式为 ${时间戳} * 10000 + 线程自增序列([0, 9999])
。例如:15127007074950012 。具体生成方法的代码,在 GlobalIdGenerator 中详细解析。encoding
属性,编码后的字符串。格式为 "${part1}.${part2}.${part3}"
。例如,"12.35.15127007074950000"
。isValid
属性,编号是否合法。org.skywalking.apm.agent.core.context.ids.GlobalIdGenerator
,全局编号生成器。
#generate()
方法,生成 ID 对象。代码如下:
org.skywalking.apm.agent.core.context.ids.DistributedTraceId
,分布式链路追踪编号抽象类。
id
属性,全局编号。DistributedTraceId 有两个实现类:
GlobalIdGenerator#generate()
方法,创建 ID 对象。org.skywalking.apm.agent.core.context.ids.DistributedTraceIds
,DistributedTraceId 数组的封装。
relatedGlobalTraces
属性,关联的 DistributedTraceId 链式数组。#append(DistributedTraceId)
方法,添加分布式链路追踪编号( DistributedTraceId )。代码如下:
org.skywalking.apm.agent.core.context.trace.AbstractSpan
,Span 接口( 不是抽象类 ),定义了 Span 通用属性的接口方法:
#getSpanId()
方法,获得 Span 编号。一个整数,在 TraceSegment 内唯一,从 0 开始自增,在创建 Span 对象时生成。#setOperationName(operationName)
方法,设置操作名。#setOperationId(operationId)
方法,设置操作编号。考虑到操作名是字符串,Agent 发送给 Collector 占用流量较大。因此,Agent 会将操作注册到 Collector ,生成操作编号。在 《SkyWalking 源码分析 —— Agent DictionaryManager 字典管理》 有详细解析。#setComponent(Component)
方法,设置 org.skywalking.apm.network.trace.component.Component
,例如:MongoDB / SpringMVC / Tomcat 等等。目前,官方在 org.skywalking.apm.network.trace.component.ComponentsDefine
定义了目前已经支持的 Component 。#setComponent(componentName)
方法,直接设置 Component 名字。大多数情况下,我们不使用该方法。
Only use this method in explicit instrumentation, like opentracing-skywalking-bridge.
It it higher recommend don't use this for performance consideration.#setLayer(SpanLayer)
方法,设置 org.skywalking.apm.agent.core.context.trace.SpanLayer
。目前有,DB 、RPC_FRAMEWORK 、HTTP 、MQ ,未来会增加 CACHE 。#tag(key, value)
方法,设置键值对的标签。可以调用多次,构成 Span 的标签集合。在 「2.2.1 Tag」 详细解析。#log(timestampMicroseconds, fields)
方法,记录一条通用日志,包含 fields
键值对集合。#log(Throwable)
方法,记录一条异常日志,包含异常信息。#errorOccurred()
方法,标记发生异常。大多数情况下,配置 #log(Throwable)
方法一起使用。#start()
方法,开始 Span 。一般情况的实现,设置开始时间。#isEntry()
方法,是否是入口 Span ,在 「2.2.2.1 EntrySpan」 详细解析。#isExit()
方法,是否是出口 Span ,在 「2.2.2.2 ExitSpan」 详细解析。org.skywalking.apm.agent.core.context.tag.AbstractTag<T>
,标签抽象类。注意,这个类的用途是将标签属性设置到 Span 上,或者说,它是设置 Span 的标签的工具类。代码如下:
key
属性,标签的键。#set(AbstractSpan span, T tagValue)
抽象方法,设置 Span 的标签键 key
的值为 tagValue
。org.skywalking.apm.agent.core.context.tag.StringTag
,值类型为 String 的标签实现类。
#set(AbstractSpan span, String tagValue)
实现方法,设置 Span 的标签键 key
的值为 tagValue
。org.skywalking.apm.agent.core.context.tag.Tags
,常用 Tag 枚举类,内部定义了多个 HTTP 、DB 相关的 StringTag 的静态变量。
在 《opentracing-specification-zh —— 语义惯例》 里,定义了标准的 Span Tag 。
AbstractSpan 实现类如下图:
抛开右半边的 Span 实现类的特殊处理,Span 只有三种实现类:
下面,我们分小节逐步分享。
org.skywalking.apm.agent.core.context.trace.AbstractTracingSpan
,实现 AbstractSpan 接口,链路追踪 Span 抽象类。
在创建 AbstractTracingSpan 时,会传入 spanId
, parentSpanId
, operationName
/ operationId
参数。参见构造方法:
#AbstractTracingSpan(spanId, parentSpanId, operationName)
#AbstractTracingSpan(spanId, parentSpanId, operationId)
大部分是 setting / getting 方法,或者类似方法,已经添加注释,胖友自己阅读。
#finish(TraceSegment)
方法,完成( 结束 ) Span ,将当前 Span ( 自己 )添加到 TraceSegment 。为什么会调用该方法,在 「3. Context」 详细解析。
org.skywalking.apm.agent.core.context.trace.StackBasedTracingSpan
,实现 AbstractTracingSpan 抽象类,基于栈的链路追踪 Span 抽象类。这种 Span 能够被多次调用 #start(...)
和 #finish(...)
方法,在类似堆栈的调用中。在 「2.2.2.2.1 EntrySpan」 中详细举例子。代码如下:
stackDepth
属,栈深度。重点
org.skywalking.apm.agent.core.context.trace.EntrySpan
,实现 StackBasedTracingSpan 抽象类,入口 Span ,用于服务提供者( Service Provider ) ,例如 Tomcat 。
EntrySpan 是 TraceSegment 的第一个 Span ,这也是为什么称为"入口" Span 的原因。
那么为什么 EntrySpan 继承 StackBasedTracingSpan ?
例如,我们常用的 SprintBoot 场景下,Agent 会在 SkyWalking 插件在 Tomcat 定义的方法切面,创建 EntrySpan 对象,也会在 SkyWalking 插件在 SpringMVC 定义的方法切面,创建 EntrySpan 对象。那岂不是出现两个 EntrySpan ,一个 TraceSegment 出现了两个入口 Span ?
答案是当然不会!Agent 只会在第一个方法切面,生成 EntrySpan 对象,第二个方法切面,栈深度 + 1。这也是上面我们看到的 #finish(TraceSegment)
方法,只在栈深度为零时,出栈成功。通过这样的方式,保持一个 TraceSegment 有且仅有一个 EntrySpan 对象。
当然,多个 TraceSegment 会有多个 EntrySpan 对象 ,例如【服务 A】远程调用【服务 B】。
另外,虽然 EntrySpan 在第一个服务提供者创建,EntrySpan 代表的是最后一个服务提供者,例如,上面的例子,EntrySpan 代表的是 Spring MVC 的方法切面。所以,startTime
和 endTime
以第一个为准,componentId
、componentName
、layer
、logs
、tags
、operationName
、operationId
等等以最后一个为准。并且,一般情况下,最后一个服务提供者的信息也会更加详细。
ps:如上内容信息量较大,胖友可以对照着实现方法,在理解理解。HOHO ,良心笔者当然也是加了注释的。
如下是一个 EntrySpan 在 SkyWalking 展示的例子:
重点
org.skywalking.apm.agent.core.context.trace.ExitSpan
,继承 StackBasedTracingSpan 抽象类,出口 Span ,用于服务消费者( Service Consumer ) ,例如 HttpClient 、MongoDBClient 。
ExitSpan 实现 org.skywalking.apm.agent.core.context.trace.WithPeerInfo
接口,代码如下:
peer
属性,节点地址。peerId
属性,节点编号。如下是一个 ExitSpan 在 SkyWalking 展示的例子:
那么为什么 ExitSpan 继承 StackBasedTracingSpan ?
例如,我们可能在使用的 Dubbox 场景下,【Dubbox 服务 A】使用 HTTP 调用【Dubbox 服务 B】时,实际过程是,【Dubbox 服务 A】=》【HttpClient】=》【Dubbox 服务 B】。Agent 会在【Dubbox 服务 A】创建 ExitSpan 对象,也会在 【HttpClient】创建 ExitSpan 对象。那岂不是一次出口,出现两个 ExitSpan ?
答案是当然不会!Agent 只会在【Dubbox 服务 A】,生成 EntrySpan 对象,第二个方法切面,栈深度 + 1。这也是上面我们看到的 #finish(TraceSegment)
方法,只在栈深度为零时,出栈成功。通过这样的方式,保持一次出口有且仅有一个 ExitSpan 对象。
当然,一个 TraceSegment 会有多个 ExitSpan 对象 ,例如【服务 A】远程调用【服务 B】,然后【服务 A】再次远程调用【服务 B】,或者然后【服务 A】远程调用【服务 C】。
另外,虽然 ExitSpan 在第一个消费者创建,ExitSpan 代表的也是第一个服务提消费者,例如,上面的例子,ExitSpan 代表的是【Dubbox 服务 A】。
ps:如上内容信息量较大,胖友可以对照着实现方法,在理解理解。HOHO ,良心笔者当然也是加了注释的。
org.skywalking.apm.agent.core.context.trace.LocalSpan
,继承 AbstractTracingSpan 抽象类,本地 Span ,用于一个普通方法的链路追踪,例如本地方法。
如下是一个 EntrySpan 在 SkyWalking 展示的例子:
org.skywalking.apm.agent.core.context.trace.NoopSpan
,实现 AbstractSpan 接口,无操作的 Span 。配置 IgnoredTracerContext 一起使用,在 IgnoredTracerContext 声明单例 ,以减少不收集 Span 时的对象创建,达到减少内存使用和 GC 时间。
org.skywalking.apm.agent.core.context.trace.NoopExitSpan
,实现 org.skywalking.apm.agent.core.context.trace.WithPeerInfo
接口,继承 StackBasedTracingSpan 抽象类,出口 Span ,无操作的出口 Span 。和 ExitSpan 相对,不记录服务消费者的出口 Span 。
org.skywalking.apm.agent.core.context.trace.TraceSegmentRef
,TraceSegment 指向,通过 traceSegmentId
和 spanId
属性,指向父级 TraceSegment 的指定 Span 。
type
属性,指向类型( SegmentRefType ) 。不同的指向类型,使用不同的构造方法。CROSS_PROCESS
,跨进程,例如远程调用,对应构造方法 #TraceSegmentRef(ContextCarrier)。CROSS_THREAD
,跨线程,例如异步线程任务,对应构造方法 #TraceSegmentRef(ContextSnapshot) 。traceSegmentId
属性,父 TraceSegment 编号。重要spanId
属性,父 Span 编号。重要peerId
属性,节点编号。注意,此处的节点编号就是应用( Application )编号。peerHost
属性,节点地址。entryApplicationInstanceId
属性,入口应用实例编号。例如,在一个分布式链路 A->B->C
中,此字段为 A 应用的实例编号。parentApplicationInstanceId
属性,父应用实例编号。entryOperationName
属性,入口操作名。entryOperationId
属性,入口操作编号。parentOperationName
属性,父操作名。parentOperationId
属性,父操作编号。在看完了 TraceSegment 的各个元素,我们来看看 TraceSegment 内部实现的方法。
TraceSegment 构造方法,代码如下:
GlobalIdGenerator#generate()
方法,生成 ID 对象,赋值给 traceSegmentId
。spans
数组。#ref(TraceSegmentRef)
方法,添加 TraceSegmentRef 对象,到 refs
属性,即指向父 Segment 。
在 「2. Trace」 中,我们看了 Trace 的数据结构,本小节,我们一起来看看 Context 是怎么收集 Trace 数据的。
org.skywalking.apm.agent.core.context.ContextManager
,实现了 BootService 、TracingContextListener 、IgnoreTracerContextListener 接口,链路追踪上下文管理器。
CONTEXT
静态属性,线程变量,存储 AbstractTracerContext 对象。为什么是线程变量呢?
一个 TraceSegment 对象,关联到一个线程,负责收集该线程的链路追踪数据,因此使用线程变量。
而一个 AbstractTracerContext 会关联一个 TraceSegment 对象,ContextManager 负责获取、创建、销毁 AbstractTracerContext 对象。
#getOrCreate(operationName, forceSampling)
静态方法,获取 AbstractTracerContext 对象。若不存在,进行创建。
在下面的 #createEntrySpan(...)
、#createLocalSpan(...)
、#createExitSpan(...)
等等方法中,都会调用 AbstractTracerContext 提供的方法。这些方法的代码,我们放在 「3.2 AbstractTracerContext」 一起解析,保证流程的整体性。
另外,ContextManager 封装了所有 AbstractTracerContext 提供的方法,从而实现,外部调用者,例如 SkyWalking 的插件,只调用 ContextManager 的方法,而不调用 AbstractTracerContext 的方法。
#boot()
实现方法,启动时,将自己注册到 [TracingContext.ListenerManager]() 和 [IgnoredTracerContext.ListenerManager]() 中,这样一次链路追踪上下文( Context )完成时,从而被回调如下方法,清理上下文:
org.skywalking.apm.agent.core.context.AbstractTracerContext
,链路追踪上下文接口。定义了如下方法:
#getReadableGlobalTraceId()
方法,获得关联的全局链路追踪编号。#createEntrySpan(operationName)
方法,创建 EntrySpan 对象。#createLocalSpan(operationName)
方法,创建 LocalSpan 对象。#createExitSpan(operationName, remotePeer)
方法,创建 ExitSpan 对象。#activeSpan()
方法,获得当前活跃的 Span 对象。#stopSpan(AbstractSpan)
方法,停止( 完成 )指定 AbstractSpan 对象。#inject(ContextCarrier)
方法,将 Context 注入到 ContextCarrier ,用于跨进程,传播上下文。#extract(ContextCarrier)
方法,将 ContextCarrier 解压到 Context ,用于跨进程,接收上下文。#capture()
方法,将 Context 快照到 ContextSnapshot ,用于跨线程,传播上下文。#continued(ContextSnapshot)
方法,将 ContextSnapshot 解压到 Context ,用于跨线程,接收上下文。org.skywalking.apm.agent.core.context.TracingContext
,实现 AbstractTracerContext 接口,链路追踪上下文实现类。
segment
属性,上下文对应的 TraceSegment 对象。activeSpanStack
属性,AbstractSpan 链表数组,收集当前活跃的 Span 对象。正如方法的调用与执行一样,在一个调用栈中,先执行的方法后结束。spanIdGenerator
属性,Span 编号自增序列。创建的 Span 的编号,通过该变量自增生成。TracingContext 构造方法 ,代码如下:
spanIdGenerator = 0
。#getReadableGlobalTraceId()
实现方法,获得 TraceSegment 的首个 DistributedTraceId 作为返回。
调用 ContextManager#createEntrySpan(operationName, carrier)
方法,创建 EntrySpan 对象。代码如下:
#getOrCreate(operationName, forceSampling)
方法,获取 AbstractTracerContext 对象。若不存在,进行创建。TracingContext#createEntrySpan(operationName)
方法,创建 EntrySpan 对象。调用 TracingContext#createEntrySpan(operationName)
方法,创建 EntrySpan 对象。代码如下:
#push(AbstractSpan)
方法,添加到 activeSpanStack
中。"The Entry Span can't be the child of Non-Entry Span"
。调用 ContextManager#createLocalSpan(operationName)
方法,创建 LocalSpan 对象。
#getOrCreate(operationName, forceSampling)
方法,获取 AbstractTracerContext 对象。若不存在,进行创建。TracingContext#createLocalSpan(operationName)
方法,创建 LocalSpan 对象。调用 TracingContext#createLocalSpan(operationName)
方法,创建 LocalSpan 对象。代码如下:
#push(AbstractSpan)
方法,添加到 activeSpanStack
中。LocalSpan#start()
方法,开始 LocalSpan 。#push(AbstractSpan)
方法,添加到 activeSpanStack
中。调用 ContextManager#createExitSpan(operationName, carrier, remotePeer)
方法,创建 ExitSpan 对象。
#getOrCreate(operationName, forceSampling)
方法,获取 AbstractTracerContext 对象。若不存在,进行创建。TracingContext#createExitSpan(operationName, remotePeer)
方法,创建 ExitSpan 对象。TracingContext#inject(ContextCarrier)
方法,将 Context 注入到 ContextCarrier ,跨进程,传播上下文。在 「3.2.3 ContextCarrier」 详细解析。调用 TracingContext#createEntrySpan(operationName)
方法,创建 ExitSpan 对象。代码如下:
activeSpanStack
中。调用 ContextManager#stopSpan()
方法,结束 Span 。代码如下:
TracingContext#stopSpan(AbstractSpan)
方法,结束 Span 。当所有活跃的 Span 都被结束后,当前线程的 TraceSegment 完成。调用 TracingContext#stopSpan(AbstractSpan)
方法,结束 Span 。代码如下:
AbstractTracingSpan#finish(TraceSegment)
方法,完成 Span 。#pop()
方法,移除出 activeSpanStack
。#finish()
方法,当前线程的 TraceSegment 完成。调用 TracingContext#stopSpan(AbstractSpan)
方法,完成 Context 。代码如下:
TraceSegment#finish(isSizeLimited)
方法,完成 TraceSegment 。TraceSegment#setIgnore(true)
方法,标记该 TraceSegment 忽略,不发送给 Collector 。org.skywalking.apm.agent.core.context.IgnoredTracerContext
,实现 AbstractTracerContext 接口,忽略( 不记录 )链路追踪的上下文。代码如下:
NOOP_SPAN
静态属性,NoopSpan 单例。stackDepth
属性,栈深度。代码比较简单,胖友自己阅读该类的实现。
org.skywalking.apm.agent.core.context.ContextCarrier
,实现 java.io.Serializable
接口,跨进程 Context 传输载体。
我们来打开 #TraceSegmentRef(ContextCarrier)
构造方法,该方法用于将 ContextCarrier 转换成 TraceSegmentRef ,对比下两者的属性,基本一致,差异如下:
peerHost
属性,节点地址。entryOperationName
属性,入口操作名。parentOperationName
属性,父操作名。类似 entryOperationName
属。primaryDistributedTraceId
属性,分布式链路追踪全局编号。它不在此处处理,而在 `TracingContext#extract(ContextCarrier)` 方法中。在 ContextManager#createEntrySpan(operationName, carrier)
方法中,当存在 ContextCarrier 传递时,创建 Context 后,会将 ContextCarrier 解压到 Context 中,以达到跨进程传播。TracingContext#extract(ContextCarrier)
方法,代码如下:
TraceSegment#ref(TraceSegmentRef)
方法,进行指向父 TraceSegment。TraceSegment#relatedGlobalTraces(DistributedTraceId)
方法,将传播的分布式链路追踪全局编号,添加到 TraceSegment 中,进行指向全局编号。另外,ContextManager 单独提供 #extract(ContextCarrier)
方法,将多个 ContextCarrier 注入到一个Context 中,从而解决"多个爸爸"的场景,例如 RocketMQ 插件的 AbstractMessageConsumeInterceptor#beforeMethod(...)
方法。
在 ContextManager#createExitSpan(operationName, carrier, remotePeer)
方法中,当需要Context 跨进程传递时,将 Context 注入到 ContextCarrier 中,为 「3.2.3.3 传输」 做准备。TracingContext#inject(ContextCarrier)
方法,代码比较易懂,胖友自己阅读理解。
友情提示:胖友,请先阅读 《Skywalking Cross Process Propagation Headers Protocol》 。
org.skywalking.apm.agent.core.context.CarrierItem
,传输载体项。代码如:
headKey
属性,Header 键。headValue
属性,Header 值。next
属性,下一个项。CarrierItem 有两个子类:
header = sw3
,用于传输 ContextCarrier 。如下是 Dubbo 插件,使用 CarrierItem 的代码例子:
org.skywalking.apm.agent.core.context.ContextSnapshot
,跨线程 Context 传递快照。和 ContextCarrier 基本一致,由于不需要跨进程传输,可以少传递一些属性:
parentApplicationInstanceId
peerHost
ContextSnapshot 和 ContextCarrier 比较类似,笔者就列举一些方法:
org.skywalking.apm.agent.core.sampling.SamplingService
,实现 Service 接口,Agent 抽样服务。该服务的作用是,如何对 TraceSegment 抽样收集。考虑到如果每条 TraceSegment 都进行追踪,会带来一定的 CPU ( 用于序列化与反序列化 ) 和网络的开销。通过配置 Config.Agent.SAMPLE_N_PER_3_SECS
属性,设置每三秒,收集 TraceSegment 的条数。默认情况下,不开启抽样服务,即全部收集。
代码如下:
on
属性,是否开启抽样服务。samplingFactorHolder
属性,抽样计数器。通过定时任务,每三秒重置一次。scheduledFuture
属性,定时任务。#boot()
实现方法,若开启抽样服务( Config.Agent.SAMPLE_N_PER_3_SECS > 0
) 时,创建定时任务,每三秒,调用一次 #resetSamplingFactor()
方法,重置计数器。#trySampling()
方法,若开启抽样服务,判断是否超过每三秒的抽样上限。若不是,返回 true
,并增加计数器。否则,返回 false
。#forceSampled()
方法,强制增加计数器加一。一般情况下,该方法用于链路追踪上下文传播时,被调用服务必须记录链路,参见调用处的代码。#resetSamplingFactor()
方法,重置计数器。