Appearance
链路追踪与可观测性
本篇是 Go 微服务系列的第七篇。微服务系统一旦出问题,排查难度远高于单体——一个请求可能经过十几个服务,没有完整的可观测能力根本无从下手。可观测性(Observability)由三根支柱构成:日志(Logging)、指标(Metrics)、追踪(Tracing)。本篇将聚焦追踪,并整合 OpenTelemetry、Jaeger、Prometheus、Grafana 给出一套完整的可观测性方案。
一、可观测性三支柱
1. 三支柱是什么
- 日志(Logging):离散的事件记录,包含时间戳、级别、上下文字段。适合排查具体问题。
- 指标(Metrics):可聚合的数值时序数据,如 QPS、延迟分位、错误率。适合监控告警。
- 追踪(Tracing):单个请求在分布式系统中的完整调用链。适合理解服务间关系和瓶颈定位。
三者互补,不能相互替代:
| 维度 | 日志 | 指标 | 追踪 |
|---|---|---|---|
| 数据量 | 大(每个事件一条) | 小(聚合后) | 中(采样后) |
| 成本 | 高 | 低 | 中 |
| 排查 | 具体事件 | 趋势 / 告警 | 调用链 / 瓶颈 |
| 典型工具 | ELK / Loki | Prometheus | Jaeger / Zipkin |
2. 三者关联的关键:TraceID
最有价值的实践是把三者通过 TraceID 串起来:
- 一条追踪有一个全局唯一的
TraceID。 - 该请求经过的每个服务打的日志都带上这个
TraceID。 - 指标也按
TraceID(或更粗的维度)聚合。
排查问题时:先在指标上看到异常 → 在追踪上找到具体的慢调用链 → 用 TraceID 在日志里看具体错误。这就是「三支柱联动」。
二、分布式追踪原理
1. Trace 与 Span
- Trace:一次完整的请求链路,由唯一的
TraceID标识。 - Span:链路中的一个操作单元,有
SpanID和ParentSpanID。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 标准:
traceparentheader: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 / clientotelgrpc:gRPC server / clientotelsql/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.5516686:Jaeger UI4318: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)
}启动后:
- 访问
http://localhost:8080/api/orders几次。 - 在
http://localhost:16686查看追踪。 - 在 Prometheus 查询
rate(order_requests_total[1m])看请求速率。 - 日志里能看到
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:统一可视化平台,整合多数据源。
下一篇我们将学习熔断、降级与限流,让微服务在面对故障和流量洪峰时保持稳定。
延伸阅读:
- OpenTelemetry Go 文档:https://opentelemetry.io/docs/languages/go/
- Jaeger 文档:https://www.jaegertracing.io/docs/
- Prometheus 文档:https://prometheus.io/docs/
- 《Distributed Tracing in Practice》