本文主要基于 SkyWalking 3.2.6 正式版
1. 概述
- 2. Trace
- 2.1 ID
- 2.2 AbstractSpan
- 2.3 TraceSegmentRef
- 2.4 TraceSegment
- 3. Context
- 3.1 ContextManager
- 3.2 AbstractTracerContext
- 3.3 SamplingService
666. 彩蛋
RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
您对于源码的疑问每条留言都将得到认真回复甚至不知道如何读源码也可以请教噢
新的源码解析文章实时收到通知每周更新一篇左右
认真的源码交流微信群
1. 概述
分布式链路追踪系统, 链路的追踪大体流程如下:
Agent 收集 Trace 数据
Agent 发送 Trace 数据给 Collector
Collector 接收 Trace 数据
Collector 存储 Trace 数据到存储器, 例如, 数据库
本文主要分享第一部分 SkyWalking Agent 收集 Trace 数据文章的内容顺序如下:
Trace 的数据结构
Context 收集 Trace 的方法
不包括插件对 Context 收集的方法的调用, 后续单独文章专门分享, 胖友也可以阅读完本文后, 自己去看 apm-sdk-plugin 的实现代码
本文涉及到的代码如下图:
红框部分: Trace 的数据结构, 在 2. Trace 分享
黄框部分: Context 收集 Trace 的方法, 在 3. Context 分享
2. Trace
友情提示: 胖友, 请先行阅读 OpenTracing 语义标准
本小节, 笔者认为胖友已经对 OpenTracing 有一定的理解
org.skywalking.apm.agent.core.context.trace.TraceSegment
, 是一次分布式链路追踪( Distributed Trace ) 的一段
一条 TraceSegment , 用于记录所在线程 ( Thread ) 的链路
一次分布式链路追踪, 可以包含多条 TraceSegment , 因为存在跨进程( 例如, RPC MQ 等等), 或者垮线程( 例如, 并发执行异步回调等等 )
TraceSegment 属性, 如下:
traceSegmentId 属性, TraceSegment 的编号, 全局唯一在 2.1 ID 详细解析
refs 属性, TraceSegmentRef 数组, 指向的父 TraceSegment 数组
为什么会有多个爸爸? 下面统一讲
TraceSegmentRef , 在 2.3 TraceSegmentRef 详细解析
relatedGlobalTraces 属性, 关联的 DistributedTraceId 数组
为什么会有多个爸爸? 下面统一讲
DistributedTraceId , 在 2.1.2 DistributedTraceId 详细解析
spans 属性, 包含的 Span 数组在 2.2 AbstractSpan 详细解析这是 TraceSegment 的主体, 总的来说, TraceSegment 是 Span 数组的封装
ignore 属性, 是否忽略该条 TraceSegment 在一些情况下, 我们会忽略 TraceSegment , 即不收集链路追踪, 在下面 3. Context 部分内容, 我们将会看到这些情况
isSizeLimited 属性, Span 是否超过上限(
Config.Agent.SPAN_LIMIT_PER_SEGMENT
)超过上限, 不在记录 Span
为什么会有多个爸爸?
我们先来看看一个爸爸的情况, 常见于 RPC 调用例如, 服务 A 调用服务 B 时, 服务 B 新建一个 TraceSegment 对象:
将自己的 refs 指向服务 A 的 TraceSegment
将自己的 relatedGlobalTraces 设置为 服务 A 的 DistributedTraceId 对象
我们再来看看多个爸爸的情况, 常见于 MQ / Batch 调用例如, MQ 批量消费消息时, 消息来自多个服务每次批量消费时, 消费者新建一个 TraceSegment 对象:
将自己的 refs 指向多个服务的多个 TraceSegment
将自己的 relatedGlobalTraces 设置为多个服务的多个 DistributedTraceId
友情提示: 多个爸爸的故事, 可能比较难懂, 等胖友读完全文, 在回过头想想或者拿起来代码调试调试
下面, 我们来具体看看 TraceSegment 的每个元素, 最后, 我们会回过头, 在 2.4 TraceSegment 详细解析它
- 2.1 ID
- org.skywalking.apm.agent.core.context.ids.ID
, 编号从类的定义上, 这是一个通用的编号, 由三段整数组成
目前使用 GlobalIdGenerator 生成, 作为全局唯一编号属性如下:
part1 属性, 应用实例编号
part2 属性, 线程编号
part3 属性, 时间戳串, 生成方式为
${时间戳} * 10000 + 线程自增序列([0, 9999])
例如: 15127007074950012 具体生成方法的代码, 在 GlobalIdGenerator 中详细解析
encoding 属性, 编码后的字符串格式为
"${part1}.${part2}.${part3}"
例如,
"12.35.15127007074950000"
使用 #encode() 方法, 编码编号
isValid 属性, 编号是否合法
使用 ID(encodingString) 构造方法, 解析字符串, 生成 ID
- 2.1.1 GlobalIdGenerator
- org.skywalking.apm.agent.core.context.ids.GlobalIdGenerator
, 全局编号生成器
#generate() 方法, 生成 ID 对象代码如下:
第 67 行: 获得线程对应的 IDContext 对象
第 69 至 73 行: 生成 ID 对象
第 70 行: ID.part1 属性, 应用编号实例
第 71 行: ID.part2 属性, 线程编号
第 72 行: ID.part3 属性, 调用 IDContext#nextSeq() 方法, 生成带有时间戳的序列号
ps : 代码比较易懂, 已经添加完成注释
- 2.1.2 DistributedTraceId
- org.skywalking.apm.agent.core.context.ids.DistributedTraceId
, 分布式链路追踪编号抽象类
id 属性, 全局编号
DistributedTraceId 有两个实现类:
org.skywalking.apm.agent.core.context.ids.NewDistributedTraceId , 新建的分布式链路追踪编号当全局链路追踪开始, 创建 TraceSegment 对象的过程中, 会调用
DistributedTraceId()
构造方法, 创建 DistributedTraceId 对象该构造方法内部会调用
GlobalIdGenerator#generate()
方法, 创建 ID 对象
org.skywalking.apm.agent.core.context.ids.PropagatedTraceId , 传播的分布式链路追踪编号例如, A 服务调用 B 服务时, A 服务会将 DistributedTraceId 对象带给 B 服务, B 服务会调用
PropagatedTraceId(String id)
构造方法 , 创建 PropagatedTraceId 对象该构造方法内部会解析 id , 生成 ID 对象
- 2.1.3 DistributedTraceIds
- org.skywalking.apm.agent.core.context.ids.DistributedTraceIds
,DistributedTraceId 数组的封装
relatedGlobalTraces 属性, 关联的 DistributedTraceId 链式数组
#append(DistributedTraceId)
方法, 添加分布式链路追踪编号 ( DistributedTraceId ) 代码如下:
第 51 至 54 行: 移除首个 NewDistributedTraceId 对象为什么呢? 在 2.4 TraceSegment 的构造方法中, 会默认创建 NewDistributedTraceId 对象在跨线程或者跨进程的情况下时, 创建的 TraceSegment 对象, 需要指向父 Segment 的 DistributedTraceId , 所以需要移除默认创建的
第 56 至 58 行: 添加 DistributedTraceId 对象到数组
- 2.2 AbstractSpan
- 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 详细解析
- 2.2.1 Tag
- 2.2.1.1 AbstractTag
- org.skywalking.apm.agent.core.context.tag.AbstractTag<T>
, 标签抽象类注意, 这个类的用途是将标签属性设置到 Span 上, 或者说, 它是设置 Span 的标签的工具类代码如下:
key 属性, 标签的键
#set(AbstractSpan span, T tagValue)
抽象方法, 设置 Span 的标签键 key 的值为 tagValue
- 2.2.1.2 StringTag
- org.skywalking.apm.agent.core.context.tag.StringTag
, 值类型为 String 的标签实现类
#set(AbstractSpan span, String tagValue)
实现方法, 设置 Span 的标签键 key 的值为 tagValue
- 2.2.1.3 Tags
- org.skywalking.apm.agent.core.context.tag.Tags
, 常用 Tag 枚举类, 内部定义了多个 HTTP DB 相关的 StringTag 的静态变量
在 opentracing-specification-zh 语义惯例 里, 定义了标准的 Span Tag
2.2.2 AbstractSpan 实现类
AbstractSpan 实现类如下图:
左半边的 Span 实现类: 有具体操作的 Span
右半边的 Span 实现类: 无具体操作的 Span , 和左半边的 Span 实现类相对, 用于不需要收集 Span 的场景
抛开右半边的 Span 实现类的特殊处理, Span 只有三种实现类:
EntrySpan : 入口 Span
LocalSpan : 本地 Span
ExitSpan : 出口 Span
下面, 我们分小节逐步分享
- 2.2.2.1 AbstractTracingSpan
- 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 详细解析
- 2.2.2.2 StackBasedTracingSpan
- org.skywalking.apm.agent.core.context.trace.StackBasedTracingSpan
, 实现 AbstractTracingSpan 抽象类, 基于栈的链路追踪 Span 抽象类这种 Span 能够被多次调用 #start(...) 和 #finish(...) 方法, 在类似堆栈的调用中在 2.2.2.2.1 EntrySpan 中详细举例子代码如下:
stackDepth 属, 栈深度
#finish(TraceSegment)
实现方法, 完成 ( 结束 ) Span , 将当前 Span ( 自己 ) 添加到 TraceSegment 当且仅当 stackDepth == 0 时, 添加成功代码如下:
第 53 至 73 行: 栈深度为零, 出栈成功调用
super#finish(TraceSegment)
方法, 完成 ( 结束 ) Span , 将当前 Span ( 自己 ) 添加到 TraceSegment
第 55 至 72 行: 当操作编号为空时, 尝试使用操作名获得操作编号并设置用于减少 Agent 发送 Collector 数据的网络流量
第 74 至 76 行: 栈深度非零, 出栈失败
2.2.2.2.1 EntrySpan
重点
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 展示的例子:
2.2.2.2.2 ExitSpan
重点
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 服务 BAgent 会在 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 , 良心笔者当然也是加了注释的
- 2.2.2.3 LocalSpan
- org.skywalking.apm.agent.core.context.trace.LocalSpan
, 继承 AbstractTracingSpan 抽象类, 本地 Span , 用于一个普通方法的链路追踪, 例如本地方法
如下是一个 EntrySpan 在 SkyWalking 展示的例子:
- 2.2.2.4 NoopSpan
- org.skywalking.apm.agent.core.context.trace.NoopSpan
, 实现 AbstractSpan 接口, 无操作的 Span 配置 IgnoredTracerContext 一起使用, 在 IgnoredTracerContext 声明单例 , 以减少不收集 Span 时的对象创建, 达到减少内存使用和 GC 时间
- 2.2.2.3.1 NoopExitSpan
- org.skywalking.apm.agent.core.context.trace.NoopExitSpan
, 实现
org.skywalking.apm.agent.core.context.trace.WithPeerInfo
接口, 继承 StackBasedTracingSpan 抽象类, 出口 Span , 无操作的出口 Span 和 ExitSpan 相对, 不记录服务消费者的出口 Span
- 2.3 TraceSegmentRef
- org.skywalking.apm.agent.core.context.trace.TraceSegmentRef
,TraceSegment 指向, 通过 traceSegmentId 和 spanId 属性, 指向父级 TraceSegment 的指定 Span
type 属性, 指向类型( SegmentRefType ) 不同的指向类型, 使用不同的构造方法
CROSS_PROCESS , 跨进程, 例如远程调用, 对应构造方法 #TraceSegmentRef(ContextCarrier)
CROSS_THREAD , 跨线程, 例如异步线程任务, 对应构造方法 #TraceSegmentRef(ContextSnapshot)
构造方法的代码, 在 3. Context 中, 伴随着调用过程, 一起解析
traceSegmentId 属性, 父 TraceSegment 编号重要
spanId 属性, 父 Span 编号重要
peerId 属性, 节点编号注意, 此处的节点编号就是应用 ( Application ) 编号
peerHost 属性, 节点地址
entryApplicationInstanceId
属性, 入口应用实例编号例如, 在一个分布式链路 A->B->C 中, 此字段为 A 应用的实例编号
parentApplicationInstanceId
属性, 父应用实例编号
entryOperationName 属性, 入口操作名
entryOperationId 属性, 入口操作编号
parentOperationName 属性, 父操作名
parentOperationId 属性, 父操作编号
2.4 TraceSegment
在看完了 TraceSegment 的各个元素, 我们来看看 TraceSegment 内部实现的方法
TraceSegment 构造方法, 代码如下:
第 80 行: 调用
GlobalIdGenerator#generate()
方法, 生成 ID 对象, 赋值给 traceSegmentId
第 81 行: 创建 spans 数组
#archive(AbstractTracingSpan)
方法, 被
AbstractSpan#finish(TraceSegment)
方法调用, 添加到 spans 数组
第 83 至 84 行: 创建 DistributedTraceIds 对象, 并添加 NewDistributedTraceId 到它
注意, 当 TraceSegment 是一次分布式链路追踪的首条记录, 创建的 NewDistributedTraceId 对象, 即为分布式链路追踪的全局编号
#relatedGlobalTraces(DistributedTraceId)
方法, 添加 DistributedTraceId 对象被
TracingContext#continued(ContextSnapshot)
或者
TracingContext#extract(ContextCarrier)
方法调用, 在 3. Context 详细解析
#ref(TraceSegmentRef)
方法, 添加 TraceSegmentRef 对象, 到 refs 属性, 即指向父 Segment
3. Context
在 2. Trace 中, 我们看了 Trace 的数据结构, 本小节, 我们一起来看看 Context 是怎么收集 Trace 数据的
- 3.1 ContextManager
- org.skywalking.apm.agent.core.context.ContextManager
, 实现了 BootService TracingContextListener IgnoreTracerContextListener 接口, 链路追踪上下文管理器
CONTEXT 静态属性, 线程变量, 存储 AbstractTracerContext 对象为什么是线程变量呢?
一个 TraceSegment 对象, 关联到一个线程, 负责收集该线程的链路追踪数据, 因此使用线程变量
而一个 AbstractTracerContext 会关联一个 TraceSegment 对象, ContextManager 负责获取创建销毁 AbstractTracerContext 对象
#getOrCreate(operationName, forceSampling)
静态方法, 获取 AbstractTracerContext 对象若不存在, 进行创建
要需要收集 Trace 数据的情况下, 创建 TracingContext 对象
不需要收集 Trace 数据的情况下, 创建 IgnoredTracerContext 对象
在下面的
- #createEntrySpan(...)
- #createLocalSpan(...)
- #createExitSpan(...)
等等方法中, 都会调用 AbstractTracerContext 提供的方法这些方法的代码, 我们放在 3.2 AbstractTracerContext 一起解析, 保证流程的整体性
另外, ContextManager 封装了所有 AbstractTracerContext 提供的方法, 从而实现, 外部调用者, 例如 SkyWalking 的插件, 只调用 ContextManager 的方法, 而不调用 AbstractTracerContext 的方法
- #boot() 实现方法, 启动时, 将自己注册到 TracingContext.ListenerManager 和 IgnoredTracerContext.ListenerManager 中, 这样一次链路追踪上下文 ( Context ) 完成时, 从而被回调如下方法, 清理上下文:
- #afterFinished(TraceSegment)
- #afterFinished(IgnoredTracerContext)
- 3.2 AbstractTracerContext
- org.skywalking.apm.agent.core.context.AbstractTracerContext
, 链路追踪上下文接口定义了如下方法:
#getReadableGlobalTraceId()
方法, 获得关联的全局链路追踪编号
#createEntrySpan(operationName)
方法, 创建 EntrySpan 对象
#createLocalSpan(operationName)
方法, 创建 LocalSpan 对象
#createExitSpan(operationName, remotePeer)
方法, 创建 ExitSpan 对象
- #activeSpan() 方法, 获得当前活跃的 Span 对象
- #stopSpan(AbstractSpan)
方法, 停止 ( 完成 ) 指定 AbstractSpan 对象
--------- 跨进程( cross-process ) ---------
#inject(ContextCarrier)
方法, 将 Context 注入到 ContextCarrier , 用于跨进程, 传播上下文
#extract(ContextCarrier)
方法, 将 ContextCarrier 解压到 Context , 用于跨进程, 接收上下文
--------- 跨线程( cross-thread ) ---------
- #capture() 方法, 将 Context 快照到 ContextSnapshot , 用于跨线程, 传播上下文
- #continued(ContextSnapshot)
方法, 将 ContextSnapshot 解压到 Context , 用于跨线程, 接收上下文
- 3.2.1 TracingContext
- org.skywalking.apm.agent.core.context.TracingContext
, 实现 AbstractTracerContext 接口, 链路追踪上下文实现类
segment 属性, 上下文对应的 TraceSegment 对象
activeSpanStack 属性, AbstractSpan 链表数组, 收集当前活跃的 Span 对象正如方法的调用与执行一样, 在一个调用栈中, 先执行的方法后结束
spanIdGenerator 属性, Span 编号自增序列创建的 Span 的编号, 通过该变量自增生成
TracingContext 构造方法 , 代码如下:
第 80 行: 创建 TraceSegment 对象
第 81 行: 设置 spanIdGenerator = 0
#getReadableGlobalTraceId()
实现方法, 获得 TraceSegment 的首个 DistributedTraceId 作为返回
3.2.1.1 创建 EntrySpan
调用
ContextManager#createEntrySpan(operationName, carrier)
方法, 创建 EntrySpan 对象代码如下:
第 121 至 131 行: 调用
#getOrCreate(operationName, forceSampling)
方法, 获取 AbstractTracerContext 对象若不存在, 进行创建
第 122 至 125 行: 有传播 Context 的情况下, 强制收集 Trace 数据
第 127 行: 调用
TracingContext#extract(ContextCarrier)
方法, 将 ContextCarrier 解压到 Context , 跨进程, 接收上下文在 3.2.3 ContextCarrier 详细解析
第 133 行: 调用
TracingContext#createEntrySpan(operationName)
方法, 创建 EntrySpan 对象
调用
TracingContext#createEntrySpan(operationName)
方法, 创建 EntrySpan 对象代码如下:
第 223 至 227 行: 调用
#isLimitMechanismWorking()
方法, 判断 Span 数量超过上限, 创建 NoopSpan 对象, 并调用 #push(AbstractSpan) 方法, 添加到 activeSpanStack 中
第 229 至 231 行: 调用 #peek() 方法, 获得当前活跃的 AbstractSpan 对象
第 232 至 249 行: 若父 Span 对象不存在, 创建 EntrySpan 对象
第 235 至 244 行: 创建 EntrySpan 对象
第 247 行: 调用 EntrySpan#start() 方法, 开始 EntrySpan
第 249 行: 调用 #push(AbstractSpan) 方法, 添加到 activeSpanStack 中
第 251 至 264 行: 若父 EntrySpan 对象存在, 重新开始 EntrySpan 参见 2.2.2.2.1 EntrySpan
第 265 至 267 行:
"The Entry Span can't be the child of Non-Entry Span"
3.2.1.2 创建 LocalSpan
调用
ContextManager#createLocalSpan(operationName)
方法, 创建 LocalSpan 对象
第 138 行: 调用
#getOrCreate(operationName, forceSampling)
方法, 获取 AbstractTracerContext 对象若不存在, 进行创建
第 140 行: 调用
TracingContext#createLocalSpan(operationName)
方法, 创建 LocalSpan 对象
调用
TracingContext#createLocalSpan(operationName)
方法, 创建 LocalSpan 对象代码如下:
第 280 至 283 行: 调用
#isLimitMechanismWorking()
方法, 判断 Span 数量超过上限, 创建 NoopSpan 对象, 并调用 #push(AbstractSpan) 方法, 添加到 activeSpanStack 中
第 284 至 286 行: 调用 #peek() 方法, 获得当前活跃的 AbstractSpan 对象
第 288 至 300 行: 创建 LocalSpan 对象
第 302 行: 调用 LocalSpan#start() 方法, 开始 LocalSpan
第 304 行: 调用 #push(AbstractSpan) 方法, 添加到 activeSpanStack 中
3.2.1.3 创建 ExitSpan
调用
ContextManager#createExitSpan(operationName, carrier, remotePeer)
方法, 创建 ExitSpan 对象
第 148 行: 调用
#getOrCreate(operationName, forceSampling)
方法, 获取 AbstractTracerContext 对象若不存在, 进行创建
第 150 行: 调用
TracingContext#createExitSpan(operationName, remotePeer)
方法, 创建 ExitSpan 对象
第 160 行:
TracingContext#inject(ContextCarrier)
方法, 将 Context 注入到 ContextCarrier , 跨进程, 传播上下文在 3.2.3 ContextCarrier 详细解析
调用
TracingContext#createEntrySpan(operationName)
方法, 创建 ExitSpan 对象代码如下:
第 319 行: 调用 #peek() 方法, 获得当前活跃的 AbstractSpan 对象
第 320 至 322 行: 若 ExitSpan 对象存在, 直接使用, 不重新创建参见 2.2.2.2.2 ExitSpan
第 324 至 377 行: 创建 ExitSpan 对象, 并添加到 activeSpanStack 中
第 327 行: 根据 remotePeer 参数, 查找 peerId 注意, 此处会创建一个 Application 对象, 通过 ServiceMapping 表, 和远程的 Application 进行匹配映射后续有文章会分享这块
第 322 至 324 行 || 第 335 至 358 行: 判断 Span 数量超过上限, 创建 NoopExitSpan 对象, 并调用 #push(AbstractSpan) 方法, 添加到 activeSpanStack 中
第 380 行: 开始 ExitSpan
3.2.1.4 结束 Span
调用
ContextManager#stopSpan()
方法, 结束 Span 代码如下:
第 199 行: 调用
TracingContext#stopSpan(AbstractSpan)
方法, 结束 Span 当所有活跃的 Span 都被结束后, 当前线程的 TraceSegment 完成
调用
TracingContext#stopSpan(AbstractSpan)
方法, 结束 Span 代码如下:
第 405 行: 调用 #peek() 方法, 获得当前活跃的 AbstractSpan 对象
第 408 至 414 行: 当 Span 为 AbstractTracingSpan 的子类, 即记录链路追踪的 Span , 调用
AbstractTracingSpan#finish(TraceSegment)
方法, 完成 Span
当完成成功时, 调用 #pop() 方法, 移除出 activeSpanStack
当完成失败时, 原因参见 2.2.2.2 StackBasedTracingSpan
第 416 至 419 行: 当 Span 为 NoopSpan 的子类, 即不记录链路追踪的 Span , 调用 #pop() 方法, 移除出 activeSpanStack
第 425 至 427 行: 当所有活跃的 Span 都被结束后, 调用 #finish() 方法, 当前线程的 TraceSegment 完成
调用
TracingContext#stopSpan(AbstractSpan)
方法, 完成 Context 代码如下:
第 436 行: 调用
TraceSegment#finish(isSizeLimited)
方法, 完成 TraceSegment
第 444 至 448 行: 若满足条件, 调用
TraceSegment#setIgnore(true)
方法, 标记该 TraceSegment 忽略, 不发送给 Collector
!samplingService.trySampling()
: 不采样
!segment.hasRef() : 无父 TraceSegment 指向如果此处忽略采样, 则会导致整条分布式链路追踪不完整
segment.isSingleSpanSegment()
:TraceSegment 只有一个 Span
TODO 4010
第 450 行: 调用
TracingContext.ListenerManager#notifyFinish(TraceSegment)
方法, 通知监听器, 一次 TraceSegment 完成通过这样的方式, TraceSegment 会被 TraceSegmentServiceClient 异步发送给 Collector 下一篇文章, 我们详细分享发送的过程
- 3.2.2 IgnoredTracerContext
- org.skywalking.apm.agent.core.context.IgnoredTracerContext
, 实现 AbstractTracerContext 接口, 忽略 ( 不记录 ) 链路追踪的上下文代码如下:
NOOP_SPAN 静态属性, NoopSpan 单例
所有的创建 Span 方法, 返回的都是该对象
stackDepth 属性, 栈深度
不同于 TracingContext 使用链式数组来处理 Span 的出入栈, IgnoredTracerContext 使用 stackDepth 来计数, 从而实现出入栈的效果
通过这两个属性和相应空方法的实现, 以减少 NoopSpan 时的对象创建, 达到减少内存使用和 GC 时间
代码比较简单, 胖友自己阅读该类的实现
- 3.2.3 ContextCarrier
- org.skywalking.apm.agent.core.context.ContextCarrier
, 实现
java.io.Serializable
接口, 跨进程 Context 传输载体
3.2.3.1 解压
我们来打开
#TraceSegmentRef(ContextCarrier)
构造方法, 该方法用于将 ContextCarrier 转换成 TraceSegmentRef , 对比下两者的属性, 基本一致, 差异如下:
peerHost 属性, 节点地址
当字符串不以 # 号开头, 代表节点编号, 格式为 ${peerId} , 例如 "123"
当字符串以 # 号开头, 代表地址, 格式为 ${peerHost} , 例如 "192.168.16.1:8080"
entryOperationName 属性, 入口操作名
当字符串不以 # 号开头, 代表入口操作编号, 格式为
#${entryOperationId}
, 例如 "666"
当字符串以 # 号开头, 代表入口操作名, 格式为
#${entryOperationName}
, 例如 "#user/login"
parentOperationName 属性, 父操作名类似 entryOperationName 属
primaryDistributedTraceId
属性, 分布式链路追踪全局编号它不在此处处理, 而在
TracingContext#extract(ContextCarrier)
方法中
在
ContextManager#createEntrySpan(operationName, carrier)
方法中, 当存在 ContextCarrier 传递时, 创建 Context 后, 会将 ContextCarrier 解压到 Context 中, 以达到跨进程传播
TracingContext#extract(ContextCarrier)
方法, 代码如下:
第 148 行: 将 ContextCarrier 转换成 TraceSegmentRef 对象, 调用
TraceSegment#ref(TraceSegmentRef)
方法, 进行指向父 TraceSegment
第 149 行: 调用
TraceSegment#relatedGlobalTraces(DistributedTraceId)
方法, 将传播的分布式链路追踪全局编号, 添加到 TraceSegment 中, 进行指向全局编号
另外, ContextManager 单独提供
#extract(ContextCarrier)
方法, 将多个 ContextCarrier 注入到一个 Context 中, 从而解决 "多个爸爸" 的场景, 例如 RocketMQ 插件的
AbstractMessageConsumeInterceptor#beforeMethod(...)
方法
3.2.3.2 注入
在
ContextManager#createExitSpan(operationName, carrier, remotePeer)
方法中, 当需要 Context 跨进程传递时, 将 Context 注入到 ContextCarrier 中, 为 3.2.3.3 传输 做准备
TracingContext#inject(ContextCarrier)
方法, 代码比较易懂, 胖友自己阅读理解
3.2.3.3 传输
友情提示: 胖友, 请先阅读 Skywalking Cross Process Propagation Headers Protocol
org.skywalking.apm.agent.core.context.CarrierItem
, 传输载体项代码如:
headKey 属性, Header 键
headValue 属性, Header 值
next 属性, 下一个项
CarrierItem 有两个子类:
CarrierItemHead :Carrier 项的头( Head ), 即首个元素
SW3CarrierItem :header = sw3 , 用于传输 ContextCarrier
如下是 Dubbo 插件, 使用 CarrierItem 的代码例子:
- ContextCarrier#serialize()
- ContextCarrier#deserialize(text)
- 3.2.4 ContextSnapshot
- org.skywalking.apm.agent.core.context.ContextSnapshot
, 跨线程 Context 传递快照和 ContextCarrier 基本一致, 由于不需要跨进程传输, 可以少传递一些属性:
- parentApplicationInstanceId
- peerHost
ContextSnapshot 和 ContextCarrier 比较类似, 笔者就列举一些方法:
- #TraceSegmentRef(ContextSnapshot)
- TracingContext#capture()
- TracingContext#continued(ContextSnapshot)
- 3.3 SamplingService
- 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()
方法, 重置计数器
666. 彩蛋
元旦很认真 ( 硬憋 ) 出一篇 "硬货" 哈哈哈
由于篇幅较长, 内容略多, 如果有错误的或者解释不清晰的, 烦请胖友斧正
胖友, 分享个朋友圈可好?
来源: http://www.suo.im/2O5y3Z