OpenTelemetry的概念有初步了解后,我们接着以Jaeger为例来演示如何在程序中使用实现链路追踪。

Jaeger

Jaeger\ˈyā-gər\ 是Uber开源的分布式追踪系统,是支持OpenTelemetry的系统之一,也是CNCF项目。本篇将使用Jaeger来演示如何在系统中引入分布式追踪。以下是Opentracing+Jaeger的架构图,针对于使用OpenTelemetry也是如此。

链路跟踪-基本示例 - 图2

准备工作

Jaeger提供了all-in-one镜像,方便我们快速开始测试:

  1. docker run -d --name jaeger \
  2. -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
  3. -p 5775:5775/udp \
  4. -p 6831:6831/udp \
  5. -p 6832:6832/udp \
  6. -p 5778:5778 \
  7. -p 16686:16686 \
  8. -p 14268:14268 \
  9. -p 9411:9411 \
  10. jaegertracing/all-in-one:1.14

如果docker镜像拉取太慢,您可以尝试修改docker拉取站点的镜像地址,例如:http://mirrors.ustc.edu.cn/help/dockerhub.html?highlight=docker

镜像启动后,通过 http://localhost:16686 可以打开Jaeger UI

链路跟踪-基本示例 - 图3

下载客户端library,便于后续代码开发:

  1. go get github.com/jaegertracing/jaeger-client-go

示例仓库地址

我们的示例代码托管到了github上,地址为:https://github.com/gogf/gf-tracing

下载到本地:

  1. git clone https://github.com/gogf/gf-tracing

我们随后的示例介绍都将以此仓库代码为准。

单进程链路跟踪

单进程的链路跟踪即进程内方法之间的调用链关系。这种场景的跟踪没有涉及到分布式跟踪,比较简单,以该示例作为我们入门的一个例子吧。示例代码地址:https://github.com/gogf/gf-tracing/tree/master/examples/inprocess

TracerProvider

初始化Jaeger tracer

  1. package tracing
  2. import (
  3. "go.opentelemetry.io/otel/exporters/trace/jaeger"
  4. "go.opentelemetry.io/otel/sdk/trace"
  5. "strings"
  6. )
  7. // InitJaeger initializes and registers jaeger to global TracerProvider.
  8. //
  9. // The output parameter `flush` is used for waiting exported trace spans to be uploaded,
  10. // which is useful if your program is ending and you do not want to lose recent spans.
  11. func InitJaeger(serviceName, endpoint string) (flush func(), err error) {
  12. var endpointOption jaeger.EndpointOption
  13. if strings.HasPrefix(endpoint, "http") {
  14. // HTTP.
  15. endpointOption = jaeger.WithCollectorEndpoint(endpoint)
  16. } else {
  17. // UDP.
  18. endpointOption = jaeger.WithAgentEndpoint(endpoint)
  19. }
  20. return jaeger.InstallNewPipeline(
  21. endpointOption,
  22. jaeger.WithProcess(jaeger.Process{
  23. ServiceName: serviceName,
  24. }),
  25. jaeger.WithSDK(&trace.Config{
  26. DefaultSampler: trace.AlwaysSample(),
  27. }),
  28. )
  29. }

Root Span

root span即链路中第一个span对象。在这里的单进程场景中,往往需要手动创建一个。随后在方法内部创建的span都会作为它的子级span

在分布式架构的服务间通信场景中,往往不需要开发者手动创建root span,而是由客户端/服务端请求的拦截器来自动创建。

创建tracer,生成root span

  1. func main() {
  2. flush, err := tracing.InitJaeger(ServiceName, JaegerUdpEndpoint)
  3. if err != nil {
  4. g.Log().Fatal(err)
  5. }
  6. defer flush()
  7. ctx, span := gtrace.NewSpan(context.Background(), "main")
  8. defer span.End()
  9. user1 := GetUser(ctx, 1)
  10. g.Dump(user1)
  11. user100 := GetUser(ctx, 100)
  12. g.Dump(user100)
  13. }

上述代码创建了一个root span,并将该span通过context传递给GetUser方法,以便在GetUser方法中将追踪链继续延续下去。

方法间Span创建

  1. // GetUser retrieves and returns hard coded user data for demonstration.
  2. func GetUser(ctx context.Context, id int) g.Map {
  3. ctx, span := gtrace.NewSpan(ctx, "GetUser")
  4. defer span.End()
  5. m := g.Map{}
  6. gutil.MapMerge(
  7. m,
  8. GetInfo(ctx, id),
  9. GetDetail(ctx, id),
  10. GetScores(ctx, id),
  11. )
  12. return m
  13. }
  14. // GetInfo retrieves and returns hard coded user info for demonstration.
  15. func GetInfo(ctx context.Context, id int) g.Map {
  16. ctx, span := gtrace.NewSpan(ctx, "GetInfo")
  17. defer span.End()
  18. if id == 100 {
  19. return g.Map{
  20. "id": 100,
  21. "name": "john",
  22. "gender": 1,
  23. }
  24. }
  25. return nil
  26. }
  27. // GetDetail retrieves and returns hard coded user detail for demonstration.
  28. func GetDetail(ctx context.Context, id int) g.Map {
  29. ctx, span := gtrace.NewSpan(ctx, "GetDetail")
  30. defer span.End()
  31. if id == 100 {
  32. return g.Map{
  33. "site": "https://goframe.org",
  34. "email": "john@goframe.org",
  35. }
  36. }
  37. return nil
  38. }
  39. // GetScores retrieves and returns hard coded user scores for demonstration.
  40. func GetScores(ctx context.Context, id int) g.Map {
  41. ctx, span := gtrace.NewSpan(ctx, "GetScores")
  42. defer span.End()
  43. if id == 100 {
  44. return g.Map{
  45. "math": 100,
  46. "english": 60,
  47. "chinese": 50,
  48. }
  49. }
  50. return nil
  51. }

该示例代码展示了多层级方法间的链路信息传递,即是把ctx上下文变量作为第一个方法参数传递即可。在方法内部,我们通过的固定语法来创建/开始一个Span

  1. ctx, span := gtrace.NewSpan(ctx, "xxx")
  2. defer span.End()

并通过defer的方式调用span.End来结束一个Span,这样可以很好地记录Span生命周期(开始和结束)信息,这些信息都将会展示到链路跟踪系统中。其中gtrace.NewSpan方法的第二个参数spanName我们直接给定方法的名称即可,这样在链路展示中比较有识别性。

效果查看

执行完上面的程序后,终端输出:

链路跟踪-基本示例 - 图4

打开Jaeger UI: http://localhost:16686/search,可以看到链路追踪的结果:链路跟踪-基本示例 - 图5

链路跟踪-基本示例 - 图6

点击详情可以查看具体信息,包括span的调用顺序、调用关系,执行时间轴,以及记录一些Attributes和Events信息,极大的方便我们定位系统中的异常和发现性能瓶颈。:

链路跟踪-基本示例 - 图7

其中的tracing-inprocess是我们tracer的名称,该名称往往是服务名称,由于我们这里只有一个进程和一个tracer,因此这里只看得到一个服务名称。其中的main为我们创建的root span名称,其他的span为基于该root span创建的子级span。由于我们在程序中调用了两次GetUser方法,因此这里也展示了两次GetUser方法的调用。每一次GetUser调用的内部又分别去调用了GetIndo、GetDetail、GetScores三个方法,方法间的调用层级关系展示得非常清晰明了,并且每个方法的调用时长都可以看得到。

关于其中每个span记录的TagsProcess信息其实对应了OpenTelemetry中的AttributesEvents信息,这些信息我们放到后续章节去详细介绍。

Content Menu