初探分布式链路追踪(上)

简介: 初探分布式链路追踪(上)




本篇文章,主要介绍应用如何正确使用日志系统,帮助用户从依赖、输出、清理、问题排查、报警等各方面全面掌握。


可观测性


可观察性不单是一套理论框架,而且并不强制具体的技术规格。其核心在于鼓励团队内化可观察性的理念,并确保由研发人员构建的应用程序具备可观察性。在学术领域中,尽管“可观测性”这一术语是近年来从控制理论中引进的新词,但实际上,它在计算机科学领域已有深厚的实践基础。学者们通常会把可观测性细化为三个更具体的研究方向:事件日志、链路追踪和聚合度量。这三个领域虽然各有侧重点,但并非完全孤立,它们之间存在着天然的交集与互补性。


  1. 日志(Logging),展现的是应用运行而产生的事件或者程序在执行的过程中间产生的一些日志,可以详细解释系统的运行状态,但是存储和查询需要消耗大量的资源。所以往往使用过滤器减少数据量。
  2. 度量(Metrics),是一种聚合数值,存储空间很小,可以观察系统的状态和趋势,但对于问题定位缺乏细节展示。这个时候使用等高线指标等多维数据结构来增强对于细节的表现力。例如统计一个服务的 TBS 的正确率、成功率、流量等,这是常见的针对单个指标或者某一个数据库的。
  3. 追踪(Tracing),面向的是请求,可以轻松分析出请求中异常点,但与日志有相同的问题就是资源消耗较大。通常也需要通过采样的方式减少数据量。比如一次请求的范围,也就是从浏览器或者手机端发起的任何一次调用,一个流程化的东西,需要轨迹去追踪。


在工业界,历经长期的竞争,日志和度量领域的主导技术已逐渐明朗。日志收集与分析方面,Elastic Stack(亦称为ELK Stack)已成为广泛采纳的技术选择。然而,追踪技术的发展路径与日志和度量领域显然不同。追踪技术高度依赖于特定的网络协议和编程语言。服务间是通过 HTTP 还是 gRPC 通信,会直接决定追踪实施的具体方法。同样,服务是否采用 Java、Golang、还是 Node.js 编写,也会影响到如何追踪进程内调用堆栈。这种依赖性使得追踪工具通常需要以插件或探针的形式进行深度集成,因而具有一定的侵入性。此外,由于追踪技术需要适应不同的语言和网络环境,追踪领域很难形成单一的主导者。相反,市场上存在多样化的产品,旨在满足各种不同技术栈的追踪需求。


本文主要探讨其中的追踪下日志内容。


链路追踪

分布式链路追踪公认的起源是 Google 在 2010 年发表的论文《Dapper : a Large-Scale Distributed Systems Tracing Infrastructure》,这篇论文介绍了 Google 从 2004 年开始使用的分布式追踪系统 Dapper 的实现原理。


论文Dapper : a Large-Scale Distributed Systems Tracing Infrastructure》地址:https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/papers/dapper-2010-1.pdf

 基本原理



分布式链路追踪的基本原理就是在分布式应用的接口方法上设置一些观察点,然后在入口节点给每个请求分配一个全局唯一的标识 TraceId。当请求流经这些观察点时就会记录一行对应的链路日志(包含链路唯一标识,接口名称,时间戳,主机信息等)。最后通过 TraceId 将一次请求的所有链路日志进行组装,就可以还原出该次请求的链路轨迹。


分布式链路追踪实现请求回溯的关键点有两个:一是低成本、高质量的观察点设置,也就是链路插桩,确保追踪的信息足够丰富,能够快速定位异常根因;二是保证链路上下文在不同环境下都能够完整透传,避免出现上下文丢失导致的断链现象。


 基础术语


虽然分布式链路追踪的实现方式多种多样,但是仍然有一些基础术语在业界具备广泛的共识。

  • Trace


一条 Trace 代表一次入口请求在 IT 系统内的完整调用轨迹及其关联数据集合。其中,全局唯一的链路标识 TraceId,是最具代表的一个属性。通过 TraceId 才能将同一个请求分散在不同节点的链路数据准确的关联起来,实现请求粒度的“确定性关联”价值。


以集团的Eagleeye设计的traceId设计为例,根据这个id,可以知道这个请求在2022-10-18 10:10:40发出,被11.15.148.83机器上进程号为14031的Nginx(对应标识位e)接收到。其中的四位原子递增数从0-9999,目的是为了防止单机并发造成traceId碰撞。


  • Span


Span是一个操作,它代表系统中一个逻辑运行单元。Span之间通过嵌套或者顺序排列建立因果关系。Span包含以下对象

  1. Operation Name:描述了当前接口的行为语义,比如 /api/createOrder 代表执行了一次创建订单的动作。
  2. SpanId/ParentSpanId:接口调用的层级标识,用于还原 Trace 内部的层次调用关系。
  3. Start/FinishTime:接口调用的开始和结束时间,二者相减就是该次调用的耗时。
  4. StatusCode:响应状态,标识当次调用是成功或失败。
  5. Tags & Events:调用附加信息,详见下面的描述。



其中最重要的是SpanId的设计。同样以阿里的Eagleeye的spanId(rpcId)设计为例,Eagleeye设计了RpcId来区别同一个调用链下多个网络调用的顺序和嵌套层次,用0.X1.X2.X3.....Xi来表示,根节点的RpcId固定从0开始,id的位数("."的数量)表示了Span在这棵树中的层级,Id最后一位表示了Span在这一层级中的顺序。那么给定同一个Trace中的所有RpcId,便可以很容易还原出一个完成的调用链:


- 0
  - 0.1
    - 0.1.1
    - 0.1.2
      - 0.1.2.1
  - 0.2
    - 0.2.1
  - 0.3
    - 0.3.1
      - 0.3.1.1
    - 0.3.2


  • Tags


如果需要进一步记录请求的行为特征,可以使用 Tags 来扩展语义。Tags 是一组由 {Key:Value} 组成的键值对集合,描述这一次接口调用的具体属性。比如Eagleeye里就有「Tags」的实现(UserData),阿里的全链路压测标识就是通过UserData里传递实现的。


在分布式追踪的上下文中,一个Trace可被视为一个有向无环图,其中每个Span代表图中的一个节点,而节点之间的连接则表示为链接。观察这样的图表,我们可以看到一个单独的Span节点可能拥有多条链接,这表明它具有多个子Span。这种结构揭示了各个操作间的父子或者因果关系,从而使得追踪整个请求的流程成为可能。


Trace定义了Span间两种基本关系:

  1. ChildOf:表示父 Span 在一定程度上依赖子 Span。
  2. FollowsFrom:表示父 Span 完全不依赖其子Span 的结果。


 链路追踪的挑战


  • 日志存储


本地存储埋点数据涉及磁盘操作,而磁盘I/O速度通常较慢。在高并发的环境中,如果采用同步刷新磁盘的方式,可能会对原有业务的性能造成显著影响。因此,在链路追踪系统中进行数据埋点时,应当尽量减少对系统的性能损耗,确保对原业务的逻辑和性能具有无侵入性。这意味着追踪系统的设计需要尽量轻量化,并采取策略如异步处理、缓存等方法来降低对业务性能的影响。


并发环形队列


集团的Eagleeye采用并发环形队列存储Trace数据,如下图所示:


环形队列确实是日志框架中实现异步日志写入的一种常用技术。在这种队列中,存在两个关键的指针,即读指针(take)和写指针(put)。

  1. 读指针(take): 它指向队列中的最旧数据,即下一条待消费的数据。
  2. 写指针(put): 它指向队列中下一个将要存放新数据的位置。


这个队列被设计为支持原子性的读写操作,以保证在多线程环境中数据的一致性。这意味着,当一个线程在执行读或写操作时,其他线程不能同时修改被操作的数据项。


读指针和写指针按同一方向(如时钟方向)移动,随着数据的不断生产和消费,这两个指针在队列中循环前进。然而,如果生产数据的速度持续超过消费速度,写指针最终会“追上”读指针,这种情况被称为“套圈”。


当出现套圈现象时,队列将根据预设的策略来处理新产生的数据。常见的处理策略包括:

  1. 丢弃即将写入的新数据,以保护队列中已存在的数据不被覆盖。
  2. 覆盖队列中最旧的数据,这意味着最先入队的数据将被新数据取代,这种策略适用于对最新数据保持最高优先级的场景。


选择哪种策略取决于具体应用场景的需求和对数据丢失的容忍度。环形队列的这种设计使得它在处理高并发日志数据时非常高效,能够在保证性能的同时减少资源占用。


Skywalking在实现上有所区别,采用分区的QueueBuffer存储Trace数据,多个消费线程通过Driver平均分配到各个QueueBuffer上进行数据消费:


在高性能的应用场景中,队列的实现方式对于整体性能有显著的影响。QueueBuffer 通常有两种不同的实现方式:

  1. 基于 JDK 的阻塞队列:这种实现方式利用了 Java 标准库中的 java.util.concurrent 包下的阻塞队列,例如 ArrayBlockingQueue 或 LinkedBlockingQueue。这些队列内部处理了线程同步的逻辑,使得在多线程环境下生产者和消费者可以安全地进行并发操作。基于 JDK 的阻塞队列通常用于服务端,因为服务端可以承受相对较重的负载,并且更注重于稳定性和线程安全。
  2. 普通数组 + 原子下标:这种实现方式使用了一个普通的数组,并结合了原子操作的下标(如 AtomicInteger)用于记录读写的位置。这种队列不是阻塞的,因此,当队列满时,它需要显式地处理生产者的等待逻辑,当队列空时,需要处理消费者的等待逻辑。SkyWalking 的 Agent 端采用这种方式,因为它相对更轻量级,且在高吞吐量场景下提供了更高的性能。Agent 端的性能尤其重要,因为它直接运行在应用程序内部,性能的任何抖动都可能影响到应用程序本身的性能。


在普通数组 + 原子下标的实现中,有一些值得关注的点:

  1. 原子性操作:通过使用原子类(如 AtomicInteger),可以确保在并发场景下修改下标时的线程安全性,避免了锁的开销。
  2. 非阻塞算法:由于不使用阻塞队列,这种实现可以采用非阻塞算法来优化性能,尤其是在高并发的生产者-消费者模型中。
  3. 回绕逻辑:当数组达到其末端时,读写下标需要回绕到数组的开始,形成一个环状结构,这就是所谓的环形缓冲区。
  4. 饱和策略:需要定义当队列满时的行为,是等待、丢弃数据还是覆盖旧数据。
  5. 总的来说,不同的 QueueBuffer 实现方式适用于不同的使用场景


  • 日志传输



鹰眼(EagleEye)和SkyWalking采用了不同的数据采集及传输策略,每种方法都有其适用场景和相应的权衡。


鹰眼系统首先将数据存储到本地日志文件中,然后利用一个代理(agent)来收集这些日志并将其传输到服务端。这种基于日志加边车代理的方法使得数据采集可以在不直接干扰主应用逻辑的情况下进行,但这也意味着需要处理日志文件的管理和代理的维护。


相比之下,SkyWalking提供了gRPC和Kafka两种基于服务的数据传输方式,这使得数据可以更实时地传输到处理后端。gRPC是一种高性能的远程过程调用(RPC)框架,Kafka是一个分布式流处理平台,两者都能有效地处理大量的数据流。


从架构上看,SkyWalking采取了分离应用层埋点和中间件代码的策略,这在某种程度上实现了应用级别的透明性。用户不需要在应用代码中直接管理追踪逻辑,而是依赖中间件层的插件来自动完成数据的采集。然而,这种方式意味着当中间件需要升级时,相关的插件也可能需要更新,这可能在维护方面引入一些挑战。


鹰眼的埋点方法由专门的中间件团队来维护,对于上层应用来说同样是透明的,这意味着应用开发者不需要关心数据采集的具体实现细节。这种方法非常适合阿里巴巴集团内部的环境,因为它能够提供统一的标准和容易控制的集成点。


总的来说,这两种系统提供了不同的解决方案,各自的优点和挑战都值得在选择和实施追踪系统时予以考虑。


初探分布式链路追踪(下):https://developer.aliyun.com/article/1443499

相关实践学习
分布式链路追踪Skywalking
Skywalking是一个基于分布式跟踪的应用程序性能监控系统,用于从服务和云原生等基础设施中收集、分析、聚合以及可视化数据,提供了一种简便的方式来清晰地观测分布式系统,具有分布式追踪、性能指标分析、应用和服务依赖分析等功能。 分布式追踪系统发展很快,种类繁多,给我们带来很大的方便。但在数据采集过程中,有时需要侵入用户代码,并且不同系统的 API 并不兼容,这就导致了如果希望切换追踪系统,往往会带来较大改动。OpenTracing为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范。OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。Skywalking基于OpenTracing规范开发,具有性能好,支持多语言探针,无侵入性等优势,可以帮助我们准确快速的定位到线上故障和性能瓶颈。 在本套课程中,我们将全面的讲解Skywalking相关的知识。从APM系统、分布式调用链等基础概念的学习加深对Skywalking的理解,从0开始搭建一套完整的Skywalking环境,学会对各类应用进行监控,学习Skywalking常用插件。Skywalking原理章节中,将会对Skywalking使用的agent探针技术进行深度剖析,除此之外还会对OpenTracing规范作整体上的介绍。通过对本套课程的学习,不止能学会如何使用Skywalking,还将对其底层原理和分布式架构有更深的理解。本课程由黑马程序员提供。
目录
相关文章
|
28天前
|
存储 监控 数据可视化
链路追踪所需要了解的知识
【2月更文挑战第29天】链路追踪,或称调用链监控,用于记录跨服务的逻辑请求信息,协助开发者优化性能和定位问题。它捕获异常、错误和有价值的数据。
|
2月前
|
存储 监控 Cloud Native
初探分布式链路追踪(下)
初探分布式链路追踪(下)
42 2
|
3月前
|
运维 监控 算法
面向全栈可观测的分布式链路追踪
全栈可观测App提供了一套完整的分析工具,从数据统计分析能力到数据关联,再到具备智能化和自动化特性的相关工具,以解决人们在可观测性方面所遇到的问题。未来,我们将持续提供更加丰富和强大的分析工具来满足用户的需求。
61068 1
|
9月前
|
存储 消息中间件 NoSQL
浅谈分布式链路追踪之Jaeger
随着微服务生态的盛行,在基于不同的业务场景中,一个简单的请求往往可能会涉及到多个不同服务类型,此时,若某个服务所提供的业务出现异常,从而可能会导致整个业务处理链路中的问题跟踪、定位及其分析较为困难,服务之间的依赖梳理、组件排查就变得尤为复杂。
604 0
|
12月前
|
消息中间件 数据可视化 JavaScript
什么是链路追踪?分布式系统如何实现链路追踪?
什么是链路追踪?分布式系统如何实现链路追踪?
|
12月前
|
SQL 缓存 运维
使用篇丨链路追踪(Tracing)很简单:链路拓扑
使用篇丨链路追踪(Tracing)很简单:链路拓扑
31313 0
|
存储 监控 数据可视化
分布式链路追踪-常用技术选型
分布式链路追踪-常用技术选型
1378 0
分布式链路追踪-常用技术选型
|
SQL 监控 前端开发
SkyWalking-链路追踪
SkyWalking-链路追踪
450 0
链路追踪学习三:跨服务,跨进程追踪
链路追踪学习三:跨服务,跨进程追踪
147 0
链路追踪学习三:跨服务,跨进程追踪
|
存储 消息中间件 Ubuntu
链路追踪学习二:Jaeger
链路追踪学习二:Jaeger
443 0
链路追踪学习二:Jaeger