链路追踪 Jaeger实验
目录
OpenTracing 是标准,实现的工具有很多: Zipkin, Jaeger…
Jaeger 是一个基于 opentracing 标准的链路追踪工具。
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。
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
// 档案子服务
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
// 权限子服务
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 即可看到完整时间线。
扩展实验: 给 Span 设置标记(Tag)
Span 可以带有标记信息 (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 后,界面会警告提示")
// ...
}
执行效果
扩展实验: Span 记录日志(Log)
Span 也可以记录日志,相当于记录带时间戳的 Tag。
// 入口服务
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"))
// ...
}
调用效果
扩展实验: Baggage
微服务(进程)通过 SpanContext 传递追踪上下文。SpanContext 可以带自定义数据(Baggage)。
本实验通过 HTTP 传递 SpanContext。
// 入口服务 -> 档案子服务: 将 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),
)
// 入口服务
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