0%

微服务分布式链路追踪实现方案


什么是分布式追踪?

分布式追踪可以通过对微服务调用链的跟踪,构建一个从服务请求开始到各个微服务交互的全部调用过程的视图。用户可以从中了解到诸如应用调用的时延,网络调用(HTTP,RPC)的生命周期,系统的性能瓶颈等等信息。谷歌在2010年4月发表了一篇论文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,介绍了分布式追踪的概念。对于分布式追踪,主要有以下的几个概念:

  • 追踪 Trace:就是由分布的微服务协作所支撑的一个事务。一个追踪,包含为该事务提供服务的各个服务请求。
  • 跨度 Span:Span是事务中的一个工作流,一个Span包含了时间戳,日志和标签信息。Span之间包含父子关系,或者主从(Followup)关系。
  • 跨度上下文 Span Context:跨度上下文是支撑分布式追踪的关键,它可以在调用的服务之间传递,上下文的内容包括诸如:从一个服务传递到另一个服务的时间,追踪的ID,Span的ID还有其它需要从上游服务传递到下游服务的信息。

为什么要分布式追踪?

在越来越多的应用迁移到微服务架构之后,微服务之间的调用关系变的越来越复杂,客户端一次的请求往往经过大量的微服务相关调用,那么如何对微服务的调用关系进行有效的监控和有效的 debug 变为关键。分布式追踪体系,就是追踪服务请求是如何在各个分布的组件中进行处理的细节。通过分布式追踪系统能很好地定位如下请求的每条具体请求链路,从而轻易地实现请求链路追踪,每个模块的性能瓶颈定位与分析。

OpenTracing 标准

为了解决不同的分布式追踪系统 API 不兼容的问题,诞生了 OpenTracing 规范,OpenTracing 是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间。OpenTracing 的数据模型,主要有三个:

  • Trace:一次完整请求链路
  • Span:一次调用过程(需要有开始时间和结束时间)
  • SpanContext:Trace 的全局上下文信息, 如里面有 traceId

一次下单的完整请求完整就是一个 Trace, 对于每一个请求来说,必须要有一个全局标识ID来标识这一个请求,即 TraceId。完整请求中的每一次程序调用就称为一个 Span,每一次调用都要带上全局的 TraceId, 这样才可把全局 TraceId 与每个调用关联起来,TraceId 就是通过 SpanContext 传输的。一个 Trace 由一个或者多个 Span 构成。多个 Span 之间的关系通过 SpanID 和 ParentSpanID 来关联。举例说明:

trace-span

根据这些图表信息显然可以据此来画出调用链的可视化视图如下:

trace-span-example

OpenTracing 实现

目前,链路追踪组件有 Google的 Dapper,Twitter 的 Zipkin。Zipkin和Jaeger总体架构类似:触发系统发 traces 到 collector,后者记录数据并记录 traces 之间关联。同时 tracing 系统也提供网页界面供使用。

TraceId 生成方案

TraceId 要保证整个业务体系架构内保证全局唯一。比较常见的生成规则是:

服务器IP+时间戳

1
2
服务器IP + ID产生时间 + 自增序列 + 当前进程号
0a8fd147 1661674160182929 1003 56696
  • 前 8 位 0a8fd147 即产生 TraceId 的机器的 IP,这是一个十六进制的数字,每两位代表 IP 中的一段。
  • 中间 16 位 1661674160182929 是产生ID的时间戳纳秒
  • 接下来 4 位 1003 是自增序列,1000 涨到 9000,到达 9000 后回到 1000 再开始往上涨
  • 最后 5 位 56696 是进程号

保证唯一的核心是机器IP+时间戳+自增序列

snowflake 雪花算法

snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。具体实现:

https://github.com/twitter/snowflake

Golang 版本实现:https://github.com/bwmarrin/snowflake

雪花算法依赖机器时间,如果发生 时间回拨 会导致可能生成id重复。

时钟回拨是与硬件时钟和ntp服务相关的,硬件时钟可能会因为各种原因发生不准的情况,网络中提供了ntp服务来做时间校准,做校准的时候就会发生时钟的跳跃或者回拨的问题。

时间回拨解决思路:

  1. 关闭时钟同步,不现实,不能采取。
  2. 记录上一次生成id的时间,如果发现本次小于上一次说明回拨了。本次等待时间追上来了再生成新id,若时间回拨太多,该方案不可取,可以设置一个阀值,比如超过10s钟就主动下线,并邮件告警
  3. 记录最近一段时间的id的生成最大值,当回拨的时候,继续在原来的基础上继续生成,当超过最近的记录的最大值,就转移到另一个实例,并下线当前实例,邮件告警

TraceId 生成时机

客户端本地生成

客户端请求服务端的时候,客户端自己生成一个唯一的ID,生成规则可以根据:设备ID+时间戳+自增序列 等方式

优点:从客户端发起请求时就生成 traceId,打通客户端和服务端全链路追踪
缺点:很难保证客户端生成的ID不重复,在客户端请求重放、debug 等请求时,traceId 会重复

服务端网关生成

后端微服务一般都会经过微服务网关,主要用于流量控制、接口路由转发、接口校验和签名等。同样,生成 traceId 也可以通过网关来生成
优点:客户端不需要感知,由服务端生成返回给客户端
缺点:服务端需要保证 traceID 的生成唯一,这里可以使用上面 traceId 的方案

TraceId 传递方式

TraceId 生成之后,如果串联起来整个服务请求的调用链路?

当 TraceId 生成之后,如果分布式追踪系统使用的 OpenTracing 标准的话,那么 TraceId 会通过 SpanContext 传递给下游服务。传递的协议依赖具体的服务请求协议,例如,Grpc 框架来是通过 matadata 来传递服务间的数据信息。服务内部可以通过 Context 来传递 traceId 在不同的协程或线程处理。

如果没有使用标准的 OpenTracing 设计的话,简单的实现方式是,如果下游是 http 服务,可以设置在 header 头里,如果是 rpc 服务,可以设置在服务扩展信息头里。

TraceId 传递尽量做到业务逻辑无感知,不能每次修改逻辑代码去显示传递,通常的做法是:

  1. 在应用服务框架层面默认实现,但是不够灵活
  2. 设计请求拦截器和返回拦截器,请求拦截器里来做 traceId 的传递

OpenTelemetry

分布式追踪的实现方案主要有两套:OpenTracing vs OpenCensus,区别在于:

  • OpenTracing 支持的语言更多、相对对其他系统的耦合性要更低,但是只支持 tracing
  • OpenCensus 除了 Tracing 外,它还把 Metrics 也包括进来,这样也可以在 OpenCensus 上做基础的指标监控;还一点不同是 OpenCensus 并不是单纯的规范制定,他还把包括数据采集的 Agent、Collector 全部都做了

OpenTelemetry 的终态就是实现 Metrics、Tracing、Logging 的融合。

  • Tracing:提供了一个请求从接收到处理完毕整个生命周期的跟踪路径,通常请求都是在分布式的系统中处理,所以也叫做分布式链路追踪。
  • Metrics:提供量化的系统内/外部各个维度的指标,一般包括Counter、Gauge、Histogram等。
  • Logging:提供系统/进程最精细化的信息,例如某个关键变量、事件、访问记录等。

参考资料

TraceId和SpanId生成规则
OpenTelemetry 简析
OpenTelemetry: 经得起考验的工具
分布式 id 生成器