[TOC]
当代互联网服务,通常都是用复杂,大规模分布式集群来实现,微服务化,这些软件模块分布在不同的机器,不同的数据中心,由不同团队,语言开发而成。因此,需要工具帮助理解,分析这些系统、定位问题,做到追踪每一个请求的完整调用链路,收集性能数据,反馈到服务治理中,链路追踪系统应运而生。
现有大部分 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 (中文)是一套分布式追踪协议,与平台,语言无关,统一接口,方便开发接入不同的分布式追踪系统。
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 封装了如下状态:
跨进程,机器通讯,通过传递 Spancontext 来提供足够的信息建立 span 间的关系。SpanContext 通过 Inject 操作向 Carrier 中增加,传递后通过 Extracted 从 Carrier 中取出。
OpenTracing API 不强调采样的概念,但是大多数追踪系统通过不同方式实现采样。有些情况下,应用系统需要通知追踪程序,这条特定的调用需要被记录,即使根据默认采样规则,它不需要被记录。sampling.priority tag 提供这样的方式。追踪系统不保证一定采纳这个参数,但是会尽可能的保留这条调用。 sampling.priority - integer
提供不同语言的 API,用于在自己的应用程序中执行链路记录。
Jaeger (ˈyā-gər) 是Uber开发的一套分布式追踪系统,受启发于 dapper 和 OpenZipkin,兼容 OpenTracing 标准,CNCF的开源项目。
image.png
官方释放部署的镜像到 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
生产环境系统性能很重要,所以对于所有的请求都开启 Trace 显然会带来比较大的压力,另外,大量的数据也会带来很大存储压力。为此,jaeger 支持设置采样速率,根据系统实际情况设置合适的采样频率。
Jaeger 官方提供了多种采集策略,使用者可以按需选择使用
go 程序中集成链路追踪并上报到 jaeger 需要用到一下两个包 opentracing go api 和 jaeger go 客户端。
以下代码上报一个包含一个 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)))
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句