Skip to content

链路追踪与可观测性

本篇是 Go 微服务系列的第七篇。微服务系统一旦出问题,排查难度远高于单体——一个请求可能经过十几个服务,没有完整的可观测能力根本无从下手。可观测性(Observability)由三根支柱构成:日志(Logging)、指标(Metrics)、追踪(Tracing)。本篇将聚焦追踪,并整合 OpenTelemetry、Jaeger、Prometheus、Grafana 给出一套完整的可观测性方案。

一、可观测性三支柱

1. 三支柱是什么

  • 日志(Logging):离散的事件记录,包含时间戳、级别、上下文字段。适合排查具体问题。
  • 指标(Metrics):可聚合的数值时序数据,如 QPS、延迟分位、错误率。适合监控告警。
  • 追踪(Tracing):单个请求在分布式系统中的完整调用链。适合理解服务间关系和瓶颈定位。

三者互补,不能相互替代:

维度日志指标追踪
数据量大(每个事件一条)小(聚合后)中(采样后)
成本
排查具体事件趋势 / 告警调用链 / 瓶颈
典型工具ELK / LokiPrometheusJaeger / Zipkin

2. 三者关联的关键:TraceID

最有价值的实践是把三者通过 TraceID 串起来:

  • 一条追踪有一个全局唯一的 TraceID
  • 该请求经过的每个服务打的日志都带上这个 TraceID
  • 指标也按 TraceID(或更粗的维度)聚合。

排查问题时:先在指标上看到异常 → 在追踪上找到具体的慢调用链 → 用 TraceID 在日志里看具体错误。这就是「三支柱联动」。

二、分布式追踪原理

1. Trace 与 Span

  • Trace:一次完整的请求链路,由唯一的 TraceID 标识。
  • Span:链路中的一个操作单元,有 SpanIDParentSpanID。Span 之间通过父子关系组成一棵树。
Trace (TraceID=abc)
├── Span 1: HTTP GET /order (SpanID=1, parent=none)
│   ├── Span 2: gRPC User.GetUser (SpanID=2, parent=1)
│   │   └── Span 3: SQL SELECT users (SpanID=3, parent=2)
│   └── Span 4: gRPC Product.Get (SpanID=4, parent=1)
│       └── Span 5: Redis GET product (SpanID=5, parent=4)

每个 Span 记录:

  • 操作名(如 GET /order
  • 起始/结束时间(算出耗时)
  • Tags(标签,如 http.status_code=200
  • Logs(事件,如异常堆栈)
  • SpanContext(TraceID + SpanID,用于跨进程传递)

2. Context 传递

跨进程传播的关键是 SpanContext 通过请求 Header 传递。OpenTelemetry 默认使用 W3C Trace Context 标准:

  • traceparent header:00-{trace-id}-{span-id}-{flags}
  • 例:00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

下游服务解析这个 header,创建子 Span,并把 context 继续往下游传。

三、OpenTelemetry 简介

1. 什么是 OpenTelemetry

OpenTelemetry(简称 OTel)是 CNCF 主持的可观测性标准,由 OpenTracing 和 OpenCensus 合并而来。它的核心价值:

  • 统一 API:一套 API 同时支持追踪、指标、日志。
  • 厂商中立:通过 Exporter 切换后端(Jaeger、Zipkin、Tempo、Datadog、OTLP)。
  • 多语言 SDK:Go、Java、Python、Node.js 等都有官方 SDK。
  • 自动埋点:通过 instrumentation 库自动追踪 HTTP、gRPC、数据库等。

2. OTel 核心概念

  • Tracer:创建 Span 的工厂。
  • Span:一次操作单元。
  • Propagator:跨进程传播 SpanContext(默认 W3C TraceContext)。
  • Exporter:把 Span 数据发送到后端(OTLP、Jaeger、stdout 等)。
  • Resource:标识产生遥测数据的实体(服务名、版本、host)。
  • Instrumentation:对常用库(net/http、grpc、gorm 等)的自动埋点。

3. Go SDK 包结构

go.opentelemetry.io/otel                  核心 API
go.opentelemetry.io/otel/sdk/trace        SDK 实现
go.opentelemetry.io/otel/exporters/...    各类 Exporter
go.opentelemetry.io/otel/trace            Trace API
go.opentelemetry.io/contrib/instrumentation/...  自动埋点

四、在 Go 中集成 OpenTelemetry

1. 初始化 Tracer

go
package main

import (
	"context"
	"log"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"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"
)

// InitTracer 初始化全局 Tracer,把数据通过 OTLP HTTP 发到后端(如 Jaeger / OTel Collector)
func InitTracer(ctx context.Context, serviceName, otlpEndpoint string) (func(), error) {
	// 创建 Exporter
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpointURL("http://"+otlpEndpoint+"/v1/traces"),
		otlptracehttp.WithInsecure(),
	)
	if err != nil {
		return nil, err
	}

	// Resource 标识来源
	res, err := resource.New(ctx,
		resource.WithAttributes(
			semconv.ServiceNameKey.String(serviceName),
			semconv.ServiceVersionKey.String("1.0.0"),
			attribute.String("environment", "dev"),
		),
	)
	if err != nil {
		return nil, err
	}

	// TracerProvider
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
		sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.5)), // 50% 采样
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{},
		propagation.Baggage{},
	))

	shutdown := func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := tp.Shutdown(ctx); err != nil {
			log.Printf("tracer shutdown: %v", err)
		}
	}
	return shutdown, nil
}

func main() {
	ctx := context.Background()
	shutdown, err := InitTracer(ctx, "demo-service", "localhost:4318")
	if err != nil {
		log.Fatal(err)
	}
	defer shutdown()

	tracer := otel.Tracer("demo")
	ctx, span := tracer.Start(ctx, "main-span")
	defer span.End()
	span.SetAttributes(attribute.String("foo", "bar"))

	// 模拟一段业务
	time.Sleep(50 * time.Millisecond)
	log.Println("done")
}

2. 手动创建 Span

go
package main

import (
	"context"
	"fmt"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
)

func businessLogic(ctx context.Context) error {
	tracer := otel.Tracer("app")
	ctx, span := tracer.Start(ctx, "businessLogic")
	defer span.End()

	span.SetAttributes(attribute.String("input", "xxx"))

	// 子 span
	_, sub := tracer.Start(ctx, "db.query")
	time.Sleep(20 * time.Millisecond)
	sub.SetAttributes(attribute.String("db.statement", "SELECT * FROM users"))
	sub.End()

	// 模拟错误
	if time.Now().Unix()%2 == 0 {
		span.RecordError(fmt.Errorf("simulated error"))
		span.SetStatus(codes.Error, "failed")
		return fmt.Errorf("failed")
	}
	span.SetStatus(codes.Ok, "")
	return nil
}

3. 自动埋点

OTel contrib 提供了大量自动埋点库,常见:

  • otelhttp:net/http server / client
  • otelgrpc:gRPC server / client
  • otelsql / otelgorm:数据库
  • otelredis:Redis

五、Jaeger 部署与使用

1. 启动 Jaeger

最简单的方式是 All-in-One Docker 镜像:

bash
docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4318:4318 \
  jaegertracing/all-in-one:1.55
  • 16686:Jaeger UI
  • 4318:OTLP HTTP 接收端口

启动后访问 http://localhost:16686 即可看到 UI。

2. 查看追踪

服务把 Span 通过 OTLP 发到 Jaeger 后,UI 里可以:

  • 按服务名、操作名、Tag、耗时筛选 Trace。
  • 看调用链树状图,每个 Span 的耗时一目了然。
  • 对比多次调用,找出慢请求的瓶颈服务。

六、Gin 中间件集成追踪

otelhttp 提供了 net/http 的中间件,Gin 可以包装使用:

go
package main

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

	"github.com/gin-gonic/gin"
	"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
)

// 假设已初始化 TracerProvider(见第四节)

func main() {
	r := gin.New()
	// 接入 OTel,每次请求会自动创建一个 Span
	r.Use(otelgin.Middleware("user-service"))

	r.GET("/users/:id", func(c *gin.Context) {
		// 从 context 拿到当前 span,附加业务标签
		span := otel.GetSpanFromContext(c.Request.Context())
		span.SetAttributes(attribute.String("user.id", c.Param("id")))

		// 业务逻辑:调用下游
		err := callDownstream(c.Request.Context())
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"id": c.Param("id"), "name": "alice"})
	})

	log.Fatal(r.Run(":8080"))
}

func callDownstream(ctx context.Context) error {
	// 用 otelhttp 包裱 http.Client,自动创建子 span
	// req, _ := http.NewRequestWithContext(ctx, "GET", "http://localhost:9000/x", nil)
	// resp, err := otelhttp.DefaultClient.Do(req)
	// 这里仅演示
	_ = ctx
	return nil
}

每个进入 Gin 的请求会自动创建一个 Span,调用下游时 SpanContext 通过 HTTP header 传递,下游解析后创建子 Span——整条链路串起来。

七、gRPC 拦截器集成追踪

gRPC 拦截器配合 otelgrpc 同样可以无缝集成:

go
package main

import (
	"context"
	"log"
	"net"
	"time"

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

func startGRPCServer() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatal(err)
	}
	// 服务端:流和一元拦截器都接 otelgrpc
	srv := grpc.NewServer(
		grpc.StatsHandler(otelgrpc.NewServerHandler()),
	)
	// 真实场景:userv1.RegisterUserServiceServer(srv, &userServiceServer{})
	go func() {
		log.Println("grpc server on :50051")
		_ = srv.Serve(lis)
	}()
}

func newGRPCClient(addr string) (*grpc.ClientConn, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	return grpc.DialContext(ctx, addr,
		grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
		grpc.WithInsecure(),
	)
}

func main() {
	startGRPCServer()
	conn, err := newGRPCClient("127.0.0.1:50051")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	time.Sleep(2 * time.Second)
}

新版 OTel 推荐使用 StatsHandler(替代旧的 UnaryInterceptor / StreamInterceptor),它对四种调用模式都生效,性能更好。

八、日志关联 TraceID

1. 为什么需要关联

排查问题时,从追踪 UI 看到一个慢请求,想看具体日志怎么办?如果日志带 TraceID,直接搜 ID 就能拿到该请求所有服务的全部日志。这是「三支柱联动」的关键。

2. 把 TraceID 写入日志

Go 标准库 slog(Go 1.21+)支持自定义 handler:

go
package main

import (
	"context"
	"log/slog"
	"os"

	"go.opentelemetry.io/otel/trace"
)

// TraceHandler 把 trace_id / span_id 注入到每条日志
type TraceHandler struct {
	slog.Handler
}

func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error {
	if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() {
		r.AddAttrs(
			slog.String("trace_id", span.SpanContext().TraceID().String()),
			slog.String("span_id", span.SpanContext().SpanID().String()),
		)
	}
	return h.Handler.Handle(ctx, r)
}

func NewLogger() *slog.Logger {
	base := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
	return slog.New(&TraceHandler{Handler: base})
}

func main() {
	logger := NewLogger()
	// 假设 ctx 里已经有 span(来自 otelhttp 中间件)
	ctx := context.Background()
	logger.Info(ctx, "user login", "user_id", "123")
	logger.Error(ctx, "db error", "err", "timeout")
}

实际项目把 slog 改成 zap / zerolog 也是同样思路,自定义 encoder 注入 TraceID。

3. 链路日志示例

time=2026-07-31T10:00:00 level=INFO trace_id=abc123 span_id=def456 msg="user login" user_id=123
time=2026-07-31T10:00:01 level=ERROR trace_id=abc123 span_id=ghi789 msg="db error" err=timeout

在 Loki / ELK 里搜 trace_id=abc123,立即拿到这条链路所有服务的所有日志。

九、Prometheus 指标埋点

1. Prometheus 简介

Prometheus 是 CNCF 的指标系统,核心特征:

  • 多维度标签:每个指标可带任意 label,便于切片分析。
  • Pull 模式:服务暴露 /metrics,Prometheus 主动拉取。
  • PromQL:强大的查询语言。
  • 告警:Alertmanager 配合实现告警。

2. Go 客户端埋点

prometheus/client_golang 是官方 Go 客户端:

go
package main

import (
	"log"
	"net/http"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	httpRequestsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "http_requests_total",
			Help: "Total number of HTTP requests",
		},
		[]string{"method", "path", "status"},
	)
	httpRequestDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "http_request_duration_seconds",
			Help:    "HTTP request duration in seconds",
			Buckets: prometheus.DefBuckets,
		},
		[]string{"method", "path"},
	)
	inflightRequests = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Name: "http_inflight_requests",
			Help: "Current inflight HTTP requests",
		},
	)
)

func init() {
	prometheus.MustRegister(httpRequestsTotal, httpRequestDuration, inflightRequests)
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("/metrics", promhttp.Handler())
	mux.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
		inflightRequests.Inc()
		defer inflightRequests.Dec()

		start := time.Now()
		time.Sleep(50 * time.Millisecond)
		httpRequestsTotal.WithLabelValues("GET", "/api", "200").Inc()
		httpRequestDuration.WithLabelValues("GET", "/api").Observe(time.Since(start).Seconds())
		_, _ = w.Write([]byte("ok"))
	})

	log.Println("server on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

四种核心指标类型:

类型用途示例
Counter单调递增计数请求总数、错误总数
Gauge可增可减的瞬时值当前连接数、内存占用
Histogram分布统计(分桶)延迟分布、响应大小分布
Summary分布统计(客户端分位)类似 Histogram

3. RED 指标体系

微服务接口推荐埋点 RED 三指标:

  • Rate:每秒请求数(Counter 求 rate)
  • Errors:错误率
  • Duration:延迟分布(Histogram)

十、Grafana 可视化

1. Grafana 简介

Grafana 是开源的可视化平台,支持 Prometheus、Loki、Jaeger、Tempo 等多种数据源,可以构建统一的监控大盘。

2. 部署 Grafana

bash
docker run -d --name grafana \
  -p 3000:3000 \
  grafana/grafana:10.4.0

访问 http://localhost:3000,默认账号 admin/admin

3. 配置数据源

  • Prometheus:URL http://localhost:9090
  • Loki:URL http://localhost:3100
  • Jaeger:URL http://localhost:16686
  • Tempo:URL http://localhost:3200(也可选 Tempo 替代 Jaeger)

4. 典型 Dashboard 面板

  • RED 面板:QPS、错误率、P99 延迟
  • 资源面板:CPU、内存、Goroutine 数、GC 耗时
  • 业务面板:订单量、支付成功率、库存变更
  • 链路面板:每条 Trace 的服务调用图

十一、完整示例:可观测的微服务

下面给出一个集成 OTel + Prometheus + slog 的完整 HTTP 服务示例:

go
package main

import (
	"context"
	"log"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"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"
	"go.opentelemetry.io/otel/trace"
)

// === 日志:关联 TraceID ===

type TraceHandler struct{ slog.Handler }

func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error {
	if sc := trace.SpanContextFromContext(ctx); sc.IsValid() {
		r.AddAttrs(
			slog.String("trace_id", sc.TraceID().String()),
			slog.String("span_id", sc.SpanID().String()),
		)
	}
	return h.Handler.Handle(ctx, r)
}

// === OTel 初始化 ===

func initTracer(ctx context.Context) (func(), error) {
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpoint("localhost:4318"),
		otlptracehttp.WithURLPath("/v1/traces"),
		otlptracehttp.WithInsecure(),
	)
	if err != nil {
		return nil, err
	}
	res, _ := resource.New(ctx, resource.WithAttributes(
		semconv.ServiceNameKey.String("order-service"),
	))
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.TraceContext{})
	return func() {
		_ = tp.Shutdown(context.Background())
	}, nil
}

// === Prometheus 指标 ===

var (
	reqTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "order_requests_total",
	}, []string{"path", "status"})
	reqDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
		Name:    "order_request_duration_seconds",
		Buckets: prometheus.DefBuckets,
	}, []string{"path"})
)

func init() {
	prometheus.MustRegister(reqTotal, reqDuration)
}

// === 业务 handler ===

func wrapHandler(name string, h http.HandlerFunc, logger *slog.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		tracer := otel.Tracer("order-service")
		ctx, span := tracer.Start(ctx, name)
		defer span.End()
		span.SetAttributes(attribute.String("http.path", r.URL.Path))

		start := time.Now()
		logger.Info(ctx, "request start", "path", r.URL.Path)

		rw := &statusWriter{ResponseWriter: w, status: 200}
		h(rw, r.WithContext(ctx))

		dur := time.Since(start).Seconds()
		reqTotal.WithLabelValues(r.URL.Path, http.StatusText(rw.status)).Inc()
		reqDuration.WithLabelValues(r.URL.Path).Observe(dur)
		logger.Info(ctx, "request end", "status", rw.status, "duration_s", dur)
	}
}

type statusWriter struct {
	http.ResponseWriter
	status int
}

func (sw *statusWriter) WriteHeader(code int) {
	sw.status = code
	sw.ResponseWriter.WriteHeader(code)
}

func orderHandler(w http.ResponseWriter, r *http.Request) {
	tracer := otel.Tracer("order-service")
	ctx, span := tracer.Start(r.Context(), "processOrder")
	defer span.End()
	time.Sleep(80 * time.Millisecond) // 模拟处理
	span.SetAttributes(attribute.String("order.id", "o-123"))
	_, _ = w.Write([]byte(`{"order":"o-123","status":"ok"}`))
}

func main() {
	ctx := context.Background()
	shutdown, err := initTracer(ctx)
	if err != nil {
		log.Printf("tracer init failed: %v (continue without tracing)", err)
	} else {
		defer shutdown()
	}

	logger := slog.New(&TraceHandler{
		Handler: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}),
	})

	mux := http.NewServeMux()
	mux.Handle("/metrics", promhttp.Handler())
	mux.HandleFunc("/api/orders", wrapHandler("POST /api/orders", orderHandler, logger))

	srv := &http.Server{Addr: ":8080", Handler: mux}
	go func() {
		logger.Info(ctx, "server starting", "addr", srv.Addr)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	_, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_ = srv.Shutdown(ctx)
}

启动后:

  1. 访问 http://localhost:8080/api/orders 几次。
  2. http://localhost:16686 查看追踪。
  3. 在 Prometheus 查询 rate(order_requests_total[1m]) 看请求速率。
  4. 日志里能看到 trace_id 字段。

十二、小结

本篇我们学习了微服务可观测性:

  • 三支柱:日志、指标、追踪,互补不可替代,通过 TraceID 联动。
  • 追踪原理:Trace 由多个 Span 组成树状结构,跨进程通过 W3C TraceContext header 传播。
  • OpenTelemetry:CNCF 标准,厂商中立,统一 API/SDK,支持多语言。
  • Jaeger:开箱即用的追踪 UI,通过 OTLP 协议接收数据。
  • Gin / gRPC 集成:用 otelgin / otelgrpc 自动埋点,业务代码零侵入。
  • 日志关联:自定义 slog handler 注入 TraceID,三支柱联动。
  • Prometheus:Counter / Gauge / Histogram 三种核心类型,RED 指标体系。
  • Grafana:统一可视化平台,整合多数据源。

下一篇我们将学习熔断、降级与限流,让微服务在面对故障和流量洪峰时保持稳定。

延伸阅读