基于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)))

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 在各大厂分布式链路跟踪系统架构对比中已经介绍了几大框架的对比,如果想用免费的可以用zipkin和pinpoint还...
    欢醉阅读 1,851评论 2 2
  • 分布式链路追踪(Distributed Tracing),也叫 分布式链路跟踪,分布式跟踪,分布式追踪 等等。 本...
    Daniel_adu阅读 33,689评论 0 20
  • 28岁的第一天,很平静。仿佛是再平常不过的一天。 从凌晨开始,收到了许多朋友的祝福,生在这样一个特别的日子,最大的...
    Yvonne不是我阅读 346评论 0 0
  • 今天中午刚打扫完卫生就接到孩子班主任的电话问我接孩子了吗?我说没,然后老师说你现在有时间来一趟吗?我在学校门口等你...
    新的一天从早上开始阅读 180评论 0 1
  • 十月不远——给大海,请埋葬我的爱情 十月不远 遇见你不远 爱情不远——上帝盘旋的裤衩下 海水腥咸 因此大海不远 浪...
    九松子阅读 258评论 1 1