Skip to main content

链路追踪 Jaeger实验

OpenTracing 是标准,实现的工具有很多: Zipkin, Jaeger…

Jaeger 是一个基于 opentracing 标准的链路追踪工具。

Jaeger UI 网页界面

Jaeger 组件

按照 官方描述,总结了以下 Jaeger 组件:

组件和作用列表
必要性 名称 作用
必须安装 Jaeger Collector Jaeger 数据收集组件,必须指定存储引擎,支持以下引擎:
- Elastic Search(笔者在用)
- Cassandra
- Kafka(仅缓存用)
- grpc-plugin
必须安装 StorageBackend 第三方存储引擎,看 Collector 支持哪些,自行部署
推荐安装 Jaeger Agent 事先部署好 Agent 于本地,程序不用知道后台在哪,直接将追踪数据发给本地的 Agent 即可
可选安装 Jaeger Query 查询追踪记录的后台,必须指定存储引擎。(指定存储就能看)
可选运行 Jaeger UI 可视化追踪记录的前端页面,必须指定 Jaeger Query。
程序调用 Jaeger Client Lib 各个编程语言的库,初始化任选其一:
- Jaeger Collector 地址
- Jaeger Agent 地址
可选运行 Jaeger Ingester 从 Kafka 读取追踪记录,然后再写到其他的存储引擎(Elastic Search或Cassandra)
试用 Jaeger All In One 部署一个轻量 Jaeger 全家桶到本地,用于快速体验

Jaeger 的组件很灵活,具体还是要按需求安装。

本次实验启动多个 web 服务,然后用 Jaeger 记录每个服务的工作流程和轨迹。

准备工作

  • Jaeger Collector
  • Jaeger Query
  • Jaeger UI

微服务概览

架构及各服务作用

服务名称 端口 描述
srvEntry :8088 服务主入口,按顺序执行
- HTTP 请求 srvProfile
- HTTP 请求 srvPermission
- 输出 TraceID
srvProfile :8082 档案子服务,按顺序执行
- 模拟读数据库,耗时30ms
- 模拟请求存储,耗时15ms
- 返回 200
srvPermission :8083 权限子服务,按顺序执行
- 模拟读数据库,耗时60ms
- 模拟检查权限,耗时8ms
- 返回 200

设计流程

微服务设计流程

各服务实现

以下所有代码都写在同一个 main.go 文件中。

主服务入口 srvEntry

srvEntry 处理外部 HTTP 请求,创建根 Span,调用2个子服务后,输出 TraceID。

Code Snippet
func srvEntry() {
  var (
    // Jaeger Collector URL
    URLJaegerCollector = "http://localhost:14268/api/traces?format=jaeger.thrift"
    // 档案子服务的URL
    URLProfile    = "http://localhost:8082"
    // 权限子服务URL
    URLPermission = "http://localhost:8083"
  )
  // 新建 Tracer,Tracer 用来标识服务自身、创建 Span
  tracer, closer := jaeger.NewTracer(
    // 服务名称
    "srvEntry", 
    // [采样选项] 让 Collector 必须收集所有追踪数据
    jaeger.NewConstSampler(true), 
    // 向谁发送追踪数据?可以是 Jaeger Collector、也可以是 Jaeger Agent
    jaeger.NewRemoteReporter(transport.NewHTTPTransport(URLJaegerCollector),
  ))
  // 程序结束前关掉 tracer
  defer closer.Close()

  // 新建 HTTP 服务器
  mux := http.NewServeMux()
  // 处理根路由 /
  mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
    // 用 tracer 创建根 Span
    SpanRoot := tracer.StartSpan("srvEntry/root").(*jaeger.Span)
    // 获得根 Span Context
    SpanRootCtx := SpanRoot.Context()

    // 请求 srvProfile
    // 用根 Span Context 新建子span: SpanProfile
    SpanProfile := tracer.StartSpan("srvEntry/callProfile", opentracing.ChildOf(SpanRootCtx))
    // 新建 srvProfile HTTP 请求
    reqSrvProfile, _ := http.NewRequest("GET", URLProfile, strings.NewReader("HELLO srvProfile"))
    // 将 Span Context 注入 HTTP Header
    _ = tracer.Inject(SpanProfile.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(reqSrvProfile.Header))
    // 发起请求,获得 srvProfile 应答
    respSrvProfile, _ := http.DefaultClient.Do(reqSrvProfile)
    // 释放应答资源
    respSrvProfile.Body.Close()
    // 结束 SpanProfile
    SpanProfile.Finish()

    // 请求 srvPermission
    // 用根 Span Context 新建子span: SpanPermission
    SpanPermission := tracer.StartSpan("srvEntry/callPermission", opentracing.ChildOf(SpanRootCtx))
    // 新建 srvPermission HTTP 请求
    reqSrvPermission, _ := http.NewRequest("GET", URLPermission, strings.NewReader("HELLO srvPermission"))
    // 将 Span Context 注入 HTTP Header
    _ = tracer.Inject(SpanPermission.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(reqSrvPermission.Header))
    // 发起请求,获得 srvPermission 应答
    respSrvPermission, _ := http.DefaultClient.Do(reqSrvPermission)
    // 释放应答资源
    respSrvPermission.Body.Close()
    // 结束 Span Permission
    SpanPermission.Finish()

    // 流程结束,结束根 Span
    SpanRoot.Finish()
    // 获得 jaeger TraceID,并写出 Header
    rw.Header().Set("tracer_id", SpanRoot.SpanContext().TraceID().String())
    return
  })
  // srvEntry 主服务监听 :8088 端口
  http.ListenAndServe(":8088", mux)
}

档案子服务 srvProfile

Code Snippet
// 档案子服务
func srvProfile() {
  var (
    URLCollector = "http://localhost:14268/api/traces?format=jaeger.thrift"
  )
  // 新建 tracer
  tracer, closer := jaeger.NewTracer(
    // 服务名称
    "srvProfile", 
    // 子服务的采样选项
    jaeger.NewConstSampler(true), 
    // 向谁发送追踪数据
    jaeger.NewRemoteReporter(transport.NewHTTPTransport(URLCollector)),
  )
  // 程序结束前关掉 tracer
  defer closer.Close()

  // 新建 HTTP 服务器
  mux := http.NewServeMux()
  // 处理 HTTP 根路由
  mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
    // 读取 HTTP Header,用 tracer 解压出 Entry 传来的 Span
    SpanProfileContext, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))

    // 动作: 读取数据库,耗时30ms
    // 用 SpanProfileContext 创建子span: SpanDB
    SpanDB := tracer.StartSpan("srvProfile/DB", opentracing.ChildOf(SpanProfileContext))
    time.Sleep(30 * time.Millisecond)
    // 模拟读取数据库动作结束,结束 SpanDB
    SpanDB.Finish()

    // 请求存储,耗时15ms
    // 用 SpanProfileContext 创建子span: SpanAsset
    spanAsset := tracer.StartSpan("srvProfile/Asset", opentracing.ChildOf(SpanProfileContext))
    time.Sleep(15 * time.Millisecond)
    // 模拟请求存储动作结束,结束 SpanAsset
    spanAsset.Finish()

    // 返回 200
    rw.WriteHeader(200)
    return
  })
  // srvProfile 服务监听 :8082 端口
  http.ListenAndServe(":8082", mux)
}

权限子服务 srvPermission

Code Snippet
// 权限子服务
func srvPermission() {
  var (
    URLCollector = "http://localhost:14268/api/traces?format=jaeger.thrift"
  )
  // 新建 tracer
  tracer, closer := jaeger.NewTracer(
    // 服务名称
    "srvPermission", 
    // 子服务的采样选项
    jaeger.NewConstSampler(true), 
    // 向谁发送追踪数据
    jaeger.NewRemoteReporter(transport.NewHTTPTransport(URLCollector),
  ))
  // 程序结束前关掉 tracer
  defer closer.Close()

  // 新建 HTTP 服务器
  mux := http.NewServeMux()
  // HTTP 处理根路由
  mux.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
    // 读取 HTTP Header,用 tracer 解压,得到 SpanPermissionContext
    SpanPermissionContext, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))

    // 模拟读取数据库,耗时60ms
    // 用 SpanPermissionContext 新建子span: SpanDB
    SpanDB := tracer.StartSpan("srvPermission/DB", opentracing.ChildOf(SpanPermissionContext))
    time.Sleep(60 * time.Millisecond)
    // 模拟读取数据库动作结束,关闭 SpanDB
    SpanDB.Finish()

    // 模拟检查权限,耗时8ms
    // 同样地,用 SpanPermissionContext 新建子span: SpanCalc
    SpanCalc := tracer.StartSpan("srvPermission/Calc", opentracing.ChildOf(SpanPermissionContext))
    time.Sleep(8 * time.Millisecond)
    // 模拟检查权限动作结束,关闭 SpanCalc
    SpanCalc.Finish()

    // 返回 200
    rw.WriteHeader(200)
    return
  })
  // srvPermission 服务监听 :8083 端口
  http.ListenAndServe(":8083", mux)
}

运行微服务

把上面的方法全部写到 main.go,得到以下结果:

package main
import (...)
func srvEntry () { ... }
func srvProfile () { ... }
func srvPermission () { ... }

在主函数 func main() 中按照需求启动相应的服务。

func main() {
  S := os.Getenv("S")
  switch S {
  case "profile":
    srvProfile()
  case "permission":
    srvPermission()
  default:
    srvEntry()
  }
}

最后 main.go 大概看起来像这样:

package main
import (...)
func srvEntry () { ... }
func srvProfile () { ... }
func srvPermission () { ... }
func main () { ... }

打开3个终端,分别指令:

# 终端1 启动档案子服务
S=profile go run main.go
# 终端2 启动权限子服务
S=permission go run main.go
# 终端3 启动主服务入口
go run main.go

以上,3个服务全部跑起来了。

检查运行结果

按照架构,主服务入口在 :8088,则执行:

curl -I localhost:8088

可以得到响应头部,里面有 trace_id:

HTTP/1.1 200 OK
Tracer_id: 45328258952e9235
Date: Sat, 07 Aug 2021 03:36:10 GMT

拿着 trace_id 访问 Jaeger UI 即可看到完整时间线。

Jaeger UI 展示出本次追踪过程

扩展实验: 给 Span 设置标记(Tag)

Span 可以带有标记信息 (Tag)。

Tag 示例
// 入口服务
func srvEntry () {
  // ...

  // 请求 srvProfile
  // 用根 Span Context 新建子span: SpanProfile
  SpanProfile := tracer.StartSpan("srvEntry/callProfile", opentracing.Child(SpanRootCtx))
  // Span Tag 示例
  SpanProfile.SetTag("error", true)
  SpanProfile.SetTag("error_description", "手动设置 error 为 true 后,界面会警告提示")

  // ...
}

执行效果

SetTag 效果,注意红色框区域

扩展实验: Span 记录日志(Log)

Span 也可以记录日志,相当于记录带时间戳的 Tag。

Span Log 示例
// 入口服务
func srvEntry () {
  // ...

  // 请求 srvProfile
  // 用根 Span Context 新建子span: SpanProfile
  SpanProfile := tracer.StartSpan("srvEntry/callProfile", opentracing.Child(SpanRootCtx))
  // Span Tag 示例
  SpanProfile.LogFields(log.Message("已调用 档案子服务"))
  SpanProfile.LogFields(log.Message("请求完成,现在结束 SpanProfile"))

  // ...
}

调用效果

LogField 效果,注意红色框区域

扩展实验: Baggage

微服务(进程)通过 SpanContext 传递追踪上下文。SpanContext 可以带自定义数据(Baggage)。

本实验通过 HTTP 传递 SpanContext。

SpanContext 通过 HTTP Header 编码和解码
// 入口服务 -> 档案子服务: 将 SpanContext 注入 HTTP Header
reqSrvProfile, _ := http.NewRequest()
tracer.Inject(
  SpanProfile.Context(), 
  opentracing.HTTPHeaders, 
  opentracing.HTTPHeadersCarrier(reqSrvProfile.Header),
)

// ...

// 档案子服务从 HTTP Header 读取 SpanContext
// 假设存在变量 r,类型是 *http.Request
SpanProfileContext, _ := tracer.Extract(
  opentracing.HTTPHeaders, 
  opentracing.HTTPHeadersCarrier(r.Header),
)
Baggage 示例

// 入口服务
func srvEntry () {
  // ...

  // 用根 Span Context 新建子span: SpanProfile
  SpanProfile := tracer.StartSpan("srvEntry/callProfile", opentracing.ChildOf(SpanRootCtx))
  // 设置 Baggage "user_id"
  SpanProfile.SetBaggageItem("user_id", "xxxxxxxxxxx")
  // 注入到 HTTP Header 发送给 档案子服务

  // ...
}


// 档案子服务
func srvProfile () {
  // ...

  // 从 HTTP Header 读取 SpanContext
  SpanProfileContext, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))
  // 遍历 baggage item
  SpanProfileContext.ForeachBaggageItem(func(k, v string) bool {
    fmt.Println("srvProfile 从 SpanContext 获得共享数据 baggage item: ", k, " = ", v)
    return true
  })

  // ...
}

运行效果

srvProfile 从 SpanContext 获得共享数据 baggage item:  user_id = xxxxxxxxxxx