Jaeger 是一个开源的分布式追踪系统,由 Uber Technologies 开发并捐赠给 Cloud Native Computing Foundation (CNCF)。它用于监控和排除基于微服务架构的复杂分布式系统中的故障。通过收集、存储和可视化请求在各个服务之间的调用链,Jaeger 帮助开发者理解请求流、识别性能瓶颈和诊断错误。

核心思想:Jaeger 实现了 OpenTracing API(现已融合到 OpenTelemetry 中),通过在请求流经每个服务时生成和传递独特的追踪上下文 (Trace Context),并在每个服务中记录操作信息 (Span),将分散的日志和指标关联起来,形成完整的请求链路视图。


一、为什么需要分布式追踪?

在单体应用时代,通过日志和 APM (Application Performance Monitoring) 工具可以相对容易地定位问题。然而,随着服务架构向微服务演进,一个用户请求可能涉及数十甚至上百个独立服务的协同处理。这带来了新的挑战:

  1. 请求链路复杂性:难以追踪一个请求从前端到后端,再穿越多个微服务的完整路径。
  2. 性能瓶颈识别:难以确定哪个服务或哪个环节导致了请求延迟。
  3. 故障定位:当请求失败时,难以 pinpoint 是哪个服务抛出了异常,以及是上游还是下游服务的影响。
  4. 调用依赖分析:难以可视化服务之间的相互调用关系,以及请求的 fan-out/fan-in 模式。

分布式追踪系统 (Distributed Tracing System) 正是为了解决这些问题而生。它提供了对整个请求生命周期的可见性,将分散的事件关联起来,形成统一的视图。

二、Jaeger 核心概念

Jaeger 建立在 OpenTracing/OpenTelemetry 规范之上,其核心概念包括:

2.1 Trace (追踪)

Trace 代表了分布式系统中一个完整的操作或请求。它由一个或多个 Span 组成,这些 Span 共同描述了从请求开始到完成的全过程。一个 Trace 通常由一个唯一的 ID 标识。

2.2 Span (跨度)

Span 代表 Trace 中一个独立的、命名的操作单元。每个 Span 都有开始时间、结束时间、操作名称,以及一组标签 (Tags) 和日志 (Logs)。Span 可以嵌套,形成父子关系,以表示操作的层级结构。

  • 操作名称 (Operation Name):描述 Span 所代表的操作,例如 HTTP GET /users/{id}, authenticateUser, database.query
  • 开始时间 (Start Time):Span 开始执行的时间戳。
  • 结束时间 (End Time):Span 完成执行的时间戳。
  • Duration (持续时间):结束时间减去开始时间,表示操作耗时。
  • Span Context (Span 上下文):包含 Trace ID、Span ID 和其他追踪元数据,用于在服务之间传递追踪信息。
  • Tags (标签):键值对,用于存储 Span 的元数据,例如 HTTP 状态码、数据库查询语句、用户 ID 等。常用于筛选和搜索 Trace。
  • Logs (日志):时间戳事件,记录特定时间点的日志信息,例如异常发生、关键业务事件等。

2.3 Span 之间的关系

Span 之间可以存在关系,最常见的是父子关系:

  • ChildOf (子级):一个 Span 是另一个 Span 的直接子级。例如,一个 HTTP 请求 Span 可能是处理该请求的数据库查询 Span 的父级。
  • FollowsFrom (跟随):一个 Span 逻辑上依赖于另一个 Span,但不是直接的父子关系,例如异步操作。

Trace 和 Span 示意图

在上图中,Trace: 用户请求 包含 Span A, Span B, Span C, Span E, Span FSpan ASpan BSpan E 的父级。Span BSpan C 的父级。

2.4 Jaeger 架构

Jaeger 的主要组件包括:

  1. Jaeger Client (客户端):集成到应用程序中,用于生成和报告 Span。它实现了 OpenTracing/OpenTelemetry API。
  2. Agent (代理):一个网络守护进程,运行在与应用程序相同的宿主机上。它接收 Jaeger Client 发送的 Span,并批量发送给 Collector。这减轻了 Client 直接与 Collector 通信的负担,并提供了更可靠的传输。
  3. Collector (收集器):接收 Agent 发送的 Span,对它们进行验证、处理和索引,然后写入存储后端。
  4. Query (查询服务):接收 UI 请求,从存储后端检索 Trace 数据,并提供给 Jaeger UI。
  5. Storage (存储后端):用于持久化 Trace 数据。支持 Cassandra, Elasticsearch, Kafka 等。
  6. UI (用户界面):提供 Web 界面,用于可视化、搜索和分析 Trace 数据。

Jaeger 架构示意图

三、Go 语言集成 Jaeger (OpenTelemetry)

在 Go 语言中集成 Jaeger,目前推荐使用 OpenTelemetry。OpenTelemetry 是 CNCF 的一个可观测性项目,旨在提供一套标准的 API、SDK 和工具,用于生成、收集和导出追踪、指标和日志。它已将 OpenTracing 和 OpenCensus 合并。

以下是一个 Go 应用程序如何使用 OpenTelemetry (导出到 Jaeger) 的基本示例。

3.1 准备工作

  1. 运行 Jaeger All-in-One
    为了方便演示,可以使用 Docker 运行 Jaeger 的 All-in-One 镜像,它包含了 Agent, Collector, Query 和 UI。

    1
    2
    3
    4
    5
    6
    7
    8
    docker run -d --name jaeger \
    -e COLLECTOR_OTLP_ENABLED=true \
    -p 6831:6831/udp \
    -p 6832:6832/udp \
    -p 16686:16686 \
    -p 4317:4317 \
    -p 4318:4318 \
    jaegertracing/all-in-one:latest
    • -p 16686:16686:Jaeger UI 端口。访问 http://localhost:16686 查看追踪。
    • -p 4317:4317:OTLP/gRPC 端口,用于 Go 应用发送追踪数据。
  2. 创建 Go 项目

    1
    2
    3
    mkdir go-jaeger-example
    cd go-jaeger-example
    go mod init go-jaeger-example
  3. 安装 OpenTelemetry Go SDK 和 Jaeger Exporter

    1
    2
    3
    4
    5
    6
    go get go.opentelemetry.io/otel \
    go.opentelemetry.io/otel/trace \
    go.opentelemetry.io/otel/sdk/trace \
    go.opentelemetry.io/otel/exporters/otlp/otlptrace \
    go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc \
    google.golang.org/grpc

3.2 Go 示例代码

创建一个 main.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
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package main

import (
"context"
"fmt"
"io"
"log"
"net/http"
"time"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace" // 引入 trace 包
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

// serviceName 定义当前服务的名称,将在 Jaeger UI 中显示
const serviceName = "go-example-service"

// initTracerProvider 初始化 OpenTelemetry TracerProvider,并配置 Jaeger Exporter
func initTracerProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {
// 创建 OTLP gRPC 追踪导出器,连接到 Jaeger Collector 的 OTLP gRPC 端口 (4317)
conn, err := grpc.DialContext(ctx, "localhost:4317",
// grpc.WithInsecure() is deprecated. Use credentials.WithInsecure() instead.
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(), // 阻塞直到连接成功
)
if err != nil {
return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err)
}

traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
if err != nil {
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
}

// 定义资源,包含服务名称
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
attribute.String("environment", "development"),
)

// 创建 TracerProvider
bsp := sdktrace.NewBatchSpanProcessor(traceExporter) // 批量处理 Span
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()), // 总是采样所有 Span
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(bsp),
)

// 将 TracerProvider 设置为全局默认
otel.SetTracerProvider(tracerProvider)

return tracerProvider, nil
}

// simulateWork 模拟一些耗时操作
func simulateWork(ctx context.Context, parentSpan trace.Span) {
// 创建子 Span
_, span := otel.Tracer(serviceName).Start(ctx, "simulateWork", trace.WithLinks(trace.Link{SpanContext: parentSpan.SpanContext()}))
defer span.End()

log.Println("Simulating some work...")
time.Sleep(100 * time.Millisecond) // 模拟耗时 100ms
span.SetAttributes(attribute.String("work.status", "completed"))
span.AddEvent("WorkStep1 Finished", trace.WithAttributes(attribute.Int("progress", 50)))
time.Sleep(50 * time.Millisecond)
span.AddEvent("WorkStep2 Finished", trace.WithAttributes(attribute.Int("progress", 100)))
}

// handler 是 HTTP 请求处理器
func handler(w http.ResponseWriter, r *http.Request) {
// 从 HTTP 请求上下文中提取父 Span context (如果存在)
// 如果是第一个服务,会创建一个新的 Trace
ctx, span := otel.Tracer(serviceName).Start(r.Context(), "HTTP GET /hello",
trace.WithSpanKind(trace.SpanKindServer), // 标记为服务器端 Span
trace.WithAttributes(
semconv.HTTPMethod(r.Method),
semconv.HTTPTarget(r.URL.Path),
semconv.NetHostName(r.Host),
),
)
defer span.End()

// 将 Span context 注入到 HTTP 响应头部,以便下游服务可以继续追踪
// 如果是纯 Go 服务间调用,可以手动传递 ctx

// 模拟处理逻辑
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}

// 调用模拟的工作函数,它会创建子 Span
simulateWork(ctx, span) // 传递当前 Span 作为父 Span

// 模拟对外部服务的调用 (使用 http.Client 进行追踪)
_, subSpan := otel.Tracer(serviceName).Start(ctx, "CallExternalService",
trace.WithLinks(trace.Link{SpanContext: span.SpanContext()}),
)
defer subSpan.End()

// Create a new request context with the current span injected
req, err := http.NewRequestWithContext(ctx, "GET", "http://example.com", nil) // 模拟调用外部服务
if err != nil {
span.RecordError(err) // 记录错误
http.Error(w, "Failed to create request", http.StatusInternalServerError)
return
}

// Use HTTP propagator to inject trace context into the outgoing request headers
// This is crucial for distributed tracing across services
otel.GetTextMapPropagator().Inject(ctx, otel.HeaderCarrier(req.Header))

// In a real microservice scenario, you would use this client to call another internal service
client := http.Client{}
externalResp, err := client.Do(req)
if err != nil {
span.RecordError(err) // 记录错误
http.Error(w, "Failed to call external service", http.StatusInternalServerError)
return
}
defer externalResp.Body.Close()

subSpan.SetAttributes(attribute.Int("http.status_code", externalResp.StatusCode))
_, _ = io.ReadAll(externalResp.Body) // 读取响应体以确保连接关闭

// 记录日志事件
span.AddEvent("Processing complete", trace.WithAttributes(attribute.String("user.name", name)))
span.SetAttributes(attribute.String("response.message", "Greeting sent"))

response := fmt.Sprintf("Hello, %s! (Processed by %s)", name, serviceName)
fmt.Fprintln(w, response)
}

func main() {
ctx := context.Background()

// 初始化 TracerProvider
tp, err := initTracerProvider(ctx)
if err != nil {
log.Fatalf("failed to initialize TracerProvider: %v", err)
}
defer func() {
// 在应用程序退出时确保所有 Span 都被刷新和导出
if err := tp.Shutdown(ctx); err != nil {
log.Fatalf("failed to shutdown TracerProvider: %v", err)
}
}()

// 注册 HTTP 处理器
http.HandleFunc("/hello", handler)

log.Printf("Starting %s on :8080...", serviceName)
log.Fatal(http.ListenAndServe(":8080", nil))
}

3.3 运行与验证

  1. 确保 Jaeger All-in-One Docker 容器正在运行。

  2. 编译并运行 Go 应用程序:

    1
    go run main.go
  3. 在浏览器中访问 http://localhost:8080/hello?name=GoUser

  4. 打开 Jaeger UI:http://localhost:16686

  5. 在 Jaeger UI 中,选择 Servicego-example-service,然后点击 Find Traces
    你将看到一个完整的 Trace,其中包含 HTTP GET /hello 主 Span,以及其子 Span simulateWorkCallExternalService。每个 Span 都将包含自定义的 Tags 和 Logs。

3.4 关键点解释

  • initTracerProvider
    • 创建 otlptracegrpc.New 导出器,通过 gRPC 将 Span 发送到 Jaeger Collector (默认端口 4317)。
    • resource.NewWithAttributes 用于定义服务的基本信息,如 service.name,这在 Jaeger UI 中用于标识服务。
    • sdktrace.NewBatchSpanProcessor 批量处理 Span,减少网络开销。
    • sdktrace.WithSampler(sdktrace.AlwaysSample()) 配置为总是采样所有 Span (生产环境可能需要更智能的采样策略)。
    • otel.SetTracerProvider 将配置好的 TracerProvider 设置为全局默认,方便在代码中获取 Tracer
  • otel.Tracer(serviceName).Start(ctx, "Operation Name", ...)
    • otel.Tracer(serviceName) 获取一个 Tracer 实例。
    • Start 方法开始一个新的 Span。它返回一个新的上下文 (ctx) 和 Span 对象。新的 ctx 会包含新 Span 的上下文信息。
    • trace.WithSpanKind(trace.SpanKindServer) 标记 Span 的类型。
    • semconv (Semantic Conventions) 提供了一组标准化的属性键,有助于统一不同服务报告的追踪数据。
    • trace.WithLinks(trace.Link{SpanContext: parentSpan.SpanContext()}):在新 Span 和其父 Span 之间建立链接,确保它们在同一 Trace 中。在 OpenTelemetry 中,如果 Start 方法的第一个参数 ctx 中已经包含父 Span 信息,则会自动建立父子关系,无需显式 WithLinks。这里的 WithLinks 更多是示例性质。
  • defer span.End():确保 Span 在函数结束时被关闭,并计算其持续时间。
  • span.SetAttributes()span.AddEvent()
    • SetAttributes 添加键值对标签,用于记录 Span 的元数据。
    • AddEvent 记录时间戳事件,类似于日志,但更紧密地绑定到 Span。
  • 分布式上下文传播:在 handler 中,otel.Tracer(serviceName).Start(r.Context(), ...) 自动从传入的 http.Request 中提取追踪上下文。在调用外部服务时,otel.GetTextMapPropagator().Inject(ctx, otel.HeaderCarrier(req.Header)) 将当前的追踪上下文注入到传出请求的 HTTP 头部,确保追踪链路的连续性。这是分布式追踪的核心机制

四、生产环境考虑

  1. 采样策略 (Sampling)
    在生产环境中,不可能对所有请求进行追踪,这会产生巨大的性能开销和存储成本。需要配置采样器:
    • AlwaysSample:总是采样。
    • NeverSample:从不采样。
    • TraceIDRatioBased:基于 Trace ID 决定是否采样,例如 1% 的请求。
    • ParentBased:如果父 Span 已被采样,则子 Span 也被采样。
  2. 异步发送与批量处理
    使用 sdktrace.NewBatchSpanProcessor 异步批量发送 Span,减少对应用程序性能的影响。
  3. 日志与指标集成
    将 Trace ID 注入到应用程序日志中,以便在查看日志时能够快速跳转到 Jaeger 中的相关 Trace。未来 OpenTelemetry 将提供统一的 API 来关联追踪、指标和日志。
  4. 配置外部化
    通过环境变量或配置文件来配置 Jaeger Collector 地址、采样率等,方便部署。
  5. 高可用性和伸缩性
    部署多个 Jaeger Collector 实例,并使用 Kafka 等消息队列作为 Collector 和 Storage 之间的缓冲。存储后端也需要具备高可用和伸缩能力。

五、总结

Jaeger 作为一款强大的分布式追踪系统,结合 OpenTelemetry Go SDK,为 Go 语言开发的微服务架构提供了出色的可观测性。它使得开发者能够:

  • 可视化请求流:清晰地看到请求在各个服务间的调用路径。
  • 识别性能瓶颈:通过 Span 的持续时间快速定位哪个服务或操作导致了延迟。
  • 加速故障诊断:在错误发生时,能够快速找到出错的服务和上下文信息。
  • 理解服务依赖:分析服务之间的调用关系和拓扑结构。

通过在 Go 应用程序中正确集成 OpenTelemetry 和 Jaeger,我们能够获得对复杂分布式系统深层次的洞察力,从而提升系统的稳定性、性能和可维护性。