Go 微服务链路追踪——OpenTelemetry + Jaeger 在 Go 中的完整接入
Go 微服务链路追踪——OpenTelemetry + Jaeger 在 Go 中的完整接入
适读人群:Go 微服务工程师、想在分布式系统中实现可观测性的开发者 | 阅读时长:约19分钟 | 核心价值:完整接入 OpenTelemetry + Jaeger,让每个跨服务请求都有完整的链路信息
那次要靠"猜"来排障的深夜经历
2023年8月,凌晨2点,监控告警:用户下单成功率降到了60%。
我打开日志,订单服务日志显示"创建订单失败",但没有更详细的信息。然后翻商品服务日志,翻支付服务日志,每个服务都在各自的日志里打了一堆东西,但这些日志之间没有任何关联——我根本不知道是哪个调用链出了问题,也不知道哪个节点耗时异常。
那次排障花了40分钟,靠的是猜+运气,最终发现是商品服务的一个 Redis 查询超时。
那次之后,我下决心给所有服务接入链路追踪。这篇文章就是我那次接入的完整记录。
链路追踪的核心概念
Java 工程师背景:如果你用过 Spring Cloud Sleuth + Zipkin,OpenTelemetry 是它的升级版,标准更统一,支持更广。
三个核心概念:
- Trace(链路):一次完整请求的全链路记录,由多个 Span 组成
- Span(跨度):链路中的一个操作(比如一次 RPC 调用、一次数据库查询),有开始/结束时间
- Context Propagation(上下文传播):把 Trace ID 和 Span ID 从一个服务传递到下一个服务
环境准备
# 启动 Jaeger(all-in-one 版本,开发用)
docker run -d --name jaeger \
-p 16686:16686 \ # Web UI
-p 4317:4317 \ # OTLP gRPC
-p 4318:4318 \ # OTLP HTTP
jaegertracing/all-in-one:latest
# 安装 Go 依赖
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp初始化 OpenTelemetry
把这段代码提取成公共库,所有服务共用:
package telemetry
import (
"context"
"fmt"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"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.21.0"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// InitTracer 初始化 OpenTelemetry Tracer
// serviceName: 服务名称(会显示在 Jaeger UI 上)
// jaegerEndpoint: Jaeger OTLP gRPC 地址(如 "localhost:4317")
func InitTracer(serviceName, jaegerEndpoint string) (func(context.Context) error, error) {
ctx := context.Background()
// 建立到 Jaeger 的 gRPC 连接
conn, err := grpc.DialContext(ctx, jaegerEndpoint,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithTimeout(5*time.Second),
)
if err != nil {
return nil, fmt.Errorf("连接 Jaeger 失败: %w", err)
}
// 创建 OTLP exporter(把 Trace 数据发到 Jaeger)
exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
if err != nil {
return nil, fmt.Errorf("创建 exporter 失败: %w", err)
}
// 定义服务资源信息(这些信息会附加到所有 Span 上)
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(serviceName),
semconv.ServiceVersion("v1.0.0"),
attribute.String("environment", "production"),
),
)
if err != nil {
return nil, fmt.Errorf("创建资源信息失败: %w", err)
}
// 创建 TracerProvider
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
// 采样率:生产中推荐使用 ParentBased + TraceIDRatioBased
// 1.0 = 100% 采样,生产中通常设置 0.1(10%)
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
// 设置全局 TracerProvider 和 Propagator
otel.SetTracerProvider(tp)
// W3C TraceContext 是标准传播格式,跨语言兼容
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
// 返回 shutdown 函数,程序退出时调用
return tp.Shutdown, nil
}gRPC 服务接入:自动追踪所有 RPC 调用
package main
import (
"context"
"log"
"net"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc"
"your-project/telemetry"
pb "your-project/pb/order"
)
func main() {
// 初始化追踪
shutdown, err := telemetry.InitTracer("order-service", "localhost:4317")
if err != nil {
log.Fatalf("初始化追踪失败: %v", err)
}
defer shutdown(context.Background())
// 服务端:添加 otelgrpc 拦截器,自动为每个 RPC 创建 Span
grpcServer := grpc.NewServer(
grpc.StatsHandler(otelgrpc.NewServerHandler()),
)
pb.RegisterOrderServiceServer(grpcServer, &OrderServer{})
lis, _ := net.Listen("tcp", ":50051")
grpcServer.Serve(lis)
}
// 客户端:添加追踪拦截器
func newProductClient() pb.ProductServiceClient {
conn, _ := grpc.Dial("localhost:50052",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()), // 自动传播 Trace 上下文
)
return pb.NewProductServiceClient(conn)
}在业务代码中手动创建 Span
自动追踪 gRPC 调用是基础,但业务中的关键操作(数据库查询、缓存读取、外部 API 调用)也需要手动添加 Span:
package service
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
// tracer 全局 tracer(使用服务名作为 tracer 名称)
var tracer = otel.Tracer("order-service")
type OrderService struct {
db *sql.DB
}
func (s *OrderService) CreateOrder(ctx context.Context, userID, productID int64, quantity int32) (*Order, error) {
// 创建一个新的 Span(这个 Span 会自动成为当前 Span 的子 Span)
ctx, span := tracer.Start(ctx, "CreateOrder")
defer span.End()
// 给 Span 添加属性(会显示在 Jaeger 的 Tags 里)
span.SetAttributes(
attribute.Int64("user.id", userID),
attribute.Int64("product.id", productID),
attribute.Int32("quantity", quantity),
)
// 1. 查询商品信息(在子 Span 里)
product, err := s.getProductWithTrace(ctx, productID)
if err != nil {
// 记录错误到 Span
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// 2. 创建订单(数据库操作也加追踪)
order, err := s.saveOrderWithTrace(ctx, userID, product, quantity)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// 添加事件(重要的时间点记录在 Span 的 Events 里)
span.AddEvent("订单创建成功", trace.WithAttributes(
attribute.Int64("order.id", order.ID),
attribute.Int64("amount", order.Amount),
))
return order, nil
}
func (s *OrderService) saveOrderWithTrace(ctx context.Context, userID int64, product *Product, quantity int32) (*Order, error) {
ctx, span := tracer.Start(ctx, "DB.SaveOrder")
defer span.End()
// 记录 SQL 相关属性
span.SetAttributes(
attribute.String("db.system", "mysql"),
attribute.String("db.operation", "INSERT"),
attribute.String("db.table", "orders"),
)
// 实际数据库操作
result, err := s.db.ExecContext(ctx,
"INSERT INTO orders (user_id, product_id, quantity, amount, status) VALUES (?, ?, ?, ?, ?)",
userID, product.ID, quantity, product.Price*int64(quantity), 1,
)
if err != nil {
return nil, fmt.Errorf("插入订单失败: %w", err)
}
id, _ := result.LastInsertId()
return &Order{ID: id}, nil
}HTTP 服务接入
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
// 用 otelhttp.NewHandler 包装 HTTP handler,自动追踪所有 HTTP 请求
http.Handle("/api/orders", otelhttp.NewHandler(
http.HandlerFunc(createOrderHandler),
"POST /api/orders",
))踩坑实录
坑1:Trace Context 没有传递,Jaeger 里看不到跨服务链路
现象: Jaeger 里订单服务和商品服务的 Trace 是分开的,没有组成一个完整链路。
原因: gRPC 客户端没有添加 otelgrpc.NewClientHandler(),导致 Trace ID 没有随请求传递到下游服务。
解法: 客户端和服务端都必须加 otelgrpc handler,缺一不可:
// 客户端
grpc.WithStatsHandler(otelgrpc.NewClientHandler())
// 服务端
grpc.StatsHandler(otelgrpc.NewServerHandler())坑2:生产环境 100% 采样,Jaeger 数据量暴涨,存储撑不住
现象: 接入当天,Jaeger 的 Elasticsearch 存储在12小时内用掉了500GB,紧急扩容。
原因: AlwaysSample() 在高并发服务下,每秒会产生几千个 Trace,数据量极大。
解法: 生产环境改为基于概率的采样:
// 10% 采样率(对1000 QPS 的服务,每秒产生100个 Trace,可接受)
sdktrace.WithSampler(
sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1),
),
),坑3:Span 忘记 defer span.End(),内存泄漏
现象: 服务运行一段时间后,内存持续增长,pprof 显示大量 Span 对象没有被释放。
原因: 某个函数里 Start() 了 Span 但忘记调用 End(),Span 对象一直持有在内存里。
解法: span.End() 必须用 defer 调用,紧跟在 Start() 之后:
ctx, span := tracer.Start(ctx, "操作名称")
defer span.End() // 紧跟在下一行,不要忘记链路追踪让排障从40分钟变5分钟
接入链路追踪后,再次出现类似问题,我打开 Jaeger,搜索失败的 Trace,3次点击就能看到:
- 这次请求经过了哪些服务
- 每个服务/操作耗时多少
- 哪个节点返回了错误,错误详情是什么
从40分钟的"靠猜排障"变成了5分钟的精准定位。这是链路追踪最直接的价值。
