分布式链路追踪(二)— Jaeger简单使用

对OpenTelemetry的概念有初步了解后,我们接着以Jaeger为例来演示如何在程序中使用实现链路追踪。

Jaeger

Jaeger是Uber开源的分布式追踪系统,是支持OpenTelemetry的系统之一,也是CNCF项目。本篇将使用Jaeger来演示如何在系统中引入分布式追踪。以下是Opentracing+Jaeger的架构图,针对于使用OpenTelemetry也是如此。

1. 测试环境部署

Jaeger官方提供了all-in-one的docker镜像,可以基于此进行一键部署。

Docker

Docker命令如下:

$ docker run --rm --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \
  -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.55

Docker Compose

也可以使用docker compose来启动。

version: '3.1'
services:
  db:
    image: jaegertracing/all-in-one
    restart: always
    environment:
      COLLECTOR_ZIPKIN_HTTP_PORT: 9411
    ports:
      - 5775:5775/udp
      - 6831:6831/udp
      - 6832:6832/udp
      - 5778:5778
      - 16686:16686
      - 14268:14268
      - 9411:9411

Binary

甚至还可以通过下载二进制文件直接运行。

下载地址:Jaeger Binaries

启动参数:

$ jaeger-all-in-one --collector.zipkin.http-port=9411

UI界面

启动之后就可以在http://localhost:16686看到Jaeger的UI界面了。

2. Hello World

1. 说明

大致步骤如下:

  • 1)初始化一个tracer
  • 2)记录一个简单的span
  • 3)在span上添加注释信息

2. 例子

func main() {
    // 解析命令行参数
    if len(os.Args) != 2 {
        panic("ERROR: Expecting one argument")
    }

    // 1.初始化 tracer
    tracer, closer := config.NewTracer("hello")
    defer closer.Close()

    // 2.开始新的 Span (注意:必须要调用 Finish()方法span才会上传到后端)
    span := tracer.StartSpan("say-hello")
    defer span.Finish()

    helloTo := os.Args[1]
    helloStr := fmt.Sprintf("Hello, %s!", helloTo)
    // 3.通过tag、log记录注释信息
    // LogFields 和 LogKV底层是调用的同一个方法
    span.SetTag("hello-to", helloTo)
    span.LogFields(
        log.String("event", "string-format"),
        log.String("value", helloStr),
    )
    span.LogKV("event", "println")
    println(helloStr)
}
func NewTracer(service string) (opentracing.Tracer, io.Closer) {
    // 参数详解 https://www.jaegertracing.io/docs/1.20/sampling/
    cfg := jaegerConfig.Configuration{
        ServiceName: service,
        // 采样配置
        Sampler: &jaegerConfig.SamplerConfig{
            Type:  jaeger.SamplerTypeConst,
            Param: 1,
        },
        Reporter: &jaegerConfig.ReporterConfig{
            LogSpans:          true,
            CollectorEndpoint: "http://localhost:14268/api/traces", // 将span发往jaeger-collector的服务地址
        },
    }
    tracer, closer, err := cfg.NewTracer(jaegerConfig.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
    }
    opentracing.SetGlobalTracer(tracer)
    return tracer, closer
}

运行上述例子后就可以在Jaeger UI界面看到对应的链路信息了。

3. 使用ctx包装tracer

1. 说明

  • 1)通过opentracing.ChildOf(rootSpan.Context())保留span之间的因果关系。
  • 2)通过ctx来实现在各个功能函数之间传递span。

2. span因果关系

span是链路追踪里的最小组成单元,为了保留各个功能之间的因果关系,必须在各个方法之间传递span并且新建span时指定opentracing.ChildOf(rootSpan.Context()),否则新建的span会是独立的,无法构成一个完整的trace。

比如方法A调用了B、C、D,那么就需要将方法A中的span传递到方法BCD中。

childSpan := rootSpan.Tracer().StartSpan(
    "formatString",
    opentracing.ChildOf(rootSpan.Context()),
)

通过opentracing.ChildOf(rootSpan.Context())建立两个span之间的引用关系,如果不指定则会创建一个新的span(UI中查看的时候就是一个新的trace)。

将前面的例子稍微修改一下,将formatString和printHello提成单独的方法,并新增span参数。

func main() {
    // 解析命令行参数
    if len(os.Args) != 2 {
        panic("ERROR: Expecting one argument")
    }

    // 1.初始化 tracer
    tracer, closer := config.NewTracer("hello")
    defer closer.Close()
    // 2.开始新的 Span (注意:必须要调用 Finish()方法span才会上传到后端)
    span := tracer.StartSpan("say-hello")
    defer span.Finish()

    helloTo := os.Args[1]
    helloStr := formatString(span, helloTo)
    printHello(span, helloStr)
}

func formatString(span opentracing.Span, helloTo string) string {
    childSpan := span.Tracer().StartSpan(
        "formatString",
        opentracing.ChildOf(span.Context()),
    )
    defer childSpan.Finish()

    return fmt.Sprintf("Hello, %s!", helloTo)
}

func printHello(span opentracing.Span, helloStr string) {
    childSpan := span.Tracer().StartSpan(
        "printHello",
        opentracing.ChildOf(span.Context()),
    )
    defer childSpan.Finish()

    println(helloStr)
}

运行之后可以清楚的在UI界面中看到say-helloformatStringprintHello两个功能组成。

3. 通过ctx传递span

前面虽然保留了span的因果关系,但是需要在各个方法中传递span。这可能会污染整个程序,我们可以借助Go语言中的context.Context对象来进行传递。

实例代码如下:

ctx := context.Background()
ctx = opentracing.ContextWithSpan(ctx, span)

helloStr := formatString(ctx, helloTo)
printHello(ctx, helloStr)

func formatString(ctx context.Context, helloTo string) string {
    span, _ := opentracing.StartSpanFromContext(ctx, "formatString")
    defer span.Finish()
    ...
}

func printHello(ctx context.Context, helloStr string) {
    span, _ := opentracing.StartSpanFromContext(ctx, "printHello")
    defer span.Finish()
    ...
}

opentracing.StartSpanFromContext()返回的第二个参数是子ctx,如果需要的话可以将该子ctx继续往下传递,而不是传递父ctx。

需要注意的是opentracing.StartSpanFromContext()默认使用GlobalTracer来开始一个新的span,所以使用之前需要设置GlobalTracer。

opentracing.SetGlobalTracer(tracer)

打 赏