问题是:微服务出问题了怎么办

假设你负责一个电商系统,用户下单失败,你知道问题在哪吗?

用户 → API Gateway → Order Service → Inventory Service → Database
                       ↓
                  Payment Service → 第三方支付

可能出问题的点太多了:网络超时、数据库慢查询、第三方接口挂了、代码 bug…

如果没有任何追踪手段,你只能靠经验和日志去猜。

分布式追踪就是来解决这个问题的。

核心概念:三根柱子

可观测性有三根支柱:Traces(追踪)、Metrics(指标)、Logs(日志)。这里重点聊 Traces。

Trace:一次请求的完整路径

一个 Trace 由多个 Span 组成,每个 Span 代表一个操作:

Trace: 用户下单流程
├── Span: 接收 HTTP 请求
│   ├── Span: 查询库存 (Inventory Service)
│   │   ├── Span: SELECT * FROM inventory WHERE sku = ?
│   ├── Span: 创建订单 (Order Service)
│   │   ├── Span: INSERT INTO orders ...
│   ├── Span: 调用支付 (Payment Service)
│   │   └── Span: POST /api/pay

每个 Span 记录:

  • Operation name:做什么
  • Start/End time:耗时
  • Status:成功/失败
  • Attributes:额外信息(用户 ID、订单号等)
  • Events:关键事件(日志点)
  • Parent Span ID:谁调用的

Context Propagation:跨服务传递

关键问题:请求从 Service A 到 Service B,Service B 怎么知道这是同一个 Trace?

答案是:注入 Context。常见方式:

  1. HTTP Header:把 TraceContext 放到 HTTP Header 里
  2. gRPC Metadata:类似 HTTP Header
  3. Message Queue:放到消息属性里
tracestate: congo=t61rcWkgMzE
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

格式是 W3C TraceContext 标准,00 表示版本,16 字符是 Trace ID,8 字符是 Parent Span ID,01 是采样标志。

OpenTelemetry 架构

你的代码
    ↓
OTel SDK(自动/手动插桩)
    ↓
OTel Collector(可选,聚合处理)
    ↓
Backend(Jaeger / Tempo / Honeycomb / 云服务)

关键组件

  • OTel SDK:各种语言的客户端库
  • OTel Collector:中间层,可以做过滤、聚合、导出
  • Exporters:把数据导出的协议(OTLP、Jaeger、Zipkin)
  • Instrumentation:插桩方式
    • Auto:自动拦截 HTTP/gRPC/数据库调用
    • Manual:手写代码记录自定义 Span

Go 实战:从零集成

1. 安装依赖

go get go.opentelemetry.io/otel \
       go.opentelemetry.io/otel/sdk \
       go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc \
       go.opentelemetry.io/otel/instrumentation/google.golang.org/grpc/otelgrpc \
       go.opentelemetry.io/otel/instrumentation/net/http/otelhttp

2. 初始化 Tracer Provider

package main

import (
    "context"
    "log"
    "net/http"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
    "go.opentelemetry.io/otel/trace"
)

func initTracer() (func(context.Context) error, error) {
    ctx := context.Background()

    // 创建 OTLP Exporter(发送给 Collector)
    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("localhost:4317"),
        otlptracegrpc.WithInsecure(), // 本地开发不用 TLS
    )
    if err != nil {
        return nil, err
    }

    // 创建 Resource(服务信息)
    res, err := resource.Merge(
        resource.Default(),
        resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName("order-service"),
            semconv.ServiceVersion("1.0.0"),
            attribute.String("environment", "production"),
        ),
    )

    // 创建 Tracer Provider
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(res),
        sdktrace.WithSampler(sdktrace.AlwaysSample()), // 生产环境可以改用概率采样
    )

    otel.SetTracerProvider(tp)

    return tp.Shutdown, nil
}

3. HTTP 中间件(自动插桩)

package middleware

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/trace"
)

func Tracing(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 从 Header 提取 Context(假设上游已经注入)
        ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))

        // 开始 Span
        spanName := r.Method + " " + r.URL.Path
        ctx, span := otel.Tracer("http").Start(ctx, spanName,
            trace.WithSpanKind(trace.SpanKindServer),
        )
        defer span.End()

        // 设置常见属性
        span.SetAttributes(
            attribute.String("http.method", r.Method),
            attribute.String("http.url", r.URL.String()),
            attribute.String("http.host", r.Host),
            attribute.String("http.user_agent", r.UserAgent()),
        )

        // 把 Context 传下去
        r = r.WithContext(ctx)

        // 调用真正的 Handler
        next.ServeHTTP(w, r)
    })
}

4. gRPC 拦截器(自动插桩)

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

func dialOpts() []grpc.DialOption {
    return []grpc.DialOption{
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
        grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
    }
}

func serverOpts() []grpc.ServerOption {
    return []grpc.ServerOption{
        grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
        grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
    }
}

5. 手动记录自定义 Span

func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
    // 获取当前 Span(如果存在的话)
    span := trace.SpanFromContext(ctx)
    span.SetAttributes(attribute.String("order.user_id", req.UserID))

    // 创建子 Span(内部操作)
    ctx, span = span.Tracer().Start(ctx, "query-inventory")
    inventory, err := s.queryInventory(ctx, req.Items)
    span.End()

    if err != nil {
        // 记录错误
        span.RecordError(err)
        span.SetAttributes(attribute.Bool("error", true))
        return nil, err
    }

    // 另一个子 Span
    ctx, span = span.Tracer().Start(ctx, "save-order")
    order, err := s.saveOrder(ctx, inventory)
    span.End()

    return order, err
}

func (s *OrderService) queryInventory(ctx context.Context, items []*Item) (*Inventory, error) {
    // ...
}

6. 跨服务传递 Context

调用方(HTTP Client):

func callInventoryService(ctx context.Context, sku string) (*Inventory, error) {
    // 注入 Context 到 HTTP Header
    req, _ := http.NewRequestWithContext(ctx, "GET", "http://inventory:8080/"+sku, nil)
    otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))

    resp, err := http.DefaultClient.Do(req)
    // ...
}

调用方(gRPC Client):

// gRPC 自动处理 Context 传递,只需要确保用 WithContext 版本
resp, err := client.GetInventory(ctx, req)

Docker Compose 快速搭一套可用的环境

version: '3.8'

services:
  # Jaeger UI + Collector
  jaeger:
    image: jaegertracing/all-in-one:1.52
    ports:
      - "16686:16686"  # UI
      - "4317:4317"    # OTLP gRPC
      - "4318:4318"    # OTLP HTTP
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  # 你的服务
  order-service:
    build: ./order-service
    ports:
      - "8080:8080"
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=jaeger:4317
      - OTEL_SERVICE_NAME=order-service
    depends_on:
      - jaeger

启动后访问 http://localhost:16686 就能看到 Trace UI 了。

生产环境的几个建议

1. 采样策略

100% 采样太贵,用概率采样:

tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(exporter),
    sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)), // 10%
)

或者头部采样:只根据 trace ID 固定采样,相同请求一定采样或不采样:

sdktrace.WithSampler(sdktrace.ParentBased(
    sdktrace.TraceIDRatioBased(0.1),
))

2. 用 Collector 做中转

生产环境不应该直连后端,通过 Collector:

Service → Collector(聚合、过滤)→ Backend(Jaeger/Tempo)

好处:

  • 减少到后端的连接数
  • 可以在 Collector 做采样
  • 方便切换后端

3. 关键操作必须记录

以下操作一定要有 Span:

  • API 入口
  • 数据库调用
  • 外部服务调用(HTTP、gRPC)
  • 消息队列发送/消费
  • 关键业务逻辑

查看效果

Jaeger UI 里能看到:

  • Trace List:所有请求的列表
  • Trace Detail:某个请求的完整调用链
  • Span Detail:每个 Span 的耗时、属性、事件
  • Dependency Graph:服务依赖图

点开一个 Trace,能清楚看到:

  • 哪一步最慢
  • 是否有错误
  • 请求在各服务的耗时占比

总结

分布式追踪解决的核心问题:一次请求从进入系统到返回,经历了什么、哪里慢、哪里错。

用 OpenTelemetry 三步走:

  1. 初始化 TracerProvider(一次性配置)
  2. 自动插桩(HTTP/gRPC/DB 自动拦截)
  3. 手动补充(关键业务逻辑加 Span)

下一篇文章写写怎么用 Grafana + Tempo 搭一套生产可用的追踪系统。


有问题欢迎留言,讨论微服务可观测性实践。