一、为什么需要分布式追踪?
上一讲我们实现了结构化日志,但日志有一个天然缺陷:它们是一个个孤立的点。
想象一个典型的 Agent 请求链路:
用户请求 → MCP Gateway → LLM 调用 → 工具调用 A → 工具调用 B → LLM 再次调用 → 返回结果
如果每个步骤各自打日志,你看到的是:
[14:23:01] 收到用户请求
[14:23:02] LLM 调用开始
[14:23:03] 工具 A 被调用
[14:23:04] 工具 B 被调用
[14:23:05] LLM 再次调用
[14:23:06] 响应返回
问题来了:
- 这些日志属于同一个请求吗?------ 没有关联 ID 无法确认
- 哪一步最慢?------ 只知道每个步骤的耗时,但不知道它们之间的因果关系
- 如果失败了,是 LLM 的问题还是工具的问题?------ 需要手动拼凑
**分布式追踪(Distributed Tracing)** 解决的就是这个问题:通过 Trace → Span → Span 的树形结构,把一次请求的所有步骤串联起来。
二、核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| Trace | 一次请求的完整生命周期 | 一部电影的完整剧情 |
| Span | Trace 中的一个步骤/操作 | 电影中的一个场景 |
| Span Context | 携带 Trace ID、Span ID 的上下文 | 场景编号和关联信息 |
| Parent Span | 父级 Span,表示调用者 | 上层场景 |
| Child Span | 子级 Span,表示被调用者 | 下层场景 |
| Propagation | 跨进程传递 Span Context | 场景间的线索传递 |
一个 Trace 的结构:
Trace: abc123
├── Span: gateway.handle_request (耗时 3200ms)
│ ├── Span: llm.call_gpt4 (耗时 1800ms)
│ │ └── Span: http.post_openai (耗时 1750ms)
│ ├── Span: tool.get_weather (耗时 400ms)
│ │ └── Span: http.call_weather_api (耗时 380ms)
│ └── Span: llm.call_gpt4 (耗时 900ms)
│ └── Span: http.post_openai (耗时 880ms)
三、Go 实现:OpenTelemetry 兼容的追踪系统
package main
import (
"context"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"sync"
"sync/atomic"
"time"
)
// ---- 核心数据类型 ----
// SpanContext 携带跨进程传递的追踪信息
type SpanContext struct {
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
TraceFlags byte `json:"trace_flags"`
TraceState string `json:"trace_state,omitempty"`
}
type TraceID [16]byte
type SpanID [8]byte
func (t TraceID) String() string {
return hex.EncodeToString(t[:])
}
func (s SpanID) String() string {
return hex.EncodeToString(s[:])
}
// SpanKind 表示 Span 的角色
type SpanKind int
const (
SpanKindInternal SpanKind = 1 // 内部调用
SpanKindServer SpanKind = 2 // 服务端处理请求
SpanKindClient SpanKind = 3 // 客户端发起请求
SpanKindProducer SpanKind = 4 // 生产者(消息队列)
SpanKindConsumer SpanKind = 5 // 消费者(消息队列)
)
func (k SpanKind) String() string {
switch k {
case SpanKindInternal:
return "INTERNAL"
case SpanKindServer:
return "SERVER"
case SpanKindClient:
return "CLIENT"
case SpanKindProducer:
return "PRODUCER"
case SpanKindConsumer:
return "CONSUMER"
}
return "UNKNOWN"
}
// SpanStatus 表示 Span 的执行结果
type SpanStatus int
const (
StatusUnset SpanStatus = 0
StatusOK SpanStatus = 1
StatusError SpanStatus = 2
)
// SpanData 是 Span 的完整数据
type SpanData struct {
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
ParentSpanID SpanID `json:"parent_span_id,omitempty"`
Name string `json:"name"`
Kind SpanKind `json:"kind"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Status SpanStatus `json:"status"`
StatusDesc string `json:"status_description,omitempty"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
Events []SpanEvent `json:"events,omitempty"`
Links []SpanLink `json:"links,omitempty"`
Resource map[string]string `json:"resource,omitempty"`
}
type SpanEvent struct {
Timestamp time.Time `json:"timestamp"`
Name string `json:"name"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
}
type SpanLink struct {
TraceID TraceID `json:"trace_id"`
SpanID SpanID `json:"span_id"`
Attributes map[string]interface{} `json:"attributes,omitempty"`
}
// ---- TracerProvider ----
type TracerProvider struct {
mu sync.RWMutex
exporters []SpanExporter
idGenerator IDGenerator
resource map[string]string
sampler Sampler
}
type IDGenerator interface {
NewTraceID() TraceID
NewSpanID() TraceID
}
type Sampler interface {
ShouldSample(SamplingParameters) SamplingDecision
}
type SamplingParameters struct {
TraceID TraceID
Name string
Kind SpanKind
Attributes map[string]interface{}
ParentContext SpanContext
}
type SamplingDecision struct {
Sample bool
Attributes map[string]interface{}
}
// 默认采样器:总是采样
type AlwaysOnSampler struct{}
func (s *AlwaysOnSampler) ShouldSample(params SamplingParameters) SamplingDecision {
return SamplingDecision{Sample: true}
}
// 概率采样器
type ProbabilitySampler struct {
probability float64
}
func NewProbabilitySampler(prob float64) *ProbabilitySampler {
return &ProbabilitySampler{probability: prob}
}
func (s *ProbabilitySampler) ShouldSample(params SamplingParameters) SamplingDecision {
// 基于 TraceID 的前几个字节做确定性采样
traceID := params.TraceID
val := binary.BigEndian.Uint64(traceID[:8])
ratio := float64(val) / float64(^uint64(0))
sample := ratio < s.probability
return SamplingDecision{Sample: sample}
}
// 默认 ID 生成器
type DefaultIDGenerator struct {
randomSource *rand.Rand
mu sync.Mutex
}
func NewDefaultIDGenerator() *DefaultIDGenerator {
return &DefaultIDGenerator{
randomSource: rand.New(rand.NewSource(time.Now().UnixNano())),
}
}
func (g *DefaultIDGenerator) NewTraceID() TraceID {
g.mu.Lock()
defer g.mu.Unlock()
var tid TraceID
binary.BigEndian.PutUint64(tid[:8], g.randomSource.Uint64())
binary.BigEndian.PutUint64(tid[8:], g.randomSource.Uint64())
return tid
}
func (g *DefaultIDGenerator) NewSpanID() SpanID {
g.mu.Lock()
defer g.mu.Unlock()
var sid SpanID
binary.BigEndian.PutUint64(sid[:], g.randomSource.Uint64())
return sid
}
// ---- SpanExporter ----
type SpanExporter interface {
ExportSpans(spans []*SpanData) error
Shutdown() error
}
// 控制台导出器
type ConsoleSpanExporter struct {
writer io.Writer
mu sync.Mutex
}
func NewConsoleSpanExporter() *ConsoleSpanExporter {
return &ConsoleSpanExporter{
writer: os.Stdout,
}
}
func (e *ConsoleSpanExporter) ExportSpans(spans []*SpanData) error {
e.mu.Lock()
defer e.mu.Unlock()
for _, span := range spans {
data, _ := json.MarshalIndent(span, "", " ")
e.writer.Write(data)
e.writer.Write([]byte{'\n', '\n'})
}
return nil
}
func (e *ConsoleSpanExporter) Shutdown() error {
return nil
}
// ---- Tracer ----
type Tracer struct {
provider *TracerProvider
name string
version string
}
func (t *Tracer) Start(ctx context.Context, name string, opts ...SpanOption) (context.Context, *Span) {
config := &SpanConfig{}
for _, opt := range opts {
opt(config)
}
// 从上下文中提取父 SpanContext
parentSpanContext := GetSpanContext(ctx)
// 生成新的 Span ID
spanID := t.provider.idGenerator.NewSpanID()
// 确定 Trace ID
var traceID TraceID
var parentSpanID SpanID
var isRoot bool
if parentSpanContext.IsValid() {
traceID = parentSpanContext.TraceID
parentSpanID = parentSpanContext.SpanID
isRoot = false
} else {
traceID = t.provider.idGenerator.NewTraceID()
isRoot = true
}
// 采样决策
samplingParams := SamplingParameters{
TraceID: traceID,
Name: name,
Kind: config.Kind,
Attributes: config.Attributes,
ParentContext: parentSpanContext,
}
decision := t.provider.sampler.ShouldSample(samplingParams)
span := &Span{
tracer: t,
spanContext: SpanContext{
TraceID: traceID,
SpanID: spanID,
},
parentSpanID: parentSpanID,
name: name,
kind: config.Kind,
startTime: time.Now(),
attributes: config.Attributes,
events: make([]SpanEvent, 0),
links: config.Links,
resource: t.provider.resource,
isRecording: decision.Sample,
isRoot: isRoot,
}
// 将新的 SpanContext 注入到上下文中
ctx = context.WithValue(ctx, spanContextKey, span.spanContext)
return ctx, span
}
// ---- Span ----
type Span struct {
tracer *Tracer
spanContext SpanContext
parentSpanID SpanID
name string
kind SpanKind
startTime time.Time
endTime time.Time
status SpanStatus
statusDesc string
attributes map[string]interface{}
events []SpanEvent
links []SpanLink
resource map[string]string
isRecording bool
isRoot bool
ended bool
mu sync.Mutex
}
func (s *Span) SpanContext() SpanContext {
return s.spanContext
}
func (s *Span) SetAttribute(key string, value interface{}) {
if !s.isRecording {
return
}
s.mu.Lock()
if s.attributes == nil {
s.attributes = make(map[string]interface{})
}
s.attributes[key] = value
s.mu.Unlock()
}
func (s *Span) AddEvent(name string, attrs ...Attr) {
if !s.isRecording {
return
}
s.mu.Lock()
s.events = append(s.events, SpanEvent{
Timestamp: time.Now(),
Name: name,
Attributes: attrsToMap(attrs),
})
s.mu.Unlock()
}
func (s *Span) SetStatus(status SpanStatus, description string) {
if !s.isRecording {
return
}
s.mu.Lock()
s.status = status
s.statusDesc = description
s.mu.Unlock()
}
func (s *Span) End(opts ...SpanEndOption) {
if !s.isRecording {
return
}
s.mu.Lock()
if s.ended {
s.mu.Unlock()
return
}
s.ended = true
s.endTime = time.Now()
s.mu.Unlock()
// 构建 SpanData 并导出
data := &SpanData{
TraceID: s.spanContext.TraceID,
SpanID: s.spanContext.SpanID,
ParentSpanID: s.parentSpanID,
Name: s.name,
Kind: s.kind,
StartTime: s.startTime,
EndTime: s.endTime,
Status: s.status,
StatusDesc: s.statusDesc,
Attributes: s.attributes,
Events: s.events,
Links: s.links,
Resource: s.resource,
}
// 异步导出
go func() {
for _, exporter := range s.tracer.provider.exporters {
if err := exporter.ExportSpans([]*SpanData{data}); err != nil {
log.Printf("[TRACE] 导出 Span 失败: %v", err)
}
}
}()
}
// ---- Span 选项 ----
type SpanConfig struct {
Kind SpanKind
Attributes map[string]interface{}
Links []SpanLink
}
type SpanOption func(*SpanConfig)
type SpanEndOption func(*SpanConfig)
func WithSpanKind(kind SpanKind) SpanOption {
return func(c *SpanConfig) {
c.Kind = kind
}
}
func WithAttributes(attrs map[string]interface{}) SpanOption {
return func(c *SpanConfig) {
c.Attributes = attrs
}
}
func WithLinks(links []SpanLink) SpanOption {
return func(c *SpanConfig) {
c.Links = links
}
}
// ---- 上下文操作 ----
type spanContextKeyType struct{}
var spanContextKey = spanContextKeyType{}
func GetSpanContext(ctx context.Context) SpanContext {
if ctx == nil {
return SpanContext{}
}
sc, ok := ctx.Value(spanContextKey).(SpanContext)
if !ok {
return SpanContext{}
}
return sc
}
func (sc SpanContext) IsValid() bool {
return sc.TraceID != TraceID{} && sc.SpanID != SpanID{}
}
// ---- W3C Trace Context 传播(HTTP Headers) ----
// traceparent 格式: 00-traceid-spanid-01
// tracestate 格式: vendor1=value1,vendor2=value2
func InjectSpanContextIntoHTTPHeaders(ctx context.Context, header http.Header) {
sc := GetSpanContext(ctx)
if !sc.IsValid() {
return
}
// traceparent
tp := fmt.Sprintf("00-%s-%s-%02x",
sc.TraceID.String(),
sc.SpanID.String(),
sc.TraceFlags,
)
header.Set("traceparent", tp)
// tracestate
if sc.TraceState != "" {
header.Set("tracestate", sc.TraceState)
}
}
func ExtractSpanContextFromHTTPHeaders(header http.Header) SpanContext {
// 从 traceparent 头提取
tp := header.Get("traceparent")
if tp == "" {
return SpanContext{}
}
// 格式: 00-traceid-spanid-flags
parts := splitTraceParent(tp)
if len(parts) != 4 {
return SpanContext{}
}
traceID, err := hex.DecodeString(parts[1])
if err != nil || len(traceID) != 16 {
return SpanContext{}
}
spanID, err := hex.DecodeString(parts[2])
if err != nil || len(spanID) != 8 {
return SpanContext{}
}
flags, _ := hex.DecodeString(parts[3])
var tid TraceID
var sid SpanID
copy(tid[:], traceID)
copy(sid[:], spanID)
sc := SpanContext{
TraceID: tid,
SpanID: sid,
TraceFlags: flags[0],
TraceState: header.Get("tracestate"),
}
return sc
}
func splitTraceParent(tp string) []string {
// 简单分割,实际应更健壮
parts := make([]string, 0, 4)
current := ""
for _, c := range tp {
if c == '-' {
parts = append(parts, current)
current = ""
} else {
current += string(c)
}
}
parts = append(parts, current)
return parts
}
// ---- MCP Gateway 追踪中间件 ----
type TracingMiddleware struct {
tracer *Tracer
}
func NewTracingMiddleware(tracer *Tracer) *TracingMiddleware {
return &TracingMiddleware{tracer: tracer}
}
func (m *TracingMiddleware) Wrap(next func(context.Context, map[string]interface{}) (interface{}, error)) func(context.Context, map[string]interface{}) (interface{}, error) {
return func(ctx context.Context, req map[string]interface{}) (interface{}, error) {
// 创建根 Span
ctx, span := m.tracer.Start(ctx, "gateway.handle_request",
WithSpanKind(SpanKindServer),
WithAttributes(map[string]interface{}{
"http.method": extractFromReq(req, "method"),
"http.path": extractFromReq(req, "path"),
"agent.id": extractFromReq(req, "agent_id"),
}),
)
defer span.End()
// 执行后续逻辑
resp, err := next(ctx, req)
if err != nil {
span.SetStatus(StatusError, err.Error())
span.AddEvent("error", String("error.message", err.Error()))
} else {
span.SetStatus(StatusOK, "")
}
return resp, err
}
}
// ---- LLM 追踪装饰器 ----
type TracingLLMDecorator struct {
tracer *Tracer
}
func NewTracingLLMDecorator(tracer *Tracer) *TracingLLMDecorator {
return &TracingLLMDecorator{tracer: tracer}
}
func (d *TracingLLMDecorator) DecorateLLMCall(ctx context.Context, model string, prompt string, fn func() (string, int, int, error)) (string, error) {
ctx, span := d.tracer.Start(ctx, "llm.call",
WithSpanKind(SpanKindClient),
WithAttributes(map[string]interface{}{
"llm.model": model,
"llm.prompt_length": len(prompt),
"llm.prompt_preview": truncateString(prompt, 200),
}),
)
defer span.End()
response, promptTokens, completionTokens, err := fn()
if err != nil {
span.SetStatus(StatusError, err.Error())
span.AddEvent("llm.error", String("error", err.Error()))
return "", err
}
span.SetAttribute("llm.prompt_tokens", promptTokens)
span.SetAttribute("llm.completion_tokens", completionTokens)
span.SetAttribute("llm.total_tokens", promptTokens+completionTokens)
span.SetStatus(StatusOK, "")
return response, nil
}
// ---- 工具调用追踪装饰器 ----
type TracingToolDecorator struct {
tracer *Tracer
}
func NewTracingToolDecorator(tracer *Tracer) *TracingToolDecorator {
return &TracingToolDecorator{tracer: tracer}
}
func (d *TracingToolDecorator) DecorateToolCall(ctx context.Context, toolName string, args map[string]interface{}, fn func() (interface{}, error)) (interface{}, error) {
ctx, span := d.tracer.Start(ctx, fmt.Sprintf("tool.%s", toolName),
WithSpanKind(SpanKindClient),
WithAttributes(map[string]interface{}{
"tool.name": toolName,
"tool.args": args,
}),
)
defer span.End()
result, err := fn()
if err != nil {
span.SetStatus(StatusError, err.Error())
span.AddEvent("tool.error", String("error", err.Error()))
return nil, err
}
span.SetAttribute("tool.success", true)
span.SetStatus(StatusOK, "")
return result, nil
}
// ---- 演示 ----
func main() {
// 创建 TracerProvider
provider := &TracerProvider{
exporters: []SpanExporter{NewConsoleSpanExporter()},
idGenerator: NewDefaultIDGenerator(),
resource: map[string]string{
"service.name": "mcp-gateway",
"service.version": "1.0.0",
"environment": "development",
},
sampler: &AlwaysOnSampler{},
}
// 创建 Tracer
tracer := &Tracer{
provider: provider,
name: "mcp-gateway",
version: "1.0.0",
}
fmt.Println("========== 分布式追踪演示 ==========\n")
// 1. 基本用法:创建根 Span 和子 Span
fmt.Println("--- 1. 基本 Span 创建 ---")
ctx := context.Background()
// 根 Span
ctx, rootSpan := tracer.Start(ctx, "handle_user_query",
WithSpanKind(SpanKindServer),
WithAttributes(map[string]interface{}{
"user.id": "user-xiaoming",
"query": "北京的天气怎么样?",
"session": "sess-001",
}),
)
// 模拟一些工作
time.Sleep(10 * time.Millisecond)
// 子 Span 1:LLM 调用
_, llmSpan := tracer.Start(ctx, "llm.call_gpt4",
WithSpanKind(SpanKindClient),
WithAttributes(map[string]interface{}{
"model": "gpt-4o",
}),
)
time.Sleep(100 * time.Millisecond)
llmSpan.AddEvent("llm.response_received",
String("tokens", "450 input, 85 output"),
)
llmSpan.SetStatus(StatusOK, "")
llmSpan.End()
// 子 Span 2:工具调用
_, toolSpan := tracer.Start(ctx, "tool.get_weather",
WithSpanKind(SpanKindClient),
WithAttributes(map[string]interface{}{
"tool": "get_weather",
"city": "北京",
}),
)
time.Sleep(50 * time.Millisecond)
toolSpan.AddEvent("tool.api_call",
String("endpoint", "https://api.weather.com/v1/current"),
Int("status_code", 200),
)
toolSpan.SetStatus(StatusOK, "")
toolSpan.End()
// 结束根 Span
rootSpan.SetStatus(StatusOK, "")
rootSpan.End()
// 2. 嵌套 Span(自动父子关系)
fmt.Println("\n--- 2. 嵌套 Span ---")
ctx2 := context.Background()
ctx2, parentSpan := tracer.Start(ctx2, "complex_workflow",
WithSpanKind(SpanKindServer),
)
// 第一层嵌套
ctx2, child1 := tracer.Start(ctx2, "step1.validate_input")
time.Sleep(20 * time.Millisecond)
child1.End()
// 第二层嵌套
ctx2, child2 := tracer.Start(ctx2, "step2.process_with_llm")
time.Sleep(80 * time.Millisecond)
// 第三层嵌套(LLM 内部的 HTTP 调用)
_, httpSpan := tracer.Start(ctx2, "http.post_to_openai",
WithSpanKind(SpanKindClient),
)
time.Sleep(60 * time.Millisecond)
httpSpan.AddEvent("http.response",
Int("status", 200),
Int("latency_ms", 58),
)
httpSpan.End()
child2.End()
parentSpan.End()
// 3. 错误 Span
fmt.Println("\n--- 3. 错误 Span ---")
ctx3 := context.Background()
ctx3, errorSpan := tracer.Start(ctx3, "failed_tool_call",
WithSpanKind(SpanKindClient),
)
time.Sleep(30 * time.Millisecond)
errorSpan.AddEvent("error.occurred",
String("type", "HTTPTimeout"),
String("message", "连接超时"),
)
errorSpan.SetStatus(StatusError, "HTTP 504 Gateway Timeout")
errorSpan.End()
// 4. 追踪中间件模拟
fmt.Println("\n--- 4. 追踪中间件 ---")
middleware := NewTracingMiddleware(tracer)
handler := middleware.Wrap(func(ctx context.Context, req map[string]interface{}) (interface{}, error) {
// 在 handler 内部创建子 Span
ctx, innerSpan := tracer.Start(ctx, "process_request",
WithSpanKind(SpanKindInternal),
)
time.Sleep(25 * time.Millisecond)
innerSpan.End()
return map[string]interface{}{"status": "ok"}, nil
})
handler(ctx, map[string]interface{}{
"method": "POST",
"path": "/api/v1/chat",
"agent_id": "agent-001",
})
// 5. 展示 Trace 树形结构
fmt.Println("\n--- 5. Trace 结构可视化 ---")
fmt.Println("Trace Tree:")
fmt.Println(" handle_user_query [200ms]")
fmt.Println(" ├── llm.call_gpt4 [100ms]")
fmt.Println(" │ └── (LLM 内部调用)")
fmt.Println(" └── tool.get_weather [50ms]")
fmt.Println(" └── http.call_weather_api [48ms]")
// 6. 采样演示
fmt.Println("\n--- 6. 概率采样 ---")
sampler := NewProbabilitySampler(0.5) // 50% 采样率
sampled := 0
total := 1000
for i := 0; i < total; i++ {
tid := NewDefaultIDGenerator().NewTraceID()
decision := sampler.ShouldSample(SamplingParameters{
TraceID: tid,
Name: "test",
})
if decision.Sample {
sampled++
}
}
fmt.Printf("采样率: %.1f%% (采样 %d / 共 %d)\n",
float64(sampled)/float64(total)*100, sampled, total)
}
四、追踪数据流架构
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Agent Pod │ │ Agent Pod │ │ Agent Pod │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │ Tracer │ │ │ │ Tracer │ │ │ │ Tracer │ │
│ └───┬────┘ │ │ └───┬────┘ │ │ └───┬────┘ │
└──────┼───────┘ └──────┼───────┘ └──────┼───────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌───────▼────────┐
│ OpenTelemetry │
│ Collector │ ← 聚合、批处理、重试
└───────┬────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌───────▼──────┐ ┌───▼─────┐ ┌───▼──────┐
│ Jaeger │ │ Zipkin │ │ Tempo │ ← 追踪存储/UI
└──────────────┘ └─────────┘ └──────────┘
五、追踪最佳实践
| 原则 | 说明 | 反例 |
|---|---|---|
| 每个入口创建 Trace | HTTP 请求、消息消费、定时任务都要有根 Span | 只在部分路径加追踪 |
| Span 粒度适中 | 一个 Span 对应一个有意义的操作单元 | 每行代码一个 Span |
| 携带关键属性 | 用户 ID、请求参数、错误信息 | 空 Span,什么都没有 |
| 记录事件 | 重要的中间状态用 Event 记录 | 只在 Span 结束时才记录 |
| 正确设置 Status | 成功设 OK,失败设 Error | 所有 Span 都是 Unset |
| 跨服务传播 | 通过 HTTP Header 传递 SpanContext | 每个服务独立 Trace |
| 合理采样 | 高流量服务用概率采样 | 100% 采样导致存储爆炸 |
六、采样策略选择
| 场景 | 推荐采样率 | 理由 |
|---|---|---|
| 开发环境 | 100% | 需要完整追踪 |
| 低流量服务(<100 QPS) | 100% | 存储压力小 |
| 中等流量服务(100-1000 QPS) | 10%-50% | 平衡成本和覆盖率 |
| 高流量服务(>1000 QPS) | 1%-10% | 控制存储成本 |
| 错误追踪 | 100%(错误时) | 保证所有错误都被记录 |
| 关键用户 | 100%(按用户 ID 采样) | 保证 VIP 用户体验 |
七、延伸阅读
- OpenTelemetry Tracing Specification:OTel 追踪规范官方文档
- W3C Trace Context:HTTP 追踪传播的标准协议
- Jaeger 文档:Uber 开源的分布式追踪系统
- Google Dapper 论文:分布式追踪的奠基之作
- Sampling in Distributed Tracing:各种采样策略详解
八、下一讲预告
第3讲:指标度量(Metrics)------用数字说话
日志告诉我们发生了什么,追踪告诉我们在哪里发生的,而指标告诉我们系统处于什么状态。下一讲实现 Prometheus 兼容的指标系统:延迟分布、吞吐量、错误率、并发数,以及如何设计对 LLM Agent 有意义的业务指标。
🧰 开发之余的小工具推荐
处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。