问题是:微服务出问题了怎么办
假设你负责一个电商系统,用户下单失败,你知道问题在哪吗?
用户 → 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。常见方式:
- HTTP Header:把 TraceContext 放到 HTTP Header 里
- gRPC Metadata:类似 HTTP Header
- 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 三步走:
- 初始化 TracerProvider(一次性配置)
- 自动插桩(HTTP/gRPC/DB 自动拦截)
- 手动补充(关键业务逻辑加 Span)
下一篇文章写写怎么用 Grafana + Tempo 搭一套生产可用的追踪系统。
有问题欢迎留言,讨论微服务可观测性实践。