从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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。