饮墨

子安饮墨馀三斗,留与卿儿作赋来

Grafana Tempo:Kubernetes 环境下分布式链路追踪的生产实践

痛点

微服务架构下,一个用户请求可能穿越 10+ 个服务。当延迟飙升或错误率突增时,仅靠日志(Loki)和指标(Mimir/Prometheus)很难定位到底是哪个服务、哪个调用链出了问题。你需要的是 分布式链路追踪(Distributed Tracing)

传统方案如 Jaeger、Zipkin 依赖 Elasticsearch 或 Cassandra 做存储后端,运维复杂且成本高昂——尤其在高流量场景下,存储费用能占到可观测性总成本的 40% 以上。

Grafana Tempo 的核心优势:只用对象存储(S3/GCS/MinIO)做后端,无需索引,成本降低一个数量级,且与 Grafana 生态(Loki、Mimir、Alloy)无缝集成,补齐可观测性三大支柱的最后一块拼图。

方案

采用 Tempo Distributed 模式 部署到 Kubernetes,配合 Grafana Alloy 作为 Trace 收集器,应用侧通过 OpenTelemetry SDK 上报,存储使用 S3(或 MinIO)。架构如下:

App (OTel SDK) → Grafana Alloy (OTel Collector) → Tempo Distributor → Tempo Ingester → S3
                                                                                         ↓
                                                         Grafana ← Tempo Query Frontend ← Tempo Querier → S3

关键组件职责: - Distributor:接收 Span,按 Trace ID 哈希分发到 Ingester - Ingester:内存聚合 Trace,定期 Flush 为 Block 写入对象存储 - Compactor:后台合并小 Block,优化查询性能 - Querier:从 Ingester(近期数据)和对象存储(历史数据)拉取 Trace - Query Frontend:分片大查询,缓存结果

实操步骤

Step 1:Helm 部署 Tempo Distributed

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

cat > tempo-values.yaml << 'EOF'
# Tempo Distributed mode values
tempo:
  storage:
    trace:
      backend: s3
      s3:
        bucket: tempo-traces
        endpoint: s3.ap-northeast-1.amazonaws.com
        region: ap-northeast-1
      wal:
        path: /var/tempo/wal
      block:
        version: vParquet4   # 最新列式格式,查询性能提升 2-5x

  # 全局配置
  global_overrides:
    max_bytes_per_trace: 5000000   # 单条 Trace 最大 5MB

  metrics_generator:
    enabled: true
    storage:
      path: /var/tempo/wal-metrics
    remote_write:
      - url: http://mimir-distributor:8080/api/v1/push   # Trace 生成 RED 指标推送到 Mimir

# 各组件副本数(中等流量参考)
distributor:
  replicas: 3
  resources:
    requests:
      cpu: 500m
      memory: 512Mi

ingester:
  replicas: 3
  persistence:
    enabled: true
    size: 20Gi        # WAL 磁盘,建议 SSD
  resources:
    requests:
      cpu: "1"
      memory: 2Gi

querier:
  replicas: 2
  resources:
    requests:
      cpu: 500m
      memory: 1Gi

queryFrontend:
  replicas: 2

compactor:
  replicas: 1
  resources:
    requests:
      cpu: "1"
      memory: 2Gi

# 采样率控制(Alloy 侧做 tail-based sampling 更推荐)
overrides:
  ingestion_rate_limit_bytes: 15000000   # 15MB/s per tenant
  ingestion_burst_size_bytes: 20000000
EOF

helm install tempo grafana/tempo-distributed \
  -n observability --create-namespace \
  -f tempo-values.yaml

Step 2:配置 Grafana Alloy 采集 Trace

在已有的 Alloy 配置中添加 OTel Receiver 和 Tempo Exporter:

// 接收 OTel gRPC 协议的 Trace 数据
otelcol.receiver.otlp "default" {
  grpc {
    endpoint = "0.0.0.0:4317"
  }
  http {
    endpoint = "0.0.0.0:4318"
  }
  output {
    traces = [otelcol.processor.batch.default.input]
  }
}

// 批量处理,减少网络开销
otelcol.processor.batch "default" {
  timeout = "5s"
  send_batch_size = 1000
  output {
    traces = [otelcol.processor.tail_sampling.default.input]
  }
}

// Tail-based Sampling:只保留有价值的 Trace
otelcol.processor.tail_sampling "default" {
  decision_wait = "10s"
  num_traces    = 100000

  policy {
    name = "errors"
    type = "status_code"
    status_code {
      status_codes = ["ERROR"]
    }
  }
  policy {
    name = "slow-requests"
    type = "latency"
    latency {
      threshold_ms = 2000
    }
  }
  policy {
    name = "random-sample"
    type = "probabilistic"
    probabilistic {
      sampling_percentage = 10
    }
  }

  output {
    traces = [otelcol.exporter.otlp.tempo.input]
  }
}

// 发送到 Tempo Distributor
otelcol.exporter.otlp "tempo" {
  client {
    endpoint = "tempo-distributor.observability.svc.cluster.local:4317"
    tls {
      insecure = true
    }
  }
}

Step 3:应用侧接入 OpenTelemetry(Python 示例)

pip install opentelemetry-api opentelemetry-sdk \
    opentelemetry-exporter-otlp \
    opentelemetry-instrumentation-fastapi \
    opentelemetry-instrumentation-requests \
    opentelemetry-instrumentation-sqlalchemy
# tracing.py - 应用初始化代码
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource

def init_tracing(service_name: str):
    resource = Resource.create({"service.name": service_name})
    provider = TracerProvider(resource=resource)

    exporter = OTLPSpanExporter(
        endpoint="grafana-alloy.observability.svc.cluster.local:4317",
        insecure=True,
    )
    provider.add_span_processor(BatchSpanProcessor(exporter))
    trace.set_tracer_provider(provider)

# main.py
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

app = FastAPI()
init_tracing("order-service")
FastAPIInstrumentor.instrument_app(app)

@app.get("/orders/{order_id}")
async def get_order(order_id: str):
    # 自动生成 Span,包含 HTTP method、status code、latency
    ...

Step 4:Grafana 中配置 Tempo 数据源并关联 Loki

在 Grafana 中添加 Tempo 数据源后,配置 Trace to Logs 关联:

# Grafana datasource provisioning
apiVersion: 1
datasources:
  - name: Tempo
    type: tempo
    url: http://tempo-query-frontend.observability.svc.cluster.local:3100
    jsonData:
      tracesToLogsV2:
        datasourceUid: loki
        filterByTraceID: true
        filterBySpanID: true
        customQuery: true
        query: '{namespace="$${__span.tags.k8s.namespace.name}"} | json | trace_id="$${__span.traceId}"'
      tracesToMetrics:
        datasourceUid: mimir
        queries:
          - name: Request Rate
            query: 'sum(rate(traces_spanmetrics_calls_total{service="$${__span.tags["service.name"]}"}[5m]))'
      serviceMap:
        datasourceUid: mimir   # Metrics Generator 产生的 Service Graph 指标
      nodeGraph:
        enabled: true

配置完成后,在 Grafana Explore 中可以: 1. 通过 Trace ID 直接搜索完整调用链 2. 从 Loki 日志点击跳转到对应 Trace 3. 查看 Service Map(服务依赖拓扑 + RED 指标)

避坑

1. Ingester OOM:Trace 太大或流量突增

现象:Ingester Pod 频繁 OOMKilled

解决

# tempo-values.yaml 限制单 Trace 大小
tempo:
  global_overrides:
    max_bytes_per_trace: 5000000      # 5MB 上限
    max_spans_per_trace: 10000        # 单 Trace 最多 1 万 Span

# 同时在 Alloy 侧做 tail-based sampling 降低总量
# 生产建议:只保留错误 + 慢请求 + 10% 随机采样

2. 查询超时:大时间范围查询卡死

现象:查询 24h 内的 Trace 超时

解决:确保 Query Frontend 开启分片 + 使用 vParquet4 格式:

queryFrontend:
  config:
    search:
      max_duration: 168h          # 允许查询最近 7 天
      query_backend_after: 15m    # 15 分钟前的数据走对象存储
      query_ingesters_until: 30m  # 最近 30 分钟查 Ingester
    trace_by_id:
      query_shards: 20            # 按 Trace ID 查询的并行分片数

3. 存储成本失控:采样策略缺失

现象:S3 存储费用远超预期

解决:三层采样策略组合使用: - Head-based(SDK 侧):高流量服务设置 0.1(10%)采样率 - Tail-based(Alloy 侧):保留全部错误和慢请求,正常请求只留 10% - Retention(Tempo 侧):配置 Compactor 保留策略

compactor:
  config:
    compaction:
      block_retention: 336h   # 保留 14 天,超期自动清理

总结

维度 Tempo Jaeger (ES)
存储成本 低(对象存储,~$0.023/GB) 高(ES 热数据 ~$0.10/GB)
运维复杂度 中(无状态组件 + 对象存储) 高(ES 集群运维)
查询能力 TraceQL 强大 基础标签查询
生态集成 Grafana 原生 需额外配置
适用规模 中大型(日均 TB 级 Trace) 中小型

核心结论

  1. 如果你已经在用 Grafana + Loki + Mimir/Prometheus 栈,Tempo 是分布式追踪的最佳选择——统一 UI、统一查询、数据互相关联
  2. 生产部署务必配置 tail-based sampling,否则存储成本会失控
  3. 开启 Metrics Generator,让 Tempo 自动生成 RED 指标(Rate/Error/Duration),无需额外 instrumentation 即可获得 Service Map
  4. 使用 vParquet4 格式,相比旧版本查询性能提升显著

日均 10TB Trace 数据参考成本:S3 存储 ~$5/天 + EC2 计算 ~$15/天,远低于等量 Elasticsearch 方案。

您还没有登录,请登录后发表评论。