Go微服务调用链追踪核心是统一传播trace_id/span_id并集成OpenTelemetry;需用otelhttp自动拦截HTTP请求、手动创建子span传递context、配置OTLP/Jaeger导出器并调用shutdown。

Go 微服务中实现调用链追踪,核心是统一传播 trace_id 和 span_id,并集成 OpenTelemetry(OTel)——它已取代 OpenTracing 成为事实标准,且官方 SDK 对 Go 支持成熟、轻量、无侵入式中间件依赖。
用 otelhttp 自动拦截 HTTP 客户端和服务端 span
绝大多数 Go 微服务基于 HTTP(如 REST/gRPC-HTTP gateway),otelhttp 是最省力的起点。它通过包装 http.RoundTripper 和 http.Handler 实现自动注入/提取 trace 上下文。
注意:必须确保所有 HTTP 请求都走被包装的客户端,否则 span 会断开;服务端 handler 也需显式注册,不能直接传 nil 或裸函数。
- 客户端需用
otelhttp.NewTransport()包装底层 transport,再传给http.Client - 服务端需用
otelhttp.NewHandler()包装原始 handler,而非直接http.ListenAndServe() - 若使用
gin或echo,要替换默认 middleware,例如 gin 中用gin.WrapH(otelhttp.NewHandler(...)) - 默认不采集请求体、响应体,如需调试可启用
otelhttp.WithBodyCapture(),但生产环境禁用(性能和隐私风险)
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"client := &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), }
mux := http.NewServeMux() mux.HandleFunc("/api/user", userHandler) http.ListenAndServe(":8080", otelhttp.NewHandler(mux, "/"))
手动创建子 span 并传递 context(非 HTTP 场景)
数据库查询、消息队列消费、本地方法调用等无法被 otelhttp 覆盖的路径,必须显式创建 span 并将 context.Context 向下传递。漏传 context 是链路断裂最常见原因。
立即学习“go语言免费学习笔记(深入)”;
关键点:
- 始终从上游传入的
ctx创建新 span,不要用context.Background() - 用
trace.SpanFromContext(ctx)检查是否已有有效 span,避免意外新建 root span - 数据库驱动需支持 OTel(如
pgx/v5+otelpgx),否则需手动 wrapQueryContext等方法 - 异步任务(如 goroutine)必须显式拷贝含 span 的 context,不能直接传原始 ctx(可能已被 cancel)
func processOrder(ctx context.Context, orderID string) error {
ctx, span := tracer.Start(ctx, "process_order")
defer span.End()
// 传递 ctx 给下游
if err := db.QueryRowContext(ctx, "SELECT ...").Scan(&name); err != nil {
span.RecordError(err)
return err
}
return nil}
配置 OTel Exporter 到 Jaeger / OTLP / Zipkin
Go SDK 默认不导出数据,必须显式配置 exporter。推荐优先选 OTLP(协议统一、支持指标/日志/trace 一体),其次 Jaeger(兼容老环境)。
常见陷阱:
-
jaeger.NewExporter默认用 UDP,容器内易丢包;应改用jaeger.WithAgentEndpoint+ 显式 IP+端口,或切到jaeger.WithCollectorEndpoint -
otlphttp.NewExporter需设置WithEndpoint("otel-collector:4318"),路径默认是/v1/traces,别漏写https://或配错端口(4317=grpc, 4318=http) - 未调用
shutdown()会导致进程退出前最后一批 trace 丢失(尤其测试或短命 job) - 采样率设为
AlwaysSample()仅用于调试;生产建议用ParentBased(TraceIDRatioBased(0.01))控制量级
exp, err := otlphttp.NewExporter(otlphttp.WithEndpoint("otel-collector:4318"))
if err != nil { /* handle */ }
defer exp.Shutdown(context.Background())
tp := trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(0.01))),
)
真正难的不是埋点,而是确保每个 goroutine、每个 callback、每个第三方库调用都携带并透传 context —— 这需要团队约定 + 代码审查,光靠工具覆盖不了所有分支。










