一、为什么要做链路追踪
微服务架构中,一个请求会经过多个服务:
用户请求 → API网关 → 认证服务 → 订单服务 → 库存服务 → 支付服务 → 数据库
问题排查时:
1. 用户说"下单很慢",到底是哪个服务慢?
2. 某个接口报错了,是哪个环节出的问题?
3. 一次请求产生了多少 SQL 查询?有没有 N+1 问题?
4. 各个服务之间的依赖关系是怎样的?
链路追踪的核心目标:还原一次请求在分布式系统中的完整调用链,可视化每个环节的耗时和状态。
二、核心概念:Trace、Span、Context
一次请求的链路结构:
Trace(一次完整的请求)
├── Span A(API 网关) [200ms]
│ ├── Span B(认证服务) [50ms]
│ └── Span C(订单服务) [150ms]
│ ├── Span D(库存服务) [80ms]
│ │ └── Span E(MySQL 查询) [30ms]
│ └── Span F(支付服务) [60ms]
│ └── Span G(Redis 缓存) [5ms]
每个 Span 包含:
- trace_id: 全局唯一的追踪 ID
- span_id: 当前 Span 的 ID
- parent_span_id: 父 Span 的 ID
- operation_name: 操作名称
- start_time / end_time: 开始和结束时间
- tags: 标签(HTTP 方法、状态码等)
- logs: 日志事件
- status: 状态(OK / ERROR)
// distributed/tracing/model.go
package tracing
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"sync"
"time"
)
// SpanContext Span 上下文(跨进程传递)
type SpanContext struct {
TraceID string `json:"trace_id"`
SpanID string `json:"span_id"`
ParentSpanID string `json:"parent_span_id,omitempty"`
Sampled bool `json:"sampled"`
}
// SpanStatus Span 状态
type SpanStatus int
const (
SpanStatusOK SpanStatus = 0
SpanStatusError SpanStatus = 1
)
// Span 链路追踪的基本单元
type Span struct {
SpanContext
OperationName string `json:"operation_name"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Tags map[string]string `json:"tags,omitempty"`
Logs []SpanLog `json:"logs,omitempty"`
Status SpanStatus `json:"status"`
Children []*Span `json:"children,omitempty"`
mu sync.Mutex
}
// SpanLog Span 日志
type SpanLog struct {
Timestamp time.Time `json:"timestamp"`
Fields map[string]string `json:"fields"`
}
// NewSpan 创建新的 Span
func NewSpan(operationName string, parentCtx SpanContext) *Span {
spanID := generateID()
return &Span{
SpanContext: SpanContext{
TraceID: parentCtx.TraceID,
SpanID: spanID,
ParentSpanID: parentCtx.SpanID,
Sampled: parentCtx.Sampled,
},
OperationName: operationName,
StartTime: time.Now(),
Tags: make(map[string]string),
Logs: make([]SpanLog, 0),
}
}
// NewRootSpan 创建根 Span(入口请求)
func NewRootSpan(operationName string) *Span {
traceID := generateID()
spanID := generateID()
return &Span{
SpanContext: SpanContext{
TraceID: traceID,
SpanID: spanID,
Sampled: true,
},
OperationName: operationName,
StartTime: time.Now(),
Tags: make(map[string]string),
Logs: make([]SpanLog, 0),
}
}
// SetTag 设置标签
func (s *Span) SetTag(key, value string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Tags[key] = value
}
// Log 记录日志
func (s *Span) Log(fields map[string]string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Logs = append(s.Logs, SpanLog{
Timestamp: time.Now(),
Fields: fields,
})
}
// Finish 结束 Span
func (s *Span) Finish() {
s.EndTime = time.Now()
}
// Duration 获取持续时间
func (s *Span) Duration() time.Duration {
return s.EndTime.Sub(s.StartTime)
}
// Context 获取 Span 上下文(用于跨进程传递)
func (s *Span) Context() SpanContext {
return s.SpanContext
}
// generateID 生成 16 字节随机 ID
func generateID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
// ─── Context 传播 ───
// contextKey 用于存储 SpanContext 的 context key
type contextKey struct{}
// WithSpanContext 将 SpanContext 存入 context
func WithSpanContext(ctx context.Context, sc SpanContext) context.Context {
return context.WithValue(ctx, contextKey{}, sc)
}
// SpanContextFromContext 从 context 中取出 SpanContext
func SpanContextFromContext(ctx context.Context) (SpanContext, bool) {
sc, ok := ctx.Value(contextKey{}).(SpanContext)
return sc, ok
}
三、Tracer 实现
// distributed/tracing/tracer.go
package tracing
import (
"context"
"log"
"sync"
"time"
)
// Reporter Span 上报接口
type Reporter interface {
Report(span *Span) error
Flush() error
Close() error
}
// Tracer 链路追踪器
type Tracer struct {
reporter Reporter
sampler Sampler
spans map[string]*Span // traceID -> root span
mu sync.RWMutex
serviceName string
}
// Sampler 采样器接口
type Sampler interface {
ShouldSample(traceID string) bool
}
// AlwaysSample 总是采样
type AlwaysSample struct{}
func (s *AlwaysSample) ShouldSample(traceID string) bool {
return true
}
// RateSampling 概率采样
type RateSampling struct {
rate float64 // 0.0 ~ 1.0
}
func (s *RateSampling) ShouldSample(traceID string) bool {
// 根据 traceID 哈希决定是否采样
hash := 0
for _, c := range traceID {
hash += int(c)
}
return float64(hash%1000)/1000.0 < s.rate
}
// NewTracer 创建 Tracer
func NewTracer(serviceName string, reporter Reporter, sampler Sampler) *Tracer {
return &Tracer{
reporter: reporter,
sampler: sampler,
spans: make(map[string]*Span),
serviceName: serviceName,
}
}
// StartSpan 开始一个新的 Span
func (t *Tracer) StartSpan(ctx context.Context, operationName string) (*Span, context.Context) {
var span *Span
if parentCtx, ok := SpanContextFromContext(ctx); ok {
// 有父 Span,创建子 Span
span = NewSpan(operationName, parentCtx)
} else {
// 没有父 Span,创建根 Span
span = NewRootSpan(operationName)
span.Sampled = t.sampler.ShouldSample(span.TraceID)
}
// 设置服务名
span.SetTag("service", t.serviceName)
// 存入 context
ctx = WithSpanContext(ctx, span.SpanContext)
return span, ctx
}
// EndSpan 结束 Span 并上报
func (t *Tracer) EndSpan(span *Span) {
span.Finish()
if !span.Sampled {
return // 未采样,不上报
}
// 如果是根 Span,上报整个 Trace
if span.ParentSpanID == "" {
t.reportTrace(span)
}
}
// reportTrace 上报完整的 Trace
func (t *Tracer) reportTrace(root *Span) {
if err := t.reporter.Report(root); err != nil {
log.Printf("[Tracing] Report failed: %v", err)
}
}
// Inject 将 SpanContext 注入到 carrier 中(用于跨进程传递)
func (t *Tracer) Inject(ctx context.Context, carrier map[string]string) {
if sc, ok := SpanContextFromContext(ctx); ok {
carrier["x-trace-id"] = sc.TraceID
carrier["x-span-id"] = sc.SpanID
carrier["x-parent-span-id"] = sc.ParentSpanID
if sc.Sampled {
carrier["x-sampled"] = "1"
} else {
carrier["x-sampled"] = "0"
}
}
}
// Extract 从 carrier 中提取 SpanContext(用于跨进程接收)
func (t *Tracer) Extract(carrier map[string]string) (SpanContext, bool) {
traceID := carrier["x-trace-id"]
spanID := carrier["x-span-id"]
if traceID == "" || spanID == "" {
return SpanContext{}, false
}
return SpanContext{
TraceID: traceID,
SpanID: spanID,
ParentSpanID: carrier["x-parent-span-id"],
Sampled: carrier["x-sampled"] == "1",
}, true
}
// Flush 刷新所有待上报的 Span
func (t *Tracer) Flush() error {
return t.reporter.Flush()
}
// Close 关闭 Tracer
func (t *Tracer) Close() error {
t.Flush()
return t.reporter.Close()
}
四、Reporter 实现(控制台 + HTTP)
// distributed/tracing/reporter.go
package tracing
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
)
// ConsoleReporter 控制台输出 Reporter
type ConsoleReporter struct {
mu sync.Mutex
}
func NewConsoleReporter() *ConsoleReporter {
return &ConsoleReporter{}
}
func (r *ConsoleReporter) Report(span *Span) error {
r.mu.Lock()
defer r.mu.Unlock()
duration := span.Duration().Milliseconds()
indent := ""
// 打印根 Span
fmt.Printf("\n=== Trace: %s ===\n", span.TraceID)
r.printSpan(span, 0)
return nil
}
func (r *ConsoleReporter) printSpan(span *Span, depth int) {
prefix := ""
for i := 0; i < depth; i++ {
prefix += " "
}
duration := span.Duration()
status := "OK"
if span.Status == SpanStatusError {
status = "ERROR"
}
fmt.Printf("%s├── %s [%v] %s\n", prefix, span.OperationName, duration, status)
// 打印标签
for k, v := range span.Tags {
fmt.Printf("%s│ %s: %s\n", prefix, k, v)
}
// 递归打印子 Span
for _, child := range span.Children {
r.printSpan(child, depth+1)
}
}
func (r *ConsoleReporter) Flush() error {
return nil
}
func (r *ConsoleReporter) Close() error {
return nil
}
// HTTPReporter HTTP 上报 Reporter(发送到 Jaeger/Zipkin)
type HTTPReporter struct {
endpoint string
client *http.Client
buffer []*Span
mu sync.Mutex
batchSize int
}
func NewHTTPReporter(endpoint string) *HTTPReporter {
return &HTTPReporter{
endpoint: endpoint,
client: &http.Client{Timeout: 5 * time.Second},
buffer: make([]*Span, 0),
batchSize: 100,
}
}
func (r *HTTPReporter) Report(span *Span) error {
r.mu.Lock()
defer r.mu.Unlock()
r.buffer = append(r.buffer, span)
if len(r.buffer) >= r.batchSize {
return r.flush()
}
return nil
}
func (r *HTTPReporter) flush() error {
if len(r.buffer) == 0 {
return nil
}
data, err := json.Marshal(r.buffer)
if err != nil {
return err
}
resp, err := r.client.Post(r.endpoint, "application/json", bytes.NewReader(data))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("report failed: %d", resp.StatusCode)
}
r.buffer = r.buffer[:0]
return nil
}
func (r *HTTPReporter) Flush() error {
r.mu.Lock()
defer r.mu.Unlock()
return r.flush()
}
func (r *HTTPReporter) Close() error {
return r.Flush()
}
五、HTTP 中间件集成
// distributed/tracing/middleware.go
package tracing
import (
"context"
"net/http"
"strconv"
"time"
)
// HTTPMiddleware HTTP 链路追踪中间件
type HTTPMiddleware struct {
tracer *Tracer
}
func NewHTTPMiddleware(tracer *Tracer) *HTTPMiddleware {
return &HTTPMiddleware{tracer: tracer}
}
// WrapHandler 包装 HTTP Handler
func (m *HTTPMiddleware) WrapHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头中提取 SpanContext
carrier := make(map[string]string)
for _, key := range []string{"x-trace-id", "x-span-id", "x-parent-span-id", "x-sampled"} {
if v := r.Header.Get(key); v != "" {
carrier[key] = v
}
}
ctx := r.Context()
if sc, ok := m.tracer.Extract(carrier); ok {
ctx = WithSpanContext(ctx, sc)
}
// 创建 Span
operationName := r.Method + " " + r.URL.Path
span, ctx := m.tracer.StartSpan(ctx, operationName)
// 设置 HTTP 标签
span.SetTag("http.method", r.Method)
span.SetTag("http.url", r.URL.String())
span.SetTag("http.host", r.Host)
// 包装 ResponseWriter 捕获状态码
wrapper := &responseWrapper{ResponseWriter: w, statusCode: http.StatusOK}
// 执行后续 handler
next.ServeHTTP(wrapper, r.WithContext(ctx))
// 记录状态码
span.SetTag("http.status_code", strconv.Itoa(wrapper.statusCode))
if wrapper.statusCode >= 500 {
span.Status = SpanStatusError
span.Log(map[string]string{
"event": "error",
"message": http.StatusText(wrapper.statusCode),
})
}
// 结束 Span
m.tracer.EndSpan(span)
// 传递 Trace ID 到响应头
w.Header().Set("x-trace-id", span.TraceID)
})
}
// responseWrapper 包装 ResponseWriter 以捕获状态码
type responseWrapper struct {
http.ResponseWriter
statusCode int
}
func (w *responseWrapper) WriteHeader(code int) {
w.statusCode = code
w.ResponseWriter.WriteHeader(code)
}
// HTTPClientMiddleware HTTP 客户端中间件
type HTTPClientMiddleware struct {
tracer *Tracer
}
func NewHTTPClientMiddleware(tracer *Tracer) *HTTPClientMiddleware {
return &HTTPClientMiddleware{tracer: tracer}
}
// RoundTrip 实现 http.RoundTripper 接口
func (m *HTTPClientMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
// 创建客户端 Span
operationName := "HTTP " + req.Method + " " + req.URL.Host
span, ctx := m.tracer.StartSpan(ctx, operationName)
defer m.tracer.EndSpan(span)
// 注入 SpanContext 到请求头
carrier := make(map[string]string)
m.tracer.Inject(ctx, carrier)
for k, v := range carrier {
req.Header.Set(k, v)
}
// 执行请求
start := time.Now()
resp, err := http.DefaultTransport.RoundTrip(req)
duration := time.Since(start)
span.SetTag("http.method", req.Method)
span.SetTag("http.url", req.URL.String())
span.SetTag("http.duration_ms", strconv.FormatInt(duration.Milliseconds(), 10))
if err != nil {
span.Status = SpanStatusError
span.Log(map[string]string{"event": "error", "message": err.Error()})
return resp, err
}
span.SetTag("http.status_code", strconv.Itoa(resp.StatusCode))
if resp.StatusCode >= 500 {
span.Status = SpanStatusError
}
return resp, nil
}
六、完整使用示例
// examples/tracing_demo.go
package main
import (
"context"
"fmt"
"net/http"
"time"
"distributed/tracing"
)
func main() {
fmt.Println("=== 分布式链路追踪演示 ===\n")
// 1. 创建 Tracer
tracer := tracing.NewTracer(
"demo-service",
tracing.NewConsoleReporter(),
&tracing.AlwaysSample{},
)
defer tracer.Close()
// 2. 模拟一个完整的请求链路
fmt.Println("【场景1】模拟用户下单请求链路\n")
// 根 Span:API 网关
rootSpan, ctx := tracer.StartSpan(context.Background(), "POST /api/orders")
rootSpan.SetTag("http.method", "POST")
rootSpan.SetTag("http.url", "/api/orders")
rootSpan.SetTag("user_id", "12345")
// 模拟认证服务调用
authSpan, authCtx := tracer.StartSpan(ctx, "AuthService.VerifyToken")
authSpan.SetTag("token", "eyJhbGciOiJIUzI1NiIs...")
time.Sleep(30 * time.Millisecond) // 模拟耗时
authSpan.Log(map[string]string{"event": "verify", "result": "success"})
tracer.EndSpan(authSpan)
// 模拟订单服务调用
orderSpan, orderCtx := tracer.StartSpan(authCtx, "OrderService.CreateOrder")
orderSpan.SetTag("product_id", "P10086")
orderSpan.SetTag("quantity", "2")
// 模拟库存服务调用
stockSpan, stockCtx := tracer.StartSpan(orderCtx, "InventoryService.DeductStock")
stockSpan.SetTag("product_id", "P10086")
stockSpan.SetTag("deduct", "2")
time.Sleep(50 * time.Millisecond) // 模拟库存查询
stockSpan.Log(map[string]string{"event": "stock_check", "remaining": "98"})
tracer.EndSpan(stockSpan)
// 模拟支付服务调用
paySpan, payCtx := tracer.StartSpan(stockCtx, "PaymentService.Charge")
paySpan.SetTag("amount", "199.99")
paySpan.SetTag("currency", "CNY")
time.Sleep(80 * time.Millisecond) // 模拟支付处理
paySpan.Log(map[string]string{"event": "payment", "status": "success"})
// 模拟支付中的数据库查询
dbSpan, _ := tracer.StartSpan(payCtx, "MySQL.Query")
dbSpan.SetTag("db.statement", "UPDATE accounts SET balance = balance - 199.99 WHERE user_id = 12345")
dbSpan.SetTag("db.instance", "orders_db")
time.Sleep(20 * time.Millisecond)
tracer.EndSpan(dbSpan)
tracer.EndSpan(paySpan)
tracer.EndSpan(orderSpan)
// 结束根 Span
rootSpan.SetTag("http.status_code", "200")
tracer.EndSpan(rootSpan)
fmt.Println("\n(请查看上方输出的调用链)\n")
// 3. HTTP 中间件演示
fmt.Println("【场景2】HTTP 中间件集成\n")
middleware := tracing.NewHTTPMiddleware(tracer)
// 创建一个简单的 HTTP 服务
mux := http.NewServeMux()
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
// 模拟业务处理
time.Sleep(40 * time.Millisecond)
w.Write([]byte(`{"users": ["Alice", "Bob"]}`))
})
mux.HandleFunc("/api/products", func(w http.ResponseWriter, r *http.Request) {
// 模拟业务处理
time.Sleep(25 * time.Millisecond)
w.Write([]byte(`{"products": ["P10086", "P10087"]}`))
})
// 包装中间件
handler := middleware.WrapHandler(mux)
fmt.Println("启动 HTTP 服务(模拟请求)...\n")
// 模拟两个请求
for _, path := range []string{"/api/users", "/api/products"} {
req, _ := http.NewRequest("GET", "http://localhost:8080"+path, nil)
req.Header.Set("x-trace-id", tracing.NewRootSpan("test").TraceID)
req.Header.Set("x-sampled", "1")
// 创建 ResponseRecorder
rr := &mockResponseWriter{header: make(http.Header)}
handler.ServeHTTP(rr, req)
fmt.Printf(" GET %s → %d\n", path, rr.statusCode)
}
fmt.Println("\n演示完成!")
}
// mockResponseWriter 模拟 ResponseWriter
type mockResponseWriter struct {
header http.Header
statusCode int
body []byte
}
func (w *mockResponseWriter) Header() http.Header {
return w.header
}
func (w *mockResponseWriter) Write(b []byte) (int, error) {
w.body = b
return len(b), nil
}
func (w *mockResponseWriter) WriteHeader(code int) {
w.statusCode = code
}
七、采样策略
// distributed/tracing/sampling.go
package tracing
import (
"hash/fnv"
"sync"
"time"
)
// HeadBasedSampling 头部采样(请求开始时决定是否采样)
type HeadBasedSampling struct {
rate float64
}
func NewHeadBasedSampling(rate float64) *HeadBasedSampling {
return &HeadBasedSampling{rate: rate}
}
func (s *HeadBasedSampling) ShouldSample(traceID string) bool {
h := fnv.New32a()
h.Write([]byte(traceID))
return float64(h.Sum32()%1000)/1000.0 < s.rate
}
// TailBasedSampling 尾部采样(根据结果决定是否采样)
// 只采样慢请求和错误请求
type TailBasedSampling struct {
slowThreshold time.Duration
sampleAllError bool
mu sync.Mutex
}
func NewTailBasedSampling(slowThreshold time.Duration, sampleAllError bool) *TailBasedSampling {
return &TailBasedSampling{
slowThreshold: slowThreshold,
sampleAllError: sampleAllError,
}
}
func (s *TailBasedSampling) ShouldSample(traceID string) bool {
// 尾部采样在 Span 结束时判断
// 这里返回 true,实际决策在 EndSpan 中
return true
}
// ShouldSampleSpan 根据 Span 决定是否采样
func (s *TailBasedSampling) ShouldSampleSpan(span *Span) bool {
if span.Status == SpanStatusError && s.sampleAllError {
return true // 错误请求全部采样
}
if span.Duration() >= s.slowThreshold {
return true // 慢请求采样
}
return false
}
// PrioritySampling 优先级采样
// 关键业务全量采样,非关键业务概率采样
type PrioritySampling struct {
highPriorityPaths []string
defaultRate float64
}
func NewPrioritySampling(highPriorityPaths []string, defaultRate float64) *PrioritySampling {
return &PrioritySampling{
highPriorityPaths: highPriorityPaths,
defaultRate: defaultRate,
}
}
func (s *PrioritySampling) ShouldSample(traceID string) bool {
// 实际应该在创建 Span 时根据 operationName 判断
return true
}
// IsHighPriority 判断是否是高优先级路径
func (s *PrioritySampling) IsHighPriority(operationName string) bool {
for _, path := range s.highPriorityPaths {
if operationName == path {
return true
}
}
return false
}
八、生产坑位
| # | 坑位 | 现象 | 解决方案 |
|---|---|---|---|
| 1 | 采样率过高 | 全量采样导致性能下降 10%-20% | 概率采样(1%-10%)/ 尾部采样 |
| 2 | Context 泄漏 | Goroutine 泄漏导致内存上涨 | 及时调用 Finish() / defer 确保释放 |
| 3 | Span 上报阻塞 | Reporter 慢导致业务线程阻塞 | 异步上报 / 批量上报 / 丢弃策略 |
| 4 | TraceID 冲突 | 不同请求的 TraceID 重复 | 使用足够长的随机 ID(128 bit) |
| 5 | 跨进程传播丢失 | 消息队列场景 Context 传递中断 | 显式注入/提取 SpanContext |
| 6 | 采样不一致 | 同一 Trace 的部分 Span 被采样部分没有 | 头部采样保证一致性 |
九、本讲总结
┌──────────────────────────────────────────────────────────────────────────┐
│ 第8讲 核心收获 │
│ │
│ ✅ 理解了链路追踪的核心概念: │
│ - Trace(完整请求链路) │
│ - Span(单个操作单元) │
│ - SpanContext(跨进程传播) │
│ │
│ ✅ 实现了 Tracer 核心功能: │
│ - 创建/结束 Span │
│ - Context 传播 │
│ - Inject/Extract(跨进程传递) │
│ │
│ ✅ 实现了 Reporter: │
│ - 控制台输出(调试用) │
│ - HTTP 上报(Jaeger/Zipkin 兼容) │
│ │
│ ✅ 集成了 HTTP 中间件: │
│ - 服务端自动采集 │
│ - 客户端自动注入 │
│ │
│ ✅ 实现了多种采样策略: │
│ - 头部采样 / 尾部采样 / 优先级采样 │
│ │
│ ✅ 覆盖了 6 个生产坑位与解决方案 │
│ │
└──────────────────────────────────────────────────────────────────────────┘
十、下一讲预告
第9讲:《分布式配置中心》
你将学到:
- 配置中心的三大核心能力
- etcd 实现配置存储与监听
- 配置热更新机制
- 配置版本管理与回滚
- 从零实现一个配置中心 SDK
🧰 开发之余的小工具推荐
在调试链路追踪的 Span 结构和采样策略时,zz365.top 的 JSON 格式化工具可以将 Trace 数据导出为结构化视图,清晰展示每个 Span 的父子关系和耗时分布。其文本差异比较工具则可以对比两次请求的调用链差异,快速定位新增或消失的 Span。所有处理均在浏览器本地完成,不会泄露你的业务数据。