从Jaeger走到OpenTelemetry:分布式追踪笔记

分布式追踪笔记我没按教科书顺序做。

这次做微服务可观测性改造,从 Jaeger 到 OpenTelemetry,。

为什么需要分布式追踪

微服务架构下,一个请求会经过多个服务:

用户 -> 网关 -> 用户服务 -> 订单服务 -> 支付服务 -> 通知服务

出了问题怎么排查?哪个服务慢了?哪个服务出错了?

分布式追踪提供:

  • 完整的请求链路
  • 每个服务的耗时
  • 服务间的依赖关系

Jaeger 基础

部署 Jaeger

# 使用 Docker 部署
docker run -d --name jaeger \
  -e COLLECTOR_ZIPKIN_HOST_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。

Jaeger 架构

Jaeger 由以下组件组成:

  • Client SDK:在应用中埋点
  • Agent:接收 trace 数据
  • Collector:处理 trace 数据
  • Storage:存储 trace 数据
  • Query:查询 trace 数据

集成 Jaeger

Go 应用

import (
    "context"
    "net/http"
    
    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go"
    jaegercfg "github.com/uber/jaeger-client-go/config"
    "github.com/uber/jaeger-client-go/zipkin"
)

// 初始化 Jaeger Tracer
func initTracer(serviceName string) (opentracing.Tracer, io.Closer, error) {
    cfg := jaegercfg.Configuration{
        ServiceName: serviceName,
        Sampler: &jaegercfg.SamplerConfig{
            Type:  jaeger.SamplerTypeConst,
            Param: 1,
        },
        Reporter: &jaegercfg.ReporterConfig{
            LogSpans: true,
        },
    }
    
    tracer, closer, err := cfg.NewTracer()
    if err != nil {
        return nil, nil, err
    }
    
    opentracing.SetGlobalTracer(tracer)
    return tracer, closer, nil
}

// 使用 Tracer
func handler(w http.ResponseWriter, r *http.Request) {
    span := opentracing.SpanFromContext(r.Context())
    if span == nil {
        span = opentracing.StartSpan("handler")
        defer span.Finish()
    }
    
    // 创建子 Span
    childSpan := opentracing.StartSpan("database_query", opentracing.ChildOf(span.Context()))
    defer childSpan.Finish()
    
    // 模拟数据库查询
    time.Sleep(100 * time.Millisecond)
    
    w.Write([]byte("Hello"))
}

Node.js 应用

const initTracer = require('jaeger-client').initTracer;

// 初始化 Tracer
const tracer = initTracer({
  serviceName: 'my-service',
  sampler: {
    type: 'const',
    param: 1,
  },
  reporter: {
    logSpans: true,
  },
});

// 使用 Tracer
app.get('/', (req, res) => {
  const span = tracer.startSpan('http_request');
  span.setTag('http.url', req.url);
  span.setTag('http.method', req.method);
  
  // 模拟业务逻辑
  setTimeout(() => {
    span.finish();
    res.send('Hello');
  }, 100);
});

OpenTelemetry 迁移

为什么迁移到 OpenTelemetry

Jaeger 的 SDK 有一些限制:

  • 只支持特定语言
  • 采样策略不够灵活
  • 与其他工具集成麻烦

OpenTelemetry 提供了:

  • 统一的 API
  • 多种导出格式
  • 更好的扩展性

OpenTelemetry 架构

OpenTelemetry 由以下组成:

  • API:定义接口
  • SDK:实现 API
  • Collector:处理遥测数据
  • Exporters:导出数据到后端

集成 OpenTelemetry

Go 应用

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/jaeger"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
    "go.opentelemetry.io/otel/trace"
)

// 初始化 OpenTelemetry
func initTracer(serviceName string) (trace.TracerProvider, error) {
    exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("localhost:14268/api/traces")))
    if err != nil {
        return nil, err
    }
    
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String(serviceName),
        )),
    )
    
    otel.SetTracerProvider(tp)
    return tp, nil
}

// 使用 Tracer
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    tracer := otel.Tracer("example")
    
    ctx, span := tracer.Start(ctx, "handler")
    defer span.End()
    
    // 创建子 Span
    ctx, childSpan := tracer.Start(ctx, "database_query")
    defer childSpan.End()
    
    time.Sleep(100 * time.Millisecond)
    
    w.Write([]byte("Hello"))
}

Node.js 应用

const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');

// 初始化 Provider
const provider = new NodeTracerProvider({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'my-service',
  }),
});

const exporter = new JaegerExporter({
  endpoint: 'http://localhost:14268/api/traces',
});

provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();

// 使用 Tracer
const tracer = opentelemetry.trace.getTracer('example');

app.get('/', (req, res) => {
  const span = tracer.startSpan('http_request');
  span.setAttribute('http.url', req.url);
  span.setAttribute('http.method', req.method);
  
  setTimeout(() => {
    span.end();
    res.send('Hello');
  }, 100);
});

采样策略

固定采样

所有请求都采样,适合小流量场景。

sampler := sdktrace.TraceIDRatioBased(1.0)

概率采样

按比例采样,适合高流量场景。

sampler := sdktrace.TraceIDRatioBased(0.1) // 10% 采样

自定义采样

根据业务逻辑决定是否采样。

type CustomSampler struct{}

func (s *CustomSampler) ShouldSample(parameters sdktrace.SamplingParameters) sdktrace.SamplingResult {
    // 只采样特定的请求
    if strings.Contains(parameters.Name, "api") {
        return sdktrace.SamplingResult{
            Decision:   sdktrace.RecordAndSample,
            Tracestate: trace.SpanFromContext(parameters.ParentContextContext).TraceState(),
        }
    }
    
    return sdktrace.SamplingResult{
        Decision: sdktrace.Drop,
    }
}

上下文传播

HTTP 请求传播

import (
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

// 使用 otelhttp 包装 HTTP Handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    tracer := otel.Tracer("example")
    
    ctx, span := tracer.Start(ctx, "handler")
    defer span.End()
    
    w.Write([]byte("Hello"))
})

http.Handle("/", otelhttp.NewHandler(handler, "handler"))

gRPC 请求传播

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)

// 使用 otelgrpc 包装 gRPC Server
server := grpc.NewServer(
    grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
)

踩过的坑

坑一:采样率设置不当

一开始设置采样率 100%,结果存储压力大,Jaeger UI 很卡。

解决:根据流量调整采样率,生产环境用 1-10%。

坑二:上下文传播失败

服务间调用没有传播 trace context,导致链路断开。

解决

  • HTTP 使用 otprop.HTTPTracePropagator
  • gRPC 使用 otelgrpc 自动处理
  • RPC 需要手动传播
// 手动传播
propagator := otel.GetTextMapPropagator()
carrier := propagation.MapCarrier{}

propagator.Inject(ctx, propagation.MapCarrier(carrier))

// 在另一个服务中提取
ctx = propagator.Extract(ctx, propagation.MapCarrier(carrier))

坑三:Tags 太多

每个 Span 都加了太多 tags,导致性能问题。

解决

  • 只加必要的 tags
  • Attributes 比 Tags 更高效
  • 用 Baggage 传递元数据
// 使用 Attributes
span.SetAttributes(
    attribute.String("http.method", "GET"),
    attribute.String("http.url", "/api/users"),
)

// 使用 Baggage
baggage := baggage.FromContext(ctx)
entry, err := baggage.NewMember("user.id", "123")
ctx = baggage.ContextWithBaggage(ctx, baggage.Set(baggage, entry))

写在最后

分布式追踪这东西,不是上了就能解决问题,需要:

  • 合理的采样策略
  • 完整的上下文传播
  • 适当的 tags 和 attributes
  • 定期的数据清理

选型之前先评估:

  • 团队规模
  • 服务数量
  • 流量大小
  • 存储成本

对于小团队,Jaeger 就够用。对于大团队,OpenTelemetry 的扩展性更好。


这次分布式追踪改造花了一个月,Jaeger 到 OpenTelemetry 迁移花了两周。迁移完成后,排查问题的效率提升了 50%,服务依赖关系也清楚了。

版权声明: 本文首发于 指尖魔法屋-从Jaeger走到OpenTelemetry:分布式追踪笔记https://blog.thinkmoon.cn/post/59-distributed-tracing-jaeger-opentelemetry-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!