Jaeger 教程

  2018-05-18 14:13:00 CST

  Jingwen Peng

  OpenTracing Microservice

Jaeger

注:阅读本文需要对 OpenTracing 有大致的理解,可参考之前的文章《OpenTracing 详解》

Jaeger 是 Uber 开源的分布式追踪系统,兼容 OpenTracing 标准,于 2017 年 9 月加入 CNCF 基金会。

由来

由于 Uber 的业务增长迅猛,其软件架构也越来越复杂,截止 2015 年下半年,Uber 内部已经有 500 多个微服务在运行,给问题排查和性能分析带来巨大困难。2016 年 4 月,Uber 启动 Jaeger 项目,并逐渐在内部推行分布式追踪系统,一年之后(2017 年 4 月),Uber 宣布正式将 Jaeger 开源。Uber Engineering Blog 有一篇文章介绍了分布式追踪系统在 Uber 的演进过程,建议阅读,《Evolving Distributed Tracing at Uber Engineering》

题外话,2017 年的 QCon 上,滴滴也分享了分布式追踪系统的实践,题为《异构系统链路追踪——滴滴 trace 实践》,可以搜索演讲视频学习一下。

系统架构

分布式追踪的概念和原理在之前的文章中有介绍,这里就不再赘述,直接来看 Jaeger 的架构。

Jaeger

图片来源:Jaeger Architecture

按照数据流向,整体可以分为四个部分:

  • jaeger-client:Jaeger 的客户端,实现了 OpenTracing 的 API,支持主流编程语言。客户端直接集成在目标 Application 中,其作用是记录和发送 Span 到 Jaeger Agent。在 Application 中调用 Jaeger Client Library 记录 Span 的过程通常被称为埋点。
  • jaeger-agent:暂存 Jaeger Client 发来的 Span,并批量向 Jaeger Collector 发送 Span,一般每台机器上都会部署一个 Jaeger Agent。官方的介绍中还强调了 Jaeger Agent 可以将服务发现的功能从 Client 中抽离出来,不过从架构角度讲,如果是部署在 Kubernetes 或者是 Nomad 中,Jaeger Agent 存在的意义并不大。
  • jaeger-collector:接受 Jaeger Agent 发来的数据,并将其写入存储后端,目前支持采用 Cassandra 和 Elasticsearch 作为存储后端。个人还是比较推荐用 Elasticsearch,既可以和日志服务共用同一个 ES,又可以使用 Kibana 对 Trace 数据进行额外的分析。架构图中的存储后端是 Cassandra,旁边还有一个 Spark,讲的就是可以用 Spark 等其他工具对存储后端中的 Span 进行直接分析。
  • jaeger-query & jaeger-ui:读取存储后端中的数据,以直观的形式呈现。

Jaeger 的架构非常清晰,部署起来也很轻松,Docker Hub 中有官方打好的 Image,可以拿来直接用,https://hub.docker.com/u/jaegertracing/。如果是本地测试,可以直接用 Jaeger 的 all-in-one Image,

具体使用

整体思路

首先假设某微服务已经有了中心化的日志收集和处理系统,如果还没有的话,强烈建议部署一套 ELK。再假设对于每一个请求,都会有一个贯穿整个请求流程的 Request ID,如果还没有的话,强烈建议加一个。以上准备完毕后,可以选取一个分布式追踪系统,集成到服务当中,建议采用 Jaeger。重点在最后,在 Trace 的起始处,将 Trace ID 设置为 Request ID,这么一来就打通了日志系统和分布式追踪系统,可以使用同一个 ID 查询请求的事件流和日志流,从此开启了上帝视角。

例子

Jaeger 官方提供了一个 Demo Application,可以用作代码参考,链接为 Hot R.O.D. - Rides on Demand

采样率

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

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
}

最终效果

对于 HTTP 的 API 请求,通常会在 Response Header 中返回当前请求的 Request ID,然后依据 Request ID 就能看到整个请求的全貌。

Jaeger

在 Jaeger UI 查看 Trace 情况

Jaeger

在 Kibana 中分析日志

尾巴

Jaeger 是我比较喜欢的一个开源项目,大概是从去年 8 月份开始使用的,一路跟过来,眼看着它“打败” Zipkin 加入 CNCF 基金会,也眼看着它的项目地址从 https://github.com/uber/jaeger 迁移到 https://github.com/jaegertracing/jaeger,这段时间见证了一个优秀开源项目的成长,很荣幸曾为它贡献过微不足道的代码。

开源是什么?开源是一份见证的喜悦。

参考资料

  1. Jaeger https://www.jaegertracing.io
  2. CNCF Hosts Jaeger https://www.cncf.io/blog/2017/09/13/cncf-hosts-jaeger/
  3. Evolving Distributed Tracing at Uber Engineering https://eng.uber.com/distributed-tracing/
  4. Jaeger Architecture https://www.jaegertracing.io/docs/architecture/
  5. Hot R.O.D. - Rides on Demand https://github.com/jaegertracing/jaeger/tree/master/examples/hotrod

如果您有疑问或建议,请在下方评论区域留言

遵循 BY-NC-ND 协议

评论功能加载中...