简介
受 Dapper 和 OpenZipkin 启发的 Jaeger 是由 Uber 开源的分布式跟踪系统,兼容 OpenTracing 标准。它用于监视和诊断基于微服务的分布式系统,功能包括:
- 分布式上下文传播
- 分布式交易监控
- 根本原因分析
- 服务依赖性分析
- 性能/延迟优化
特性
- 兼容 OpenTracing 的数据模型和工具库
- 多语言支持
- 对每个服务/端点概率使用一致的前期采样
- 多种存储后端支持:Cassandra,Elasticsearch,内存。
- 系统拓扑图
- 自适应采样
- 收集后数据处理管道
架构
jaeger 架构如下:
- Collector 直接写入存储
- Collecter 写入 Kafka 作为初始缓冲区
Jaeger 主要包括以下这些组件:(每一个组件都支持单独部署)
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
:读取存储后端中的数据,以直观的形式呈现。
ingester
:从 Kafka Topic 中读取数据并写入到另一个存储后端(Cassandra, Elasticsearch)。
快速部署本地环境
All in one
多合一是用于快速本地测试的可执行文件,具有内存存储组件,可启动 Jaeger UI,收集器,查询和代理。开始多合一的最简单方法是使用发布到 DockerHub 的预构建映像(单个命令行)。
1
2
3
4
5
6
7
8
9
10
|
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
|
容器需要暴露的端口
端口 |
协议 |
所属模块 |
功能 |
6831 |
UDP |
agent |
accept jaeger.thrift over Thrift-compact protocol (used by most SDKs) |
6832 |
UDP |
agent |
accept jaeger.thrift over Thrift-binary protocol (used by Node.js SDK) |
5775 |
UDP |
agent |
(deprecated) accept zipkin.thrift over compact Thrift protocol (used by legacy clients only) |
5778 |
HTTP |
agent |
serve configs (sampling, etc.) |
16686 |
HTTP |
query |
serve frontend |
14268 |
HTTP |
collector |
accept jaeger.thrift directly from clients |
14250 |
HTTP |
collector |
accept model.proto |
9411 |
HTTP |
collector |
Zipkin compatible endpoint (optional) |
手动安装
根据不同系统,下载安装包
运行 jaeger-all-in-one[.exe]
1
|
jaeger-all-in-one --collector.zipkin.http-port=9411
|
安装好之后可以通过 http://localhost:16686 访问 Jaeger UI。
采样
Jaeger 库实现了一致的前期(或基于头)的采样。例如,假设我们有一个简单的调用图,其中服务 A 调用服务 B,服务 B 调用服务 C:A-> B->C。当服务 A 收到不包含跟踪信息的请求时,Jaeger 跟踪器将开始新的跟踪,为其分配一个随机跟踪 ID,然后根据当前安装的采样策略做出采样决定。采样决策将与请求一起传播到 B 和 C,因此这些服务将不会再次做出采样决策,而是会尊重顶级服务 A 做出的决策。这种方法保证了,如果对跟踪进行了采样,则所有其跨度将记录在后端。如果每个服务都做出自己的抽样决定,那么我们很少会在后端获得完整的跟踪。
支持设置采样率是 Jaeger 的一个亮点,在生产环境中,如果对每个请求都开启 Trace,必然会对系统性能带来一定压力,除此之外,数量庞大的 Span 也会占用大量的存储空间。为了尽量消除分布式追踪采样对系统带来的影响,设置采样率是一个很好的办法。
客户端采样配置
当使用配置对象来实例化 tracer 时,可以通过 sampler.type 和 sampler.param 属性选择采样类型。Jaeger 支持下面四种采样策略:
- Constant(sampler.type=const):const 意为常量,采样器始终对所有 traces 做出相同的决定。sample.param=1 则采样所有 tracer,sample.param=0 则都不采样。
- Probabilistic (sampler.type=probabilistic):概率采样,采样概率介于 0-1 之间,通过 sample.param 属性进行配置,例如,在 sampler.param=0.1 的情况下,将在 10 条 traces 中大约采样 1 条。
- Rate Limiting (sampler.type=ratelimiting):设置每秒的采样次数上限。当 sampler.param=2 的时候,将以每秒 2 条 traces 的速率对请求进行采样。
- Remote (sampler.type=remote):默认配置,client 将从 jaeger-agent 中获取当前服务使用的采样策略,这允许 Client 从 Jaeger Agent 中动态获取采样率设置。
自适应采样器
自适应采样器是一个组合了两个功能的复合采样器:
- 它基于每个操作(即基于 span 操作名称)做出抽样决策。这在 API 服务中特别有用,这些 API 服务的端点的流量可能非常不同,并且对整个服务使用单个概率采样器可能会使某些低 QPS 端点饿死(从不采样)。
- 它支持最低的保证采样率,例如始终允许每秒最多 N 条 traces,然后以一定的概率采样所有高于此值的采样率(一切都是针对每个操作,而不是针对每个服务)。
Jaeger 采样配置示例
收集器可以通过 –sampling.strategies-file 选项通过静态采样策略实例化(如果使用 Remote sample r 配置, 则将传播到相应的服务)。该选项需要一个已定义采样策略的 json 文件的路径。
如果未提供任何配置,则收集器将为所有服务返回默认概率抽样策略,概率为 0.001(0.1%)
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
|
{
"service_strategies": [
{
"service": "foo",
"type": "probabilistic",
"param": 0.8,
"operation_strategies": [
{
"operation": "op1",
"type": "probabilistic",
"param": 0.2
},
{
"operation": "op2",
"type": "probabilistic",
"param": 0.4
}
]
},
{
"service": "bar",
"type": "ratelimiting",
"param": 5
}
],
"default_strategy": {
"type": "probabilistic",
"param": 0.5,
"operation_strategies": [
{
"operation": "/health",
"type": "probabilistic",
"param": 0.0
},
{
"operation": "/metrics",
"type": "probabilistic",
"param": 0.0
}
]
}
}
|
service_strategies 元素定义特定于服务的采样策略,而 operation_strategies 元素定义特定于操作的采样策略。可能有两种策略:概率策略和速率限制,如上所述(注意:operation_strategies 不支持速率限制)。如果服务不是 service_strategies 定义在内的操作,则采用 default_strategy 定义的采样策略。
Go 简单示例
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
|
package main
import (
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"io"
"time"
)
func main() {
tracer, closer, err := NewTracer("demo-service", "127.0.0.1:6831")
if err != nil {
panic(err)
}
defer closer.Close()
// 创建第一个 span A
ASpan := tracer.StartSpan("A")
// 可手动调用 Finish()
defer ASpan.Finish()
CallBService(tracer, ASpan)
}
// CallBService 调用B方法,可以假设是另外一个服务
func CallBService(tracer opentracing.Tracer, parentSpan opentracing.Span) {
// 继承上下文关系,创建子 span
BSpan := tracer.StartSpan("B", opentracing.ChildOf(parentSpan.Context()))
BSpan.Finish()
}
// NewTracer 新建tracer
func NewTracer(serviceName, addr string) (opentracing.Tracer, io.Closer, error) {
cgf := config.Configuration{
// 服务名称
ServiceName: serviceName,
// 采样配置
Sampler: &config.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
// 上报配置
Reporter: &config.ReporterConfig{
BufferFlushInterval: 1 * time.Second,
LogSpans: true,
LocalAgentHostPort: addr,
// 直接上报到collector
// CollectorEndpoint: "http://127.0.0.1:14268/api/traces",
},
}
return cgf.NewTracer()
}
|
Go HTTP 示例
- 客户端
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
|
package main
import (
"fmt"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"io"
"net/http"
"time"
)
func main() {
tracer, closer, err := NewClientTracer("demo-service", "127.0.0.1:6831")
if err != nil {
panic(err)
}
defer closer.Close()
// 创建第一个 span A
ASpan := tracer.StartSpan("A")
// 可手动调用 Finish()
defer ASpan.Finish()
CallUserInfo(tracer, ASpan)
}
// CallUserInfo 请求远程服务,获得用户信息
func CallUserInfo(tracer opentracing.Tracer, parentSpan opentracing.Span) {
// 继承上下文关系,创建子 span
childSpan := tracer.StartSpan(
"B",
opentracing.ChildOf(parentSpan.Context()),
)
url := "http://127.0.0.1:8081/Get?username=jeff"
req, _ := http.NewRequest("GET", url, nil)
// 设置 tag
ext.SpanKindRPCClient.Set(childSpan)
ext.HTTPUrl.Set(childSpan, url)
ext.HTTPMethod.Set(childSpan, "GET")
err := tracer.Inject(childSpan.Context(), opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
if err != nil {
log.Error(err)
}
resp, _ := http.DefaultClient.Do(req)
log.Event(fmt.Sprintf("%v", resp))
defer childSpan.Finish()
}
// NewClientTracer 新建tracer
func NewClientTracer(serviceName, addr string) (opentracing.Tracer, io.Closer, error) {
cgf := config.Configuration{
// 服务名称
ServiceName: serviceName,
// 采样配置
Sampler: &config.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
// 上报配置
Reporter: &config.ReporterConfig{
BufferFlushInterval: 1 * time.Second,
LogSpans: true,
LocalAgentHostPort: addr,
// 直接上报到collector
// CollectorEndpoint: "http://127.0.0.1:14268/api/traces",
},
}
return cgf.NewTracer()
}
|
- 服务端
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
|
package main
import (
"github.com/gin-gonic/gin"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/config"
"io"
"net/http"
"time"
)
func main() {
r := gin.Default()
// 插入中间件处理
r.Use(UseOpenTracing())
r.GET("/Get", GetUserInfo)
err := r.Run("0.0.0.0:8081") // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
if err != nil {
panic(err)
}
}
func GetUserInfo(ctx *gin.Context) {
userName := ctx.Param("username")
log.Event("收到请求,用户名称为:" + userName)
ctx.String(http.StatusOK, "他的博客是 https://luzhifang.github.io/")
}
func UseOpenTracing() gin.HandlerFunc {
handler := func(c *gin.Context) {
tracer, closer, _ := NewServerTracer("userInfoWebService", "127.0.0.1:6831")
spanContext, err := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request.Header))
if err != nil {
log.Error(err)
}
defer closer.Close()
// 生成依赖关系,并新建一个 span、
// 这里很重要,因为生成了 References []SpanReference 依赖关系
startSpan := tracer.StartSpan(c.Request.URL.Path, ext.RPCServerOption(spanContext))
defer startSpan.Finish()
// 记录 tag
// 记录请求 Url
ext.HTTPUrl.Set(startSpan, c.Request.URL.Path)
// Http Method
ext.HTTPMethod.Set(startSpan, c.Request.Method)
// 记录组件名称
ext.Component.Set(startSpan, "Gin-Http")
// 在 header 中加上当前进程的上下文信息
c.Request = c.Request.WithContext(opentracing.ContextWithSpan(c.Request.Context(), startSpan))
// 传递给下一个中间件
c.Next()
// 继续设置 tag
ext.HTTPStatusCode.Set(startSpan, uint16(c.Writer.Status()))
}
return handler
}
// NewServerTracer 新建tracer
func NewServerTracer(serviceName, addr string) (opentracing.Tracer, io.Closer, error) {
cgf := config.Configuration{
// 服务名称
ServiceName: serviceName,
// 采样配置
Sampler: &config.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
// 上报配置
Reporter: &config.ReporterConfig{
BufferFlushInterval: 1 * time.Second,
LogSpans: true,
LocalAgentHostPort: addr,
// 直接上报到collector
// CollectorEndpoint: "http://127.0.0.1:14268/api/traces",
},
}
return cgf.NewTracer()
}
|
参考
https://juejin.cn/post/6844903971010641934
https://xiaoming.net.cn/2021/03/25/Opentracing%E6%A0%87%E5%87%86%E5%92%8CJaeger%E5%AE%9E%E7%8E%B0