目录
干货分享,感谢您的阅读!
在当今的互联网时代,网络应用已经成为我们生活中不可或缺的一部分。从在线购物到社交媒体,再到银行交易和医疗服务,这些应用无不依赖于庞大而复杂的分布式系统。然而,当我们遇到网页加载缓慢或功能异常时,我们可能会感到沮丧,却很少去思考背后究竟发生了什么。
通过分布式链路追踪,我们不仅可以了解各个服务之间的调用关系,还可以获取每个请求在各个服务中的详细时间信息。接下来,我们将聊聊分布式链路追踪,揭开这项技术的神秘面纱,探索它如何在复杂的分布式系统中发挥至关重要的作用,确保我们的网络应用始终高效、可靠地为用户服务。
一、什么是链路追踪?
随着分布式系统变得日益复杂,越来越多的组件开始走向分布式化,例如微服务、分布式数据库和分布式缓存等,使得后台服务构成了一个庞大而复杂的网络。在提高服务能力的同时,这种复杂的网络结构也让问题定位变得更加困难。当一个请求在多个服务之间穿梭时,任何一个服务的调用失败都会引发问题。此时,查找具体的异常来源变得十分抓狂,问题定位和处理的效率也会非常低。
分布式链路追踪正是为了解决这个问题而生。它就像一位侦探,能够将一次分布式请求的全过程还原成一条详细的调用链路。通过分布式链路追踪,我们可以集中展示一次请求在各个服务节点上的详细情况,比如每个节点的响应时间、请求具体到达的机器,以及每个节点的请求状态等信息。
想象一下,分布式链路追踪就像是在复杂的公路系统中安装了GPS。每辆车的行驶路径、停靠站点和行驶时间都清晰可见。当某一段路出现拥堵时,GPS能迅速指示问题所在,帮助我们快速采取措施,恢复道路畅通。同样,分布式链路追踪让我们在面对分布式系统中的故障时,能够迅速定位问题所在,从而提高问题处理的效率,确保系统的稳定运行。
二、核心思想Dapper
目前业界的链路追踪系统,如Twitter的OpenZipkin、Uber的Jaegertracing、阿里的链路监控-鹰眼、蚂蚁金服的SOFATracer、美团的Mtrace等,基本上都受到了Google发表的Dapper的启发(针对Dapper的讲解见:大规模分布式系统跟踪基础设施Dapper)。
Dapper是Google提出的一套分布式链路追踪系统,它详细阐述了在分布式系统,特别是微服务架构中,如何实现链路追踪的概念、数据表示、埋点、传递、收集、存储与展示等技术细节。
(一)Dapper链路追踪基本概念概要
Dapper的核心思想是通过对分布式系统中各个服务的调用情况进行追踪和记录,生成一条完整的调用链路,从而帮助开发和运维人员快速定位和解决系统中的问题。它不仅定义了一套标准的数据模型来表示链路追踪信息,还提出了一系列的方法和工具,用于在分布式环境中收集和展示这些数据。
具体来说,Dapper在以下几个方面对链路追踪进行了详细的说明:
-
数据表示:Dapper定义了一套标准的数据格式,用于表示一次请求在分布式系统中经过的所有节点和调用关系。这些数据通常包括Trace ID、Span ID、Parent ID、开始时间、结束时间等。
-
埋点:Dapper提出了一种无侵入式的埋点方法,通过在应用代码中插入少量代码,即可实现对分布式调用的监控和记录。
-
数据传递:在分布式系统中,调用链路的上下游服务之间需要传递追踪信息。Dapper定义了一套传递机制,使得追踪信息可以在不同的服务之间传递和共享。
-
数据收集:Dapper设计了一套高效的数据收集机制,能够从分布式系统中的各个服务节点收集追踪数据,并将其集中存储起来。
-
数据存储与展示:Dapper使用分布式存储系统来存储追踪数据,并提供了一套查询和展示工具,使得开发和运维人员可以方便地查看和分析调用链路。
Dapper的这些创新和实践,为后续的链路追踪系统提供了宝贵的经验和参考,极大地推动了分布式链路追踪技术的发展,详细的介绍和说明见 大规模分布式系统跟踪基础设施Dapper。
(二)Trace、Span、Annotations
为了实现链路追踪,Dapper提出了三个核心概念:Trace、Span和Annotation。
Trace
Trace的含义比较直观,它表示一次请求经过后端所有服务的完整路径。可以将其想象成一棵树状的图形结构,展示了请求从开始到结束所经过的每一个节点。
每一条链路(即每一个Trace)都有一个全局唯一的Trace ID,用来标识这条链路。
Span
Span是Trace中的基本单元,表示一个具体的操作或调用。每个Span由一个唯一的Span ID标识,并且有一个Parent ID来表示它的父Span,从而形成父子关系。这种层级关系构建了Trace的树状结构。一个Span通常包含以下信息:
- Span ID:唯一标识该Span的ID。
- Parent ID:标识父Span的ID,如果没有父Span,则为空。
- 操作名称:例如,HTTP请求的URL或数据库查询的名称。
- 时间戳:操作开始和结束的时间,用于计算耗时。
- 服务节点信息:包括服务名称、IP地址和端口等。
Annotation
Annotation是对Span的补充,用于记录特定时间点的事件或状态信息。
Dapper中定义了四种关键的Annotation:
- cs(Client Send):客户端发送请求的时间。
- sr(Server Receive):服务端接收请求的时间。
- ss(Server Send):服务端发送响应的时间。
- cr(Client Receive):客户端接收响应的时间。
这些Annotation帮助我们详细了解一次调用的全过程,尤其是客户端和服务端之间的交互。除此之外,用户还可以自定义Annotation,用于记录特定的业务事件或状态。
案例说明
让我们看一个具体的例子,假设一个前端服务(frontend.request)调用了一个后端服务(backend.dosomething),并且backend服务又调用了一个辅助服务(helper.call)。
这个过程中会产生如下的Span(为了加深理解,暂时忽略backend.call):
- frontend.request:生成一个Trace ID和一个Span ID(称为root span),记录客户端发送请求(cs)和接收响应(cr)的时间。例如,Trace ID: trace1,Span ID: span1。
- backend.dosomething:生成一个新的Span ID,并记录服务端接收请求(sr)和发送响应(ss)的时间。这个Span的Parent ID是frontend.request的Span ID。例如,Trace ID: trace1,Span ID: span2,Parent ID: span1。
- helper.call:生成一个新的Span ID,记录辅助服务的开始和结束时间。这个Span的Parent ID是backend.dosomething的Span ID。例如,Trace ID: trace1,Span ID: span3,Parent ID: span2。
一个完整的Span由客户端和服务端两个部分的信息合并而成。按照时间顺序:
- 客户端(Client):发送请求,产生“Client Send”(cs)事件。
- 服务端(Server):接收请求,产生“Server Receive”(sr)事件。处理请求后发送响应,产生“Server Send”(ss)事件。
- 客户端(Client):接收响应,产生“Client Receive”(cr)事件。
通过合并这些信息,一个Span不仅展示了客户端和服务端之间的交互过程,还记录了整个请求的详细时间和状态。这样就可以通过这些Span和它们的父子关系,清晰地看到整个调用链路,并快速定位和解决系统中的问题。
(三)带内数据与带外数据
在链路追踪过程中,链路信息的还原依赖于两种数据:带内数据和带外数据。这两种数据相辅相成,共同构成了完整的链路追踪信息。
带外数据
带外数据是由各个节点产生的事件信息,例如Client Send(cs)、Server Send(ss)等。带外数据可以独立于链路的整体结构生成,并且需要集中上报到存储端进行分析和存储。这些数据通常包括:
- 时间戳:记录事件发生的具体时间。
- 事件类型:例如,cs(客户端发送)、sr(服务端接收)、ss(服务端发送)、cr(客户端接收)等。
- 节点信息:包括服务名称、IP地址和端口等。
带外数据提供了详细的事件信息,帮助分析和理解每个节点的具体操作和状态。
带内数据
带内数据包括Trace ID、Span ID和Parent ID,用来标识Trace、Span,以及Span在一个Trace中的位置。带内数据需要从链路的起点一直传递到终点,通过带内数据的传递,可以将一个链路的所有过程串联起来。带内数据通常包括:
- Trace ID:唯一标识一个请求的整个链路。
- Span ID:唯一标识一个操作或调用。
- Parent ID:标识当前Span的父Span。
带内数据确保了链路的连续性,使我们能够跟踪一次请求从起点到终点的完整路径。
数据的传递与集中
通过带内数据的传递,可以将一个链路的所有过程串联起来。例如,当一个前端服务(frontend.request)调用一个后端服务(backend.dosomething)时,前端服务会生成一个Trace ID和一个Span ID,并将这些带内数据传递给后端服务。后端服务在处理请求时,会生成新的Span ID和Parent ID(即前端服务的Span ID),并将这些信息传递给下一个服务,以此类推。
与此同时,各个节点会生成带外数据,并将这些数据上报到存储端。存储端通过带内数据,将所有的事件信息关联起来,从而还原出完整的调用链路。带外数据提供了详细的时间和事件信息,而带内数据提供了链路的结构和位置,两者结合使得链路追踪系统能够全面展示和分析分布式请求的过程和状态。
假设我们有一个前端服务调用后端服务,再调用数据库服务的场景:
- 前端服务(frontend.request):生成Trace ID:trace1,生成Span ID:span1,记录Client Send(cs)事件,将Trace ID和Span ID传递给后端服务
- 后端服务(backend.dosomething):,接收到Trace ID:trace1,接收到Parent ID:span1,生成新的Span ID:span2,记录Server Receive(sr)事件,将Trace ID、Span ID和Parent ID传递给数据库服务
- 数据库服务(database.query):,接收到Trace ID:trace1,接收到Parent ID:span2,生成新的Span ID:span3,记录Server Receive(sr)事件
各个服务将产生的带外数据上报到存储端,带内数据则通过链路传递,使得存储端能够将所有数据关联起来,形成完整的调用链路。通过带内数据和带外数据的结合,链路追踪系统能够准确、详细地记录和还原分布式系统中每一次请求的全过程。
(四)采样
在分布式系统中,每个请求都会生成一个完整的调用链路。若将每一个请求的链路数据都上报和存储,不仅会导致性能消耗,还会浪费大量存储资源。因此,Dapper等链路追踪系统通常不会上报所有的Span数据,而是使用采样的方式来控制数据量。
采样的目的
采样的主要目的是在确保能够发现性能瓶颈和问题的同时,减少系统的性能开销和存储资源消耗。通过对请求进行抽样,仅上报一部分请求的链路数据,可以达到以下目的:
采样率的调整
采样率是指在所有请求中选择进行链路追踪和上报的比例。例如,采样率为10%时,只有10%的请求会被上报其完整的链路数据。采样率的调整可以是静态的,也可以是自适应的。自适应采样通过实时监控系统的负载情况,动态调整采样率,以在高负载和低负载情况下都能保持合理的采样比例。
采样机制的实现
在Dapper和其他链路追踪系统(如Zipkin、Jaeger)中,采样机制通常由采集端(即各个服务节点)实现。每个服务节点在生成Span时,会根据设定的采样率决定是否上报该Span的数据。
例如,在Zipkin中,采样机制可以通过配置采样策略来实现:
- 固定采样率:设定一个固定的采样比例,对所有请求进行统一的采样。
- 自适应采样:根据实时负载情况,动态调整采样率。
- 基于请求类型的采样:根据请求的不同类型(如关键请求和普通请求)设定不同的采样率。
假设一个系统设定了10%的采样率:
- 前端服务(frontend.request)接收到请求,生成一个Trace ID,并随机决定是否上报该请求的链路数据。
- 若决定上报,则生成Span ID,并将采样决定传递给后端服务。
- 后端服务(backend.dosomething)接收到前端服务的请求,并继承前端服务的采样决定,继续生成和上报Span数据。
- 数据库服务(database.query)同样接收到后端服务的请求,继承采样决定,生成并上报Span数据。
通过这种机制,整个链路中的所有服务节点都会统一按照采样决定上报数据,确保采样的一致性。
采样是分布式链路追踪系统中的重要机制,通过控制上报的数据量,实现了性能优化和存储节约。在实际应用中,采样率的合理设定和调整,可以帮助系统在高效监控的同时,避免因大量数据上报导致的性能和存储问题。
(五)存储
分布式链路追踪系统中的 Span 数据在经过收集和上报之后,需要集中存储在一个地方,以便后续的查询和分析。Dapper选择了使用 Google 的 BigTable 数据仓库来存储这些数据。
为什么选择 BigTable
BigTable 是一种分布式存储系统,具有高可扩展性和高性能的特点,特别适合用于存储大规模的结构化数据。由于分布式链路追踪数据的特性,各种 Trace 的 Span 数量不尽相同,BigTable 的稀疏表格布局非常适合这一场景。
- 稀疏表格布局:每个 Trace 中的 Span 数量不同,BigTable 的设计允许这些 Span 数据分散存储,而不会造成存储空间的浪费。
- 按 TraceID 和 SpanID 存储:Span 数据按照 TraceID 和 SpanID 进行存储,使得查询和检索特定 Trace 及其相关的所有 Span 变得非常高效。
- 无状态收集程序:由于 Span 数据的分散存储方式,收集程序无需进行复杂的计算和状态维护,只需按照 TraceID 和 SpanID 插入数据即可。
存储结构
在 BigTable 中,Span 数据以稀疏表的形式存储,每行对应一个 Trace,行内的列则对应该 Trace 下的各个 Span。如下图所示:
这种存储结构具有以下优点:
- 高效查询:通过 TraceID,可以快速查询到一个 Trace 及其所有相关的 Span 数据。
- 灵活存储:不同 Trace 可以有不同数量的 Span,BigTable 的稀疏表格布局能很好地适应这种变化。
- 易于扩展:BigTable 的分布式架构使其可以轻松扩展以应对数据量的增加。
查询和分析
使用 BigTable 存储 Span 数据后,查询和分析变得非常方便。可以通过简单的行查询,快速获取一个 Trace 的完整调用链数据,从而进行性能分析、故障定位等操作。具体而言,查询一个 Trace 的步骤如下:
- 获取 TraceID:确定需要查询的 TraceID。
- 查询 TraceID 对应的所有 Span:在 BigTable 中,按照 TraceID 查询该 Trace 下的所有 Span。
- 数据解析:将查询到的 Span 数据按照时间顺序和层级关系进行解析,还原出完整的调用链。
通过这种方式,可以高效地进行链路数据的查询和分析,快速定位和解决分布式系统中的问题。
三、Twitter的Zipkin
Zipkin 最初由 Twitter 开发并开源,后来成为了一个 Apache 认可的开源项目。它提供了一种方式来收集、存储、可视化和查询服务之间的分布式追踪数据。Zipkin 的设计依旧受到了 Google 的 Dapper 论文启发,致力于解决微服务架构下的调用链路跟踪问题。可以理解Zipkin是Dapper的一种开源实现,也是业界做链路追踪系统的一个重要参考,其系统也可以即插即用。
(一)架构概述
Zipkin 的整体架构包括以下核心组件:
- Collector(收集器):负责接收来自各个微服务的跟踪数据,并将其存储到后端存储系统中。
- Storage(存储):用于持久化存储跟踪数据,支持多种后端存储系统,提供高效的数据查询和检索功能。适配底层的存储系统,zipkin提供默认的in-memory存储,并支持Mysql、Cassandra、ElasticSearch存储系统。
- Query(查询服务):提供查询接口和用户界面,用于用户查看和分析存储在 Zipkin 中的跟踪数据,接口的定义见zipkin-api。
- Web UI(用户界面):提供可视化的界面,让用户能够直观地查看和分析调用链路数据。
Zipkin 将 Collector、Storage、API、UI 打包为一个可执行的 JAR 包,用户可以直接下载并运行。
(二)Zipkin v2 版本的数据模型
Zipkin v2 版本的数据模型定义了跟踪数据的结构,主要包括 Span、Endpoint 和 Annotation 这三个消息类型。
Span 消息类型
Span 用于表示一次请求的跟踪信息,包括请求的起始时间、持续时间、节点信息、事件列表等。
- trace_id: 跟踪标识符,通常为 16 或 32 位的十六进制字符串,唯一标识整个请求链路。
- parent_id: 父 Span 的标识符,如果没有父 Span,则为空。
- id: 当前 Span 的标识符。
- kind: Span 的类型,可以是客户端、服务器、生产者或消费者等几种类型。
- name: Span 的名称,例如 RPC 调用的名称。
- timestamp: Span 的开始时间戳,以微秒为单位。
- duration: Span 的持续时间,即从客户端发送请求到服务器返回响应的时间,以微秒为单位。
- local_endpoint: 本地节点的信息,包括服务名称、IP 地址和端口号。
- remote_endpoint: 远端节点的信息,用于标识被调用服务的信息。
- annotations: 事件列表,每个事件包含一个时间戳和描述该事件的名称。
- tags: 用户自定义的键值对信息,例如
{“user-id”:”lidawn”}
。 - debug: 表示是否为调试模式,如果设置为 true,则会忽略采样率限制,强制上报该 Span。
- shared: 暂时未理解其具体含义。
Endpoint 消息类型
Endpoint 用于描述节点的基本信息。
- service_name: 节点所属的服务名称。
- ipv4: 节点的 IPv4 地址。
- ipv6: 节点的 IPv6 地址。
- port: 节点的端口号。
Annotation 消息类型
Annotation 用于表示事件的时间戳和事件的名称。
- timestamp: 事件发生的时间戳,以微秒为单位。
- value: 描述事件的名称。
这些消息类型构成了 Zipkin v2 版本的数据模型,用于存储和表示分布式系统中请求的跟踪信息。
(三)Zipkin带内数据和带外数据
在 Zipkin 中,带内数据和带外数据扮演着不同但互补的角色,对于全面的链路追踪和系统监控至关重要。
带内数据(b3-propagation)
带内数据主要指的是通过请求传递的关键信息,通常包括以下几个核心字段:
- TraceId: 标识一条完整请求链路的唯一 ID,通常是一个 16 或 32 位的十六进制字符串。
- SpanId: 表示当前 span 的 ID,用于唯一标识一个请求中的某个操作或调用。
- ParentSpanId: 表示当前 span 的父 span 的 ID,用于构建请求的调用关系树。
- Sampled: 标记当前请求是否被采样,决定是否上报这个 span 的数据。
带内数据通过请求头或者请求体中的特定字段传递,例如使用 HTTP 头部的自定义字段(如 X-B3-TraceId、X-B3-SpanId)或者在 RPC 框架中使用特定的上下文传递机制(如 gRPC 的 Context)。这些数据在请求传递过程中串联了整个分布式系统中各个服务节点的调用链路,使得整个请求的执行路径可以被还原和跟踪。
带内数据主要用于链路追踪和性能优化。通过分析带内数据,可以还原和可视化每个请求的完整调用链路,诊断性能瓶颈和请求异常。
带外数据
带外数据则是指每个服务节点独立产生的事件和度量数据,通常包括:
- 性能指标: 如响应时间、吞吐量、请求成功率、错误率等。
- 系统状态: 如 CPU 使用率、内存使用量、磁盘 I/O 等。
- 异常信息: 如超时事件、异常堆栈信息等。
这些数据通常由每个服务节点的监控代理或者自定义的度量代码生成,并通过日志系统、监控系统或者专门的度量收集器(如 Prometheus、StatsD 等)进行收集和存储。带外数据的收集和分析能够帮助监控系统的健康状况和性能表现,并在出现问题时提供实时的告警和诊断支持。
带外数据主要用于系统监控和运维。通过分析带外数据,可以监控系统的实时运行状态,预测和预防潜在的故障,并进行容量规划和资源优化。
(四)Zipkin 采样
在 Zipkin 中,采样是控制哪些分布式跟踪数据应该被收集和上报的重要机制。它通过设置 sampled
字段来决定每个 span 是否应该被记录和报告。
采样状态
Zipkin 的 sampled
字段有四种可能的状态:
-
Defer: 表示采样状态尚未确定。这种状态通常在下游服务收到请求时,根据自身的采样策略决定是否重新确定采样状态。
-
Deny: 表示该 span 不应该被采样和上报。如果一个 span 被标记为 Deny,它将不会生成或者传播给下游服务。
-
Accept: 表示该 span 需要被采样和上报。如果一个 span 被标记为 Accept,它将被记录和传递给下游服务。
-
Debug: 通常用于开发环境中,强制性地标记 span 为采样状态。无视真实的采样率,所有的 span 都会被标记为 Debug。
采样决策
采样决策在整个请求链路中是逐层传递和遵循的。通常情况下,根 span 的 sampled
字段由系统的全局采样率决定。例如,如果全局采样率设为 50%,则只有大约一半的根 span 会被标记为 Accept,其余的会被标记为 Deny。
对于非根 span,它们的 sampled
字段通常依赖于父 span 的 sampled
字段决定。如果父 span 被采样(Accept),则其子 span 通常也会被采样,除非在中间的某个节点上出现了特定的采样策略(如 Defer)。
实现和应用
在实际应用中,Zipkin 的客户端和中间件库通常会根据配置和运行环境自动处理采样逻辑。例如,在 HTTP 或 gRPC 请求中,可以通过传递特定的请求头(如 X-B3-Sampled
)来标记和传递 sampled
字段,以确保整个分布式系统中的一致性采样策略。
(五)数据埋点及上报过程
根据zipkin的span定义,模拟一个简单的调用过程,分析数据埋点和上报过程。
- server-1发起对server-2的调用,生成一个root_span, 生成trace_id,id,parent_id为空,并记录kind为CLIENT,name,timestamp,local_endpoint(server-1)信息,并将trace_id,id,parent_id,sampled信息传递给server-2。
- server-2收到server-1的请求,并收到trace_id,id,parent_id,sampled信息,生成一个相同的span,并记录kind为SERVER,name,timestamp,local_endpoint(server-2)信息。
- server-2发起对server-3的调用,生成一个新的span,该span为root_span的子span。 并记录kind为CLIENT,name,timestamp,local_endpoint(server-2)信息,并将trace_id,id,parent_id,sampled信息传递给server-3。
- server-3收到server-2的请求,并收到trace_id,id,parent_id,sampled信息,生成一个相同的span,并记录kind为SERVER,name,timestamp,local_endpoint(server-3)信息。
- server-3回复server-2的调用,记录duration,并上报span。
- server-2收到server-3的回复,记录duration,并上报span。
- server-2回复server-1的调用,记录duration,并上报span。
- server-1收到server-2的回复,记录duration,并上报span。
整个过程中上报4个临时的span,最终在zipkin中被合并和存储为两个span。
四、总结
分布式链路追踪技术通过跟踪和记录分布式系统中各个服务之间的调用关系和时间信息,为系统监控、故障排查和性能优化提供了强大的支持。在现代复杂的微服务架构中,每个请求往往会经过多个服务节点的处理,这使得问题定位和性能分析变得异常复杂。分布式链路追踪系统的出现,特别是Google的Dapper和其开源实现如Zipkin、Jaeger等,极大地简化了这一挑战。
关键概念如Trace、Span和Annotation定义了分布式链路追踪数据的基本模型,帮助开发人员和运维团队快速定位和解决问题。通过Trace ID和Span ID的唯一标识,以及Annotation的时间戳记录,我们能够清晰地了解每次请求的全过程。采样机制则有效控制了数据量,避免了对系统性能和存储资源的过度消耗。
存储方面,使用像Google的BigTable这样的高扩展性和性能的存储系统,能够有效地存储和查询大规模的跟踪数据。结合查询服务和用户界面,例如Zipkin提供的UI,用户可以轻松地查看和分析复杂的调用链路,从而优化系统性能和用户体验。
总之,分布式链路追踪技术在当今互联网时代的分布式系统中具有不可替代的重要性。它不仅提升了系统的可观察能力和稳定性,还为开发人员和运维团队提供了强大的工具和数据支持,使得我们能够更加高效地管理和优化复杂的分布式应用。
希望本文对分布式链路追踪技术有所启发,并能为您在实际应用中提供有价值的参考和指导。
参考文章和链接
OpenZipkin · A distributed tracing system
Jaeger: open source, distributed tracing platform
打造立体化监控体系的最佳实践_分布式应用服务_链路监控_鹰眼_业务监控_实时监控_分布式系统-阿里云
https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/36356.pdf