第12讲:剖析 Trace 在 SkyWalking 中的落地实现方案(上)

news2024/11/26 4:35:24

SkyWalking 中 Trace 的相关概念以及实现类与 OpenTracing 中的概念基本类似,像 Trace、Span、Tags、Logs 等核心概念,在 SkyWalking Agent 中都有对应实现,只是在细微实现上略有区别的,其中最重要的是: SkyWalking 的设计在 Trace 级别和 Span 级别之间加了一个 Segment 概念,用于表示一个服务实例内的 Span 集合。

Trace ID

在分布式链路追踪系统中,用户请求的处理过程会形成一条 Trace 。Trace ID 作为 Trace 数据的唯一标识,在面对海量请求的时候,需要保证其唯一性。与此同时,还要保证生成 Trace ID 不会带来过多开销,所以在业务场景中依赖数据库(自增键或是类似 Meituan-Dianping/Leaf 的 ID 生成方式)都不适合 Trace 的场景。

这种要求快速、高性能生成唯一 ID 的需求场景,一般会将 snowflake 算法与实际的场景集合进行改造。

snowflake 算法是 Twitter 开源的分布式 ID 生成算法 。snowflake 算法的核心思想是将一个 ID(long类型)的 64 个 bit 进行切分,其中使用 41 个 bit 作为毫秒数,10 个 bit 作为机器的 ID( 5 个 bit 记录数据中心的 ID,5 个 bit 记录机器的 ID ),12 bit 作为毫秒内的自增 ID,还有一个 bit 位永远是 0。snowflake 算法生成的 ID 结构如下图所示:

snowflake 算法的好处是 ID 可以直接靠算法在内存中产生,内存内的锁控制并发,不需依赖 MySQL 这样的外部依赖,无维护成本。缺点就是每个机器节点在每毫秒内只可以产生 4096 个 ID,超出这个范围就会溢出。另外,如果机器回拨了时间,就会生成重复的 ID。

ID 类是 SkyWalking 中对全局唯一标识的抽象,其生成策略与 snowflake 算法类似。SkyWalking ID 由三个 long 类型的字段(part1、part2、part3)构成,分别记录了 ServiceInstanceId、Thread ID 和 Context 生成序列。Context 生成序列的格式是:

${时间戳} * 10000 + 线程自增序列([09999])

ID 对象序列化之后的格式是将 part1、part2、part3 三部分用“.”分割连接起来 :

${ServiceInstanceId}.${Thread ID}.(${时间戳} * 10000 + 线程自增序列([09999]))

GlobalIdGenerator 是 Agent 中用来生成全局唯一 ID 的基础工具类,在 generate() 方法中的实现如下:

public static ID generate() {
    // THREAD_ID_SEQUENCE是 ThreadLocal<IDContext>类型,即每个线程
    // 维护一个 IDContext对象
    IDContext context = THREAD_ID_SEQUENCE.get(); 
    return new ID(SERVICE_INSTANCE_ID, // service_intance_id
        Thread.currentThread().getId(), // 当前线程的ID
        context.nextSeq() // 线程内生成的序列号
    );
}

IDContext.nextSeq() 方法的实现如下,其中 timestamp() 方法在返回时间戳的时候,会处理时间回拨的场景(使用 Random 随机生成一个时间戳),nextThreadSeq() 方法的返回值在 [0 , 9999] 这个范围内循环:

private long nextSeq() {
    return timestamp() * 10000 + nextThreadSeq();
}

GlobalIdGenerator 不仅用于生成 Trace ID ,其他需要唯一 ID 的地方也会通过其 nextSeq() 方法生成。

SkyWalking 中使用 DistributedTraceId 类来抽象 Trace ID,其中封装了一个 ID 类型的字段。DistributedTraceId 有两个实现类,如下图所示:

其中,NewDistirbutedTraceId 负责生成新 Trace ID,请求刚刚进入系统时,会创建 NewDistirbutedTraceId 对象,其构造方法内部会调用 GlobalIdGenerator.generate() 方法生成 ID 对象。

PropagatedTraceId 负责处理 Trace 传播过程中的 TraceId。PropagatedTraceId 的构造方法接收一个 String 类型参数(也就是在跨进程传播时序列化后的 Trace ID),解析之后得到 ID 对象。

在后面的介绍中还会涉及另一个与 Trace ID 相关的类 —— DistributedTraceIds,它表示多个 Trace ID 的集合,其底层封装了一个 LinkedList<DistributedTraceId> 集合,用于记录相关的 Trace ID。

TraceSegment

在 SkyWalking 中,TraceSegment 是一个介于 Trace 与 Span 之间的概念,它是一条 Trace 的一段,可以包含多个 Span。在微服务架构中,一个请求基本都会涉及跨进程(以及跨线程)的操作,例如, RPC 调用、通过 MQ 异步执行、HTTP 请求远端资源等,处理一个请求就需要涉及到多个服务的多个线程。TraceSegment 记录了一个请求在一个线程中的执行流程(即 Trace 信息)。将该请求关联的 TraceSegment 串联起来,就能得到该请求对应的完整 Trace。

下面我们先来介绍 TraceSegment 的核心字段:

  • traceSegmentId(ID 类型):TraceSegment 的全局唯一标识,是由前面介绍的 GlobalIdGenerator 生成的。
  • refs(List<TraceSegmentRef> 类型):它指向父 TraceSegment。在我们常见的 RPC 调用、HTTP 请求等跨进程调用中,一个 TraceSegment 最多只有一个父 TraceSegment,但是在一个 Consumer 批量消费 MQ 消息时,同一批内的消息可能来自不同的 Producer,这就会导致 Consumer 线程对应的 TraceSegment 有多个父 TraceSegment 了,当然,该 Consumer TraceSegment 也就属于多个 Trace 了。
  • relatedGlobalTraces(DistributedTraceIds 类型):记录当前 TraceSegment 所属 Trace 的 Trace ID。
  • spans(List<AbstractTracingSpan> 类型):当前 TraceSegment 包含的所有 Span。
  • ignore(boolean 类型):ignore 字段表示当前 TraceSegment 是否被忽略。主要是为了忽略一些问题 TraceSegment(主要是对只包含一个 Span 的 Trace 进行采样收集)。
  • isSizeLimited(boolean 类型):这是一个容错设计,例如业务代码出现了死循环 Bug,可能会向相应的 TraceSegment 中不断追加 Span,为了防止对应用内存以及后端存储造成不必要的压力,每个 TraceSegment 中 Span 的个数是有上限的(默认值为 300),超过上限之后,就不再添加 Span了。

下图展示了一个 TraceSegment 的核心结构:

Span

TraceSegment 是由多个 Span 构成的,AbstractSpan 抽象类是 SkyWalking 对 Span 概念的抽象,下图是 Span 的继承关系:

首先需要明确的是,我们最终直接使用的 Span 分为 3 类:

  • EntrySpan:当请求进入服务时会创建 EntrySpan 类型的 Span,它也是 TraceSegment 中的第一个 Span。例如,HTTP 服务、RPC 服务、MQ-Consumer 等入口服务的插件在接收到请求时都会创建相应的 EntrySpan。
  • LocalSpan:它是在本地方法调用时可能创建的 Span 类型,在后面介绍 @Trace 注解的时候我们还会看到 LocalSpan。
  • ExitSpan:当请求离开当前服务、进入其他服务时会创建 ExitSpan 类型的 Span。例如, Http Client 、RPC Client 发起远程调用或是 MQ-producer 生产消息时,都会产生该类型的 Span。

下面我们按照 Span 的继承结构,自顶层接口开始逐个向下介绍。首先,AsyncSpan 接口定义了一个异步 Span 的基本行为:

  • prepareForAsync() 方法:Span 在当前线程结束了,但是未被彻底关闭,依然是存活的。
  • asyncFinish()方法:当前 Span 真正关闭。它与 prepareForAsync() 方法成对出现。

这两个方法在异步框架的插件中会见到。

AbstractSpan 也是一个接口,其中定义了 Span 的基本行为,其中的方法比较重要:

  • getSpanId() 方法:用来获得当前 Span 的 ID,Span ID 是一个 int 类型的值,在其所属的 TraceSegment 中唯一,在创建 Span 对象时生成,从 0 开始自增。
  • setOperationName()/setOperationId() 方法:用来设置 operation 名称(或 operation ID),这两个信息是互斥的。它们在 AbstractSpan 的具体实现(即 AbstractTracingSpan)中,分别对应 operationId 和 operationName 两个字段,两者只能有一个字段有值。

operationName 即前文介绍的 EndpointName,可以是任意字符串,例如,在 Tomcat 插件中 operationName 就是 URI 地址,Dubbo 插件中 operationName 为 URL + 接口方法签名。

  • setComponent() 方法:用于设置组件类型。它有两个重载,在 AbstractTracingSpan 实现中,有 componentId 和 componentName 两个字段,两个重载分别用于设置这两个字段。在 ComponentsDefine 中可以找到 SkyWalking 目前支持的组件类型。
  • setLayer() 方法:用于设置 SpanLayer,也就是当前 Span 所处的位置。SpanLayer 是个枚举,可选项有 DB、RPC_FRAMEWORK、HTTP、MQ、CACHE。
  • tag(AbstractTag, String) 方法:用于为当前 Span 添加键值对的 Tags。一个 Span 可以有多个 Tags。AbstractTag 中不仅包含了 String 类型的 Key 值,还包含了 Tag 的 ID 以及 canOverwrite 标识。AbstractTracingSpan 实现通过维护一个  List<TagValuePair> 集合(tags 字段)来记录 Tag 信息,TagValuePair 中则封装了 AbstractTag 类型的 Key 以及 String 类型的 Value。
  • log() 方法:用于向当前 Span 中添加 Log,一个 Span 可以包含多条日志。在 AbstractTracingSpan 实现中通过维护一个 List<LogDataEntity> 集合(logs 字段)来记录 Log。LogDataEntity 会记录日志的时间戳以及 KV 信息,以异常日志为例,其中就会包含一个 Key 为“stack”的 KV,其 value 为异常堆栈。
  • start() 方法:开启 Span,其中会设置当前 Span 的开始时间以及调用层级等信息。
  • isEntry() 方法:判断当前是否是 EntrySpan。EntrySpan 的具体实现后面详细介绍。
  • isExit() 方法:判断当前是否是 ExitSpan。ExitSpan  的具体实现后面详细介绍。
  • ref() 方法:用于设置关联的 TraceSegment 。

AbstractTracingSpan 实现了 AbstractSpan 接口,定义了一些 Span 的公共字段,其中的部分字段在介绍 AbstractSpan 接口时已经提到了,下面简单介绍一下前面未涉及的字段含义:

protected int spanId; // span的ID
protected int parentSpanId; // 记录父Span的ID
protected List<TagValuePair> tags; // 记录Tags的集合
protected long startTime, endTime; // Span的起止时间
protected boolean errorOccurred = false; // 标识该Span中是否发生异常
protected List<TraceSegmentRef> refs; // 指向所属TraceSegment
// context字段指向TraceContext,TraceContext与当前线程绑定,与TraceSegment
// 一一对应
protected volatile AbstractTracerContext context;

AbstractTracingSpan 中提供的方法也比较简单,基本都是上述字段的 getter/setter 方法,这些方法不再展开赘述。这里需要注意两个方法:

  • finish(TraceSegment) 方法:该方法会关闭当前 Span ,具体行为是用 endTime 字段记录当前时间,并将当前 Span 记录到所属 TraceSegment 的 spans 集合中。
  • transform() 方法:该方法会在 Agent 上报 TraceSegment 数据之前调用,它会将当前 AbstractTracingSpan 对象转换成 SpanObjectV2 对象。SpanObjectV2 是在 proto 文件中定义的结构体,后面 gRPC 上报 TraceSegment 数据时会将其序列化。

StackBasedTracingSpan 在继承 AbstractTracingSpan 存储 Span 核心数据能力的同时,还引入了栈的概念,这种 Span 可以多次调用 start() 方法和 end() 方法,但是两者调用次数必须要配对,类似出栈和入栈的操作。

下面以 EntrySpan 为例说明为什么需要“栈”这个概念,EntrySpan 表示的是一个服务的入口 Span,是 TraceSegment 的第一个 Span,出现在服务提供方的入口,例如,Dubbo Provider、Tomcat、Spring MVC,等等。 那么为什么 EntrySpan 继承 StackBasedTracingSpan 呢? 从前面对 SkyWalking Agent 的分析来看,Agent 插件只会拦截指定类的指定方法并对其进行增强,例如,Tomcat、Spring MVC 等插件的增强逻辑中就包含了创建 EntrySpan 的逻辑(后面在分析具体插件实现的时候,会看到具体的实现代码)。很多 Web 项目会同时使用到这两个插件,难道一个 TraceSegment 要有两个 EntrySpan 吗?显然不行。

SkyWalking 的处理方式是让 EntrySpan 继承了 StackBasedTracingSpan,多个插件同时使用时,整个架构如下所示:

其中,请求相应的 EntrySpan 处理流程如下:

  1. 当请求经过 Tomcat 插件时(即图中 ① 处),会创建 EntrySpan 并第一次调用 start() 方法,启动该 EntrySpan。

在 start() 方法中会有下面几个操作:

  1. 将 stackDepth 字段(定义在 StackBasedTracingSpan 中)加 1,stackDepth 表示当前所处的插件栈深度 。
  2. 更新 currentMaxDepth 字段(定义在 EntrySpan 中),currentMaxDepth 会记录该EntrySpan 到达过的插件栈的最深位置。
  3. 此时第一次启动 EntrySpan 时会更新 startTime 字段,记录请求开始时间。

此时插件栈(这是为了方便理解而虚拟出来一个栈结构,实际上只有 stackDepth、currentMaxDepth 两个字段,并不会用到栈结构,也不会记录请求经过的插件)的状态如下图所示:

  1. 当请求经过 Spring MVC 插件时(即图中 ② 处),不会再创建新的 EntrySpan 了,而重新调用该 EntrySpan 的 start() 方法,其中会继续将 stackDepth 以及 currentMaxDepth 字段加 1 。注意,再次调用 start() 方法时不会更新 startTime 字段了,因为请求已经开始处理了。此时插件栈的状态如下图:

  1. 当请求经过业务逻辑处理完成之后,开始进入 Spring MVC 插件的后置处理逻辑时(即图中 ③ 处),会第 1 次调用 EntrySpan.finish() 方法,其中会将 stackDepth 减 1,即 Spring MVC 插件出栈,此时插件栈的状态如下图:

  1. 最后进入 Tomcat 插件的后置处理逻辑(即图中 ④ 处),其中会第 2 次调用 finish() 方法,此时 stackDepth 再次减 1,此时 stackDepth 减到了 0 ,整个插件栈已经空了,会调用父类 AbstractTracingSpan 的 finish() 方法将当前 EntrySpan 添加到关联的 TraceSegment 中。

这里需要注意两个点,一是在调用 start() 方法时,会将之前设置的 component、Tags、Log 等信息全部清理掉(startTime不会清理),上例中请求到 Spring MVC 插件之前(即 ② 处之前)设置的这些信息都会被清理掉。二是 stackDepth 与 currentMaxDepth 不相等时(上例中 ③ 处),无法记录上述字段的信息。通过这两点,我们知道 EntrySpan 实际上只会记录最贴近业务侧的 Span 信息。

StackBasedTracingSpan 除了将“栈”概念与 EntrySpan 结合之外,还添加了 peer(以及 peerId)字段来记录远端地址,在发送远程调用时创建的 ExitSpan 会将该记录用于对端地址。

ExitSpan 表示的是出口 Span,如果在一个调用栈里面出现多个插件嵌套的场景,也需要通过“栈”的方式进行处理,与上述逻辑类似,只会在第一个插件中创建 ExitSpan,后续调用的 ExitSpan.start() 方法并不会更新 startTime,只会增加栈的深度。当然,在设置 Tags、Log 等信息时也会进行判断,只有 stackDepth 为 1 的时候,才会能正常写入相应字段。也就是说,ExitSpan 中只会记录最贴近当前服务侧的 Span 信息。

一个 TraceSegment 可以有多个 ExitSpan,例如,Dubbo A 服务在处理一个请求时,会调用 Dubbo B 服务,在得到响应之后,会紧接着调用 Dubbo C 服务,这样,该 TraceSegment 就有了两个完全独立的 ExitSpan。

LocalSpan 则比较简单,它表示一个本地方法调用。LocalSpan 直接继承了 AbstractTracingSpan,由于它未继承 StackBasedTracingSpan,所以也不能 start 或 end 多次,在后面介绍 @Trace 注解的相关实现时,还会看到 LocalSpan 的身影。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/742917.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis——redis的java客户端+(Jedis和SpringDataRedis)

客户端对比 一个Spring Data Redis底层可以兼容前两个 Jedis快速入门 直接使用命令名作为方法名就是Jedis好学的原因 第一步 &#xff1a; 创建一个maven项目并导入如下依赖&#xff0c;除了redis的以来还有一个junit5的依赖 <!--redis的依赖--><dependency>&l…

如何使用3D转换工具HOOPS Exchange与LibConverter进行流缓存导出?

如果您正在使用HOOPS Communicator&#xff0c;您可能想在生成流缓存模型之前利用HOOPS Exchange的高级功能和转换选项。 申请HOOPS试用 HOOPS中文网 如何使用 如您所知&#xff0c;LibConverter是HOOPS Communicator软件包中包含的一个简单的API&#xff0c;conver…

【GeoDa实用技巧100例】003:GeoDa连接并打开多种格式的数据源

GeoDa支持 shapefile、地理数据库、GeoJSON、MapInfo、GML、KML 以及 GDAL 库支持的其他矢量数据格式。该程序还将表格格式&#xff08;.csv、.dbf、.xls、.ods&#xff09;中的坐标转换为这些空间数据格式之一&#xff0c;并在不同文件格式之间转换数据。 文章目录 一、连接文…

深入了解 CSS 变量,让 CSS 创造更多可能!

日常开发中所说的 CSS 变量&#xff0c;实际上是 CSS 的变量函数 var() 与 CSS 自定义属性的统称。 CSS 变量带来的提升: 使得开发和维护成本更低了&#xff0c;如让整个网站的换肤变得更容易&#xff1b;改变了在图形交互效果的实现中 JavaScript 的占据比重&#xff0c;使得…

EasyUI Pagination 分页的两种做法小结

EasyUI Pagination 分页的两种做法小结 EasyUI 的 datagrid 支持服务器端分页&#xff0c;但是官方的资料比较少&#xff0c;以下总结了两种 datagrid 的服务器端分页机制&#xff0c;一种是datagrid默认机制&#xff0c;另一种是利用 Ajax 获取数据并填充 Datagrid&#xff0c…

矩形面积 (力扣)数学推理 JAVA

给你 二维 平面上两个 由直线构成且边与坐标轴平行/垂直 的矩形&#xff0c;请你计算并返回两个矩形覆盖的总面积。 每个矩形由其 左下 顶点和 右上 顶点坐标表示&#xff1a; 第一个矩形由其左下顶点 (ax1, ay1) 和右上顶点 (ax2, ay2) 定义。 第二个矩形由其左下顶点 (bx1, b…

系统驱动作业

作业1 myled_time.c #include <linux/init.h> #include <linux/module.h> #include<linux/of.h> #include<linux/gpio.h> #include<linux/of_gpio.h> #include<linux/timer.h>struct device_node *dnode; unsigned int gpiono; struct t…

Redis — 不仅仅是缓存

1*qIy3PMmEWNcD9Czh_21C8g.png Redis是一种快速、开源的内存键值&#xff08;NoSQL&#xff09;数据库&#xff0c;远远超越了缓存的功能。Redis使用RAM进行操作&#xff0c;提供亚毫秒级的响应时间&#xff0c;支持每秒数百万次请求。Redis主要用于缓存&#xff0c;但它也可以…

【CSS】文字渐变色

CSS设置文字渐变色 background-image: -webkit-linear-gradient(bottom,red,#fd8403,yellow); -webkit-background-clip: text; // 设置背景作用域 -webkit-text-fill-color: transparent;

docker数据卷权限管理--理论和验证

一、Docker容器中用户权限管理 Linux系统的权限管理是由uid和gid负责&#xff0c;Linux系统会检查创建进程的uid和gid&#xff0c;以确定它是否有足够的权限修改文件&#xff0c;而非是通过用户名和用户组来确认。 同样&#xff0c;在docker容器中主机上运行的所有容器共享同一…

【DC系列06】DC-6靶机复盘

【DC系列06】DC-6靶机复盘 我们打开这个靶机后首先还是先对靶机进行一个ip扫描&#xff0c;然后再单独对这个ip进行一个全局扫描。 扫描后ip地址是192.168.102.147。 我们再单独扫描一下 nmap -A 192.168.102.147 -p-这个靶机有一个特点就是扫描到ip后&#xff0c;你需要往…

vulnhub靶机渗透:PWNLAB: INIT

PWNLAB: INIT 靶机环境介绍nmap扫描端口扫描服务扫描漏洞扫描扫描总结 80端口目录爆破LFI利用 3306端口回到80端口文件上传 获得立足点横向移动提权总结参考 靶机环境介绍 https://www.vulnhub.com/entry/skytower-1,96/ 靶机IP&#xff1a;192.168.56.103 kali IP&#xff…

OceanBase 特殊的 INT 与时间类型隐式转换问题

本文作者分享了Oceanbase时间与数值类型隐式转换导致查询结果不符合预期或“不正确”问题的排查思路。 作者&#xff1a;任仲禹 爱可生 DBA 团队成员&#xff0c;擅长故障分析和性能优化&#xff0c;文章相关技术问题&#xff0c;欢迎大家一起讨论。 本文来源&#xff1a;原创投…

create-react-app -V 报错无法将“create-react-app”项识别为 cmdlet、函数、脚本文件或可运行程序的名称

create-react-app -Vcreate-react-app : 无法将“create-react-app”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果 包括路径,请确保路径正确,然后再试一次。 所在位置 行:1 字符: 1 + create-react-app -V + ~~~~~~~~~~~~~~~~+ CategoryInfo …

移动端 van-datetime-picker 组件不支持选择秒,自己写组件支持选择 “年月日时分秒”

需求 使用 van-datetime-picker 组件写时间选择时&#xff0c;不支持选择 “秒” 要求能够选择精确到秒 代码 封装组件 TimePicker <template><div><!-- 弹出层 --><van-popup get-container"body" v-model"isPicker" position&qu…

实时数仓-DWS层

理论依据&#xff1a;指标体系 如果表有相似&#xff0c;可以抽取成一张表 轻度聚合&#xff1a;比如聚合10秒的先放到DWS&#xff0c;这样ADS在计算的时候&#xff0c;会更快

计讯物联5G工业路由器加速赋能公交车监控系统解决方案智能升级

一、方案概述 为提升公交车智能化管理水平为目的&#xff0c;保障公交车营运人员和乘客的安全&#xff0c;计讯物联以公交车智能调度营运为核心&#xff0c;推出计讯物联5G公交车一站式解决方案&#xff0c;通过构建感知层、传输层、设备层、应用层四层架构&#xff0c;通过感…

将名为“普通高等学校本科专业目录.pdf”的pdf文件转换成csv文件

文章目录 任务描述2023年普通高等学校本科专业目录pdf链接代码代码解析运行截图 任务描述 将名为“普通高等学校本科专业目录.pdf”的pdf文件转换成csv文件。这个pdf每页是个表格&#xff0c;表格有7列。 下面是pdf的第一页和第二页&#xff1a; 2023年普通高等学校本科专业…

10.3.2 【Linux】历史命令:history

[dmtsaistudy ~]$ alias hhistory 在正常的情况下&#xff0c;历史命令的读取与记录是这样的&#xff1a; 当我们以 bash 登陆 Linux 主机之后&#xff0c;系统会主动的由主文件夹的 ~/.bash_history 读取以前曾经下过的指令&#xff0c;那么 ~/.bash_history 会记录几笔数据呢…

microblaze生成download.bit 报错:Program FPGA failed Reason: couldn‘t open......

报错信息&#xff1a; couldn’t open “E:/Xilinx_Project/……/……/…….sdk/top_wrapper_hw_platform_0/download.bit”: no such file or directory [Updatemem 57-153] Failed to update the BRAM INIT strings for E:\Xilinx_Project\……\……\…….sdk\……\Debug\……