分布式链路追踪 OpenTelemetry与Jaeger

引言

随着微服务架构的流行,服务之间的调用变得越来越复杂,如何追踪服务之间的调用链路成为了一个重要的问题。分布式链路追踪技术应运而生,它可以帮助开发者监控服务之间的调用链路,定位和解决请求在服务间流转时的问题。本文将介绍分布式链路追踪的发展历程,以及如何在Go-Kratos框架中集成OpenTelemetry和Jaeger实现链路追踪。

一、分布式链路追踪发展简介

1.1 分布式链路追踪介绍

分布式链路追踪技术是随着微服务架构的流行而发展起来的,它用于监控服务间的调用链路,帮助开发者定位和监控请求在服务间流转时的问题。OpenTelemetry作为该领域的集大成者,它整合了 OpenTracing 和 OpenCensus,为开发者提供了统一的追踪标准和SDK。

1.2 OpenTracing:统一的追踪标准

OpenTracing 制定了一套与平台无关、厂商无关的协议标准,使得开发人员能够方便的添加或更换底层APM的实现。

它是 CNCF 的项目。OpenTracing 协议的产品有 Jaeger、Zipkin 等等。

OpenTracing 数据模型

Trace(s): Trace(s) 在 OpenTracing 中是被 spans 隐式定义的。一个 trace 可以被认为是由一个或多个 span 组成的有向无环图。

比如,下图示例就表示一个 trace 由 8 个 span 组成,也就是一次链路追踪由 8 个 span 组成:

单个 trace(链路) 中 span 之间的关系

        [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)

用时间轴来可视化这次链路追踪图,更容易理解:

Temporal relationships between Spans in a single Trace


––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]
(来自:https://opentracing.io/specification/)

Span 是一次链路追踪里的基本组成元素,一个 Span 表示一个独立工作单元,比如一次 http 请求,一次函数调用等。

每个 span 里元素:

  • An operation name,服务/操作名称
  • A start timestamp,开始时间
  • A finish timestamp,结束时间
  • Span Tags,一组键值对构成的 Span 标签集合。键值对中,键必须为 string,值可以是字符串,布尔,或者数字类型
  • Span Logs,一组 span 的日志集合。每次 log 操作包含一个键值对,以及一个时间戳
  • References,对 0 或 更多个相关 span 的引用(通过 SpanContext 来引用),多个 span 中的对应关系。
  • SpanContext,携带跨进程(跨服务)通信的数据,比如 span_id, trace_id。
  • Baggage Items,为整条追踪链路保存跨进程(跨服务)的数据,数据形式是 key:value

OpenTracing 目前定义了 2 种关系:ChildOf 和 FollowsFrom:

ChildOf,一个子 span 可能是父 span 的 ChildOf

    [-Parent Span---------]
         [-Child Span----]

    [-Parent Span--------------]
         [-Child Span A----]
          [-Child Span B----]
        [-Child Span C----]
         [-Child Span D---------------]
         [-Child Span E----]

FollowsFrom,一些父 span 不依赖任何的子 span

    [-Parent Span-]  [-Child Span-]


    [-Parent Span--]
     [-Child Span-]


    [-Parent Span-]
                [-Child Span-]

(来自:https://opentracing.io/specification/)

1.3 OpenCensus:Google背书的追踪项目

OpenCensus由Google开发,旨在统一Go语言的Metrics采集和链路跟踪。随着项目的不断演进,它也开始涉足分布式追踪领域,与OpenTracing形成互补。

其实,刚开始它并不是要抢 OpenTracing 的饭碗,它只是为了把 Go 语言的 Metrics 采集、链路跟踪与 Go 语言自带的,profile 工具打通,统一用户的使用方式。但是随着项目发展,它也想把链路相关的统一一下。它不仅要做 Metrics 基础指标监控,还要做 OpenTracing 的老本行:分布式跟踪。

1.4 OpenTelemetry:追踪技术的集大成者

OpenTelemetry的诞生标志着OpenTracing和OpenCensus的完美结合。它不仅统一了追踪标准,还进一步拓展了Metrics和Logging的融合,为开发者提供了全面的监控解决方案。

OpenTelemetry 的核心工作目前主要集中在 3 个部分:

规范的制定和协议的统一,规范包含数据传输、API 的规范,协议的统一包含:HTTP W3C 的标准支持及GRPC等框架的协议标准 多语言 SDK 的实现和集成,用户可以使用 SDK 进行代码自动注入和手动埋点,同时对其他三方库(Log4j、LogBack等)进行集成支持; 数据收集系统的实现,当前是基于 OpenCensus Service 的收集系统,包括 Agent 和 Collector。 (来自: https://github.com/open-telemetry/docs-cn)

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

Tracing API 中几个重要概念:

TracerProvider:是 API 的入口点,提供了对 tracer 的访问。在代码里主要是创建一个 Tracer,一般是第三方分布式链路管理软件提供具体实现。默认是一个空的 TracerProvider(""),虽然也创建 Tracer,但是内部不会执行数据流传输逻辑。

Tracer:负责创建 span,一个 tracer 表示一次完整的追踪链路。tracer 由一个或多个 span 组成。跟上面的 OpenTracing 数据模型很像,所以说是两者合并。

Span:一次链路追踪操作里的基本操作元素。比如一次函数调用,一次 http 请求。

里面还有很多详细介绍:https://opentelemetry.io/docs/reference/specification/trace/api/

一条链路追踪信息:

有一条链路 trace,它是由一个或多个 span 组成, span 里会记录各种链路中的信息,跨进程的信息,各种 span 之间的关系。

使用哪种链路管理软件,则由 traceprovider 来设置。可以是 Jaeger,Pinpoint,Zipkin,Skywalking 等等。

span 中的信息收集到链路管理软件,然后可以用图来展示记录的链路信息和链路之间的关系。

二、jaeger: 分布式链路追踪的佼佼者

Jaeger 是受到 Dapper 和 OpenZipkin 启发,是 Uber 开发的一款分布式链路追踪系统。 它不仅提供了强大的追踪能力,还拥有直观的用户界面,使得微服务的监控变得简单而高效,用于监控微服务和排查微服务中出现的故障。

jaeger 安装:

docker run -d --name jaeger \
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 14250:14250 \
  -p 14268:14268 \
  -p 14269:14269 \
  -p 9411:9411 \
  jaegertracing/all-in-one:1.59

三、Go-kratos 中应用链路追踪

在Go-Kratos框架中,集成OpenTelemetry和Jaeger可以显著提升服务的监控能力。通过在服务端配置TraceProvider,并在GRPC和HTTP服务器中加入链路追踪中间件,可以确保服务调用的每一个环节都被有效追踪。

当前版本环境

Go 1.21.

Kratos v2.7.3

Jaeger v1.11.2

第一步,设置 TraceProvider()

// get trace provider
func tracerProvider(url string) (*tracesdk.TracerProvider, error) {
	// create the jaeger exporter
	exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		return nil, err
	}

	// New trace provider
	tp := tracesdk.NewTracerProvider(
		tracesdk.WithSampler(tracesdk.AlwaysSample()),
		// always be sure to batch in production
		tracesdk.WithBatcher(exp),
		// Record information about this application in an Resource.
		tracesdk.WithResource(
			resource.NewWithAttributes(
				semconv.SchemaURL,
				semconv.ServiceNameKey.String(Name), // service name,实例名称
				attribute.String("env", Env),        // environment
				attribute.String("ID", Version),     // version
			)),
	)
	return tp, nil
}

第二步,给grpc server添加链路追踪中间件(http server一样)

url := "http://jaeger:14268/api/traces"
if os.Getenv("jaeger_url") != "" {
    url = os.Getenv("jeager_url")
}

tp, err := tracerProvider(url) // tracer provider
if err != nil {
    log.Error(err)
}

s := &server{}

// grpc server
grpcSrv := grpc.NewServer(
    grpc.Address(":9000"),
    grpc.Middleware(
        middleware.Chain(
            recovery.Recovery(),
            tracing.Server(tracing.WithTracerProvider(tp)), //设置trace,传入 trace provider
            logging.Server(logger),
        ),
    ),
)

实用封装函数, 用于创建新的context, 并将span传递到新的context中

// NewContextWithTraceID 从原 context 中提取 Span,并将其设置到新 context 中
func NewContextWithTraceID(ctx context.Context, traceName string) (context.Context, Span) {
	// 从全局 TracerProvider 中获取 Tracer
	tracer := otel.GetTracerProvider().Tracer("goroutine")
	// 从旧上下文中提取 Span Context
	oldSpanCtx := trace.SpanContextFromContext(ctx)
	newCtx := trace.ContextWithSpanContext(context.Background(), oldSpanCtx)
	// 使用 tracer 开始一个新的 Span
	newCtx, span := tracer.Start(newCtx, traceName, trace.WithSpanKind(trace.SpanKindInternal))
	return newCtx, wrapSpan(span, nil)
}

// 使用实例
newCtx, span := xtrace.NewContextWithTraceID(ctx, "异步更新饰品磨损信息")
go func(newCtx context.Context, span xtrace.Span) {
	defer span.End()
	// do something
}(newCtx, span)

五、参考

https://go-kratos.dev/docs/component/middleware/tracing/ 链路追踪

https://go-kratos.dev/blog/go-kratos-opentelemetry-practice/ 基于OpenTelemetry的链路追踪

https://opentracing.io/specification/ opentracing doc

https://opentelemetry.io/docs/instrumentation opentelemetry doc

https://opentelemetry.io/docs opentelemetry trace api

https://opencensus.io/ opencensus 官网

https://www.jaegertracing.io/docs/1.35/ jaeger doc

https://www.cnblogs.com/jiujuan/p/16349519.html