基于opentracing + jaeger 实现全链路追踪

[TOC]

链路追踪

当代互联网服务,通常都是用复杂,大规模分布式集群来实现,微服务化,这些软件模块分布在不同的机器,不同的数据中心,由不同团队,语言开发而成。因此,需要工具帮助理解,分析这些系统、定位问题,做到追踪每一个请求的完整调用链路,收集性能数据,反馈到服务治理中,链路追踪系统应运而生。

现有大部分 APM(Application Performance Management) 理论模型大多借鉴 google dapper 论文,Twitter的zipkin,Uber的 jaeger,淘宝的鹰眼,大众的cat,京东的Hydra等。

微服务问题:

  1. 故障定位难
  2. 链路梳理难
  3. 容量预估难

举个例子,一个场景下,一个请求进来,入口服务是 serviceA, serviceA 接到请求后访问数据库读取用户数据,然后向 serviceB 发起 rpc,serviceB 收到 rpc 请求时同时向后端服务 serviceC 和 serviceD 发起请求,等待请求回复后再返回 serviceA 的 rpc 调用。如果我们发现发起的请求失败,或者请求的时延很大,我们该如何去定位呢?

基于这个需求,我们将服务介入追踪系统。

分布式追踪系统发展很快,种类繁多,但核心步骤一般有三个:代码埋点,数据存储、查询展示

在数据采集过程,需要侵入用户代码做埋点,不同系统的API不兼容会导致切换追踪系统需要做很大的改动。为了解决这个问题,诞生了opentracing 规范。

   +-------------+  +---------+  +----------+  +------------+
   | Application |  | Library |  |   OSS    |  |  RPC/IPC   |
   |    Code     |  |  Code   |  | Services |  | Frameworks |
   +-------------+  +---------+  +----------+  +------------+
          |              |             |             |
          |              |             |             |
          v              v             v             v
     +-----------------------------------------------------+
     | · · · · · · · · · · OpenTracing · · · · · · · · · · |
     +-----------------------------------------------------+
       |               |                |               |
       |               |                |               |
       v               v                v               v
 +-----------+  +-------------+  +-------------+  +-----------+
 |  Tracing  |  |   Logging   |  |   Metrics   |  |  Tracing  |
 | System A  |  | Framework B |  | Framework C |  | System D  |
 +-----------+  +-------------+  +-------------+  +-----------+

OpenTracing

opentracing (中文)是一套分布式追踪协议,与平台,语言无关,统一接口,方便开发接入不同的分布式追踪系统。

  • 语义规范 : 描述定义的数据模型 Tracer,Sapn 和 SpanContext 等;
  • 语义惯例 : 罗列出 tag 和 logging 操作时,标准的key值;

Trace 和 sapn

opentracing 中的 Trace(调用链)通过归属此链的 Span 来隐性定义。一条 Trace 可以认为一个有多个 Span 组成的有向无环图(DAG图),Span 是一个逻辑执行单元,Span 与 Span 的因果关系命名为 References。

opentracing 定义两种关系:

  • Childof:如下例子中, SpanC 是 childof SpanA
  • FollowsFrom:如下例子中,SpanG 是 followsFrom SpanF

例子 Trace 包含 8个 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)

通过时间轴显示一个 Tracer 更加直观,

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

 [Span A···················································]
   [Span B··············································]
      [Span D··········································]
    [Span C········································]
         [Span E·······]        [Span F··] [Span G··] [Span H··]

每个Span封装了如下状态:

  • 操作名称
  • 开始时间戳
  • 结束时间戳
  • 一组零或多个键:值结构的 Span标签 (Tags)。键必须是字符串。值可以是字符串,布尔或数值类型.
  • 一组零或多个 Span日志 (Logs),其中每个都是一个键:值映射并与一个时间戳配对。键必须是字符串,值可以是任何类型。 并非所有的 OpenTracing 实现都必须支持每种值类型。
  • 一个 SpanContext (见下文)
  • 零或多个因果相关的 Span 间的 References (通过那些相关的 SpanSpanContext )

每个 SpanContext 封装了如下状态:

  • 任何需要跟跨进程 Span 关联的,依赖于 OpenTracing 实现的状态(例如 Trace 和 Span 的 id)
  • 键:值结构的跨进程的 Baggage Items(区别于 span tag,baggage 是全局范围,在 span 间保持传递,而tag 是 span 内部,不会被子 span 继承使用。)

InjectExtract 操作

跨进程,机器通讯,通过传递 Spancontext 来提供足够的信息建立 span 间的关系。SpanContext 通过 Inject 操作向 Carrier 中增加,传递后通过 ExtractedCarrier 中取出。

关于inject 和 extract

Sampling,采样

OpenTracing API 不强调采样的概念,但是大多数追踪系统通过不同方式实现采样。有些情况下,应用系统需要通知追踪程序,这条特定的调用需要被记录,即使根据默认采样规则,它不需要被记录。sampling.priority tag 提供这样的方式。追踪系统不保证一定采纳这个参数,但是会尽可能的保留这条调用。 sampling.priority - integer

  • 如果大于 0, 追踪系统尽可能保存这条调用链
  • 等于 0, 追踪系统不保存这条调用链
  • 如果此tag没有提供,追踪系统使用自己的默认采样规则

OpenTracing 多语言支持

提供不同语言的 API,用于在自己的应用程序中执行链路记录。

Jaeger

Jaeger (ˈyā-gər) 是Uber开发的一套分布式追踪系统,受启发于 dapper 和 OpenZipkin,兼容 OpenTracing 标准,CNCF的开源项目。

系统框架

image.png

  • Jaeger Client - 为不同语言实现了符合 OpenTracing 标准的 SDK。应用程序通过 API 写入数据,client library 把 trace 信息按照应用程序指定的采样策略传递给 jaeger-agent。
  • Agent - 是一个监听在 UDP 端口上接收 span 数据的网络守护进程,它会将数据批量发送给 collector。它被设计成一个基础组件,推荐部署到所有的宿主机上。Agent 将 client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节。
  • Collector - 接收 jaeger-agent 发送来的数据,然后将数据写入后端存储。Collector 被设计成无状态的组件,因此您可以同时运行任意数量的 jaeger-collector。
  • Data Store - 后端存储被设计成一个可插拔的组件,支持将数据写入 cassandra、elastic search。
  • Query - 接收查询请求,然后从后端存储系统中检索 trace 并通过 UI 进行展示。Query 是无状态的,您可以启动多个实例,把它们部署在 nginx 这样的负载均衡器后面。

官方释放部署的镜像到 dockerhub,所以部署 jaeger 非常方便,如果是本地测试,可以直接用 jaeger 提供的 all-in-one 镜像部署。

快速搭建,all-in-one

执行一下命令,可以在本机拉起一个 jaeger 环境,上报的链路数据保存在本地内存,所以只能用于测试。

$ docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  -p 5775:5775/udp \kaixiao
  -p 6831:6831/udp \
  -p 6832:6832/udp \
  -p 5778:5778 \
  -p 16686:16686 \
  -p 14268:14268 \
  -p 9411:9411 \
  jaegertracing/all-in-one:latest

通过 http://localhost:16686 可以在浏览器查看 Jaeger UI

官方提供的一个例子: HotROD

采样速率

生产环境系统性能很重要,所以对于所有的请求都开启 Trace 显然会带来比较大的压力,另外,大量的数据也会带来很大存储压力。为此,jaeger 支持设置采样速率,根据系统实际情况设置合适的采样频率。

Jaeger 官方提供了多种采集策略,使用者可以按需选择使用

  1. const,全量采集,采样率设置0,1 分别对应打开和关闭
  2. probabilistic ,概率采集,默认万份之一,0~1之间取值,
  3. rateLimiting ,限速采集,每秒只能采集一定量的数据
  4. remote ,一种动态采集策略,根据当前系统的访问量调节采集策略

追踪实践 - go

go 程序中集成链路追踪并上报到 jaeger 需要用到一下两个包 opentracing go api 和 jaeger go 客户端。

一个简单的 trace

以下代码上报一个包含一个 span 的 trace,程序在初始化阶段通过环境变量获取 jaeger 的配置并初始化全局 tracer。之后便可以通过这个 tracer 开启 span(root span) 记录程序链路。

package main
import (
    "fmt"
    "io"
    "time"

    opentracing "github.com/opentracing/opentracing-go"
    jaeger "github.com/uber/jaeger-client-go"
    jaegercfg "github.com/uber/jaeger-client-go/config"
)
// InitJaeger ...
func InitJaeger(service string) (opentracing.Tracer, io.Closer) {
    cfg, err := jaegercfg.FromEnv()
    /*
        cfg.Sampler.Type = "const"
        cfg.Sampler.Param = 1
        cfg.Reporter.LocalAgentHostPort = "127.0.0.1:6831"
        cfg.Reporter.LogSpans = true
    */
    tracer, closer, err := cfg.New(service, jaegercfg.Logger(jaeger.StdLogger))
    if err != nil {
        panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
    }
    return tracer, closer
}

func main() {
    tracer, closer := InitJaeger("hello-world")
    defer closer.Close()
    opentracing.InitGlobalTracer(tracer)

    helloStr := "hello jaeger"
    span := tracer.StartSpan("say-hello")
    time.Sleep(time.Duration(2) * time.Millisecond)
    println(helloStr)
    span.Finish() 
}

然后通过 jaeger ui 可以看到本次上报的 trace。

$ export JAEGER_DISABLED=false
$ export JAEGER_SAMPLER_TYPE="const"
$ export JAEGER_SAMPLER_PARAM=1
$ export JAEGER_REPORTER_LOG_SPANS=true
$ export JAEGER_AGENT_HOST="127.0.0.1"
$ export JAEGER_AGENT_PORT=6831
$ go run ./test.go
2019/06/09 23:01:31 Initializing logging reporter
hello jaeger
2019/06/09 23:01:31 Reporting span 2813d696ced4431:2813d696ced4431:0:1

在开启 span 记录一个过程时,还可以通过 api 进行 tag,logs等操作 ,并能在 UI 看到相应设置的键z值

span.SetTag("value", helloStr)
span.LogFields(
    log.String("event", "sayhello"),
    log.String("value", helloStr),
)
//span.LogKV("event", "sayhello") // 单一设置

tag 和 logs 在opentarcing中提到一些推荐命名:语义惯例

使用 tag 是用于描述 span 中的特性,是对整个过程而言,而 log 是用于记录 span 这个过程中的一个时间,因为记录 log 时会携带一个发生的时间戳,是有先后之分的。

baggage

相比 tag,log 限制在 span 中, baggage 同样提供保存键值对设置,但是 baggage 数据有效是全 trace 的,所以使用的时候避免设置不必要的值,导致传递开销。

// set
span.SetBaggageItem("greeting", greeting)
// get
greeting := span.BaggageItem("greeting")

使用上下文传递 span

当我们提到调用链,一般涉及多个函数,多个进程甚至多个机器上运行的过程,用 tracer 开启 root span 后,需要向其他过程传递以保持他们之间的关联性,我们通过上下文来存储 span 并传递。

// 存储到 context 中
ctx := context.Background()
ctx = opentracing.ContextWithSpan(ctx, span)
//....

// 其他过程获取并开始子 span
span, ctx := opentracing.StartSpanFromContext(ctx, "newspan")
defer span.Finish()
// StartSpanFromContext 会将新span保存到ctx中更新

或者先取出 parent span,然后在以 childof 开启span,需要手动写入新 span 到 ctx中。

//获取上一级 span
parent := opentracing.SpanFromContext(ctx) 
span1 := opentracing.StartSpan("from-sayhello-1", opentracing.ChildOf(span2.Context()))
...
span1.Finish()
ctx = opentracing.ContextWithSpan(ctx, span2) //更新ctx

span2 := opentracing.StartSpan("from-sayhello-2", opentracing.ChildOf(span2.Context()))
...
span2.Finish()
ctx = opentracing.ContextWithSpan(ctx, span2) //更新ctx

tracing grpc 调用

由于 grpc 调用和服务端都声明了 UnaryInterceptor 和 StreamInterceptor 两回调函数,因此只需要重写这两个函数,在函数中调用 opentracing 的借口进行链路追踪,并初始化客户端或者服务端时候注册进去就可以。

相应的函数已经有现成的包 grpc-opentracing

使用如下:

var tracer opentracing.Tracer = ...
//client
conn, err := grpc.Dial(
    address,
    ... // other options
    grpc.WithUnaryInterceptor(
        otgrpc.OpenTracingClientInterceptor(tracer)),
    grpc.WithStreamInterceptor(
        otgrpc.OpenTracingStreamClientInterceptor(tracer)))


// server
s := grpc.NewServer(
    ... // other options
    grpc.UnaryInterceptor(
        otgrpc.OpenTracingServerInterceptor(tracer)),
    grpc.StreamInterceptor(
        otgrpc.OpenTracingStreamServerInterceptor(tracer)))

参考

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券