微服务架构下,一个用户请求可能经过网关、认证、业务处理、缓存、数据库等多个服务节点。当请求出现延迟或错误时,仅靠单服务日志很难定位根因——链路追踪(Distributed Tracing)通过在请求链路上传递TraceID,将分散的日志关联成完整的调用链,是微服务可观测性的核心组件。OpenTelemetry作为CNCF的可观测性标准,已逐步取代Jaeger和Zipkin的专有SDK,成为链路追踪的事实标准。
OpenTelemetry核心概念与Go SDK初始化
OpenTelemetry定义了三个信号(Signal):Traces(链路追踪)、Metrics(指标)、Logs(日志)。链路追踪的核心概念:
Trace:一个请求的完整调用链,由唯一的TraceID标识。
Span:调用链中的一个操作单元,包含操作名称、起止时间、状态和属性。Span之间通过ParentID构成树形结构。
Context Propagation:跨服务传递Trace上下文的机制,通常通过HTTP Header(W3C TraceContext格式)或消息队列元数据实现。
Go项目中初始化OpenTelemetry Tracer Provider:
package telemetry
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
func InitTracer(serviceName, collectorAddr string) (func(context.Context) error, error) {
exporter, err := otlptracegrpc.New(context.Background(),
otlptracegrpc.WithEndpoint(collectorAddr),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("创建OTLP exporter失败: %w", err)
}
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String("1.0.0"),
),
)
if err != nil {
return nil, fmt.Errorf("创建resource失败: %w", err)
}
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)),
)
otel.SetTracerProvider(provider)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContextPropagator{},
propagation.BaggagePropagator{},
))
return provider.Shutdown, nil
}
WithSampler配置采样率,生产环境建议0.01-0.1之间,高流量服务降低采样率减少后端存储压力。错误请求建议100%采样,通过自定义Sampler实现。
HTTP服务自动插桩与上下文传播
OpenTelemetry提供了Gin和net/http的自动插桩中间件,无需手动创建Span:
import (
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
func main() {
shutdown, err := InitTracer("user-service", "otel-collector:4317")
if err != nil {
log.Fatal(err)
}
defer shutdown(context.Background())
r := gin.Default()
r.Use(otelgin.Middleware("user-service"))
r.GET("/api/users/:id", func(c *gin.Context) {
span := trace.SpanFromContext(c.Request.Context())
span.SetAttributes(attribute.String("user.id", c.Param("id")))
user, err := userService.GetByID(c.Request.Context(), c.Param("id"))
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, user)
})
r.Run(":8080")
}
自动插桩中间件会为每个HTTP请求创建一个Span,并将W3C TraceContext Header注入响应。下游服务收到请求后,OpenTelemetry的TextMapPropagator自动从Header中提取TraceID和ParentSpanID,实现跨服务链路串联。
gRPC服务间链路追踪配置
gRPC服务间调用需要同时在Client和Server端配置拦截器:
import (
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
)
// Server端
func NewGRPCServer() *grpc.Server {
server := grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
pb.RegisterUserServiceServer(server, &userServiceServer{})
return server
}
// Client端
func NewGRPCClient(addr string) pb.UserServiceClient {
conn, err := grpc.Dial(addr,
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
grpc.WithInsecure(),
)
if err != nil {
log.Fatal(err)
}
return pb.NewUserServiceClient(conn)
}
gRPC拦截器自动处理metadata中的Trace上下文传播,无需手动传递TraceID。Client端拦截器在发送请求前创建客户端Span并注入metadata,Server端拦截器从metadata提取上下文并创建服务端Span。
数据库与Redis访问的Span关联
数据库和Redis操作是请求链路中耗时最长的环节,关联到Trace中才能定位性能瓶颈:
// 自定义数据库查询Span
func QueryUser(ctx context.Context, db *sql.DB, id string) (*User, error) {
tracer := otel.Tracer("user-service")
ctx, span := tracer.Start(ctx, "db.QueryUser",
trace.WithAttributes(
attribute.String("db.system", "mysql"),
attribute.String("db.operation", "SELECT"),
attribute.String("db.sql.table", "users"),
),
)
defer span.End()
var user User
err := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = ?", id,
).Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
return &user, nil
}
// Redis操作Span
func GetCachedUser(ctx context.Context, rdb *redis.Client, id string) (string, error) {
tracer := otel.Tracer("user-service")
ctx, span := tracer.Start(ctx, "redis.GetCachedUser",
trace.WithAttributes(
attribute.String("db.system", "redis"),
attribute.String("redis.key", fmt.Sprintf("user:%s", id)),
),
)
defer span.End()
val, err := rdb.Get(ctx, fmt.Sprintf("user:%s", id)).Result()
if err != nil {
span.RecordError(err)
}
return val, err
}
数据库和Redis的Span会自动关联到上层HTTP请求的Trace中,在Jaeger/Tempo UI中可以看到完整的调用链:HTTP请求、业务逻辑、数据库查询、Redis访问,每一步的耗时一目了然。
采样策略优化与Span数据量控制
百万QPS服务的链路追踪数据量惊人,10%采样率下每秒产生10万个Span。优化采样策略是控制成本的关键:
1. 尾部采样(Tail-Based Sampling):先收集所有Span到内存缓冲区,根据请求结果决定是否保留。错误请求100%保留,成功请求按比例采样。OpenTelemetry Collector的tail_sampling处理器支持此策略。
2. 优先级采样:对关键业务链路(如支付、下单)设置更高的采样率,对低优先级链路(如健康检查、日志写入)降低采样率。
3. Span属性精简:避免在Span中存储大量业务数据(如完整请求体),只记录关键字段(如订单ID、用户ID),减少每个Span的存储大小。
链路追踪不是越全越好,而是要在可观测性和成本之间找到平衡点。OpenTelemetry的模块化设计让团队能按需组合采样策略和后端存储,逐步构建适合自身的可观测性体系。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-wei-fu-wu-lian-lu-zhui-zong-opentelemetry-ji/