在开发skywalking nginx探针过程中重点分析了“Skywalking跨进程链路信息传递协议”和“Skywalking链路数据采集协议”。skywalking nginx探针的实现目前已完成一个版本并在生产环境上线,有兴趣的朋友可以交流。
本文基于Skywalking v5.0.0-GA分析,版本虽然有些落后,但两个协议的核心没有多大变化。两个协议的官方英文称呼及文档参考如下链接:
为了便于理解,本文将 Skywalking-Cross-Process-Propagation-Headers-Protocol-v1
解读为 跨进程链路信息传递协议
, Trace-Data-Protocol
解读为 Skywalking链路数据采集协议
。官方对两个协议称呼有中文翻译,可自行查阅。
“跨进程链路信息传递协议”本身理解不困难,但在实现时需要考虑很多因素,本系列文章将结合Java探针源码深入分析。本文重点讲解 链路ID
生成过程。
在分布式系统中,用户发起一次数据请求后,后端系统会有多个服务服务节点处理数据请求。链路跟踪系统的重要作用之一就是要能将数据在各个服务节点处理的先后顺序,逻辑关系,经理时长,拓扑结构等信息客观真实的反应出来。系统中一般一个进程充当一个服务节点, 跨进程
的描述由此而来。
一个完整的分布式系统,各个服务节点往往是有不同的组件,不同的语言,不同的技术栈组成。如java服务,node服务,nginx组件等等。要解决链路数据在不同节点的生成、传递、识别、解析、编码等问题,就不得不有一套规范进行约定,以便不同的语言能处理链路数据。于是 跨进程链路信息传递协议
就诞生了。
在讲trace segment id生成逻辑前先简要介绍一下Skywalking Java Agent的源码结构。
Skywalking v5.0.0-GA中,Java Agent对Skywalking链路信息的抽象与封装是位于源码的 apm-sniffer/apm-agent-core
模块中。本文对该模块的链路数据处理逻辑做如下分析。
作用是封装链路数据协议约定的数据抽象,同时总体兼容了Opentracing规范。重点类有如下几个
org.apache.skywalking.apm.agent.core.context.trace.TraceSegment org.apache.skywalking.apm.agent.core.context.trace.TraceSegmentRef org.apache.skywalking.apm.agent.core.context.trace.EntrySpan org.apache.skywalking.apm.agent.core.context.trace.ExitSpan org.apache.skywalking.apm.agent.core.context.ids.DistributedTraceIds
重点类是 org.apache.skywalking.apm.agent.core.context.ContextCarrier
。即是对本文所讲解的协议规范的封装。
重点类是 org.apache.skywalking.apm.agent.core.context.TracingContext
。TracingContext保存在ThreadLocal中,包含了链路中的所有数据,以及对数据的管控方法,主要是对Span的管控。同时提供对ContextCarrier数据的处理,包括:
重点是 org.apache.skywalking.apm.agent.core.context.ContextManager
类。ContextManager类是各种skywalking agent插件的枢纽。不同组件的skywalking插件,如mq,dubbo,tomcat,spring等skywalking agent插件均是通过调用ContextManager创建和管控TracingContext,TraceSegment,EntrySpan,ExitSpan,ContextCarrier等数据。可以说ContextManager管控着agent内的链路数据生命周期。
重点是 org.apache.skywalking.apm.agent.core.remote
包中的 TraceSegmentServiceClient
类。
ContextManager采集节点内的链路数据片段(TraceSegment)后,通知TraceSegmentServiceClient将数据上报到Collector Server。其中涉及到
TracingContextListener
与skywalking封装的 内存MQ
组件,后面会详细分析。
以上每个部分都涉及复杂的处理逻辑,后面会分篇一一做详细讲解。
官方文档 Skywalking-Cross-Process-Propagation-Headers-Protocol-v1 已做了详细的说明,本文结合Java实现做进一步的分析。
The trace segment id is the unique id for the part of the distributed trace. Each id is only used in a single thread. The id includes three parts(Long), e.g. "1.2343.234234234
3.1. A timestamp, measured in milliseconds
3.2.A seq, in current thread, between 0(included) and 9999(included)
If you are using other language, you can generate your own id, but make sure it is unique and combined by three longs.
首先skywalking java agent提供了 org.apache.skywalking.apm.agent.core.context.ids.ID
类用于对协议规范描述的数据结构,并提供了合法性校验方法isValid和序列化方法transform,重写了toString,equals,hashCode方法。
通过grpc上报到collector server的链路采集数据都提供了一个transform方法,用于序列化为protobuf数据。如 org.apache.skywalking.apm.agent.core.context.trace.TraceSegment#transform
,
org.apache.skywalking.apm.agent.core.context.trace.TraceSegmentRef#transform
等。
Trace Segment Id的创建发生在实例化 TracingContext
过程中。 TracingContext.new()
的序列图参考如下:
上图红框为生成 Trace Segment Id
的流程,通过调用 org.apache.skywalking.apm.agent.core.context.ids.GlobalIdGenerator#generate
方法生成满足协议规范的ID字符串。
skywalking中分别有两类全局唯一的id,即Global(Distributed) Trace Id与Trace Segment Id。Global(Distributed) Trace Id是指分布系统下的某条链路的唯一ID。Trace Segment Id是指链路所经过的分布系统服务节点事生成的链路片段ID。两者是一对多的关系。
上述序列图表明,在实例化TracingContext时,会实例化TraceSegment,而在实例化TraceSegment时,同时生成了Global(Distributed) Trace Id与Trace Segment Id,源码如下:
public TraceSegment() { this.traceSegmentId = GlobalIdGenerator.generate(); this.spans = new LinkedList<AbstractTracingSpan>(); this.relatedGlobalTraces = new DistributedTraceIds(); this.relatedGlobalTraces.append(new NewDistributedTraceId()); }
其中,第1行代码 this.traceSegmentId = GlobalIdGenerator.generate();
,即是生成Trace Segment Id,调用了一次GlobalIdGenerator.generate()方法。
在第4行代码的 new NewDistributedTraceId()
创建了Global(Distributed) Trace Id。源码如下,可见同样是调用 GlobalIdGenerator.generate()
方法。
public class NewDistributedTraceId extends DistributedTraceId { public NewDistributedTraceId() { super(GlobalIdGenerator.generate()); } }
全局链路id的的逻辑较复杂,java agent中为这个id封装了 DistributedTraceIds
类,通过该类的append方法添加 NewDistributedTraceId
对象,表示一个Global(Distributed) Trace Id数据。NewDistributedTraceId是DistributedTraceId的子类,而DistributedTraceId类就是对ID类的包装,提供了一些工具方法。
DistributedTraceIds
代表了相关链路id的集合,大多数情况下仅包含一条链路id。
两个id均是调用同样的方法生成,即 org.apache.skywalking.apm.agent.core.context.ids.GlobalIdGenerator#generate
。
GlobalIdGenerator是生产全局ID字符串的逻辑封装类。下面看看 GlobalIdGenerator#generate
的源码。
public static ID generate() { if (RemoteDownstreamConfig.Agent.APPLICATION_INSTANCE_ID == DictionaryUtil.nullValue()) { throw new IllegalStateException(); } IDContext context = THREAD_ID_SEQUENCE.get(); return new ID( RemoteDownstreamConfig.Agent.APPLICATION_INSTANCE_ID, Thread.currentThread().getId(), context.nextSeq() ); }
上述源码最终返回的即是根据协议规范封装的ID数据结构,第一个参数为当前应用的实例ID,第二个参数为当前线程号。第三个参数是通过IDContext.nextSeq()方法获取。而IDContext是从ThreadContext中获取的,所以IDContext是线程独有的。IDContext的源码比较好理解,IDContext.nextSeq()返回当前时间戳与线程内的自增序号组合的字符串。
综上,在新链路开始,生成TraceSegment实例时,第一次调用 GlobalIdGenerator.generate()
作为 Trace Segment Id
,第二次调用 GlobalIdGenerator.generate()
作为 Global(Distributed) Trace Id
。
org.apache.skywalking.apm.agent.core.context.trace.TraceSegment
是skywalking java agent链路逻辑的核心类,后面会单独讲解。
作者:易企秀高级工程师-Frank