在微服务架构的系统中,请求在各服务之间流转,调用链错综复杂,一旦出现了问题和异常,很难追查定位,这个时候就须要链路追踪来帮忙了。链路追踪系统能追踪并记录请求在系统中的调用顺序,调用时间等一系列关键信息,从而帮助咱们定位异常服务和发现性能瓶颈。git
Opentracing是分布式链路追踪的一种规范标准,是CNCF(云原生计算基金会)下的项目之一。和通常的规范标准不一样,Opentracing不是传输协议,消息格式层面上的规范标准,而是一种语言层面上的API标准。以Go语言为例,只要某链路追踪系统实现了Opentracing规定的接口(interface),符合Opentracing定义的表现行为,那么就能够说该应用符合Opentracing标准。这意味着开发者只需修改少许的配置代码,就能够在符合Opentracing标准的链路追踪系统之间自由切换。github
github.com/opentracing…docker
在使用Opentracing来实现全链路追踪前,有必要先了解一下它所定义的数据模型。api
Span是一条追踪链路中的基本组成要素,一个span表示一个独立的工做单元,好比能够表示一次函数调用,一次http请求等等。span会记录以下基本要素:bash
Tags以K/V键值对的形式保存用户自定义标签,主要用于链路追踪结果的查询过滤。例如: http.method="GET",http.status_code=200
。其中key值必须为字符串,value必须是字符串,布尔型或者数值型。 span中的tag仅本身可见,不会随着 SpanContext传递给后续span。 例如:网络
span.SetTag("http.method","GET")
span.SetTag("http.status_code",200)
复制代码
Logs与tags相似,也是K/V键值对形式。与tags不一样的是,logs还会记录写入logs的时间,所以logs主要用于记录某些事件发生的时间。logs的key值一样必须为字符串,但对value类型则没有限制。例如:架构
span.LogFields(
log.String("event", "soft error"),
log.String("type", "cache timeout"),
log.Int("waited.millis", 1500),
)
复制代码
Opentracing列举了一些惯用的Tags和Logs: github.com/opentracing…异步
SpanContext携带着一些用于跨服务通讯的(跨进程)数据,主要包含:分布式
span_id,trace_id
。Baggage Items与tags相似,也是K/V键值对。与tags不一样的是:函数
Opentracing定义了两种引用关系:ChildOf
和FollowFrom
。
ChildOf: 父span的执行依赖子span的执行结果时,此时子span对父span的引用关系是ChildOf
。好比对于一次RPC调用,服务端的span(子span)与客户端调用的span(父span)是ChildOf
关系。
FollowFrom:父span的执不依赖子span执行结果时,此时子span对父span的引用关系是FollowFrom
。FollowFrom
经常使用于异步调用的表示,例如消息队列中consumer
span与producer
span之间的关系。
Trace表示一次完整的追踪链路,trace由一个或多个span组成。下图示例表示了一个由8个span组成的trace:
[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)
复制代码
时间轴的展示方式会更容易理解:
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
复制代码
示例来源: github.com/opentracing…
对Opentracing的概念有初步了解后,下面使用Jaeger来演示如何在程序中使用实现链路追踪。
更多更详细的示例可参考: Opentracing Go Tutorial
Jaeger\ˈyā-gər\ 是Uber开源的分布式追踪系统,是遵循Opentracing的系统之一,也是CNCF项目。本篇将使用Jaeger来演示如何在系统中引入分布式追踪。
Jaeger提供了all-in-one镜像,方便咱们快速开始测试:
$ docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 9411:9411 \
jaegertracing/all-in-one:1.14
复制代码
镜像启动后,经过http://localhost:16686能够打开Jaeger UI。
下载客户端library:
go get github.com/jaegertracing/jaeger-client-go
复制代码
初始化Jaeger tracer:
import (
"context"
"errors"
"fmt"
"io"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/log"
"github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
)
// initJaeger 将jaeger tracer设置为全局tracer
func initJaeger(service string) io.Closer {
cfg := jaegercfg.Configuration{
// 将采样频率设置为1,每个span都记录,方便查看测试结果
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
// 将span发往jaeger-collector的服务地址
CollectorEndpoint: "http://localhost:14268/api/traces",
},
}
closer, err := cfg.InitGlobalTracer(service, jaegercfg.Logger(jaeger.StdLogger))
if err != nil {
panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
}
return closer
}
复制代码
建立tracer,生成root span:
func main() {
closer := initJaeger("in-process")
defer closer.Close()
// 获取jaeger tracer
t := opentracing.GlobalTracer()
// 建立root span
sp := t.StartSpan("in-process-service")
// main执行完结束这个span
defer sp.Finish()
// 将span传递给Foo
ctx := opentracing.ContextWithSpan(context.Background(), sp)
Foo(ctx)
}
复制代码
上述代码建立了一个root span,并将该span经过context
传递给Foo
方法,以便在Foo
方法中将追踪链继续延续下去:
func Foo(ctx context.Context) {
// 开始一个span, 设置span的operation_name=Foo
span, ctx := opentracing.StartSpanFromContext(ctx, "Foo")
defer span.Finish()
// 将context传递给Bar
Bar(ctx)
// 模拟执行耗时
time.Sleep(1 * time.Second)
}
func Bar(ctx context.Context) {
// 开始一个span,设置span的operation_name=Bar
span, ctx := opentracing.StartSpanFromContext(ctx, "Bar")
defer span.Finish()
// 模拟执行耗时
time.Sleep(2 * time.Second)
// 假设Bar发生了某些错误
err := errors.New("something wrong")
span.LogFields(
log.String("event", "error"),
log.String("message", err.Error()),
)
span.SetTag("error", true)
}
复制代码
Foo
方法调用了Bar
,假设在Bar
中发生了一些错误,能够经过span.LogFields
和span.SetTag
将错误记录在追踪链中。 经过上面的例子能够发现,若是要确保追踪链在程序中不断开,须要将函数的第一个参数设置为context.Context
,经过opentracing.ContextWithSpan
将保存到context
中,经过opentracing.StartSpanFromContext
开始一个新的子span。
执行完上面的程序后,打开Jaeger UI: http://localhost:16686/search,能够看到链路追踪的结果: