OpenTracing原理简介
January 7, 2022
最近在阅读微服务相关书籍时,提到了微服务相关的监控措施,其中分布式追踪就属于其中不可或缺的一部分,恰好之前也有计划在团队内引入,所以就花了两天时间稍微深入了解了一下相关概念和实现。动手实验了一遍jaeger提供All-In-One+HotROD示例,也大致了解了其基本原理和实现思路。时间关系就没有去学习jaeger之类的系统源码了,深入了解其存储方案之类的细节了,感觉必要性不大。
为什么需要Tracing
微服务在部署上线后,需要有一套完善的监控体系来监控服务的质量。可观察性就是更关注的是从系统自身出发,去展现系统的运行状况,为开发运营人员提供故障排查、维护及优化的相关信息。
可观察性目前主要包含以下三大支柱:
- 日志(Logging):Logging 主要记录一些离散的事件,应用往往通过将定义好格式的日志信息输出到文件,然后用日志收集程序收集起来用于分析和聚合。虽然可以用时间将所有日志点事件串联起来,但是却很难展示完整的调用关系路径;
- 度量(Metrics):Metric 往往是一些聚合的信息,相比 Logging 丧失了一些具体信息,但是占用的空间要比完整日志小的多,可以用于监控和报警,在这方面 Prometheus 已经基本上成为了事实上的标准;
- 分布式追踪(Tracing):Tracing 介于 Logging 和 Metric 之间,以请求的维度来串联服务间的调用关系并记录调用耗时,即保留了必要的信息,又将分散的日志事件通过 Span 串联,帮助我们更好的理解系统的行为、辅助调试和排查性能问题。
Tracing及OpenTracing
分布式追踪(Tracing)是一种用于分析和监视应用程序的方法,特别是那些使用微服务体系结构构建的应用程序;分布式追踪有助于查明故障发生的位置或导致性能低下的原因,开发人员可以使用分布式追踪来帮助调试和优化他们的代码,IT 和 DevOps 团队可以使用分布式追踪来监视应用程序。
分布式追踪系统的核心步骤一般有三个:代码埋点,数据存储、查询展示。
OpenTracing 旨在标准化 Trace 数据结构和格式,其目的是:
- 不同语言开发的 Trace 客户端的互操作性。各语言开发的客户端,只要遵循 OpenTracing 规范,就都可以对接 OpenTracing 兼容的监控后端。
- Tracing 监控后端的互操作性。只要遵循 OpenTracing 规范,企业可以根据需要替换具体的 Tracing 监控后端产品,比如从 Zipkin 替换成Jaeger/CAT/Skywalking 等后端。
OpenTracing 数据模型
核心概念:
- Trace (调用链/链路):在广义上,一个 Trace 代表了一个事务或者流程在(分布式)系统中的执行过程。一个 Trace 是由多个 Span 组成的一个有向无环图(DAG),每一个 Span 代表 Trace 中被命名并计时的连续性的执行片段。
- Span (跨度):一个 Span 代表系统中具有开始时间和执行时长的逻辑运行单元,即应用中的一个逻辑操作。Span 之间通过嵌套或者顺序排列建立逻辑因果关系。一个 Span 可以被理解为一次方法调用,一个程序块的调用,或者一次 RPC / 数据库访问,只要是一个具有完整时间周期的程序访问,都可以被认为是一个 Span。
- Logs:每个 Span 可以进行多次 Logs 操作,每一次 Logs 操作,都需要一个带时间戳的时间名称,以及可选的任意大小的存储结构。
- Tags:每个Span可以有多个键值对(key:value)形式的 Tags,Tags 是没有时间戳的,支持简单的对 Span 进行注解和补充。
- SpanContext:SpanContext 更像是一个“概念”,而不是通用 OpenTracing 层的有用功能。在创建 Span、向传输协议 Inject(注入)和从传输协议 中Extract(提取)调用链信息时,SpanContext 发挥着重要作用
Traces
Trace 描述在分布式系统中的端到端事务,例如来自客户端的一个请求从接收到处理完成的过程。一个 Trace 可以被认为是由一个或多个 Span 组成的有向无环图(DAG)。
以下为从jaeger中摘录出的trace记录:
重点字段:
- Spans: 为整个Trace包括的Span数组,根据Span信息以及内部Reference形成完整DAG。
- process:为全链条涉及到的具体模块信息,在Span中会被引用。这个是否是标准的一部分?
Span
表示分布式调用链条中的一个调用,可以理解为一次方法调用或者一次 RPC / 数据库访问。只要是一个具有完整时间周期的程序访问,都可以被认为是一个 Span 。
一个 Span 一般会记录这个调用单元内部的一些信息,例如每个 Span 包含的操作名称、开始和结束时间、附加额外信息的 Span Tag、可用于记录 Span 内特殊事件 Span Log 、用于传递 Span 上下文的 SpanContext 和定义 Span 之间关系的 References 。
以下为从jaeger中摘录出的span记录:
重点字段:
- 操作名称 (An operation name): operationName
- 开始时间 (A start timestamp): startTime
- 时长:duration
- 标签信息 (Span Tag):零个或多个键值对(keys:values)组成的 Span Tags。
规范中也约定了一些通用tag,包括:
- HTTP Server Tags:作用于基于HTTP的服务入口的span,keys包括http.url、http.method、http.status_code、span.kind等
- Peer tags:这些tag可以被客户端或者服务端提供,用于描述远程请求过程中,请求调用的方向。(客户端记录下行访问,服务端记录上行访问)。keys包括peer.hostname, peer.port, peer.service等。
- 日志信息 (Span Log):零个或多个 Span Logs。每次 log 操作包含一个键值对和一个时间戳。键值对中,键必须为 string类型,值可以是任意类型。但并不是所有的支持 OpenTracing 的 Tracer 都需要支持所有的值类型。
- References:该Span引用的其它关联Span,主要有两种引用关系,Childof 和 FollowsFrom。
- Childof:最常用的一种引用关系,表示 Parent Span 和 Child Span 之间存在直接的依赖关系。例PRC 服务端 Span 和 RPC 客户端Span,或者数据库 SQL 插入 Span 和ORM Save 动作 Span 之间的关系。
- FollowsFrom:如果Parent Span 并不依赖 Child Span 的执行结果,则可以用FollowsFrom 表示。例如网上商店购物付款后会向用户发一个邮件通知,但无论邮件通知是否发送成功,都不影响付款成功的状态,这种情况则适用于用FollowsFrom 表示。
SpanContext
分布式追踪的上下文信息,包括 Trace id,Span id 以及其它需要传递到下游服务的内容。一个 OpenTracing 的实现需要将 SpanContext 通过某种序列化协议 (Wire Protocol) 在进程边界上进行传递,以将不同进程中的 Span 关联到同一个 Trace 上。对于 HTTP 请求来说,SpanContext 一般是采用 HTTP header 进行传递的。
SpanContext 是 OpenTracing 中一个让人比较迷惑的概念。在 OpenTracing 的概念模型 SpanContext 用于跨进程边界传递分布式调用的上下文,但实际上 OpenTracing 只定义一个 SpanContext 的抽象接口,该接口封装了分布式调用中一个 Span 的相关上下文内容,包括该 Span 所属的 Trace id,Span id 以及其它需要传递到下游服务的信息。
SpanContext 自身并不能实现跨进程的上下文传递,而是需要由 Tracer(Tracer 是一个遵循 OpenTracing 协议的实现,如 Jaeger,Skywalking 的 Tracer) 将 SpanContext 序列化后通过 Wire Protocol 传递到下一个进程中,然后在下一个进程将 SpanContext 反序列化,得到相关的上下文信息,以用于生成 Child Span。
为了为各种具体实现提供最大的灵活性,OpenTracing 只是提出了跨进程传递 SpanContext 的要求,并未规定将 SpanContext 进行序列化并在网络中传递的具体实现方式。各个不同的 Tracer 可以根据自己的情况使用不同的 Wire Protocol 来传递 SpanContext。
在基于 HTTP 协议的分布式调用中,通常会使用 HTTP Header 来传递, SpanContext 的内容。常见的 Wire Protocol 包含 Zipkin 使用的 b3 HTTP header,Jaeger 使用的 uber-trace-id HTTP Header,LightStep 使用的 "x-ot-span-context" HTTP Header 等。
将jaeger Hot R.O.D example中的demo稍作修改,打印出server端收到请求的header信息:
可以看到Uber-Trace-Id中的携带的traceID、spanid等信息。其中spanid包括主调用方(发起http 调用的服务)和被调用方(接收http调用的服务)两部分。
主调用方span:
被调用方span:
实现
Opentracing不包括具体实现。Jaeger 主要有以下几个组成部分:
- Jaeger Client:为不同语言实现符合 OpenTracing 的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序制定的采样策略传递给 jaeger-agent 。
- Agent:一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector 。它被设计成一个基础组件,部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。
- Collector:接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此用户可以运行任意数量的 Collector。
- Data Store:后端存储被设计成一个可插拔的组件,支持数据写入 cassandra , elastic search 等。
- Query:接收查询请求,从后端存储系统中检索 tarce 并通过 UI 进行展示。Query 是无状态的,可以启动多个实例并把它们部署在例如 nginx 这样的负载均衡器之后。
一些开源的分布式追踪系统对比:
名称 | 厂商 | 语言 | OpenTracing 兼容 | 侵入性 | 时效性 | 可视化 | 消耗 |
Jaeger | Uber | Go | 是 | 中 | 高 | 中 | 低 |
Zipkin | Java | 是 | 高 | 高 | 中 | 低 | |
Pinpoint | NAVER | Java | 否 | 低 | - | 高 | 低 |
CAT | 大众点评 | Java | 否 | 高 | 中 | 高 | 低 |
Appdash | sourcegraph | Go | 是 | 低 | 高 | 低 | 不支持大规模部署 |
SkyWalking | 华为 | Java | 是 | 低 | 中 | 高 | 低 |