基础篇丨链路追踪(Tracing)其实很简单

Posted 阿里云云原生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基础篇丨链路追踪(Tracing)其实很简单相关的知识,希望对你有一定的参考价值。

作者:涯海

一、分布式链路追踪的起源

当周末躺在被窝里,点外卖时;双 11 的零点,疯狂提交订单时;假期和基友激情开黑,五杀超神…在这个精彩纷呈的互联网世界里,这些应用背后又隐藏着什么?每一次点击行为在 IT 世界里会流经哪些节点,调用哪些服务,带来哪些变化?这一切庞杂且精密,超出了人力探索的边界,而分布式链路追踪就是追溯请求在 IT 系统间流转路径与状态的一门技术。接下来,让我们通过对分布式链路追踪的来了解这个 IT 世界!

说到分布式链路追踪,就绕不开分布式系统与微服务的兴起。早期 IT 系统非常简单,几乎所有程序都运行在同一个节点,互相之间也没有什么依赖。但随着硬件技术突飞猛进,硬件成本大幅下降,软件复杂度却越来越高。单一系统性能无法满足复杂的数据计算任务,而软件逻辑的复杂性也导致维护成本大幅上升。另外,单节点的可靠性也难以保障,不可避免的会偶尔出现宕机等行为,影响软件的可用性。 “性能、可维护性和可用性”这三大因素促使了分布式系统与微服务的诞生。

为了解决上述问题,人们很自然想到:既然一个硬件节点无法很好的运行软件,那能不能够通过多个节点来共同完成?并且为不同节点分派不同任务去提高效率。就好比通过不同角色分工协同的汽车生产流水线,分布式系统与微服务的理念亦是如此,如下图所示。

分布式系统与微服务自诞生就被广泛应用,主要得益于以下优势:

  • 扩展性

分布式系统天然具备“按需扩展”的能力,比如双 11 大促前通过添加机器实现快速水平扩容,大促结束后释放机器,充分利用云计算的分时复用能力,节约成本。利用微服务,还可以实现按需精准扩容,比如登录服务扩容 10 倍,下单服务扩容3倍,最大化的节省资源。

  • 可靠性

分布式系统可以有效抵抗“单点风险”,不会因为某一个节点的故障,影响整体的服务可用性。结合流量调度、离群实例摘除和弹性扩容等技术,甚至可以实现故障自愈。

  • 可维护性

分布式系统可维护性更强,一方面我们将一个复杂服务拆分成多个简单的微服务,每个微服务的逻辑都更加清晰、更易理解。就好比我们写代码,将一个几百行的复杂函数重构成若干个简单函数,代码可读性就会直线上升。另一方面,一些通用的微服务可以被高度复用,无需重复开发和维护,比如你在开发一个电商 APP,可以直接调用第三方提供的支付、物流等服务接口,整体开发和维护效率将大幅提升。

虽然分布式系统与微服务具有非常显著优势,但凡事有利必有弊,它们在有效解决原有问题基础上,也为系统开发和运维带来了新挑战,主要包括以下几点

  • 模糊性

随着系统的分布式程度越来越高,异常表象与根因之间的逻辑联系变得愈加模糊,问题诊断的难度急剧上升。比如 A、B 两个服务共享同一个数据库实例,当 A 服务在压测期间,大量占用数据库的服务端连接和计算资源,会导致 B 服务出现连接超时或响应变慢等问题。如果 B 服务是通过 C 服务间接依赖该数据库实例,问题的定位就会变得更加困难。

  • 不一致

虽然分布式应用从总体上变的更加可靠,但是每一个独立节点的状态却难以保证。导致这种不一致的原因有很多,比如前文提到的单机故障这种预期外的不一致,或者应用 Owner 执行分批发布或流量灰度时导致的预期内行为不一致。这种不一致性导致我们难以确定一个用户请求在系统内的准确执行路径与行为逻辑,可能引发不可预知的逻辑灾难。

  • 去中心化

当你的系统拥有上千个微服务镜像运行在数百台机器实例上,你该如何梳理它们之间的依赖关系,又该如何找到核心业务的关键执行路径?特别是在分布式的场景下,你没有一个中心化的节点(Master)来保存每个服务之间的依赖与调度状态,每个独立节点都在自行其是,无法分辨自己在整个系统中的位置,只能“盲人摸象、管中窥豹”,缺乏全局视图。

分布式系统与微服务带来的新挑战无疑让人头痛,但也带来了新技术的发展契机,科技的发展总是这样循环往复,螺旋式上升。它们带来的新问题,促使了分布式链路追踪的诞生,使你能够有效的观察全局,追踪流量。我们将在下个章节了解分布式链路追踪的诞生历程与核心理念。

二、分布式链路追踪的诞生

为了应对分布式环境下的不一致、模糊性等前文提到的各类问题问题,人们试图通过请求粒度的轨迹追踪与数据透传,实现节点间的确定性关联,分布式链路追踪技术也由此诞生。

里程碑事件:Google Dapper

分布式链路追踪诞生的标志性事件就是 Google Dapper 论文的发表。2010 年 4 月,Benjamin H. Sigelman 等人在 Google Technical Report 上发表了《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,揭开了分布式链路追踪的技术大幕,开启了一段全新技术浪潮。

Dapper 首先明确了分布式链路追踪的两个目标:任意部署和持续监测。进而给出了三个具体的设计准则:

  • 低开销:确保核心系统不会因为额外的性能开销拒绝使用。
  • 应用级透明:对应用开发透明,无需开发人员的协助,降低接入门槛,提高迭代效率。
  • 可扩展:在未来相当长一段时间内,随着业务的高速发展,仍然可以有效运转。

下面几张图展示了 Dapper 对链路透传、调用链结构和数据采集流程的描述,我们将在后续章节详细展开介绍,对 Dapper 感兴趣的同学建议直接阅读原作。

Dapper 论文有两个重要的意义,一是详细阐述了分布式链路追踪的设计理念,为后来的实现者提供了重要的理论指导;二是通过 Dapper 在 Google 生产环境的大规模落地实践,证明了分布式链路追踪技术的企业级价值,为分布式链路追踪的推广作出了不可磨灭的贡献。

基本原理

分布式链路追踪并不是无中生有、凭空诞生的新概念,而是轨迹追踪在 IT 领域的又一次成功运用。轨迹追踪理念早已被广泛应用于社会生活方方面面,比如物流订单追踪。一个快递包裹在发件站被赋予快递单号,沿途中转节点会记录该快递到达时间等信息,而用户通过快递单号就可以查询自己的包裹途径了哪些站点,耗时多久,是否存在滞留或丢件情况。下表对比了物流追踪与链路追踪的关联与差异性,以便大家理解。

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

分布式链路追踪实现请求回溯的关键点有两个:一是低成本、高质量的观察点设置,也就是链路插桩,确保我们追踪的信息足够丰富,能够快速定位异常根因;二是保证链路上下文在不同环境下都能够完整透传,避免出现上下文丢失导致的断链现象。关于链路插桩和上下文透传的具体内容我们将在实战篇进行详细介绍。下面,我们来看一个高速公路例子,进一步加深对链路追踪实现原理的认识。

一辆汽车飞驰在高速公路上

小明、小红、小玉计划在“五一”期间去自驾游,他们的旅游路线各不相同。如果我们想追踪他们的行程轨迹与时间该如何实现?

可能你会建议在每辆车上安装一个追踪器。确实,这是一种行之有效的方法。但当出行车辆扩展到全国数以十亿计的规模,安装追踪器成本就会很高。此时我们换个角度思考,高速公路的路线是固定的,每隔一段距离就会有一个收费站,如果我们在每个收费站上安装监控,记录车辆在每个收费站的轨迹与时间,就可以很经济的实现车辆轨迹与行驶时间的追踪。最终,我们得到了如下行程记录:

游客行程路线行驶距离行驶时间
小明北京 -> 石家庄 -> 郑州 -> 西安1140 公里13 小时 34 分钟
小红北京 -> 天津 -> 济南 -> 南京 -> 杭州1280 公里14 小时 33 分钟
小玉北京 -> 天津 -> 济南 -> 南京 -> 上海1234 公里13 小时 53 分钟

如果我们将每个游客替换为服务请求,收费站替换为服务接口,那我们就可以得到每次请求在分布式系统中的调用轨迹与状态,这就是分布式链路追踪的含义。

基础术语

虽然分布式链路追踪的实现方式多种多样,不同开源或商业化产品都有自己的数据模型和定义。但是仍然有一些基础术语在业界具备广泛的共识,以 OpenTracing 为例。

Trace

一条 Trace 代表一次入口请求在 IT 系统内的完整调用轨迹及其关联数据集合。其中,全局唯一的链路标识 TraceId,是最具代表的一个属性。通过 TraceId 我们才能将同一个请求分散在不同节点的链路数据准确的关联起来,实现请求粒度的“确定性关联”价值。这也是 Trace 区别于 Metrics、Log 其他两类可观测技术的关键属性。

Span

光有 TraceId 还不够,请求在每一跳的接口方法上执行了什么动作,耗时多久,执行状态是成功还是失败?承载这些信息的基础对象就是 Span。通常一个完整的 Span 具有如下属性:

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

Tags

SpanName 的描述通常是高度抽象的,仅仅回答这个接口是做什么的。如果需要进一步记录请求的行为特征,可以使用 Tags 来扩展语义。Tags 是一组由 Key:Value 组成的键值对集合,描述这一次接口调用的具体属性,比如将 UserType 添加到 Tags 中,就可以观察某一类用户(比如 VIP 用户)的链路行为状态。如果将设备类型加到 Tags 中,可以对比不同设备的性能差异。

由于 Tags 只支持结构化的 KV 键值对,因此,它可以作为标签添加到聚合后的链路指标中,有效提升监控告警的数据精度。更准确的回答异常或性能问题发生的原因,比如集中在某个地域、设备或版本。

Logs

Tags 会随着链路上下文自动向下游透传,如果希望记录一些不需要透传的事件信息,可以使用 Logs 字段。每个 Span 都可以进行多次 Logs 操作,但每个 Logs 对象都需要带有一个时间戳,Logs 的内容可以是非结构化的复杂对象。为了节省成本,一般不会对 Logs 字段建立索引,也不支持 Logs 的查询或统计,仅仅作为附加信息关联在调用链上,用于单请求诊断。

下图展示了一个 OpenTracing 的 Span 示例,不同开源实现的链路模型我们将在后续章节再展开介绍。

分布式链路追踪已经被广泛应用于中大型企业的 IT 运维领域,为分布式应用的性能诊断与稳定性保障提供了有效的帮助。此外,分布式链路追踪的开源和商业化生态也发展迅猛,大量独立服务商或云厂商纷纷跟进,共同推动了分布式链路追踪技术的崛起。

三、分布式链路追踪的应用

狭义上分布式链路追踪(Tracing)是指跟踪请求在分布式系统中的流转路径与状态,主要用途是协助开发运维人员进行故障诊断、容量预估、性能瓶颈分析与调用链路梳理等工作。技术实现上包含了数据埋点、采集、存储、分析、可视化等环节,形成了一套完整的技术体系。

而更广义的分布式链路追踪,则涵盖了由数据透传能力衍生的生态系统,比如全链路压测、微服务流量路由、业务场景链路拆分等。我们可以为调用链路赋予业务语义,也可以将一次调用生命周期内的所有数据进行关联整合,不再局限于链路数据本身。

由此可见,分布式链路追踪的应用场景广阔,潜力巨大,它的核心属性就是“关联”。然而,分布式链路追踪(Tracing)相对于统计指标(Metrics)和应用日志(Logging)来说更加难以理解,不容易运用,更难用好。接下来,我们通过一个生动形象的例子,了解下分布式链路追踪的经典用法,加深对它的技术本质的掌握。

游客、收费站和交通局

分布式链路追踪的用法有很多,但最经典、最常用的有三种,还是以上一节的高速公路为例,不同角色对应着不同的用法。

  • 游客,只关心自身的行程路线,需要途经哪些收费站点?行驶时间有多长?沿途是否有拥堵或危险路段等。
  • 收费站,只关心自身站点的状态,比如站点吞吐量、平均过闸时间等,以便于提前安排检票口值班人数。
  • 交通局,会将所有的出行记录汇总,提前估算整个高速公路网的出行流量、易拥堵路段、事故多发路段等,以便于提前疏通或加固问题路段,并给出合理的建议出行路线,有时还需要提前制定车辆限流策略等。

分布式链路追踪的应用和行程轨迹追踪类似,游客关心的是单次请求的轨迹回溯,收费站关注的是服务接口维度的汇总统计,旅游局则类似全局链路拓扑梳理。

单请求轨迹回溯

单请求轨迹回溯是分布式链路追踪最基础的功能,它记录了一次请求经过的所有服务节点以及对应的节点状态信息(接口名称、耗时、状态码等),这就好比记录了游客自驾游时经过的所有收费站,以及沿途的路况与行驶时间等信息。单请求轨迹回溯是诊断特定请求异常/超时原因的有效手段,可以快速定位异常节点(拥堵的收费站)。

比较成熟的 Tracing 产品(比如阿里云的应用实时监控服务 ARMS)除了基础的链路数据外,还会记录请求出入参、本地方法栈、关联 SQL 与异常堆栈等信息。这些细节信息就好比车辆的型号大小、驾驶员驾龄、是否醉酒、沿途每一路段的详细路况等,当调用不符合预期(行程异常)时,就可以精准的定位根因,如下图所示:

ARMS:

https://help.aliyun.com/document_detail/64995.html

服务监控

假如你是收费站的站长,你会关注哪些信息?收费站的车辆吞吐量?平均的过闸时间?车辆的来源与去向?同理,每一个服务节点,将途经的所有调用信息汇总后,就可以得到当前服务接口的吞吐量、耗时、来源与去向等统计指标。这些指标可以帮助我们快速识别当前服务的健康状态。在实际生产系统中,还可以与告警系统结合,实现风险的快速识别与处理,降低业务损失。

链路拓扑

假如你是交通局的局长,你可能会关注全国高速公路网的整体运行状态,有哪些易拥堵或事故多发路段与站点,如何确保春运高峰期核心路段运行通畅,不会出现重大交通瘫痪事件等等。此时,你需要对所有的车辆行程轨迹进行汇总分析。

同理,链路拓扑就是将全局或某一入口服务的所有调用链路进行汇总,聚合为链路拓扑大图,进而分析当前链路的性能瓶颈点、易故障点等,提前进行性能优化或风险防控,还可以根据历史流量来指导未来(比如双 11 大促)的容量评估。

分布式链路追踪的发展现状

截止到 2021年,分布式链路追踪(Tracing)已经成为主流软件架构不可或缺的基础技术之一,它与指标(Metrics)、日志(Logging)并称为可观测领域的“三驾马车”,它们三者之间的关系如下图所示:

随着 Kubenetes 容器技术与云计算的普及,未来的软件架构会更加趋向分布式云、微服务化的方向,软件开发、部署成本将大幅下降,但是系统维护和问题诊断的难度却急剧上升。因此,分布式链路追踪以及由它提供的“确定性关联”价值将愈加凸显,如下图所示:

Tracing 在开源社区也颇受喜爱,拥有着旺盛的生命力,主流的开源标准包括 OpenTelemetry、OpenTracing、OpenCensus 和国内使用较多的 SkyWalking。其他影响力较强的实现还包括 Jaeger、Zipkin、Pinpoint 等,如下图所示。

在商业化领域,Tracing 与 APM(Application Performance Mornitoring) 密切绑定,绝大部分厂商会面向应用视角提供更加全面、易用的 APM 服务,而不仅仅是 Tracing 服务。参考 2021 年 Gartner 评测机构给出的 APM 魔力象限,可以大致评估各大厂商的 APM 与 Tracing 产品能力,如下图所示。

截止 2021年,阿里巴巴 98% 的 Java 应用(上万级别)均已接入内部自研的分布式链路追踪系统 EagleEye;阿里云上有近万家企业在持续使用 ARMS 提供的分布式链路追踪服务。而从整个业界来看,无论是谷歌、亚马逊这样的国际大厂,还是新兴的互联网公司,或是传统企业,都在大规模接入和应用分布式链路追踪技术,Tracing 生态正在蓬勃发展。

四、分布式链路追踪的挑战与限制

作为一门“新”技术,分布式链路追踪的技术演进史并不算长,仅有十数年。目前,它仍处于不断被探索、快速迭代的周期。为了更好的了解与应用分布式链路追踪技术,我们来看下它目前面临的几项关键挑战与限制。

关键挑战与应对

分布式链路追踪技术从诞生到大规模应用,中间经历了一段较长的蛰伏期,直到近几年才逐渐被大家广泛接受和认可。影响其快速推广的关键挑战包括如下几点:

  • 前期建设成本高

无论是在不同组件接口上进行插桩埋点,还是保证链路上下文能够正确传播,亦或是搭建一套稳定可靠的链路数据后端处理系统,都不是一件易事,需要投入大量的研发人力。

  • 数据处理成本高

由于链路数据与请求流量成正比,每一次请求都会记录相应的链路日志,当系统流量爆炸式增长,相应的链路数据生成、采集、处理、存储、查询的成本也会急剧上升,带来巨大的 IT 资源开销。

  • 价值没有得到普遍认可

基础的链路数据仅仅表达了接口间的调用依赖,没有释放足够的业务价值,难以得到领导和同事们的全力支持。

  • 链路标准不统一

分布式链路追踪发展前期没有统一的业界标准,各家厂商百花齐放,虽然一定程度上促进 Tracing 技术的多元化探索,但也为链路融合、迁移和推广带来了巨大的挑战。

当然,挑战同样也是机遇,为了应对上述问题,分布式链路追踪近几年实现了如下技术突破:

  • 无侵入探针 + 一体化解决方案

类似 JavaAgent 的探针插桩技术,实现了 0 代码入侵,0 改造成本的链路自动埋点,而类似 SkyWalking 的开源实现还提供了端到端的一体化解决方案,从链路数据生成到最后的可视化,中小企业可以快速搭建并享受到分布式链路追踪技术的价值,大幅降低了 Tracing 的前期建设成本和接入门槛。

  • 链路采样 + 边缘计算

链路采样策略,例如固定比例采样、限流采样、错慢全采、自定义标签采样等,可以大幅降低链路数据的传输、处理、存储成本;结合用户网络内的指标聚合,长文本编码/压缩等边缘计算技术,可以合理控制分布式链路追踪的数据成本,保障链路系统持续健康运转。

  • 关联分析 + 立体化可观测

单条链路的价值难以凸显,但是基于成千上万条链路的聚合/关联分析却能快速定位,导致系统异常的关键因素,比如版本、地域、用户类型等。同时,结合业务、容器、基础设施等其他层面的可观测数据,建立一套端到端、立体化的可观测体系,能够更加有效地释放分布式链路追踪的技术价值。

  • 开源标准趋向统一

自从 2019 年 OpenTelemetry 开源立项,得到了两大主流开源实现 OpenTracing 和 OpenCensus 的大力支持,开启了可观测性的新时代。虽然,目前 OpenTelemetry 仅在 Tracing 领域拥有比较完善的技术标准,Metrics 和 Logging 仍在探索阶段,但是可观测性“三驾马车”融合一统的趋势已经势不可挡。未来基于统一完善的可观测数据标准,分布式链路追踪的“确定性关联”将得到更加广泛的应用。

现阶段能力限制

分布式链路追踪现有的模型设计与实现,可以有效满足许多经典场景的分布式诊断诉求。但是,仍然有大量场景超出了现阶段分布式链路追踪的能力范畴,需要我们去探索更好的方案。

树形 YES!图形 NO!

前文介绍了分布式链路追踪是通过 ParentSpanId 和 SpanId 来标识依赖关系,从而准确还原链路层级与顺序。但是,每个 Span 有且仅有一个 ParentSpanId,这就限制了所有链路形态只能是单个父节点的树形结构,而不能是多个父节点的图形结构。

某些系统为了提供重复调用的效率,会将多次 RPC 调用打包成一次 RPC 调用合并发送,这种入度大于1的图形结构,就无法通过调用链真实还原调用状态,而是会被拆成多条调用链,如下图所示:

人工插桩 YES!智能插桩 NO!

无论是 SDK 或是 Agent 模式,目前工业界的链路插桩主要是依赖人工发现插桩点并实现插桩过程,很难通过算法自适应的实现插桩点的智能发现。然而,学术界在这方面已经进行了一些有意思的探索,虽然在性能开销、安全等方面还不够成熟,但是值得关注。

2019 年波士顿大学发表了一篇研究智能插桩的文章,他们实现的 Pythia 原型系统针对性能退化问题,可以自动发现更有价值的内部插桩点。例如,我们在请求一个存储系统时,可能会直接命中缓存快速返回结果,也可能未命中缓存导致加载磁盘花费了较多时间。我们仅在 RPC 层面进行插桩,只能看到请求耗时高低起伏,呈现一种双峰式的分布,但无法确认根因是什么。Pythia 通过比对分析不同的链路数据,会自动发现影响性能的潜在插桩点,比如慢请求可能会额外调用一次 fetchFromDisk 方法,从而更清晰的解释影响请求耗时的根因,如下图所示。


分布式链路追踪的能力限制远不止以上两种场景,在离线分析、机器学习等多个领域也等待我们去探索攻克。我们既要充分发挥现有的分布式链路追踪技术价值,解决当下的企业运维困难;同时也要把视野放宽,在未来更多的领域中去拓展分布式链路追踪的边界。

五、预告

在完整介绍分布式链路追踪的前世今生以及基础概念之后,在接下来的章节我们将通过实际使用场景,详细介绍分布式链路追踪的基础用法,包括:

  • 请求轨迹回溯
  • 多维链路筛选
  • 链路实时分析、监控与告警
  • 链路拓扑

更多内容,敬请期待!

点击此处了解更多产品体验

使用篇丨链路追踪(Tracing)其实很简单:请求轨迹回溯与多维链路筛选

本章我们将以业务 Owner(小帅)的视角,逐步了解分布式链路追踪的各种基础用法:小到单次用户请求的异常根因诊断,大到全局系统的强弱依赖梳理,分布式链路追踪都能给予确定性答案。

作者:涯海

在日常生活中,我们可能都经历过以下场景:去医院看病就诊,但预约页面迟迟无法打开;新款手机发布日促销秒杀,下单页面一直卡住转菊花;游戏大版本更新,在线人数过多,导致人物一直在“漂移”。这些问题令产品体验变得非常差,有耐心的同学还会吐槽几句,没耐心的同学早已转身离开。试想一下,作为该系统开发/运维人员,又该如何避免此类问题发生,或者快速定位止损?

关键路径与多条链路对比

本章我们将以业务 Owner(小帅)的视角,逐步了解分布式链路追踪的各种基础用法:小到单次用户请求的异常根因诊断,大到全局系统的强弱依赖梳理,分布式链路追踪都能给予确定性答案。

小帅作为一家电商公司订单中心的业务 Owner,核心 KPI 就是保障创建订单 createOrder 接口的可用性,如响应时延低于 3s,成功率大于 99.9%。一旦该接口可用性出现问题,会直接影响用户下单行为,造成业务资损,进而影响小帅的绩效和年终奖。

但创建订单接口直接或间接依赖多个其他系统服务,如资金、地址、优惠、安全等。一旦某个下游系统服务可用性出现问题,也会造成创建订单失败或超时。为此,小帅特别头痛,每当创建订单接口不可用时,小帅都非常心急,却不知该如何定位根因,只能拉上所有下游接口负责人一起评估,不仅费时费力,低效排查也造成业务损失进一步扩大,经常被老板痛骂。

当小美了解这个情况后,推荐接入分布式链路追踪系统,并通过一系列故障应急案例,指导如何利用 Tracing 定位问题,梳理风险,提前预警,切实提高了订单中心的可用性。小帅经常会遇到各种用户反馈的创建订单超时问题,以往对此类问题颇有些束手无策。不过,接入分布式链路追踪系统后,通过调用链准确回溯超时请求的调用轨迹,小帅就可以轻松定位耗时最长的接口信息,如下图所示,A 接口超时的主要原因是调用 D 接口导致的。

但如果是下面这种情况,A 调用 B,B 又调用 C。那么,导致 A 接口超时的根因到底是 B 接口,还是 C 接口呢?

为了区分真正影响用户体验的 Span 耗时,我们先来了解一下关键路径的概念。

关键路径

如果一次 Span 调用有 t 段耗时在关键路径上,那么去掉这 t 段耗时,整条链路的总体耗时也会相应的缩短 t 段时间。 仍以上面那条链路为例,灰色部分表示关键路径,缩短任意关键路径上的耗时都可以减少整体耗时。此时,我们可以判断 A 接口超时的主要原因是 C 接口导致的。

再来看另一种情况,如果 A 接口同一时间并行调用 B、C、D、E 接口,那么耗时最长的 D 接口就成为关键路径,如下图所示。

但是,如果我们将 D 接口耗时减少 t1+t2 两段时间,整体耗时却只减少了 t1 段时间,因为,当 D 接口耗时小于 B 接口时,D 接口就不再是关键路径,而是由 B 接口取代。这就好像主要矛盾被大幅缓解后,次要矛盾就变成了主要矛盾。

综上所述,我们在做耗时性能分析时,应该首先识别出关键路径,然后再做针对性的优化。对于非关键路径上的耗时优化不会对最终的用户体验产生价值。

多条链路对比

单条调用链路只能用来分析各个接口的绝对耗时,而无法得知每个接口的耗时变化情况。但是,绝对耗时长不代表这个接口就一定有问题,比如数据存储接口耗时通常要比单纯的计算接口耗时要长,这种长耗时是合理的,无需特别关注。

因此,在诊断性能退化问题时,我们更应该关注相对耗时的变化。比如获取同一个接口在耗时异常时段与正常时段的多条链路进行比对,从而发现导致性能退化的原因。下图展示了 A 接口的两条不同链路,我们可以清楚的看到,虽然第一条链路的 B 接口耗时要比 C 接口耗时长,但是导致 A 接口整体耗时从 2.6s 涨到 3.6s 的原因,其实是 C 接口的相对耗时变长了 1s,而 B 接口的相对耗时几乎不变。因此,当 A 接口的响应时延超过 3s,不满足可用性要求时,我们应该优先分析 C 接口相对耗时增长的原因,而不是 B 接口。

我们再来看一个缓存未命中的例子,如下图所示。第一条链路调用了5次数据库,每一次调用的耗时都不算很长,但是 A 接口整体耗时却达到了 3.6s。当我们比对之前未超时的链路时,发现 A 接口并没有调用数据库,而是请求了5次缓存,整体耗时只有 1.8s。此时,我们可以判断 A 接口超时的原因是调用依赖行为发生了变化,原本应该请求缓存的调用变成了请求数据库,很可能是缓存被打满,或者是该次请求的参数命中了冷数据,最终导致了接口超时。

通过上面两个案例,我们认识到分析性能问题时,不仅需要知道绝对耗时的多少,更要关注相对耗时的变化。当然,有经验的同学如果对自身业务的正常链路形态了若指掌,就可以直接观察异常链路得出结论。

关联信息回溯

通过前面的学习,小帅已经成功掌握了调用链的轨迹回溯能力,可以熟练运用调用链分析性能瓶颈点,快速定位异常的接口。但是,他又遇到了新的困惑,就是找到了异常接口之后,下一步该怎么办?比如 C 接口的耗时从 0.1s 增长到了 2.1s,导致了上游的 A 接口超时。但是仅仅知道这个信息还不够,C 接口耗时增长背后的原因是什么?如何解决这个问题,让它恢复到原来的性能基线?

很多线上问题,很难只通过接口粒度的链路信息定位根因,需要结合更加丰富的关联数据,指导下一步的行动。接下来,我们通过几个案例,介绍几类最典型的链路关联数据,以及相应的用法。

本地方法栈

小帅负责的订单系统,每天上午十点都会有一波周期性的业务峰值流量,偶尔出现一些超时请求,但下游调用耗时都很短,无法判断超时的具体原因,导致这个问题一直悬而未决,为此小帅十分头痛,只好求助小美。正常请求与超时请求的调用链路对比如下图所示。

由于超时请求链路的相对耗时增长主要是 A 接口本身,因此,小美建议小帅启用慢调用方法栈自动剖析功能,自动抓取超时请求的完整本地方法栈,如下图所示。

通过本地方法栈,小帅得知超时请求是卡在 log4j 日志框架 callAppenders 方法上,原来 log4j 在高并发场景的同步输出会触发 “热锁”现象,小帅将 log4j 的日志输出由同步模式改为异步模式后,就解决了业务峰值超时的问题。

如果小帅使用的分布式链路追踪系统,并没有提供慢调用方法栈自动剖析功能,也可以通过 Arthas 等在线诊断工具手动抓取方法栈,定位到异常方法后,再考虑将其添加至本地方法插桩埋点中,进行常态化追踪。

自动关联数据

基于分布式链路追踪的框架拦截点,可以自动关联多种类型的数据,比如接口请求的出/入参数,调用过程中抛出的异常堆栈,数据库请求的执行 SQL 等等。此类信息不影响调用链的形态,却会极大的丰富链路的信息,更明确的阐述为什么会出现这样或那样状况的原因。

比如小帅接到上游业务方反馈,某个新渠道的商品下单总是超时,经过排查后发现该渠道订单依赖的数据库调用非常的慢,通过分析 SQL 明细才知道这个数据库调用是获取渠道优惠信息,但没有做渠道过滤,而是全量查询了所有优惠规则,优化 SQL 查询语句后超时问题就解决了。

自动关联数据通常由分布式链路追踪产品默认提供,用户根据自身的需要选择是否开启即可,无需额外的操作成本。一般情况下,SQL明细和异常堆栈关联建议常态化开启,而记录请求出/入参数需要消耗较大的系统开销,建议默认关闭,仅在需要的时候临时开启。

主动关联数据

小帅的老板希望能够定期分析来自不同渠道、不同品类、不同用户类型的订单情况,并且将订单接口异常排查的能力向一线运营小二开放赋能,提高用户支持效率。正在小帅一筹莫展之际,小美建议小帅将业务信息关联至调用链上,提供业务标签统计、业务日志轨迹排查等能力。

小帅听取了小美的建议后,首先将渠道、品类、用户类型等业务标签添加到分布式链路追踪的 Attributes 对象中,这样就可以分别统计不同标签的流量趋势,时延分布和错误率变化;其次,小帅将业务日志也关联到分布式链路追踪的 Event 对象中,这样就可以查看一次订单请求在不同系统中的业务轨迹与信息,即使是不懂技术的运营同学也能够清晰的判断问题原因,更有效的支持客户,如下图所示。

由于业务逻辑千变万化,无法穷举,所以业务数据需要用户主动进行关联,分布式链路追踪系统仅能简化关联过程,无法实现完全自动化。此外,自定义标签和业务日志是最常用的两种主动关联数据类型,可以有效地将调用链的确定性关联能力扩展至业务领域,解决业务问题。

综合分析

通过本小节的学习,相信大家已经非常熟悉分布式链路追踪的请求轨迹回溯能力,我们再来整体回顾一下:首先调用链提供了接口维度的轨迹追踪,而本地方法栈可以详细描述某个接口内部的代码执行情况,自动关联数据和主动关联数据在不改变链路形态的前提下,极大的丰富了链路信息,有效指导我们下一步的行动。在一些比较复杂的问题场景,需要结合以上信息进行多角度的综合判断,如下图所示。

上一小节我们介绍了如何通过调用链和关联信息进行问题诊断,但是,细心的读者可能会有一个疑问,整个系统有那么多的调用链,我怎么知道哪条链路才是真正描述我在排查的这个问题?如果找到了不相符的链路岂不是会南辕北辙?

没错!在使用调用链分析问题之前,还有一个很重要的步骤,就是从海量链路数据中,通过各种条件筛选出真实反应当前问题的调用链,这个动作就叫做链路筛选。那什么叫多维呢?多维是指通过 TraceId、链路特征或自定义标签等多种维度进行链路筛选。每一种筛选条件都是由日常开发/运维的场景演变而来,最为契合当下的使用方式,提高了链路筛选的效率和精准度。

多维度链路筛选

(一)基于链路标识 TraceId 的筛选

提到链路筛选,大家很自然的就会想到使用全局链路唯一标识 TraceId 进行过滤,这是最精准、最有效的一种办法。但是,TraceId 从哪里来?我该如何获取呢?

如何获取 TraceId?

虽然TraceId 贯穿于整个 IT 系统,只不过大部分时候,它只是默默配合上下文承担着链路传播的职责,没有显式的暴露出来。常见的 TraceId 获取方式有以下几种:

  • 前端请求 Header 或响应体 Response:大部分用户请求都是在端上设备发起的,因此 TraceId 生成的最佳地点也是在端上设备,通过请求 Header 透传给后端服务。因此,我们在通过浏览器开发者模式调试时,就可以获取当前测试请求 Header 中的 TraceId 进行筛选。如果端上设备没有接入分布式链路追踪埋点,也可以将后端服务生成的 TraceId 添加到 Response 响应体中返回给前端。这种方式非常适合前后端联调场景,可以快速找到每一次点击对应的 TraceId,进而分析行为背后的链路轨迹与状态。
  • 网关日志:网关是所有用户请求发往后端服务的代理中转站,可以视为后端服务的入口。在网关的 access.log 访问日志中添加 TraceId,可以帮助我们快速分析每一次异常访问的轨迹与原因。比如一个超时或错误请求,到底是网关自身的原因,还是后端某个服务的原因,可以通过调用链中每个 Span 的状态得到确定性的结论。
  • 应用日志:应用日志可以说是我们最熟悉的一种日志,我们会将各种业务或系统的行为、中间状态和结果,在开发编码的过程中顺手记录到应用日志中,使用起来非常方便。同时,它也是可读性最强的一类日志,即使是非开发运维人员也能大致理解应用日志所表达的含义。因此,我们可以将 TraceId 也记录到应用日志中进行关联,一旦出现某种业务异常,我们可以先通过当前应用的日志定位到报错信息,再通过关联的 TraceId 去追溯该应用上下游依赖的其他信息,最终定位到导致问题出现的根因节点。
  • 组件日志:在分布式系统中,大部分应用都会依赖一些外部组件,比如数据库、消息、配置中心等等。这些外部组件也会经常发生这样或那样的异常,最终影响应用服务的整体可用性。但是,外部组件通常是共用的,有专门的团队进行维护,不受应用 Owner 的控制。因此,一旦出现问题,也很难形成有效的排查回路。此时,我们可以将 TraceId 透传给外部组件,并要求他们在自己的组件日志中进行关联,同时开放组件日志查询权限。举个例子,我们可以通过 SQL Hint 传播链 TraceId,并将其记录到数据库服务端的 Binlog 中,一旦出现慢 SQL 就可以追溯数据库服务端的具体表现,比如一次请求记录数过多,查询语句没有建索引等等。

如何在日志中关联 TraceId?

既然 TraceId 关联有这么多的好处,那么我们如何在日志输出时添加 TraceId 呢?主要有两种方式:

  • 基于 SDK 手动埋点:链路透传的每个节点都可以获取当前调用生命周期内的上下文信息。最基础的关联方式就是通过 SDK 来手动获取 TraceId,将其作为参数添加至业务日志的输出中。
  • 基于日志模板自动埋点:如果一个存量应用有大量日志需要关联 TraceId,一行行的修改代码添加 TraceId 的改造成本属实有点高,也很难被执行下去。因此,比较成熟的 Tracing 实现框架会提供一种基于日志模板的自动埋点方式,无需修改业务代码就可以在业务日志中批量注入 TraceId,使用起来极为方便。
基于 SDK 手动实现日志与 TraceId 关联示例

以 Jaeger Java SDK 为例,手动埋点主要分为以下几步:

  1. 打开应用代码工程的 pom.xml 文件,添加对 Jaeger 客户端的依赖(正常情况下该依赖已经被添加,可以跳过)。
<dependency>    
    <groupId>io.jaegertracing</groupId>     
    <artifactId>jaeger-client</artifactId>     
    <version>0.31.0</version> 
</dependency>
  1. 在日志输出代码前面,先获取当前调用生命周期的 Span 对象,再从上下文中获取 TraceId 标识。
String traceId = GlobalTracer.get().activeSpan().context().toTraceId();
  1. 将 TraceId 添加到业务日志中一并输出。
log.error("fail to create order, traceId: ", traceId);
  1. 最终的日志效果如下所示,这样我们就可以根据业务关键词先过滤日志,再通过关联的 TraceId 查询上下游全链路轨迹的信息。
fail to create order, traceId: ee14662c52387763
基于日志模板实现日志与 TraceId 自动关联示例

基于 SDK 手动埋点需要一行行的修改代码,无疑是非常繁琐的,如果需要在日志中批量添加 TraceId,可以采用日志模板注入的方式。

目前大部分的日志框架都支持 Slf4j 日志门面,它提供了一种 MDC(Mapped Dignostic Contexts)机制,可以在多线程场景下线程安全的实现用户自定义标签的动态注入。

MDC 的使用方法很简单,只需要两步。

第一步,我们先通过 MDC 的 put 方法将自定义标签添加到诊断上下文中:

@Test 
public void testMDC()      
  MDC.put("userName", "xiaoming");     
  MDC.put("traceId", GlobalTracer.get().activeSpan().context().toTraceId());     
  log.info("Just test the MDC!"); 

第二步,在日志配置文件的 Pattern 描述中添加标签变量 %XuserName 和 %XtraceId。

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">     
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">         
        <level>INFO</level>     
    </filter>     
    <encoder>         
        <pattern>%dHH:mm:ss [%thread] %-5level [userName=%XuserName] [traceId=%XtraceId] %msg%n</pattern>         
        <charset>utf-8</charset>     
    </encoder> 
</appender>

这样,我们就完成了 MDC 变量注入的过程,最终日志输出效果如下所示:

15:17:47 [http-nio-80-exec-1] INFO [userName=xiaoming] [traceId=ee14662c52387763] Just test the MDC!

看到这里,细心的读者可能会疑问,MDC 注入不是也需要修改代码么?答案是确实需要,不过好在 Tracing 框架已经提供了简易的关联方式,无需逐行修改日志代码,极大的减少了改造量。比如 Jaeger SDK 提供了 MDCScopeManager 对象,只需要在创建 Tracer 对象时顺便关联上 MDCScopeManager 就可以实现 traceId、spanId 和 sampled 自动注入到 MDC 上下文中,如下所示:

MDCScopeManager scopeManager = new MDCScopeManager.Builder().build(); 
JaegerTracer tracer = new JaegerTracer.Builder("serviceName").withScopeManager(scopeManager).build();

通过 MDC 机制,有效推动了实际生产环境中应用日志与 Trace 链路的关联,你也快动手试试吧。

日志关联 TraceId 的限制有哪些?

并不是所有日志都能够与 TraceId 进行关联,最根本的原因就是在日志输出的时机找不到相对应的链路上下文,这是怎么回事呢?

原来,链路上下文仅在调用周期内才存在,一旦调用结束,或者尚未开始,又或者由于异步线程切换导致上下文丢失等场景,都会无法获取链路上下文,也就无法与日志进行关联了。比如,在应用启动阶段,许多对象的初始化动作都不在请求处理主逻辑中,强行关联 TraceId 只会获取到一个空值。

所以,在实际应用中,如果发现无法在应用日志中输出 TraceId,可以逐一检查以下几点:

  1. 确认类似 MDCScopeManager 初始化的变量注入工作是否完成?
  2. 确认日志模板中是否添加 %XtraceId 变量?
  3. 确认当前日志是否在某个调用的生命周期内部,且确保链路上下文不会因为异步线程切换导致丢失。

综上所述,我们可以在系统报错时,快速找到关联的 TraceId,再进行整条链路的轨迹信息回溯,最终定位根因解决问题。但是,如果我们由于各种限制还没有完成 TraceId 的关联,那么该怎么办呢?接下来我们来介绍两种不需要 TraceId 的筛选方法。

(二)基于链路特征的筛选

链路特征是指调用链本身所具备的一些基础信息,比如接口名称,请求状态,响应耗时,节点IP、所属应用等等。这些基础信息被广泛应用于各类监控、告警系统。一旦应用出现异常,会根据统计数据先判断出大致的问题影响面,比如在哪个应用,哪个接口,是变慢了还是错误率升高了?

然后,再根据这些基础信息组合筛选出满足条件的调用链路,例如:

serviceName=order AND spanName=createOrder AND duration>5s

这样,我们就可以过滤出应用名称为 order,接口名称为 createOrder,请求耗时大于 5秒的一组调用链路,再结合上一小节学习的单链路或多链路轨迹回溯分析,就可以轻松定位问题根因。

(三)基于自定义标签的筛选

在排查某些业务问题时,链路特征无法实现调用链的精准筛选。比如下单接口的来源渠道可以细分为线上门店、线下批发、线下零售、直播渠道、三方推广等等。如果我们需要准确分析某个新渠道的链路问题,需要结合自定义标签来筛选。

小帅所在的公司新拓展了线下零售模式,作为集团战略,需要重点保障线下零售渠道的订单接口可用性。因此,小帅在下单接口的链路上下文中添加了渠道(channel)标签,如下所示:

@GetMapping("/createOrder") 
public ApiResponse createOrder(@RequestParam("orderId") String orderId, @RequestParam("channel") String channel)  
... 
// 在链路上下文中添加渠道标签 
GlobalTracer.get().activeSpan().setTag("channel", channel); 
... 

每当线下零售同学反馈订单接口异常时,小帅就可以根据 channel 标签精准过滤出满足条件的调用链路,快速定位异常根因,如下所示:

serviceName=order AND spanName=createOrder AND duration>5s AND attributes.channel=offline_retail

(四)一个典型的链路诊断示例

本小节我们介绍了三种不同的链路筛选方式,结合上一小节的请求轨迹回溯,我们来看一个典型的链路筛选与诊断过程,主要分为以下几步:

  1. 根据 TraceId、应用名、接口名、耗时、状态码、自定义标签等任意条件组合过滤出目标调用链。
  2. 从满足过滤条件的调用链列表中选中一条链路查询详情。
  3. 结合请求调用轨迹,本地方法栈,主动/自动关联数据(如SQL、业务日志)综合分析调用链。
  4. 如果上述信息仍无法定位根因,需要结合内存快照、Arthas 在线诊断等工具进行二次分析。

预告

在完整介绍分布式链路追踪的前世今生及基础概念之后,本文了解了请求轨迹回溯、多维链路筛选场景,接下来的章节我们将继续介绍:

  • 链路实时分析、监控与告警
  • 链路拓扑

更多内容,敬请期待!

以上是关于基础篇丨链路追踪(Tracing)其实很简单的主要内容,如果未能解决你的问题,请参考以下文章

使用篇丨链路追踪(Tracing)其实很简单:请求轨迹回溯与多维链路筛选

链路追踪(Tracing)的前世今生(上)

链路追踪(Tracing)的前世今生(上)

阿里云产品专家解读链路追踪(Tracing Analysis)

关于链路追踪所需要了解的知识

RAY TRACING THE REST OF YOUR LIFE 超详解 光线追踪 3-1