架构

如上图所示,Jaeger 主要由以下几部分组成。

Agent(客户端代理)

jaeger的agent,是一个监听在 UDP 端口上接收 span 数据的网络守护进程。 如同大多数分布式系统都拥有一个Agent一样,Jaeger的Agent有以下几类特点:

agent收集并汇聚这些span信息到collector;

agent的被设计成一个基础组件,旨在作为基础架构组件部署到所有宿主机;

agent将client library 和 collector 解耦,为 client library 屏蔽了路由和发现 collector 的细节;

Collector(数据收集处理)

collector,顾名思义,从agent收集traces信息,并通过处理管道处理他们,再写入后端存储(backends)。

当前的collector工作主要是管理trace,建立索引,执行相关转换,并最终存储它们。

Collector中运行着sampling逻辑,根据我们设定的sampling方式对数据进行收集和处理。

Data Store(数据存储)

jaeger的data store是组件的方式。

当前可以支持 Cassandra和ElasticSearch(当然也支持纯内存方式,但是不适用于生产环境).

Query & UI(数据查询与前端界面展示)

Query查询是一种从存储中检索trace,并提供UI以显示它们的服务。上图中就展示了一次Trace的数据流向,作为一次系统作用的数据传播/执行图,即可以在Jaeger UI上展示出来。

部署

Jaeger的部署由于方案的不同,会依赖不同的服务,这些第三方基础服务的部署安装不再该文范围内,如docker、Elasticsearch、Cassandra等

All in one

为了方便大家快速使用,Jaeger直接提供一个All in one的docker镜像,通过All in one的image,我们可以通过以下命令直接启动一套完整的Jaeger tracing系统:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ docker run -d -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:latest

一旦启动成功后,就可以去http://localhost:16686看到Jaeger UI了,如下所示。

注:在All in one模式下,Data Store使用的是内存,因此若重启dockre容器后就看不到之前的数据了。所以,该模式仅可用于前期demo或者验证,不可在生产环境中这样部署。

独立部署

当然我们更推荐的方式是独立部署,独立部署也可以分为docker镜像方式和binary 方式,官网有详细的docker镜像方式启动命令介绍,在这儿就不再粘贴复制了。

关于binary方式部署,可以参看github上的Jaeger的二进制包。地址提供了mac、linux和windows的三大操作系统的binary包。以linux为例,解压后我们可以发现有以下几个bin包,分别清晰地对应了我们之前说的几个模块:

1
2
3
4
5
drwxrwxr-x 3 2000 2000 4.0K May 28 23:29 jaeger-ui-build
-rwxrwxr-x 1 2000 2000  27M May 28 23:29 jaeger-standalone
-rwxrwxr-x 1 2000 2000  22M May 28 23:29 jaeger-query
-rwxrwxr-x 1 2000 2000  25M May 28 23:29 jaeger-collector
-rwxrwxr-x 1 2000 2000  16M May 28 23:29 jaeger-agent

注:Jaeger同时也提供可Kubernetes and OpenShift的模板。可参考github地址有详细介绍

端口说明

通过上述All in one启动方式,我们直接发现了Jaeger启动时占据了很多端口,当然,并不是所有的端口都是必需的,这儿简单列出这些端口的说明如下:

端口 协议 所属模块 功能
5775 UDP agent 通过兼容性Thrift协议,接收Zipkin thrift类型数据
6831 UDP agent 通过兼容性Thrift协议,接收Jaeger thrift类型数据
6832 UDP agent 通过二进制Thrift协议,接收Jaeger thrift类型数据
5778 HTTP agent 配置控制服务接口
16686 HTTP query 客户端前端界面展示端口
14268 HTTP collector 接收客户端Zipkin thrift类型数据
14267 HTTP collector 接收客户端Jaeger thrift类型数据
9411 HTTP collector Zipkin兼容endpoint

采样率

支持设置采样率是 Jaeger 的一个亮点,在生产环境中,如果对每个请求都开启 Trace,必然会对系统性能带来一定压力,除此之外,数量庞大的 Span 也会占用大量的存储空间。为了尽量消除分布式追踪采样对系统带来的影响,设置采样率是一个很好的办法。Jaeger 支持四种采样类别,分别是 const、probabilistic、rateLimiting 和 remote。const 意为常量,采样率的可设置的值为 0 和 1,分别表示关闭采样和全部采样。probabilistic 是按照概率采样,取值可在 0 至 1 之间,例如设置为 0.5 的话意为只对 50% 的请求采样。rateLimiting 则是设置每秒的采样次数上限。remote 是遵循远程设置,取值的含义和 probabilistic 相同,都意为采样的概率,只不过设置为 remote 后,Client 会从 Jaeger Agent 中动态获取采样率设置。

关于Jaeger系统中的采样方式,我们可以通过一个例子来解释。

假设有三个服务A,B,C,且存在一个简单的调用方式:A->B->C, 当服务A收到请求时,Jaeger检查该请求有没有trace信息,如果没有,将为其生成新的trace(TraceId为随机生成的),并基于当前的取样策略进行sampling。该取样策略会随着请求一路广播到服务B和C,因此这些服务将必须再做采样的策略选择。这种采样方式确保了当一个trace被采用后,它的所有后续spans都会被存储起来(若每层服务都再做一次采样策略选择的话,我们就会很难获取到完整的一个trace了)。

Jaeger使用

当我们正是使用jager后,可以通过两种方式来进行查看:

根据TraceId搜索

通过Web UI左上方,可以直接键入TraceId进行某次trace的搜索

根据服务节点查看

通过Web UI左边栏Find Traces,可以详细地进行高级搜索功能,支持服务名,操作,Tag信息(Jaeger中的tag功能,可以在context中加入tag,进行更过的标识)等。当我们确定搜索条件后,就可以查出符合条件的trace信息了,下图为我们一个腾讯云cos代理业务程序的简单请求示例:

Go-SDK

config设定

1
2
3
4
5
6
7
8
9
cfg := &config.Configuration{
    Sampler: &config.SamplerConfig{
        Type:  samplerType,
        Param: samplerParam,
    },
    Reporter: &config.ReporterConfig{
        LogSpans: true,
    },
}

其中关于SamplerConfig的Type可以选择

  • const,全量采集。param采样率设置0,1 分别对应打开和关闭
  • probabilistic ,概率采集。param默认万份之一,0~1之间取值,
  • rateLimiting ,限速采集。param每秒采样的个数
  • remote 动态采集策略。param值于probabilistic的参数一样。在收到实际值之前的初始采样率。改值可以通过环境变量的JAEGER_SAMPLER_PARAM设定

生成jaeger tracer

1
func (c Configuration) NewTracer(options ...Option) (opentracing.Tracer, io.Closer, error)

设置为全局的单例tracer

1
func SetGlobalTracer(tracer Tracer)

生成开始一个Span

1
StartSpan(operationName string, opts ...StartSpanOption) Span

返回span的SpanContext的reference

1
func ContextWithSpan(ctx context.Context, span Span) context.Context

生成子Span

1
func StartSpanFromContext(ctx context.Context, operationName string, opts ...StartSpanOption) (Span, context.Context)

记录关于Span相关的key:value数据

1
LogFields(fields ...log.Field)

到此如果只需要追踪在同一process的链路就已经可以了。如果希望能够追踪不同进程中的链路例如,客户端通过http请求服务端,服务端回应整个链路的追踪需要用到以下的处理。

使用Inject和Extract通过RPC calls传递span context

Client端

添加import

1
2
3
import (
    "github.com/opentracing/opentracing-go/ext"
)

添加Inject

1
2
3
4
5
6
7
8
ext.SpanKindRPCClient.Set(reqSpan)
ext.HTTPUrl.Set(reqSpan, reqURL)
ext.HTTPMethod.Set(reqSpan, "GET")
span.Tracer().Inject(
    span.Context(),
    opentracing.HTTPHeaders,
    opentracing.HTTPHeadersCarrier(req.Header),
)

Server端

添加import

1
2
3
4
5
6
import (
    opentracing "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
    otlog "github.com/opentracing/opentracing-go/log"
    "github.com/yurishkuro/opentracing-tutorial/go/lib/tracing"
)

从request抽取出span context

1
spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header))

通过引用从Client端传来的span context生成新的child span

1
2
span := tracer.StartSpan("format", ext.RPCServerOption(spanCtx))
defer span.Finish()

HTTP Middleware

对于每个 HTTP 请求,可以在 HTTP Server 中增加 Middleware,为每个请求都记录一个 Span,并且在生成 Trace ID 后,将其作为 Request ID 使用。

代码如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package middleware

import (
 "context"
 "net/http"

 "github.com/opentracing/opentracing-go"
 "github.com/opentracing/opentracing-go/ext"
 "github.com/jaegertracing/jaeger-client-go"
 "github.com/pengsrc/go-shared/buffer"

 "example/constants"
)

// TraceSpan is a middleware that initialize a tracing span and injects span
// context to r.Context(). In one word, this middleware kept an eye on the
// whole HTTP request that the server receives.
func TraceSpan(next http.Handler) http.Handler {
 fn := func(w http.ResponseWriter, r *http.Request) {
  tracer := opentracing.GlobalTracer()
  if tracer == nil {
   // Tracer not found, just skip.
   next.ServeHTTP(w, r)
  }

  buf := buffer.GlobalBytesPool().Get()
  buf.AppendString("HTTP ")
  buf.AppendString(r.Method)

  // Start span.
  span := opentracing.StartSpan(buf.String())
  rc := opentracing.ContextWithSpan(r.Context(), span)

  // Set request ID for context.
  if sc, ok := span.Context().(jaeger.SpanContext); ok {
   rc = context.WithValue(rc, constants.RequestID, sc.TraceID().String())
  }

  next.ServeHTTP(w, r.WithContext(rc))

  // Finish span.
  wrapper, ok := w.(WrapResponseWriter)
  if ok {
   ext.HTTPStatusCode.Set(span, uint16(wrapper.Status()))
  }
  span.Finish()
 }
 return http.HandlerFunc(fn)
}

gRPC UnaryServerInterceptor

对于每个 gRPC 请求,也可以增加一个 UnaryServerInterceptor,为每个请求都记录一个 Span,这里用到了 gRPC 的 metadata 来传递 Trace ID 等信息。同样,这里生成 Trace ID 后,也将其作为 Request ID 使用。

代码如下。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package rpc

import (
 "context"
 "encoding/base64"
 "fmt"
 "strings"

 "github.com/opentracing/opentracing-go"
 "github.com/opentracing/opentracing-go/ext"
 "github.com/jaegertracing/jaeger-client-go"
 "github.com/pengsrc/go-shared/buffer"
 "google.golang.org/grpc"
 "google.golang.org/grpc/metadata"

 "example/constants"
)

// TraceSpanClientInterceptor returns a grpc.UnaryServerInterceptor suitable
// for use in a grpc.Dial() call.
//
// For example:
//
//     conn, err := grpc.Dial(
//         address,
//         ...,  // (existing DialOptions)
//         grpc.WithUnaryInterceptor(rpc.TraceSpanClientInterceptor()),
//     )
//
// It writes current trace span to request metadata.
func TraceSpanClientInterceptor() grpc.UnaryClientInterceptor {
 return func(
  ctx context.Context,
  method string, req, resp interface{},
  cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
 ) (err error) {
  span, ctx := opentracing.StartSpanFromContext(ctx, "RPC Client "+method)
  defer span.Finish()

  // Save current span context.
  md, ok := metadata.FromOutgoingContext(ctx)
  if !ok {
   md = metadata.Pairs()
  }
  if err = opentracing.GlobalTracer().Inject(
   span.Context(), opentracing.HTTPHeaders, metadataTextMap(md),
  ); err != nil {
   log.Errorf(ctx, "Failed to inject trace span: %v", err)
  }
  return invoker(metadata.NewOutgoingContext(ctx, md), method, req, resp, cc, opts...)
 }
}

// TraceSpanServerInterceptor returns a grpc.UnaryServerInterceptor suitable
// for use in a grpc.NewServer call.
//
// For example:
//
//     s := grpc.NewServer(
//         ...,  // (existing ServerOptions)
//         grpc.UnaryInterceptor(rpc.TraceSpanServerInterceptor()),
//     )
//
// It reads current trace span from request metadata.
func TraceSpanServerInterceptor() grpc.UnaryServerInterceptor {
 return func(
  ctx context.Context,
  req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
 ) (resp interface{}, err error) {
  // Extract parent trace span.
  md, ok := metadata.FromIncomingContext(ctx)
  if !ok {
   md = metadata.Pairs()
  }
  parentSpanContext, err := opentracing.GlobalTracer().Extract(
   opentracing.HTTPHeaders, metadataTextMap(md),
  )
  switch err {
  case nil:
  case opentracing.ErrSpanContextNotFound:
   log.Info(ctx, "Parent span not found, will start new one.")
  default:
   log.Errorf(ctx, "Failed to extract trace span: %v", err)
  }

  // Start new trace span.
  span := opentracing.StartSpan(
   "RPC Server "+info.FullMethod,
   ext.RPCServerOption(parentSpanContext),
  )
  defer span.Finish()
  ctx = opentracing.ContextWithSpan(ctx, span)

  // Set request ID for context.
  if sc, ok := span.Context().(jaeger.SpanContext); ok {
   ctx = context.WithValue(ctx, constants.RequestID, sc.TraceID().String())
  }

  return handler(ctx, req)
 }
}

const (
 binHeaderSuffix = "_bin"
)

// metadataTextMap extends a metadata.MD to be an opentracing textmap
type metadataTextMap metadata.MD

// Set is a opentracing.TextMapReader interface that extracts values.
func (m metadataTextMap) Set(key, val string) {
 // gRPC allows for complex binary values to be written.
 encodedKey, encodedVal := encodeKeyValue(key, val)
 // The metadata object is a multimap, and previous values may exist, but for opentracing headers, we do not append
 // we just override.
 m[encodedKey] = []string{encodedVal}
}

// ForeachKey is a opentracing.TextMapReader interface that extracts values.
func (m metadataTextMap) ForeachKey(callback func(key, val string) error) error {
 for k, vv := range m {
  for _, v := range vv {
   if decodedKey, decodedVal, err := metadata.DecodeKeyValue(k, v); err == nil {
    if err = callback(decodedKey, decodedVal); err != nil {
     return err
    }
   } else {
    return fmt.Errorf("failed decoding opentracing from gRPC metadata: %v", err)
   }
  }
 }
 return nil
}

// encodeKeyValue encodes key and value qualified for transmission via gRPC.
// note: copy pasted from private values of grpc.metadata
func encodeKeyValue(k, v string) (string, string) {
 k = strings.ToLower(k)
 if strings.HasSuffix(k, binHeaderSuffix) {
  val := base64.StdEncoding.EncodeToString([]byte(v))
  v = string(val)
 }
 return k, v
}

参考:

Jaeger-分布式调用链跟踪系统理论与实战 Jaeger 教程