数据模型
在OpenTracing中,会话(Trace)由一组具有引用关系的操作(Span)来表示。
- 会话(Trace):分布式系统中协调各节点进程完成的逻辑事务
- 操作(Span):需要消耗一定时间来完成的计算逻辑单元
一个会话可以视为一组操作的有向无循环图,操作间的边被称为引用(Reference)。例如下图表示由8个操作构成的会话:
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G `FollowsFrom` Span F)
也可以用时间轴来可视化表示会话,像下图这样表示一次会话中操作的连续时间关系:
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
每个操作具有如下的状态:
- 操作名称
- 开始时间
- 结束时间
- 由0或多个名值对组成的Tags,Key必须为字符串,Value可以是字符串,数值和布尔类型。
- 一个操作上下文(SpanContext)
- 操作间的触发关系引用,通过相互关联的操作上下文来表示。
每个操作上下文(SpanContext)封装如下状态:
- 跨进程中唯一操作引用的任何与OpenTracing具体实现无关的状态(例如会话和操作id),也就是说这些状态在整个系统中表示唯一一次操作。
- 行李(Baggage Items),跨进程传播的字符串名值对数据。
操作间的引用
一个操作可以可能引用0或多个其他的具有触发关系的操作上下文。OpenTracing现在定义了两种类型的引用:ChildOf和FollowsFrom。两种引用都表示子操作和父操作之间的触发关系。未来,OpenTracing可能也会支持非触发关系的引用类型(例如批量聚合在一起的操作,或者在同一个队列中的操作等等)。
ChildOf引用:一个操作可以是另一个操作的子操作,在一个ChildOf引用中,父操作在一定程度上依赖于子操作。一下场景符合ChildOf关系:
- 一次RPC调用的服务端操作是客户端操作的子操作
- 一个表示SQL插入的操作是一个ORM save方法的子操作
- 一个父操作可以有多个同步进行(可能是分布式)的父操作,该父操作会在执行期限内聚合所有子操作的结果返回给用户
上述场景用时序图表示如下:
[-Parent Span---------]
[-Child Span----]
[-Parent Span--------------]
[-Child Span A----]
[-Child Span B----]
[-Child Span C----]
[-Child Span D---------------]
[-Child Span E----]
FollowsFrom引用:有些父操作不以任何方式依赖它们的子操作的结果。在这种场景下,子操作仅仅由父操作触发。符合这种关系的操作可以进一步分成很多子类型,在未来的版本中OpenTracing可能会做进一步的规范区分。
用时序图表示如下:
[-Parent Span-] [-Child Span-]
[-Parent Span--]
[-Child Span-]
[-Parent Span-]
[-Child Span-]
OpenTracing API
在OpenTracing规范中,有三个关键的,相关关联的类型:Tracer,Span和SpanContext。接下来会详细说明每种类型的行为;简单来说,每种行为对应特定编程语言中的“方法(method)”,可能是一组相关的兄弟方法或重载方法。
在这里我们讨论的“可选(optional)”参数,在不同的编程语言中有不同的方式来表示该概念。例如在Go中我们可以使用“函数式选项(functional Options)”这种惯用方法,而在Java中可以通过构造器模式(builder pattern)实现。
Tracer
Tracer接口用于创建Span,并且知道如何在跨越进程边界时Inject(序列化)和Extract(反序列化)它们。Tracer具有如下功能:
- 开始一个新的操作(Span)
必选参数:
- 一个操作名称,一个人类可读的字符串,可以简洁地表示这个操作完成的工作(例如,RPC方法的名称,函数名,子任务名称或者一个复杂计算任务中的阶段名称)。操作名称应当是具有普遍意义的能够用于识别一类操作(Span)实例,而非特指某个操作。也就是说“get_user”比“get_user/314159”更好。
例如,用于获取帐户信息的操作,可以有如下的操作明名称:
| Operation Name | |
| --------------- | ------------------------------------------ |
| get | 太笼统,不能表达获取帐户信息 |
| get_account/792 | 太具体,表达了获取指定id=792的帐户信息 |
| get_account | 合适,可以用account_id=792作为Span tag |
可选参数:
- 0或多个相关SpanContext的引用(references),如果可能需要包含引用类型(ChildOf或FollowsFrom)
- 一个可选的明确的起始时间戳;如果忽略该参数,则默认使用当前系统时间。
- 0或多个标签(tags)
返回一个已经开始但尚未结束的Span对象实例。
- 将一个操作上下文(SpanContext)注入到载体(carrier)中
必选参数:
- 操作上下文(SpanContext)实例
- 格式描述符(format),通常是一个字符串常量,告知Tracer的实现如何将SpanContext实例编码到载体的参数中
- 载体(carrier),类型根据format的定义而不同。Tracer实现会将SpanContext按照format进行编码放入载体对象中。
- 从carrier中提取SpanContext
必选参数:
- format:告知Tracer实现如何从carrier参数中解码SpanContext
- carrier:该参数的类型根据format的定义而不同。Tracer实现会按照format将carrier对象解码为SpanContext
返回一个SpanContext实例,通过Tracer启动一个Span的时候可以作为上下文引用。
注意:format参数定义了关联的载体的类型,同时也定义了如何将SpanContext编码到载体中。OpenTracing的实现必须支持以下format的注入和提取行为。
- Text Map:载体是任意的字符串映射表(string-to-string map),key和value都不限制字符集。
- HTTP Headers:载体是字符串映射表,key和value使用的字符集必须能够用于HTTP headers(遵循RFC 7230)。实践中,由于存在非常多样的HTTP Headers,所以强烈建议Tracer实现使用受限的header key空间,并对value进行保守的转义。
- Binary:使用独立的二进制数据块表示SpanContext
Span
除了用于获取操作的上下文(SpanContext)的方法外,以下方法均不可在操作结束(Span is finished)后调用。
- 获取操作上下文(SpanContxt)
没有参数。
返回指定操作(Span)的上下文(SpanContext)。即使在操作结束后,返回值依然可用。
- 覆写操作名称
必选参数:新的操作名称,覆盖Span开始时被赋予的操作名。
- 结束Span
可选参数:明确指定的Span结束时间戳(finish timestamp);如果忽略该参数,则使用当前系统时间。
- 设置Span 标签(Tag)
必选参数:
- tag key:必须是字符串
- tag value:字符串,数值或布尔类型
注意OpenTracing项目文档化了一些“标准标签”,具有预定义的语义。
- 记录结构化数据
必选参数:
- 一个或多名值对,key必须是字符串,value可以是任意类型。不同的OpenTracing实现可以处理的数据值类型可能不同。
可选参数:
- 一个明确指定的时间戳,如果指定了,时间戳必须在Span开始和结束时间中间。
注意OpenTracing项目文档定义了一些“标识日志key”,具备预定义的语义。
- 设置行李(baggage item)
Baggage Item是操作负载的字符串名值对,其作用于Span,SpanContext以及所有的直接或间接引用的本地Span上。也就是说Baggage Items在整个会话中进行传播。
Baggage items是全栈集成OpenTracing的系统中的非常强大的功能,例如任意的应用数据可以从创建它的移动应用终端透明地传输到深入底层的存储系统中,但该特性的成本也非常高,需要谨慎使用。
深思熟虑后谨慎使用该特性。每个key-value对会被拷贝到所有关联的本地和远程Span中,这会给网络带宽和CPU带来很重的负担。
必须参数:
- baggage key:字符串
- baggage value:字符串
- 获取行李(baggage item)
必选参数:
- baggage key:字符串
返回对应的baggage value,或某种找不到值的指示。
SpanContext
在通用OpenTracing层中,SpanContext更多对一种概念,而非一些有用的功能。也就是说它对OpenTracing实现很重要,但是它自身并没有API表示。大多数的OpenTracing用户只会在创建新Span,或从与一些传输协议进行inject和extract操作时通过引与SpanContext进行交互。
在OpenTracing中我们强制SpanContext实例必须是不可变的(immutable)以避免围绕Span结束和引用的复杂声明周期问题。
- 遍历所有的baggage items
依赖于具体的编程语言,有不同的编程模型实现,但是从语义上来说,一旦用户得到了一个SpanContext实例,即可高效呃遍历所有的baggage items
NoopTracer
OpenTracing API的各种语言实现,必须提供某种NoopTracer实现,该实现不进行任何有意义的操作。可以用于测试或实现OpenTracing开启关闭的控制。NoopTracer可能在自己的包构件中(例如Java的OpenTracing API构件opentracing-noop)
可选的API元素
一些语言提供了在单个进程中传递活跃Span,SpanContext的功能。例如,opentracing-go提供了在Go的context.Context中设置和获取活跃Span的辅助工具。