6.sw如何创建trace?
6.sw如何创建trace?
sw的主要功能就说链路跟踪,那么如何创建trace呢?
因为sw可以支持很多种的服务,包括同步线程,异步线程,MySQL,redis.
sw 是如何通过trace将不同的链路追踪,串联起来的呢?
这里通过http请求,来举例研究。
基础概念了解
在一个在分布式追踪(Distributed Tracing)领域非常核心和经典的数据模型结构。它将一次用户请求在复杂微服务系统中的完整生命周期,层层分解为可管理、可分析的单元。
这个模型的核心思想是:从宏观到微观,逐层解构一次请求的执行路径。其层级关系为:
Trace → Segment → Span
Trace
定义:一个
Trace
代表一次完整的端到端的用户请求或事务。无论这个请求在后台经过了多少个服务、线程或进程,只要它们属于同一个逻辑请求,就应该被标记为同一个Trace
.关键特性:
- 全局唯一性:每个 Trace 都有一个全局唯一的 Trace ID,这是贯穿整个请求链路的“身份证”。
- 宏观视角:Trace 是最高层级的概念,它描绘了请求从进入系统到最终返回结果的完整调用链路图。
- 聚合单元:性能分析、错误排查、延迟归因等通常以 Trace 为单位进行。
举例:用户在电商 App 上点击“下单”。从点击按钮开始,到订单创建成功并返回结果给用户的全过程,就是一个 Trace。
Segment (片段)
- 定义:一个 Trace 可能会跨越多个服务实例(Service Instance),而 Segment 就是在单个服务实例内部发生的、与该 Trace 相关的所有操作的集合。
- 关键特性:
- 实例级划分:Segment 的边界通常与服务实例(如一个 JVM 进程、一个容器)的边界对齐。当请求进入一个服务实例时,会生成一个 Segment;当请求离开该实例去往下一个服务时,当前 Segment 结束。
- 包含多个 Span:一个 Segment 内部可以包含一个或多个 Span,这些 Span 记录了该实例内执行的具体操作。
- 本地上下文:Segment 携带了该实例内的执行上下文信息。
- 为什么需要 Segment? 在一些追踪系统(如 Apache SkyWalking)中引入 Segment 的概念,是为了更精细地处理跨线程或异步调用的场景。一个服务实例内部可能有复杂的线程切换或异步任务,Segment 能够将这些发生在同一物理实例上的活动都归集在一起,避免因为线程切换导致 Span 断裂或难以关联。
- 举例:在“下单”这个
Trace
中,订单服务 实例接收到请求后,内部可能进行了数据库查询、调用库存服务、发送消息等多个操作。所有这些操作共同构成了 订单服务 实例的 Segment。
Span (跨度)
- 定义:Span 是追踪数据模型中最基本的工作单元。它代表了一个具体的操作或步骤,例如一次方法调用、一次数据库查询、一次 HTTP 请求等。
- 关键特性:
- 原子操作:Span 通常是不可再分的(或至少在追踪层面被视为一个整体)。
- 时间记录:每个 Span 都有明确的开始时间戳和结束时间戳,用于计算该操作的执行耗时。
- 层级与父子关系:Span 之间可以形成树状结构。例如,一个父 Span 可能包含多个子 Span,表示一个操作内部调用了其他子操作。
- 携带元数据:Span 可以附加丰富的标签(Tags)、日志(Logs)和注解(Annotations),如 SQL 语句、HTTP 状态码、异常信息等。
- 上下文传播:Span 包含 Trace ID 和 Span ID,并通过这些标识符以及父 Span ID 来建立与其他 Span 的关联,实现跨服务的上下文传递。
- 类型:
- Client Span (C-Span): 表示客户端发起请求的时刻。
- Server Span (S-Span): 表示服务端接收请求的时刻。
- 通过 C-Span 和 S-Span 的时间差,可以分析网络延迟。
- 举例:在 订单服务 的 Segment 中,Span 可以包括:“开始处理订单”、“查询用户信息(DB)”、“调用库存服务(HTTP)”、“保存订单(DB)”、“发送订单消息(MQ)”等。
上面是对skywaling的基础概念的了解,而与之相对应的就是其中
traceId: 通过ContextCarrier
来维护
segment: 通过TracingContext
来管理了一个TraceSegment
来维护 segmentId
span: 通过TracingContext
的activeSpanStack
其创建和结束。
1. 创建Trace
1.1 创建测试环境
首先,我们需要创建一个测试环境,创建一个简单的spring mvc 的controller 用于提供Http服务
这个接口接收一个参数,将其存入redis,然后在从redis读取出来,返回给浏览器客户端。
那么可以考虑这个过程,有以下几个操作:
- 接收请求
- 存入redis
- 从redis读取
- 返回结果
1.2 启动调试环境,断点来看。
D:\code\github\sw-learn
-javaagent:D:\code\github\sw-learn\skywalking-java\skywalking-agent\skywalking-agent.jar
-javaagent:D:/code/github/sw-learn/skywalking-java/skywalking-agent/skywalking-agent.jar
首先记得,重新compile以下 agent 的源码,因为如果你修改了的话,不重新compile的话,有些源码会对不上。PS D:\code\github\sw-learn\skywalking-java> mvn clean install -DskipTests
./mvnw clean package -Pall
或者说,如果源码调试的时候和代码匹配补上,那么手动选择一下源码,chosse source code, 将原来的 mvn lib 改成当前工程中 sw-java的源码目录。
将 sky walking的check Style 关闭,他不让写中文注释。
D:\code\github\sw-learn\skywalking-java\pom.xml
将失败改成false
<!-- <checkstyle.fails.on.error>true</checkstyle.fails.on.error>-->
<!-- close check style plugin-->
<checkstyle.fails.on.error>false</checkstyle.fails.on.error>
[INFO] There are 102 errors reported by Checkstyle 9.3 with D:\code\github\sw-learn\skywalking-java/apm-checkstyle/checkStyle.xml ruleset.
[ERROR] src\main\java\org\apache\skywalking\apm\agent\core\boot\OverrideImplementor.java:[30] (regexp) RegexpSingleline: Not allow chinese character !
[ERROR] src\main\java\org\apache\skywalking\apm\agent\core\boot\OverrideImplementor.java:[31] (regexp) RegexpSingleline: Not allow chinese character !
[ERROR] src\main\java\org\apache\skywalking\apm\agent\core\boot\OverrideImplementor.java:[32] (regexp) RegexpSingleline: Not allow chinese character !
[ERROR] src\main\java\org\apache\skywalking\apm\agent\core\boot\OverrideImplementor.java:[33] (regexp) RegexpSingleline: Not allow chinese character !
[ERROR] src\main\java\org\apache\skywalking\apm\agent\core\boot\OverrideImplementor.java:[35] (regexp) RegexpSingleline: Not allow chinese character !
[ERROR] src\main\java\org\apache\skywalking\apm\agent\core\boot\OverrideImplementor.java:[36] (regexp) RegexpSingleline: Not allow chinese character !
1.3 如何创建一个trace?
org.apache.skywalking.apm.plugin.pulsar.common.PulsarConsumerInterceptor
先看用法:
- 先创建一个上下文载体
ContextCarrier
,用于承接上游服务的数据信息 - 为本次行为创建一个
span
- 为
span
设置基础信息
@Override
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, MethodInterceptResult result) throws Throwable {
if (allArguments[0] != null) {
ConsumerImpl consumer = (ConsumerImpl) objInst;
final String serviceUrl = consumer.getClient().getLookup().getServiceUrl();
Message msg = (Message) allArguments[0];
// 跨平台支持,比如说 由业务服务发送消息到pulsar,pulsar 通过下面的函数,将 上个 span 中的 信息,设置到当前 span 中
// 他这里通过items,获取 了 三个 item,然后通过循环,每一级获取自己对应的消息值,然后调用自己的反序列化方式,解析消息,重新持有。
// 1. 上下文载体, 用于承接上游服务的数据信息
ContextCarrier carrier = new ContextCarrier();
// 获取三个基础组件,通过sw 承接上游服务的trace信息
CarrierItem next = carrier.items();
while (next.hasNext()) {
next = next.next();
// 将上游的数据承接到 carrier 中
next.setHeadValue(msg.getProperty(next.getHeadKey()));
}
// 2. 为本次行为创建一个span
AbstractSpan activeSpan = ContextManager.createEntrySpan(
OPERATE_NAME_PREFIX + consumer.getTopic() + CONSUMER_OPERATE_NAME + objInst.getSkyWalkingDynamicField(),
carrier
);
// 3. 为当前span 设置基础信息
activeSpan.setComponent(ComponentsDefine.PULSAR_CONSUMER);
SpanLayer.asMQ(activeSpan);
Tags.MQ_BROKER.set(activeSpan, serviceUrl);
Tags.MQ_TOPIC.set(activeSpan, consumer.getTopic());
activeSpan.setPeer(serviceUrl);
}
}
1.4 如何承接上游服务的trace信息?
通过 ContextCarrier
来承接上游服务的trace信息,实现跨进程上下文传播
- 获取item数据承接的处理链条
- 分别使用链条上的每个节点对数据进行承接。
ContextCarrier carrier = new ContextCarrier();
CarrierItem next = carrier.items();
while (next.hasNext()) {
next = next.next();
next.setHeadValue(msg.getProperty(next.getHeadKey()));
}
1.4.1 上下文数据载体 ContextCarrier
ContextCarrier的作用就是承接跨进程的上下文数据,在传统的开发中,是没有办法直接跨进程传输上下文对象的,skywalking自定义了一个这样的工具,他包含了上游的信息(trace, serviceName, serviceInstance。。。),
提供函数 serialize
将当前承接到的消息序列化之后,传输出去,
也提供了函数deserialize
解析上游服务传输进来的数据信息。
同时,管理了数据解析结构 CarrierItem
组成的单向链表
1.4.2 数据承接链表item
public CarrierItem items() {
SW8ExtensionCarrierItem sw8ExtensionCarrierItem = new SW8ExtensionCarrierItem(extensionContext, null);
SW8CorrelationCarrierItem sw8CorrelationCarrierItem = new SW8CorrelationCarrierItem(
correlationContext, sw8ExtensionCarrierItem);
SW8CarrierItem sw8CarrierItem = new SW8CarrierItem(this, sw8CorrelationCarrierItem);
return new CarrierItemHead(sw8CarrierItem);
}
items
函数返回了三个item组成的单项链表结构,每个item又只有自己的不同的作用。
当前链表结构如下:
CarrierItemHead -> SW8CarrierItem -> SW8CorrelationCarrierItem -> SW8ExtensionCarrierItem
解释:
- CarrierItemHead: 单纯的一个头节点
- SW8CarrierItem : 这是最核心的 Item,负责处理 SW8 字符串中最关键的标准追踪字段,他负责处理的信息有
- 请求的数据格式如:
1-MyServiceName@PID-1-1-0-10.0.0.1-12345-1-a2fb1a98366d4b7a-b7ca268f21c50ae9
- traceId:
- traceSegmentId
- spanId
- parentService:上游服务名
- parentServiceInstance:上游服务实例名称
- parentEndpoint:入口断点
- addressUsedAtClient:
- 职责是:
- 发送请求前,由skywalking agent 填充这些字段,到请求头中
- 接受请求后:从 SW8 头中解析出这些字段,并用于构建新的 Segment 和 Span,建立父子关系。
- 请求的数据格式如:
- SW8CorrelationCarrierItem : 用于传递
开发者
自定义的关联数据(Correlation Context)。- 依赖于
CorrelationContext
对数据进行管理。 - 职责
- 允许开发者将业务相关的上下文信息(如 user-id=123, tenant-id=orgA)注入到追踪链路中。
- 这些数据会随着请求在整个微服务链路中传递,方便在日志、告警或分析时进行关联过滤。
- 例如:你想排查某个特定用户的订单问题,可以通过 user-id 快速筛选出该用户的所有相关调用
- 依赖于
- SW8ExtensionCarrierItem :
- 角色 :这是一个可扩展的机制,允许在标准 SW8 格式之外添加自定义的扩展信息。
- 当前只支持传递两个属性:
skipAnalysis
,sendingTimestamp
1.4.3 承接跨进程传输的上下文数据
通过对链表遍历,使用单独每个CarrierItem
对自己支持的数据部分进行反序列化读取赋值,以此实现:跨进程的上下文实现。
通过各CarrierItem
自己的请求头从数据载体中获取数据值,然后赋值到自己的value对象上,一般情况下,CarrierItem
传输的时候会对多个数据值进行编码,因此,承接数据的时候,在 setHeadValue
函数中一般会调用自己的反序列化函数,对数据进行解码。
while (next.hasNext()) {
next = next.next();
next.setHeadValue(msg.getProperty(next.getHeadKey()));
}
由上面分析可知,skywalking的跨进程上下文是通过链表结构,组成一个数据承接item的链条,每一个节点都会处理自己专属的数据,实现的跨进程上下文传递,而他们的传递,又依据不同的方式来传输,比如通过http请求头,GRPC,dubbo的attent,MQ的消息等等。
如:org.apache.skywalking.apm.plugin.pulsar.common.PulsarProducerInterceptor#beforeMethod
2.如何创建span?
span分为几种类型,入口SpanEntrySpan
, 本地span LocalSpan
, 退出span ExitSpan
,还有一种啥也不干的NoopSpan
2.1 创建入口span EntrySpan
单看方法签名,很简单:AbstractSpan createEntrySpan(String operationName, ContextCarrier carrier)
方法实现:
public static AbstractSpan createEntrySpan(String operationName, ContextCarrier carrier) {
// traceContext
AbstractTracerContext context = getOrCreate(operationName, false);
//通过 context 创建 span
AbstractSpan span = context.createEntrySpan(operationName);
// 将carrier中的数据注入到 traceContext 中
context.extract(carrier);
return span;
}
可见,他首先是创建或者是获取了 traceContext 上下文对象,然后通过traceContext 创建了 span 对象。
相关信息
注意这里使用了 context.extract(carrier) 将数据从carrier中提取到了 traceContext 中。
2.2 traceContext 上下文
这个对象是一个线程会创建一个,且将这个对象保存在线程变量中。
org.apache.skywalking.apm.agent.core.context.ContextManagerExtendService#createTraceContext
创建traceContext
TraceContext 使用 TraceSegment
管理当前进程实例范围内的trace,TraceSegment
是一个分布式trace中的一帧,即当前jvm实例就是分布式trace中的一帧,TraceSegment
管理当前帧的所有span
。
注意:
默认情况下span
存放在TraceContext中,只有当调用ContextManager.stopSpan();
的时候,才会从TraceContext中,将span移动到TraceSegment
中。
2.3 创建span
2.3.1 入口span
entrySpan = new EntrySpan(
spanIdGenerator++, parentSpanId,
operationName, owner
);
entrySpan.start();
return push(entrySpan);
注意,每个span都会持有前面一个span的id,其实也是一个链条,创建完成后,添加到链表中。
通过以上方法,就创建了一个trace+span的组合。
2.3.2 localSpan
如果中间还有span怎么办?
那就不需要创建入口span了,需要创建localSpan
2.3.3 出口span
如果当前服务的操作完成了,那就需要创建exitSpan
出口span创建完成之后,需要将当前实例持有的trace信息传递给下游服务,如何传递是个问题。
AbstractSpan createExitSpan(String operationName, ContextCarrier carrier, String remotePeer)
需要三个参数:
- 操作名
- 数据载体,将当前agent的trace信息赋值到给定的数据载体
carrier
上。 - 远程地址
public static AbstractSpan createExitSpan(String operationName, ContextCarrier carrier, String remotePeer) {
AbstractTracerContext context = getOrCreate(operationName, false);
AbstractSpan span = context.createExitSpan(operationName, remotePeer);
// 将上下文数据信息注入到carrier中。
context.inject(carrier);
return span;
}
其实创建span并没有什么不同,唯一不同的是会将当前agent持有trace信息注入到给定的上下文载体ContextCarrier
中。
而后续,可以选择如何将 ContextCarrier
传递到下游服务。
3.如何将数据传递到下游服务?
PulsarProducerInterceptor#beforeMethod
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
if (allArguments[0] != null) {
// 创建上下文数据载体,用于获取当前agent中的trace信息
ContextCarrier contextCarrier = new ContextCarrier();
ProducerImpl producer = (ProducerImpl) objInst;
final String serviceUrl = producer.getClient().getLookup().getServiceUrl();
// 创建exit span,注意会将agent的trace信息注入到 contextCarrier 中。
AbstractSpan activeSpan = ContextManager.createExitSpan("exit", contextCarrier, serviceUrl);
CarrierItem next = contextCarrier.items();
MessageImpl msg = (MessageImpl) allArguments[0];
MessagePropertiesInjector propertiesInjector = (MessagePropertiesInjector) objInst.getSkyWalkingDynamicField();
if (propertiesInjector != null) {
while (next.hasNext()) {
next = next.next();
// 将 不同的CarrierItem中的数据放入到 pulsar的消息中,向后传递
propertiesInjector.inject(msg, next);
}
}
}
}
@Override
public Object afterMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Object ret) throws Throwable {
// 停止span
ContextManager.stopSpan();
return ret;
}
从上面可以执行pulsar的发送函数之前,创建了exitSpan,同时将当前agent的trace信息注入到了 上下文对象ContextCarrier
中,之后,又将这些上下文对象中的数据添加到了pulsar的消息中去。以便于后续发送的时候,trace信息伴随消息的发送向后传递。
然后,在执行发送函数之后,调用了 ContextManager.stopSpan()
函数。
这时候,traceSegment会归档收集最后一个span的信息,将来这些东西会被上报给oap分析使用。
因 span 都持有上级span的id,所以不需要归档所有的span信息,只需要最后一个span即可。
相关信息
注意这里使用了 context.inject(carrier) 将数据从carrier中提取到了 traceContext 中。