目录
4、数据存储选型:Cassandra vs Elasticsearch vs Badger
7、与Prometheus/Grafana集成:Metrics + Traces联动
9、文末三件套
10、总结
开篇:当微服务变成"黑盒"
你是否遇到过这些让人抓狂的场景:
- 用户反馈"页面加载慢",但你不知道慢在哪一层
- 一个API调用涉及8个服务,出问题像大海捞针
- 服务间调用关系像 spaghetti code,理不清头绪
- 明明每个服务都很快,但整体响应就是慢
这就像你家水管漏水,但你不知道漏在厨房、卫生间还是墙里。
链路追踪(Distributed Tracing)就是解决这个问题的"管道探测仪"。而Jaeger,正是CNCF孵化的开源链路追踪神器,由Uber开源,现已成为云原生可观测性的标配工具。
本文将带你从概念到实战,彻底掌握Jaeger的使用。
链路追踪核心概念:Trace、Span、Baggage
Trace(调用链)
Trace是一次完整请求的"人生轨迹"。
从用户点击按钮开始,到最终返回结果,中间经过的所有服务调用构成一条Trace。每个Trace有一个唯一的Trace ID,就像快递单号,全程跟踪不丢失。
┌─────────────────────────────────────────────────────────────┐
│ Trace ID: abc123-def456-ghi789 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Gateway │───▶│ Order │───▶│ Payment │───▶│ Inventory│ │
│ │ 50ms │ │ 120ms │ │ 200ms │ │ 80ms │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │ │
│ └──────────────┴──────────────┴──────────────┘ │
│ Total: 450ms │
└─────────────────────────────────────────────────────────────┘
Span(单次调用)
Span是Trace的"细胞",代表一次具体的操作。
每个Span包含:
- Operation Name:操作名称(如"GET /api/users")
- Start Time / Duration:开始时间和耗时
- Tags:元数据标签(如http.method=GET, http.status_code=200)
- Logs:结构化日志事件
- SpanContext:上下文信息(Trace ID, Span ID等)
// Span的结构示意
type Span struct {
TraceID string // 所属Trace的ID
SpanID string // 自己的ID
ParentSpanID string // 父Span的ID(根Span为空)
OperationName string // 操作名
StartTime time.Time // 开始时间
Duration time.Duration // 耗时
Tags map[string]interface{} // 标签
Logs []Log // 日志事件
}
⚠️ 避坑警告:不要把所有操作都塞进一个Span!粒度太粗会失去追踪意义,粒度太细会产生数据爆炸。建议按"服务边界"或"关键操作"划分。
Baggage(上下文传递)
Baggage是跨服务的"传声筒",让上下文信息随请求流动。
想象你在餐厅点餐,服务员把你的忌口(不吃香菜)写在订单上,传给厨房。Baggage就是干这个的——把用户ID、请求来源、灰度标记等信息,从入口服务传递到所有下游服务。
// 设置Baggage
span.SetBaggageItem("user-id", "12345")
span.SetBaggageItem("request-source", "mobile-app")
// 在下游服务获取Baggage
userID := span.BaggageItem("user-id")
💡 效率技巧:Baggage会随每个Span传播,存储成本较高。只放"必要且精简"的信息,别把整个User对象塞进去!
Jaeger架构全景:从Agent到UI
Jaeger采用经典的分层架构,各组件职责清晰:
┌─────────────────────────────────────────────────────────────────────┐
│ Jaeger UI │
│ (可视化查询与分析界面) │
└─────────────────────────────────────────────────────────────────────┘
▲
│ HTTP/gRPC
┌─────────────────────────────────────────────────────────────────────┐
│ Jaeger Query │
│ (查询API,从存储检索数据) │
└─────────────────────────────────────────────────────────────────────┘
▲
│
┌─────────────────────────────────────────────────────────────────────┐
│ Storage Backend │
│ (Cassandra / Elasticsearch / Badger / Kafka) │
└─────────────────────────────────────────────────────────────────────┘
▲
│
┌─────────────────────────────────────────────────────────────────────┐
│ Jaeger Collector │
│ (接收、验证、转换、批量写入存储) │
│ 处理能力:10万+ Span/秒 │
└─────────────────────────────────────────────────────────────────────┘
▲
│ UDP/HTTP/gRPC
┌─────────────────────────────────────────────────────────────────────┐
│ Jaeger Agent │
│ (本地守护进程,批量转发给Collector) │
│ 部署在每个主机/容器/K8s节点上 │
└─────────────────────────────────────────────────────────────────────┘
▲
│
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service A │ │ Service B │ │ Service C │ │ Service D │
│ (Java) │ │ (Go) │ │ (Python) │ │ (Node.js) │
│ OpenTelemetry│ │ OpenTelemetry│ │ OpenTelemetry│ │ OpenTelemetry│
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
各组件详解
| 组件 | 职责 | 部署方式 |
|---|---|---|
| Agent | 本地数据收集和批量转发 | DaemonSet(K8s)或 Sidecar |
| Collector | 接收多协议数据,处理后写入存储 | 多实例部署,支持负载均衡 |
| Storage | 持久化存储Span数据 | 独立集群或托管服务 |
| Query | 提供查询API | 与Collector同机或独立部署 |
| UI | 可视化界面 | 通常与Query一起部署 |
💡 效率技巧:Agent作为本地"缓冲层",能显著降低Collector压力。即使Collector短暂不可用,Agent也会缓存数据,避免丢数据。
数据存储选型:Cassandra vs Elasticsearch vs Badger
Jaeger支持多种存储后端,选择取决于你的数据规模和查询需求:
存储方案对比
| 特性 | Cassandra | Elasticsearch | Badger |
|---|---|---|---|
| 写入性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 查询灵活性 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 存储压缩比 | ~1:10 | ~1:5 | ~1:10 |
| 运维复杂度 | 高 | 中 | 低 |
| 适用规模 | 大规模生产 | 中小规模+复杂查询 | 小规模/测试 |
| 成本 | 高 | 中 | 低 |
选型建议
选Cassandra,如果你:
- 日Span量超过10亿
- 有专业的运维团队
- 追求极致写入性能
- 查询模式相对固定
选Elasticsearch,如果你:
- 需要灵活的查询能力(全文搜索、聚合分析)
- 已经使用ELK栈
- 中小规模(日Span量<1亿)
- 希望与Metrics/Logs统一存储
选Badger,如果你:
- 本地测试或开发环境
- 小规模部署(单机)
- 不想维护外部存储
- 快速验证Jaeger功能
⚠️ 避坑警告:Elasticsearch作为存储时,索引分片数要合理规划!默认5个分片对小集群是浪费,大集群又可能成为瓶颈。建议根据数据量动态调整。
OpenTelemetry:标准化采集方案
为什么选OpenTelemetry?
以前,每个追踪系统都有自己的SDK(Zipkin用Brave、Jaeger用jaeger-client),就像每个餐厅有自己的点餐App。OpenTelemetry是CNCF推出的"通用语言",一个SDK适配所有后端。
┌─────────────────────────────────────────────────────────────────┐
│ OpenTelemetry SDK │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Traces │ │ Metrics │ │ Logs │ │
│ │ (链路追踪) │ │ (指标) │ │ (日志) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ OpenTelemetry Collector │
│ (接收OTLP,导出到Jaeger/Zipkin/Prometheus等) │
└─────────────────────────────────────────────────────────────────┘
多语言SDK支持
| 语言 | SDK成熟度 | 自动埋点支持 |
|---|---|---|
| Java | ⭐⭐⭐⭐⭐ | 自动(Agent) |
| Go | ⭐⭐⭐⭐⭐ | 手动+部分自动 |
| Python | ⭐⭐⭐⭐⭐ | 自动(Instrumentation) |
| Node.js | ⭐⭐⭐⭐ | 自动(Instrumentation) |
| .NET | ⭐⭐⭐⭐⭐ | 自动(Agent) |
| C++ | ⭐⭐⭐⭐ | 手动 |
Java自动埋点示例
# 下载OpenTelemetry Java Agent
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.32.0/opentelemetry-javaagent.jar
# 启动应用时附加Agent
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://jaeger-collector:4317 \
-jar order-service.jar
**就这么简单!**不用改一行代码,自动追踪HTTP调用、数据库访问、消息队列等。
💡 效率技巧:生产环境建议开启"批处理"和"压缩",减少网络开销:
-Dotel.exporter.otlp.protocol=grpc \
-Dotel.bsp.schedule.delay=1000 \
-Dotel.exporter.otlp.compression=gzip
采样策略:如何在性能和精度间找平衡
如果全量采集,数据量会大到让你破产。采样策略就是"抓重点"的艺术。
三种采样策略
┌─────────────────────────────────────────────────────────────────┐
│ 采样策略对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 【头部采样】Head-based Sampling │
│ ┌─────────┐ │
│ │ Gateway │────┐ 采样决策在请求入口处做出 │
│ └─────────┘ │ 要么全采,要么全不采 │
│ │ ▼ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ └───▶│ Service │───▶│ Service │───▶│ Service │ │
│ │ A │ │ B │ │ C │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 【尾部采样】Tail-based Sampling │
│ ┌─────────┐ │
│ │ Gateway │────┐ 所有Span先缓存,等Trace完成后再决策 │
│ └─────────┘ │ 可以根据整体特征(如错误、延迟)采样 │
│ │ ▼ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ └───▶│ Service │───▶│ Service │───▶│ Service │ │
│ │ A │ │ B │ │ C │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ └──────────────┴──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 采样决策器 │ │
│ │ (延迟>1s? │ │
│ │ 有错误?) │ │
│ └─────────────┘ │
│ │
│ 【自适应采样】Adaptive Sampling │
│ ┌─────────┐ │
│ │ Gateway │────┐ 根据流量模式动态调整采样率 │
│ └─────────┘ │ 高频接口低采样,低频接口高采样 │
│ │ ▼ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ └───▶│ Service │───▶│ Service │───▶│ Service │ │
│ │ A │ │ B │ │ C │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
策略详解
| 策略 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 头部采样 | 在请求入口决定是否采样 | 简单、低延迟 | 可能错过下游错误 | 高流量、均匀分布 |
| 尾部采样 | 收集完整Trace后再决策 | 精准捕获异常 | 内存开销大 | 异常排查为主 |
| 自适应采样 | 根据接口频率动态调整 | 平衡覆盖面和成本 | 配置复杂 | 多接口混合场景 |
Jaeger客户端采样配置
# sampler.type 可选值:
# - const: 固定采样(0或1)
# - probabilistic: 概率采样(0.0-1.0)
# - ratelimiting: 速率限制(每秒N个)
# - remote: 从Agent动态获取配置
sampler:
type: probabilistic
param: 0.1 # 采样10%的请求
# 自适应采样配置
adaptive_sampling:
enabled: true
target_samples_per_second: 10 # 每个操作每秒目标采样数
⚠️ 避坑警告:尾部采样需要缓存未完成Trace的Span,内存占用与最大Trace时长成正比。如果设置不当,OOM会来找你!
与Prometheus/Grafana集成:Metrics + Traces联动
可观测性三大支柱
┌─────────────────────────────────────┐
│ 可观测性三大支柱 │
└─────────────────────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Metrics │ │ Logs │ │ Traces │
│ (指标) │ │ (日志) │ │ (链路追踪) │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ "什么出错了" │ │ "为什么出错" │ │ "错在哪里" │
│ CPU 80% │ │ NullPointer │ │ DB查询慢 │
│ QPS下降 │ │ Exception │ │ 在Order服务 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└───────────────────────────┼───────────────────────────┘
▼
┌─────────────────────────────────────┐
│ 联动分析 = 快速定位根因 │
│ Metrics发现异常 → Traces定位服务 │
│ → Logs查看详情 → 快速修复 │
└─────────────────────────────────────┘
Jaeger与Prometheus集成
场景:Prometheus告警"订单服务P99延迟>2s",如何快速定位?
解决方案:在Grafana中添加Jaeger链接,一键跳转。
# Grafana数据源配置
apiVersion: 1
datasources:
- name: Jaeger
type: jaeger
url: http://jaeger-query:16686
jsonData:
tracesToLogs:
datasourceUid: 'loki' # 关联日志数据源
tags: ['pod', 'namespace']
tracesToMetrics:
datasourceUid: 'prometheus' # 关联指标数据源
queries:
- name: 'Request rate'
query: 'sum(rate(http_requests_total{service="$service"}[5m]))'
从Metrics到Traces的跳转
在Grafana图表中添加Exemplar(典型样本),点击即可跳转到对应Trace:
# PromQL查询,启用exemplar
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
)
┌─────────────────────────────────────────────────────────────────┐
│ Grafana Dashboard │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ P99 Latency: 2.3s ⚠️ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ /\ │ │ │
│ │ │ / \ ┌───┐ ← 点击这个点 │ │ │
│ │ │ ─────/────\──/─────\───────────────────── │ │ │
│ │ │ / \/ \ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 点击后跳转: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Jaeger UI │ │
│ │ Trace ID: a1b2c3d4e5f6... │ │
│ │ Duration: 2.31s │ │
│ │ Services: gateway → order → payment → inventory │ │
│ │ └─ payment: 1.8s (瓶颈!) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
💡 效率技巧:在Jaeger中给关键Span打标签(如error=true、slow_query=true),可以更方便地从Metrics过滤出异常Trace。
实战代码:从零搭建Jaeger环境
步骤1:Docker Compose一键启动
# docker-compose.yml
version: '3.8'
services:
# Jaeger All-in-One(开发测试用)
jaeger:
image: jaegertracing/all-in-one:1.50
container_name: jaeger
ports:
- "16686:16686" # UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "14250:14250" # Model proto
- "14268:14268" # Jaeger HTTP thrift
- "14269:14269" # Admin
- "9411:9411" # Zipkin兼容
environment:
- COLLECTOR_OTLP_ENABLED=true
networks:
- jaeger-net
# 示例服务:订单服务(Go)
order-service:
build: ./order-service
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
- OTEL_SERVICE_NAME=order-service
depends_on:
- jaeger
networks:
- jaeger-net
# 示例服务:支付服务(Go)
payment-service:
build: ./payment-service
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
- OTEL_SERVICE_NAME=payment-service
depends_on:
- jaeger
networks:
- jaeger-net
networks:
jaeger-net:
driver: bridge
步骤2:Go服务接入OpenTelemetry
// order-service/main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var tracer trace.Tracer
func initTracer() func() {
ctx := context.Background()
// 创建OTLP exporter
conn, err := grpc.DialContext(ctx, "jaeger:4317",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
log.Fatal(err)
}
exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
if err != nil {
log.Fatal(err)
}
// 创建TracerProvider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("order-service"),
attribute.String("environment", "dev"),
attribute.String("version", "v1.0.0"),
)),
)
otel.SetTracerProvider(tp)
tracer = tp.Tracer("order-service")
return func() {
if err := tp.Shutdown(ctx); err != nil {
log.Printf("Error shutting down tracer provider: %v", err)
}
}
}
func orderHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 创建Span
ctx, span := tracer.Start(ctx, "create-order")
defer span.End()
// 添加标签
span.SetAttributes(
attribute.String("user.id", r.Header.Get("X-User-ID")),
attribute.String("order.type", "standard"),
)
// 模拟处理
time.Sleep(50 * time.Millisecond)
// 调用支付服务(模拟)
if err := callPaymentService(ctx); err != nil {
span.RecordError(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "Order created successfully!")
}
func callPaymentService(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "call-payment-service")
defer span.End()
// 模拟HTTP调用
req, _ := http.NewRequestWithContext(ctx, "POST", "http://payment-service:8081/pay", nil)
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode))
return nil
}
func main() {
cleanup := initTracer()
defer cleanup()
http.HandleFunc("/order", orderHandler)
log.Println("Order service starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
步骤3:启动并验证
# 启动服务
docker-compose up -d
# 访问Jaeger UI
open http://localhost:16686
# 发送测试请求
for i in {1..10}; do
curl -H "X-User-ID: user-$i" http://localhost:8080/order
done
步骤4:Kubernetes部署
# jaeger-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger
spec:
replicas: 1
selector:
matchLabels:
app: jaeger
template:
metadata:
labels:
app: jaeger
spec:
containers:
- name: jaeger
image: jaegertracing/all-in-one:1.50
env:
- name: COLLECTOR_OTLP_ENABLED
value: "true"
ports:
- containerPort: 16686
- containerPort: 4317
- containerPort: 4318
---
apiVersion: v1
kind: Service
metadata:
name: jaeger
spec:
selector:
app: jaeger
ports:
- name: ui
port: 16686
targetPort: 16686
- name: otlp-grpc
port: 4317
targetPort: 4317
- name: otlp-http
port: 4318
targetPort: 4318
文末三件套
1. 【源码获取】
关注此系列获取后续更新,后台回复 ‘jaeger’ 获取完整源码和配置文件。
2. 【思考题】
你们的链路追踪采样率是多少?是固定采样还是自适应采样?欢迎在评论区分享你的实践经验!
3. 【系列预告】
云原生技术栈系列持续更新中:
- 高可用架构设计 → 多活、熔断、限流实战
- 服务网格Istio → 零信任网络与流量治理
- 混合云部署 → 跨云容灾与数据同步
- 边缘计算 → K3s与边缘节点管理
总结
| 要点 | 内容 |
|---|---|
| 核心概念 | Trace=调用链, Span=单次调用, Baggage=上下文传递 |
| 架构 | Agent → Collector → Storage → Query → UI |
| 存储选型 | 大规模选Cassandra,中小规模选ES,测试用Badger |
| 采集方案 | OpenTelemetry统一SDK,支持多语言自动埋点 |
| 采样策略 | 头部采样简单,尾部采样精准,自适应采样平衡 |
| 性能指标 | 10万+ Span/秒/Collector,存储压缩1:10,查询<100ms |
链路追踪不是银弹,但没有链路追踪的微服务就是"盲人摸象"。Jaeger作为CNCF毕业项目,已经证明了它在生产环境的可靠性。希望本文能帮你快速上手,让性能瓶颈无处遁形!
标签: Jaeger 链路追踪 OpenTelemetry 分布式追踪 可观测性 微服务监控

1371

被折叠的 条评论
为什么被折叠?



