概述

本文翻译自 OpenTracing 官方语义规范,由于 OpenTracing 需要跨多种语言工作,因此在本文档中,会特意避免使用特定语言的概念。
而本文中假定所有的语言都有一种『接口』的概念,它表示了一组相关功能的封装。

版本策略

在 OpenTracing 中,使用 Major.Minor 的版本号来进行标识。
当规范进行不兼容的调整时,Major 版本号会发生变化;
当进行兼容的的升级调整时,Minor 版本号会发生变化,例如引入新的标准标签、日志字段或者SpanContext等类型。

OpenTracing 规范范围

本文档不关注于特定下游的Tracing或监控系统,而是专注于描述分布式系统中事务相关的语义,而对于事务相关的语义描述理论上不应该受到某些特定的处理数据的后端服务的影响。
因此,OpenTracing 规范和数据建模语义约定相比具体的分布式追踪系统而言有更加广的影响范围,也包含更多的数据。而具体到某一个分布式追踪系统而言,如果某些语义行为超过了当前系统需要包含的功能范围,那么可以直接简单的忽略对应OpenTracing中获取的相关数据即可。

OpenTracing 的数据模型

在 OpenTracing 中,Traces 数据实际上是由 Spans 数据来隐式定义的。
一个 Trace 可以视为一组 Spans 数据构成的有向无环图,其中,Span 之间的边可以被称为 References。
例如,在如下的一个 traces 中包含了 8个 span:

  1. Causal relationships between Spans in a single Trace
  2. [Span A] ←←←(the root span)
  3. |
  4. +------+------+
  5. | |
  6. [Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
  7. | |
  8. [Span D] +---+-------+
  9. | |
  10. [Span E] [Span F] >>> [Span G] >>> [Span H]
  11. (Span G `FollowsFrom` Span F)

有时,使用时间轴来进行可视化trace进行分析时,可以给我们更多帮助,如下所示:

  1. Temporal relationships between Spans in a single Trace
  2. ––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
  3. [Span A···················································]
  4. [Span B··············································]
  5. [Span D··········································]
  6. [Span C········································]
  7. [Span E·······] [Span F··] [Span G··] [Span H··]

每个 span 中会包含如下信息:

  • 操作的名称
  • 开始的时间戳
  • 完成的时间戳
  • 一组key:value格式的 Span Tags: 其中,key必须是string类型,value可以是string、bool或者数值类型。
  • 一组Span logs: 每条log其实都是一个key:value的键值对,且有一个时间戳标签。对于一个log项而言,key必须是string类型,value可以是任何类型。Ps: 在具体的后端实现中,可能不兼容任意类型。
  • SpanContext: 见下文介绍
  • Reference: 用于将当前的Span与一组其他的Spans来进行相关的关联。

其中,对于 SpanContext而言,包括如下信息:

  • 任何跨进程传递时依赖OpenTracing状态的信息,例如trace_id和span_id等。
  • Baggage Items: 键值对的格式,用于跨进程进行数据透传。

span之间的Reference

一个span可能会与若干个SpanContexts存在关联关系。
目前,OpenTracing支持定义了两种类型的Reference: ChildOf 和 FollowsFrom。这些Reference类型其实都是专门为子span和父span之间的关联关系进行建模的。未来,OpenTracing可能还会支持一些非因果关系的Spans Reference类型,例如同一个batch中的span、同一个Queue中的span等。

ChildOf Reference

对于ChildOf类型的Reference而言,父Span往往会依赖于子Span的相关能力,例如以下场景都属于ChildOf Reference:

  • RPC服务端的span应该是RPC客户端span的ChildOf;
  • SQL插入的span应该是ORM保存方法的span的ChildOf;
  • 许多并发执行的span应该都是一个单个span的ChildOf,其中,单个的span会汇总所有child的运行结果。

从时序上来看,ChildOf Reference 的 span 应该具备如下特点:

  1. [-Parent Span---------]
  2. [-Child Span----]
  3. [-Parent Span--------------]
  4. [-Child Span A----]
  5. [-Child Span B----]
  6. [-Child Span C----]
  7. [-Child Span D---------------]
  8. [-Child Span E----]

FollowsFrom Reference

除了Parent Span依赖Child Span的场景外,有时我们会认为Child Span在因果意义上是Follow Parent Span的。具体来说,FollowsFrom其实可以区分为很多个不同的场景,在未来的OpenTracing中,可能会对它们进行进一步的区分。
对于 FollowsFrom 的Reference,它们的时序图上可能的情况会比较多,例如:

  1. [-Parent Span-] [-Child Span-]
  2. [-Parent Span--]
  3. [-Child Span-]
  4. [-Parent Span-]
  5. [-Child Span-]

OpenTracing API

在 OpenTracing 中,有三个关键且相互关联的概念:Tracer、Span和SpanContext。
下面,我们会依次对这三个类型展开说明。
简单来说,每个动作其实都会对应的各自编程语言里的一个方法或者函数。
此外,对于『可选参数』而言,不同的编程语言中对于它其实也有不同的解读,我们不会展开进行说明。

Tracer

Tracer 接口的主要功能是创建 Spans 并实现如何跨进程完成相关数据的注入和解析。
正常情况下,一个 Tracer 接口应该包含如下函数:

开启一个新的Span

该函数需要接收如下必填参数:

  • 操作名称,它应该是一个人类语言可以理解的字符串,能够简明扼要的说明该Span需要完成的工作,例如RPC的方法名称、函数名称或者是一个子任务/阶段的名称。一般来说,操作名称是一个通用的字符串,而不是一个唯一ID,因此,get_user相比get_user/391123而言,就是一个更好的选择。

以及如下可选参数:

  • 若干个对SpanContext的引用,尽可能的可以包含Reference的类型,例如ChildOf或者FollowsFrom。
  • 开始时间戳,默认为当前时间。
  • 若干个Tag。

该函数需要返回一个已经处于Start状态的 Span 实例。

将SpanContext注入到carrier中

该函数需要接收如下必填参数:

  • 一个 SpanContext 实例;
  • 一个格式描述符,用于说明如何对 SpanContext 进行编码从而可以在 carrier 参数中传递。
  • 一个carrier对象,它的类型取决于格式描述符。Tracer 将会根据格式描述符对这个carrier中的SpanContext进行编码。

从carrier中读取SpanContext

  • 一个格式描述符,用于说明如何对 SpanContext 进行解码。
  • 一个carrier对象,它的类型取决于格式描述符。Tracer 将会根据格式描述符从这个carrier中解码得到SpanContext。

该函数需要返回适合在通过 Tracer 启动新 Span 时用作参考的一个 SpanContext 实例。

格式描述符说明

从上面介绍中我们已经可以看到,所有的SpanContext的注入和抽取操作其实都是依赖与一个格式描述符的,它设置了关联“载体”的类型以及 SpanContext 在该载体中的编码方式。
所有 Tracer 实现都必须支持以下所有格式:

  • TextMap: 任意字符串到字符串映射,键和值的字符集不受限制。
  • HTTP 标头:具有适用于 HTTP 标头的键和值的字符串到字符串映射,强烈建议 Tracer 实现使用有限的 HTTP 标头键空间并对信息进行转移编码。
  • 二进制数据:表示 SpanContext 的(单个)任意二进制 blob。

Span

需要注意的是,除了查询 Span 的 SpanContext 操作外,当一个 Span 已经完成后,不能调用以下的任意方法。

查询Span的SpanContext

不需要输入参数。
返回给定Span的SpanContext对象,即使是span已经是finished的状态,该方法仍然可以调用。

重写操作的名称

必须参数:

  • 新的操作名称,可以用于取代span创建时传入的操作名称。

标记一个span完成

可选参数:

  • span完成的时间戳,如果没有参数,则使用当前的时间。

设置span的tag

必须参数:

  • tag的key,字符串
  • tag的值,可以是字符串,布尔值或数值类型。

Ps: OpenTracing 项目建议了一些具有规定语义含义的标准标签。

记录结构化log数据

必须参数:

  • 一个或多个键值对,其中键必须是字符串,值可以是任何类型。

可选参数:

  • 显式时间戳。如果指定,则它必须介于跨度的本地开始时间和结束时间之间。不指定的话,默认为当前时间。

Ps: OpenTracing 项目建议了一些具有规定语义含义的标准日志键。

logging的概念在工业场景中并广泛使用,甚至我们可以认为所有的tracing信息其实也都是以一种相对特殊的形式组成的logging。
而此处我们提到的log其实就是key:value的键值对描述的span阶段内的一些特殊时刻的信息。

虽然可以将通用日志重定向到 OpenTracing,但这样做需要小心。
例如,未局限在特定事务或trace中的日志记录语句在trace系统中可能没有意义。
也就是说,在绝大多数传统日志记录语句已经涉及分布式事务的环境中,将数据记录到 OpenTracing 中是合理的并且通常是有益的。

Span 日志的粒度旨在比流程级日志记录框架中典型的“信息”式日志记录更精细。由于跟踪系统通常具有智能的、全有或全无的每个跟踪采样机制,因此单个跟踪中的冗长可能比适合整个进程的详细程度更高——尤其是当该进程运行在高并发的场景时。

设置baggage项

Baggage items 是 key:value 字符串对,适用于给定的 Span、它的 SpanContext 和所有直接或可传递地引用本地 Span 的 Span。 也就是说,Baggage与Trace本身一起在整体调用链传播。

Baggage在 OpenTracing 集成的情况下具备强大的功能(例如,来自移动应用程序的任意应用程序数据可以使其透明地一直进入存储系统的底层)。但是Baggage的使用需要慎重。每个键和值都被复制到关联 Span 的每个本地和远程子节点中,这会增加大量网络和 CPU 开销。

必须参数:

  • baggage的key,字符串
  • baggage的value,字符串

获取baggage项

必须参数:

  • baggage的key,字符串

返回值:对应key的baggage存在时,返回对应的baggage值,否则返回不存在的提示。

SpanContext

SpanContext其实更像是一个概念,而不是OpenTracing中一个实际的通用功能。
不过,SpanContext仍然对OpenTracing而言是至关重要的,而且也提供了自己的API。
大多数 OpenTracing 用户仅在启动一个新的 Span 时或在向从某些传输协议注入/提取跟踪时通过Reference与 SpanContext 交互。

在 OpenTracing 中,我们强制 SpanContext 实例是不可变的,以避免围绕 Span 生命周期复杂化。

在对 SpanContext 中的 Baggage 项进行遍历时,不同语言的实现可能不一样,但是至少应该保证调用者可以在给定 SpanContext 的实例情况下能够一次性遍历所有的 Baggage 项。

NoopTracer

在所有的 OpenTracing 的实现中,还需要提供一种 NoopTracer 的API来用于标记控制OpenTracing或者注入一些测试用的信息等。

其他

此外,各个语言在OpenTracing的实现中,也可以提供一些通用的函数和方法来简化OpenTracing的使用成本,例如,opentracing-go 提供了一组函数用于设置和获取 Go 的 context.Context 机制中的Span信息。