当代互联网服务,通常都是用复杂,大规模分布式集群来实现,微服务化,这些软件模块分布在不同的机器,不同的数据中心,由不同团队,语言开发而成。因此,需要工具帮助理解,分析这些系统、定位问题,做到追踪每一个请求的完整调用链路,收集性能数据,反馈到服务治理中,链路追踪系统应运而生。
现有大部分 APM(Application Performance Management) 理论模型大多借鉴 google dapper 论文,Twitter的zipkin,Uber的 jaeger,淘宝的鹰眼,大众的cat,京东的Hydra等。
微服务问题:
举个例子,一个场景下,一个请求进来,入口服务是 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 (中文)是一套分布式追踪协议,与平台,语言无关,统一接口,方便开发接入不同的分布式追踪系统。
语义规范 : 描述定义的数据模型 Tracer,Sapn 和 SpanContext 等;
语义惯例 : 罗列出 tag 和 logging 操作时,标准的key值;
opentracing 中的 Trace(调用链)通过归属此链的 Span 来隐性定义。一条 Trace 可以认为一个有多个 Span 组成的有向无环图(DAG图),Span 是一个逻辑执行单元,Span 与 Span 的因果关系命名为 References。
opentracing 定义两种关系:
例子 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封装了如下状态:
每个 SpanContext 封装了如下状态:
任何需要跟跨进程 Span 关联的,依赖于 OpenTracing 实现的状态(例如 Trace 和 Span 的 id)
键:值结构的跨进程的 Baggage Items(区别于 span tag,baggage 是全局范围,在 span 间保持传递,而tag 是 span 内部,不会被子 span 继承使用。)
跨进程,机器通讯,通过传递 Spancontext 来提供足够的信息建立 span 间的关系。SpanContext 通过 Inject 操作向 Carrier 中增加,传递后通过 Extracted 从 Carrier 中取出。
关于inject 和 extract
OpenTracing API 不强调采样的概念,但是大多数追踪系统通过不同方式实现采样。有些情况下,应用系统需要通知追踪程序,这条特定的调用需要被记录,即使根据默认采样规则,它不需要被记录。sampling.priority tag 提供这样的方式。追踪系统不保证一定采纳这个参数,但是会尽可能的保留这条调用。
sampling.priority - integer
如果大于 0, 追踪系统尽可能保存这条调用链
等于 0, 追踪系统不保存这条调用链
如果此tag没有提供,追踪系统使用自己的默认采样规则
提供不同语言的 API,用于在自己的应用程序中执行链路记录。
Jaeger (ˈyā-gər) 是Uber开发的一套分布式追踪系统,受启发于 dapper 和 OpenZipkin,兼容 OpenTracing 标准,CNCF的开源项目。
官方释放部署的镜像到 dockerhub,所以部署 jaeger 非常方便,如果是本地测试,可以直接用 jaeger 提供的 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 官方提供了多种采集策略,使用者可以按需选择使用
go 程序中集成链路追踪并上报到 jaeger 需要用到一下两个包 opentracing go api 和 jaeger go 客户端。
opentracing-go-API
jaeger-go-client
以下代码上报一个包含一个 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 时会携带一个发生的时间戳,是有先后之分的。
相比 tag,log 限制在 span 中, baggage 同样提供保存键值对设置,但是 baggage 数据有效是全 trace 的,所以使用的时候避免设置不必要的值,导致传递开销。
// set
span.SetBaggageItem("greeting", greeting)
// get
greeting := span.BaggageItem("greeting")
当我们提到调用链,一般涉及多个函数,多个进程甚至多个机器上运行的过程,用 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
由于 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)))