对 OpenTelemetry
的概念有初步了解后,我们接着以 Jaeger
为例来演示如何在程序中使用实现链路追踪。
Jaeger
Jaeger\ˈyā-gər\ 是Uber开源的分布式追踪系统,是支持 OpenTelemetry
的系统之一,也是 CNCF
项目。本篇将使用 Jaeger
来演示如何在系统中引入分布式追踪。以下是 Opentracing+Jaeger
的架构图,针对于使用 OpenTelemetry
也是如此。
准备工作
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
如果 docker
镜像拉取太慢,您可以尝试修改 docker
拉取站点的镜像地址,例如: http://mirrors.ustc.edu.cn/help/dockerhub.html?highlight=docker
镜像启动后,通过 http://localhost:16686 可以打开 Jaeger UI
。
下载客户端 library
,便于后续代码开发:
go get github.com/jaegertracing/jaeger-client-go
示例仓库地址
我们的示例代码托管到了 github
上,地址为: https://github.com/gogf/gf-tracing
下载到本地:
git clone https://github.com/gogf/gf-tracing
我们随后的示例介绍都将以此仓库代码为准。
单进程链路跟踪
单进程的链路跟踪即进程内方法之间的调用链关系。这种场景的跟踪没有涉及到分布式跟踪,比较简单,以该示例作为我们入门的一个例子吧。示例代码地址: https://github.com/gogf/gf-tracing/tree/master/examples/inprocess
TracerProvider
初始化 Jaeger tracer
:
package tracing
import (
"strings"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
// InitJaeger initializes and registers jaeger to global TracerProvider.
//
// The output parameter `tp` is used for waiting exported trace spans to be uploaded,
// which is useful if your program is ending and you do not want to lose recent spans.
func InitJaeger(serviceName, endpoint string) (tp *trace.TracerProvider, err error) {
var endpointOption jaeger.EndpointOption
if strings.HasPrefix(endpoint, "http") {
// HTTP.
endpointOption = jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(endpoint))
} else {
// UDP.
endpointOption = jaeger.WithAgentEndpoint(jaeger.WithAgentHost(endpoint))
}
// Create the Jaeger exporter
exp, err := jaeger.New(endpointOption)
if err != nil {
return nil, err
}
tp = trace.NewTracerProvider(
// Always be sure to batch in production.
trace.WithBatcher(exp),
// Record information about this application in an Resource.
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
Root Span
root span
即链路中第一个 span
对象。在这里的单进程场景中,往往需要手动创建一个。随后在方法内部创建的 span
都会作为它的子级 span
。
在分布式架构的服务间通信场景中,往往不需要开发者手动创建 root span
,而是由客户端/服务端请求的拦截器来自动创建。
创建 tracer
,生成 root span
:
func main() {
flush, err := tracing.InitJaeger(ServiceName, JaegerUdpEndpoint)
if err != nil {
g.Log().Fatal(err)
}
defer flush()
ctx, span := gtrace.NewSpan(context.Background(), "main")
defer span.End()
user1 := GetUser(ctx, 1)
g.Dump(user1)
user100 := GetUser(ctx, 100)
g.Dump(user100)
}
上述代码创建了一个 root span
,并将该 span
通过 context
传递给 GetUser
方法,以便在 GetUser
方法中将追踪链继续延续下去。
方法间Span创建
// GetUser retrieves and returns hard coded user data for demonstration.
func GetUser(ctx context.Context, id int) g.Map {
ctx, span := gtrace.NewSpan(ctx, "GetUser")
defer span.End()
m := g.Map{}
gutil.MapMerge(
m,
GetInfo(ctx, id),
GetDetail(ctx, id),
GetScores(ctx, id),
)
return m
}
// GetInfo retrieves and returns hard coded user info for demonstration.
func GetInfo(ctx context.Context, id int) g.Map {
ctx, span := gtrace.NewSpan(ctx, "GetInfo")
defer span.End()
if id == 100 {
return g.Map{
"id": 100,
"name": "john",
"gender": 1,
}
}
return nil
}
// GetDetail retrieves and returns hard coded user detail for demonstration.
func GetDetail(ctx context.Context, id int) g.Map {
ctx, span := gtrace.NewSpan(ctx, "GetDetail")
defer span.End()
if id == 100 {
return g.Map{
"site": "https://goframe.org",
"email": "john@goframe.org",
}
}
return nil
}
// GetScores retrieves and returns hard coded user scores for demonstration.
func GetScores(ctx context.Context, id int) g.Map {
ctx, span := gtrace.NewSpan(ctx, "GetScores")
defer span.End()
if id == 100 {
return g.Map{
"math": 100,
"english": 60,
"chinese": 50,
}
}
return nil
}
该示例代码展示了多层级方法间的链路信息传递,即是把 ctx
上下文变量作为第一个方法参数传递即可。在方法内部,我们通过的固定语法来创建/开始一个 Span
:
ctx, span := gtrace.NewSpan(ctx, "xxx")
defer span.End()
并通过 defer
的方式调用 span.End
来结束一个 Span
,这样可以很好地记录 Span
生命周期(开始和结束)信息,这些信息都将会展示到链路跟踪系统中。其中 gtrace.NewSpan
方法的第二个参数 spanName
我们直接给定方法的名称即可,这样在链路展示中比较有识别性。
效果查看
执行完上面的程序后,终端输出:
打开 Jaeger UI
: http://localhost:16686/search,可以看到链路追踪的结果:
点击详情可以查看具体信息,包括 span
的调用顺序、调用关系,执行时间轴,以及记录一些Attributes和 Events
信息,极大的方便我们定位系统中的异常和发 现性能瓶颈。:
其中的 tracing-inprocess
是我们 tracer
的名称,该名称往往是服务名称,由于我们这里只有一个进程和一个 tracer
,因此这里只看得到一个服务名称。其中的 main
为我们创建的 root span
名称,其他的 span
为基于该 root span
创建的子级 span
。由于我们在程序中调用了两次 GetUser
方法,因此这里也展示了两次 GetUser
方法的调用。每一次 GetUser
调用的内部又分别去调用了 GetIndo、GetDetail、GetScores
三个方法,方法间的调用层级关系展示得非常清晰明了,并且每个方法的调用时长都可以看得到。
关于其中每个 span
记录的 Tags
和 Process
信息其实对应了 OpenTelemetry
中的 Attributes
和 Events
信息,这些信息我们放到后续章节去详细介绍。