tracing、 metrics、 log 构成了分布式系统可观测性的三大指标。
目前 tracing 领域最出名的项目无非是 CNCF opentracing。 其本质上是一套 tracing 设计的 API, 可以接入实现了这套 API 的 tracing 后端,如同为 CNCF 项目的 jaeger.

本篇文章分为四个部分,分别说明从 是什么,为什么,怎么用和最佳实践的角度进行介绍 opentracing 项目。
[toc]

opentracing 解决了什么问题

tracing / logging / metric 的关系

  • tracing 提供了一次 request 过程中所有服务调用的全局视野
  • logging 记录一些零散的、重要的日志信息
  • metrics 记录了一些可以聚合的数据信息,如服务调用时长,一段时间内的调用次数等

分布式 tracing 很重要。其相较于 logging 和 metric 的局部视野,tracing 提供了一个服务调用的全局视野。

但 tracing 是侵入代码的,各家都有独特的 tracing 工具, 但不同的工具的 埋点方式不同,切换需要大量修改代码。

open tracing 统一了 tracing 的 api, 使用这套 api 可以在多个 tracing 系统中进行切换。

支持 opentracing api 的 tracer 列表

opentracing 使用场景

核心概念

  • Span
    span

  • SpanContext
    span-context

  • Carrier (Inject / Extract)
    针对跨进程、跨主机的服务调用,直接传递 SpanContext 是不可能的,那么如何在这种情况下进行 tracing 呢?

    Inject / Extract 就是官方提供的一种方式,其可以将 Span 的必要信息注入到 TextMap/Http Header/Binary 中,以便传递到下游服务中。

    其本质上就是将 Span 的 id 组织成 string, 然后用 Header/Binary或者其他任何方式传递给下游服务中,由下游服务提取 Span 信息,并记录父子关系,最后发送给 tracer 服务。

如何使用

python 子进程使用 opentracing

  1. 在每个进程中都必须初始化自己的 tracer, 如果 tracer service name 相同,则发送到同一个 service 下
  2. 进程间 tracing 的关系传递需要传递 span context
  3. open tracing 只会自添加 process 信息(包括 hostname/ ip / language + version),支持多语言调用关系
  4. (ps)python 起子进程会出现问题 tracer init 会出现问题,需要

Go opentracing 在进程间 tracing

使用 opentracing 的 Inject / Extract 方法提取出 carrier, 然后将carrier 传递给下游服务。上下游服务代码如下所示:

上游服务代码

1
2
3
4
5
6
7
8
9
10
11
func GetCarrier(span opentracing.Span) string {
tracer := opentracing.GlobalTracer()
textMap := opentracing.TextMapCarrier{}

err := tracer.Inject(span.Context(), opentracing.TextMap, textMap)
if err != nil {
fmt.Println("error: ", err)
}
carrier := textMap[headerConfig.TraceContextHeaderName]
return carrier
}

下游服务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
func extractSCFromCarrier(carrier string) opentracing.SpanContext {
tracer := opentracing.GlobalTracer()
parentSpan, err := tracer.Extract(opentracing.TextMap, opentracing.TextMapCarrier{headerConfig.TraceContextHeaderName: carrier})
if err != nil {
fmt.Println("error: ", err)
}
return parentSpan
}

func CreateChildFromCarrier(operationName string, carrier string) opentracing.Span {
parentSC := extractSCFromCarrier(carrier)
return CreateChildFromSC(operationName, parentSC)
}

具体的实验代码,见 github go socket tracing.

一些最佳实践

在 Trace 的起始处,将 Trace ID 设置为 Request ID,这么一来就打通了日志系统和分布式追踪系统,可以使用同一个 ID 查询请求的事件流和日志流,从此开启了上帝视角。

  1. grpc server 类中可以使用 span id 作为 request id 串联整个系统的调用路径
  2. client 部分也可以添加 tracing 以使得各个调用路径完整展现
  3. tracing 涉及到保存信息 + 发送到 agent 或 tracing server,一定会减弱性能,但可以通过设置 采样率/运行时关闭等设置避免(具体性能消耗没有相关的实验环境,暂时没有进行)