go背景下基于openTelementary进行链路追踪全流程

2026-06-25

基于openTelementary进行链路追踪,对于http handler和grpc服务,企业开发一般是如何追踪的,从添加span开始为我介绍,一直到最后的jaeger可视化呈现,以go语言为例

企业里用 OpenTelemetry 做 Go 服务的链路追踪,套路其实很固定:SDK 初始化 → 自动埋点(HTTP/gRPC)→ 手动补 Span → OTLP 导出 → Jaeger 可视化。下面按这条主线走一遍,每个环节都给出企业常用写法。


🧩 整体链路长什么样

一个请求从浏览器/客户端进来,经过网关 → HTTP 服务 A → gRPC → 服务 B,每一跳都应该自动续上同一个 TraceID。OpenTelemetry 在这套体系里的角色是:

  • TracerProvider:管 Span 怎么创建、采样、导出
  • Propagator:负责 TraceContext 跨进程传递(W3C Trace Context 是默认标准)
  • Instrumentation 包:HTTP / gRPC 的自动埋点拦截器
  • Exporter:把 Span 送到 Jaeger(现在推荐 OTLP,不再推荐 jaeger exporter)

第一步:初始化 TracerProvider(全局只做一次)

这一步是企业落地的地基,所有服务启动时都要先跑,配置一次挂到全局。

import (
    "context"
    "os"

    "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.21.0"
)

func InitTracer() (*sdktrace.TracerProvider, error) {
    // 1️⃣ 创建 OTLP gRPC Exporter,指向 Collector
    client := otlptracegrpc.NewClient(
        otlptracegrpc.WithInsecure(),           // 生产换成 TLS
        otlptracegrpc.WithEndpoint("otel-collector:4317"),
    )
    exporter, err := otlptracegrpc.New(context.Background(), client)
    if err != nil {
        return nil, err
    }

    // 2️⃣ 配置资源属性(Jaeger UI 里看到的 service.name 就来自这里)
    res, err := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName("order-service"), // 必须,否则 Jaeger UI 看不到服务名
            semconv.ServiceVersion("1.0.0"),
            semconv.DeploymentEnvironment(os.Getenv("ENV")),
        ),
    )
    if err != nil {
        return nil, err
    }

    // 3️⃣ 创建 TracerProvider,配置批处理和采样率
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter,          // 批量异步导出,减少 IO
            sdktrace.WithMaxQueueSize(2048),
            sdktrace.WithMaxExportBatchSize(512),
        ),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))), // 10% 采样
    )

    // 4️⃣ 设置为全局 Provider,并注册 W3C Trace Context 传播器
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
        propagation.TraceContext{},
        propagation.Baggage{},
    ))

    return tp, nil
}

⚠️ 关键踩坑点

  • go.opentelemetry.io/otel/exporters/jaeger 已废弃(2023 年 7 月起不再维护),官方推荐用 OTLP exporter 对接 Jaeger 的 OTLP Receiver。如果你在网上看到 jaeger.New(jaeger.WithCollectorEndpoint(...)) 这种写法,那是老教程,别抄。
  • service.name 必须在 Resource 里声明,否则 Jaeger UI 里服务名是空的。
  • 采样率不要写死 AlwaysSample(),生产环境 10% 或更低才扛得住流量。

main.go 里调用方式:

func main() {
    tp, err := InitTracer()
    if err != nil {
        panic(err)
    }
    defer tp.Shutdown(context.Background())

    // 下面启动 HTTP / gRPC 服务
}

第二步:HTTP Handler 的自动埋点

企业里最常见的做法是用 otelhttp 的 middleware,一行代码包住原有 handler,零侵入

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

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/orders", createOrderHandler)
    mux.HandleFunc("/healthz", healthzHandler)

    // 关键一行:用 otelhttp.NewHandler 包住整个 mux
    wrappedHandler := otelhttp.NewHandler(mux, "order-service")

    http.ListenAndServe(":8080", wrappedHandler)
}

这一行帮你做了什么

能力 自动完成
创建 Span 每个请求自动建一个根 Span,span name 为 "order-service"
注入 Context r.Context() 里已经带了 span,业务代码直接用
记录属性 http.methodhttp.routehttp.status_codehttp.host 自动打上
状态码推断 根据 HTTP 状态码自动设 span status(4xx/5xx 会标记异常)

业务 handler 里如果想追加自定义 Span,直接从 context 拿 tracer 就行,父子关系自动串联:

func createOrderHandler(w http.ResponseWriter, r *http.Request) {
    tracer := otel.GetTracerProvider().Tracer("")
    ctx, span := tracer.Start(r.Context(), "createOrder")
    defer span.End()

    span.SetAttributes(
        attribute.String("order.user_id", r.Header.Get("X-User-ID")),
        attribute.String("order.type", "express"),
    )

    // 执行业务逻辑...
    result, err := processOrder(ctx)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "order processing failed")
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(result)
}

第三步:HTTP Client 调用下游(保持链路不断)

A 服务调 B 服务时,必须把 TraceContext 注入到 HTTP 头里,否则 B 服务会新建一个 Trace,链路就断了。

client := http.Client{
    Transport: otelhttp.NewTransport(http.DefaultTransport),
}

req, _ := http.NewRequest("GET", "http://payment-service/api/pay", nil)
// TraceContext 会自动写入 traceparent header
resp, err := client.Do(req)

otelhttp.NewTransport 做的事情就是:在发出请求前把当前 span 的 TraceID/SpanID 序列化成 traceparent 头,目标服务收到后自动解析并续上链路。


第四步:gRPC 服务的自动埋点

gRPC 这边用的是 otelgrpc 的拦截器,服务端和客户端各注册一下就行

服务端

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

func main() {
    server := grpc.NewServer(
        grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
        grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
    )

    pb.RegisterOrderServiceServer(server, &OrderService{})
    server.Serve(lis)
}

客户端

conn, _ := grpc.Dial("localhost:50051",
    grpc.WithInsecure(),
    grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
    grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)

client := pb.NewPaymentServiceClient(conn)
resp, err := client.CreatePayment(ctx, &pb.CreatePaymentRequest{...})

拦截器会自动完成:提取 incoming metadata 里的 TraceContext → 创建 span → 注入到 outgoing 调用的 header 里。业务代码完全不用改

如果你想在 gRPC handler 里加业务属性,同样从 ctx 取 span:

func (s *OrderService) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.Order, error) {
    span := trace.SpanFromContext(ctx)
    span.SetAttributes(
        attribute.String("order.id", req.OrderId),
        attribute.Int64("order.amount", req.Amount),
    )
    // ...
}

第五步:Span 的生命周期管理(企业级要点)

手动创建 Span 有几个容易翻车的地方,单独说一下:

✅ 正确姿势

func doWork(ctx context.Context) {
    tracer := otel.GetTracerProvider().Tracer("")
    ctx, span := tracer.Start(ctx, "doWork") // 用传入的 ctx,自动成子 span
    defer span.End()                          // 一定记得 End,否则内存泄漏

    span.SetAttributes(attribute.String("key", "value"))
}

❌ 常见错误

  1. 用 **context.Background() 起新 span** → 链路断裂,变成独立 trace
  2. **忘记 **defer span.End() → span 不关闭,Jaeger 上看不到
  3. 用 **context.WithValue(ctx, "span", span) 传 span** → 污染 context 树,正确做法是 tracer.Start(ctx, ...) 返回的 ctx 本身就带 span
  4. RecordError(err)** 忘了配 **SetStatus(codes.Error, ...) → 错误不会标红

第六步:OpenTelemetry Collector 中转(强烈推荐)

企业里一般不推荐让每个服务直连 Jaeger,中间加一层 Collector 好处很多:

  • 统一数据格式(OTLP)
  • 采样率可以在 Collector 层动态调整
  • 后端换 Jaeger / Tempo / 阿里云 ARMS 不用改业务代码

典型 otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger]

Go 服务那边 exporter 指向 Collector(otel-collector:4317),Collector 再转发给 Jaeger。


第七步:Jaeger UI 看链路

数据跑起来后,浏览器打开 http://localhost:16686

  1. Service 下拉框选你的 service.name(比如 order-service
  2. Operation 下拉框选具体接口(比如 POST /api/orders
  3. Time Range 选时间范围,点 Find Traces
  4. 点一条 trace,下方会出现甘特图,能看到完整的调用链:
┌─ order-service (HTTP) ──────────────── 150ms ──┐
│  ├─ createOrder                                │
│  │  ├─ gRPC → payment-service                  │
│  │  │  └─ CreatePayment            80ms        │
│  │  └─ DB query                    30ms        │
└─────────────────────────────────────────────────┘

每个 span 都能展开看:

  • Tagshttp.status_codedb.statementorder.id 等业务属性
  • Process:服务名、版本、部署环境
  • Logs:span 内嵌的事件
  • Errors:红色高亮,点开能看到堆栈

如果看不到数据,按这个顺序排查:

💡 排障清单

  • Exporter 是不是真的挂到了 TracerProvider?(漏掉 otel.SetTracerProvider(tp) 是最常见原因)
  • Collector 端口通不通?(telnet otel-collector 4317
  • 采样率是不是 0?(临时改成 AlwaysSample() 验证)
  • Jaeger 的 OTLP Receiver 开了没?

🏢 企业级最佳实践速查

维度 推荐配置
Exporter OTLP gRPC(弃用 jaeger exporter)
采样率 ParentBased(TraceIDRatioBased(0.1)),错误请求可强制采样
service.name 必须注入,用 semconv 常量
健康检查 /healthz/metrics 这类接口可跳过 tracing
传播格式 W3C TraceContext(默认)
导出方式 BatchSpanProcessor,队列 2048,批次 512
Collector 中间加一层,解耦业务和后端

这套组合拳下来,从 HTTP 入口 → gRPC 内部调用 → 数据库 → 下游服务,整条链路在 Jaeger 上一目了然,定位慢接口、排查跨服务故障都很顺手。