需求背景
在一个分布式系统中,一个业务操作往往需要协调多个节点来完成。例如最简单的查询用户数据的操作也会涉及到应用服务,数据库服务,缓存服务三个节点。由于每个节点上记录和输出的信息若不具备统一标准,则很难准跟踪现业务操作在各节点上的执行情况。而操作数据跟踪在分布式系统,特别是微服务架构的分布式系统中尤为重要,它可以帮助业务维护人员:
- 还原生产环境的业务问题现场
- 找出性能瓶颈
- 监控业务健康度
- 识别网络问题,令分布式系统具备可观测性
- 输出结构化数据供大数据系统分析
提供跟踪服务的开发商可以有很多,为了令用户可以方便在不同的服务提供商间切换,特别是当前云原生成为大趋势的的情况下,能够降低对具体服务提供商的依赖,降低用户的切换成本和风险。OpenTracing在此背景下应运而生。
分布式会话跟踪的困境
在一个分布式系统中,实现完整的会话跟踪是具有相当挑战的,因为跟踪器必须在进程内和进程间传播跟踪上下文,这涉及到应用系统的每个部分,特别是如下这些:
- 自包含的开源软件服务(nginx,Cassandra,redis……)
- 引入开源软件包的自定义服务(grpc,ORM……)
- 基于上述开源软件包的各种应用胶水和业务逻辑
要求所有的开源软件和应用程序使用单一的跟踪服务提供商是不可能的,如果不过你共享相同的跟踪描述和传播机制,则业务调用链就会支离破碎。我们需要单一的标准机制来描述系统行为。
为什么使用OpenTracing
-
OpenTracing为解决上述困境提供了开发商独立的标准,其中包括
- 事务操作(Span)管理:提供编程API管理操作的生命周期(开始,结束),记录操作时间。
- 跨进程的上下文传播:提供编程API在进程间传播跟踪上下文。
- 活跃事务操作管理:提供API,在单一进程内存储和获取跨越软件包边界的活跃操作。
- 标准化的上下分和跟踪数据编码:提供精确的格式规范用于编码同构或异构系统间的跟踪数据。
-
OpenTracing是开发商独立的,且已有众多开源软件集成支持
- OpenTracing API本身提供了各种开发语言的版本(Go, JavaScript, Java, Python, Ruby, PHP, Objective-C, C++, C#)
- 第三方OpenTracing API贡献库中提供了各种开源软件的支持(nginx,Spring,Reactor,RxJava,Kafka……)支持相当广泛
OpenTracing语义规范
数据模型
在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。