跳转到内容

Tempo 面试题

10 道题
分类
可观测性
子分类
trace
题目数
10 道
已阅读 0 / 10 题
1 Tempo 的核心架构由哪些组件组成?与 Jaeger 的设计哲学有何本质差异?

答案:

Tempo 是 Grafana Labs 开源的高吞吐量、低成本的分布式追踪后端,CNCF Incubating 项目。其核心理念是 “只索引 TraceID,不索引 Span 内容”,通过牺牲部分查询灵活性换取极低的存储成本。

核心组件:

组件职责关键特性
Distributor接收追踪数据接收 OTLP/Jaeger/Zipkin/Kafka 协议,转发到 Ingester
Ingester批处理与写存储内存批处理 Trace 块,写入后端对象存储
Querier查询服务接收 TraceID 查询请求,从对象存储拉取 Trace 块
Compactor块压缩器合并、压缩、清理过期的 Trace 块
Storage后端存储S3 / GCS / Azure Blob / MinIO(仅对象存储,无 ES/Cassandra)
Metrics Generator服务图与指标生成从 Trace 派生 RED 指标,写回 Prometheus/Mimir

架构数据流:

graph LR
  A[App + OTel SDK] -->|OTLP gRPC/HTTP| B[Distributor]
  Z[Jaeger Client] -->|UDP/HTTP| B
  K[Kafka] --> B
  B --> C[Ingester]
  C -->|Trace 块| D[(Object Storage)]
  D --> E[Querier]
  E --> F[Grafana]
  G[Compactor] -->|压缩/清理| D
  C --> H[Metrics Generator]
  H --> I[Prometheus/Mimir]

与 Jaeger 的本质差异:

维度JaegerTempo
索引索引 Span 标签、操作名、服务名仅索引 TraceID + 少量元数据
存储成本高(ES/Cassandra 索引开销大)极低(对象存储按容量计费)
查询模式支持任意标签搜索依赖 TraceID 或 TraceQL 结构化查询
TraceQL不支持一等公民,专为追踪数据设计
部署复杂度需维护 ES/Cassandra 集群仅需对象存储
典型规模中小规模PB 级追踪数据

核心权衡: Tempo 的"无索引"设计使得 service.name="checkout" status=error 这类按标签搜索的成本极高(必须全表扫描),但 TraceID 精确查询极快。这种设计契合现代可观测性最佳实践 —— 通过日志(Loki)和指标(Mimir/Prometheus)定位到问题 TraceID,再用 Tempo 拉取完整调用链。

部署示例:

# tempo-distributed Helm Chart values.yaml 关键配置
distributor:
  replicas: 3
  config:
    receivers:
      otlp:
        protocols:
          grpc: {}
          http: {}
ingester:
  replicas: 3
  config:
    trace_idle_period: 10s
    max_block_duration: 5m
querier:
  replicas: 3
  config:
    search:
      max_duration: 0  # 0 = 禁用搜索
2 Tempo 与 Loki 如何共享对象存储实现统一可观测性栈?

答案:

Tempo 与 Loki 的协同设计是 Grafana 可观测性栈的核心优势 —— LGTM Stack(Loki + Grafana + Tempo + Mimir)通过共享对象存储、统一认证、跨数据源跳转,实现 Logs / Metrics / Traces 的无缝关联。

LGTM 数据流:

graph TB
  subgraph 共享基础设施
    S3[(Object Storage
S3/GCS/Azure)] G[Grafana] end L[Loki
Logs] -->|LogQL| G M[Mimir/Prometheus
Metrics] -->|PromQL| G T[Tempo
Traces] -->|TraceQL| G L --> S T --> S M --> S G -->|从 Logs 提取 traceID| T G -->|从 Traces 提取 metric| M

共享对象存储的实现机制:

维度LokiTempo
存储后端S3 / GCS / Azure / MinIOS3 / GCS / Azure / MinIO
数据组织Chunks(压缩日志块)Trace Blocks(压缩 Trace 块)
生命周期retention + compactionblock-retention + compactor
多租户tenant_id 前缀tenant-id 前缀(per-tenant blocks)
压缩格式gzip / snappysnappy / lz4 / gzip

Grafana 数据源关联配置:

# Grafana datasources.yaml
apiVersion: 1
datasources:
  - name: Loki
    type: loki
    uid: loki
    url: http://loki:3100
  - name: Tempo
    type: tempo
    uid: tempo
    url: http://tempo:3100
    jsonData:
      tracesToLogsV2:
        datasourceUid: loki
        spanStartTimeShift: -1h
        spanEndTimeShift: 1h
        filterByTraceID: true
        filterBySpanID: true
      tracesToMetrics:
        datasourceUid: prometheus
        spanStartTimeShift: -1h
        spanEndTimeShift: 1h
        queries:
          - name: "Request rate"
            query: "sum(rate(http_server_requests_seconds_count{service=`$service`}[5m]))"

关联查询工作流:

# 1. 通过 Loki 日志定位错误
{service="checkout"} |= "error" | json | traceID="abc123"

# 2. Grafana 自动从日志提取 traceID,跳转 Tempo
# 3. Tempo 返回完整调用链
# 4. 点击 Span 中的 service.name,跳转 Prometheus 查看该服务指标

运维优势:

  • 成本共担:同一套对象存储、同一套 IAM 策略、同一套备份策略
  • 统一认证:通过 Grafana 统一鉴权,按数据源 + 租户粒度授权
  • 跨数据源查询:Grafana 11+ 的 Exploable Query Editor 支持日志、指标、追踪联合查询
  • 生命周期对齐:可对 LGTM 全栈设置统一的 retention 策略
3 Tempo 与 Grafana 如何集成?Traces to Logs / Logs to Traces 的关联机制是什么?

答案:

Tempo 与 Grafana 的深度集成是其核心优势。Grafana 8+ 提供四种关联模式,将追踪数据与日志、指标双向打通。

四种关联模式:

关联模式数据流向触发条件
Traces to LogsTempo Span → Loki 日志Span 含 traceID + spanID,Loki 日志含相同 ID
Logs to TracesLoki 日志 → Tempo Trace日志提取 traceID 字段,点击跳转 Tempo
Traces to MetricsTempo Span → Prometheus 指标提取 Span 标签作为 PromQL 变量
Metrics to TracesPrometheus Exemplar → TempoPrometheus 指标含 exemplar(traceID)

Tempo 数据源配置(Traces to Logs):

# Grafana Tempo datasource
apiVersion: 1
datasources:
  - name: Tempo
    type: tempo
    uid: tempo
    url: http://tempo:3100
    jsonData:
      httpMethod: GET
      tracesToLogsV2:
        datasourceUid: loki
        spanStartTimeShift: -1h    # 日志时间窗口起点偏移
        spanEndTimeShift: 1h       # 日志时间窗口终点偏移
        filterByTraceID: true      # 通过 traceID 过滤
        filterBySpanID: true       # 通过 spanID 过滤
        customQuery: true          # 启用自定义 LogQL
        query: '{service=`$service`} | json | traceID="$${traceId}"'
      serviceMap:
        datasourceUid: prometheus
      nodeGraph:
        enabled: true
      search:
        maxDuration: 5m            # 限制 TraceQL 搜索时间范围

应用埋点要求(OpenTelemetry SDK):

# Python OTel SDK 自动注入 traceID 到日志
from opentelemetry import trace
from opentelemetry.instrumentation.logging import LoggingInstrumentor

# 启用日志注入(自动添加 trace_id / span_id 字段)
LoggingInstrumentor().instrument(set_logging_format=True)

import logging
logger = logging.getLogger(__name__)
logger.error("payment failed", extra={
    "order_id": "12345",
    "amount": 99.9,
    # OTel 自动添加:
    # "otelTraceID": "abc123...",
    # "otelSpanID": "def456...",
})

Loki 日志格式(JSON 包含 traceID):

{
  "timestamp": "2026-06-06T10:30:00Z",
  "level": "error",
  "service": "checkout",
  "message": "payment failed",
  "traceID": "abc123def456",
  "spanID": "789ghi",
  "order_id": "12345"
}

关联跳转工作流:

sequenceDiagram
    participant U as 用户
    participant G as Grafana
    participant L as Loki
    participant T as Tempo
    U->>G: 在 Explore 查询 {service="checkout"} |= "error"
    G->>L: LogQL 查询
    L-->>G: 返回日志列表(含 traceID 字段)
    U->>G: 点击日志行的 Tempo 图标
    G->>T: GET /api/traces/{traceID}
    T-->>G: 返回完整 Trace
    G-->>U: 渲染 Trace 时间线
    U->>G: 点击 Span 跳转到 Prometheus
    G->>T: 查询该 Span 的 RED 指标

Metrics to Traces(Exemplar):

# OpenTelemetry SDK 自动关联 exemplar
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from prometheus_client import Counter

trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)

request_counter = Counter('http_requests_total', 'Total requests')

# Prometheus 抓取指标时携带 traceID exemplar
with tracer.start_as_current_span("handle_request") as span:
    request_counter.labels(method="GET", status="200").inc(
        exemplar={"trace_id": span.get_span_context().trace_id}
    )
4 Tempo 的采样策略有哪些?与 Pyroscope Profiling 如何联动实现 Continuous Profiling?

答案:

Tempo 不强制实施采样策略 —— 采样由应用层(OpenTelemetry SDK)完成。Tempo 的定位是 接收并存储所有 Span,采样决策由客户端或 Collector 决定。与 Pyroscope 的联动实现了"追踪 + 持续剖析"的可观测性闭环。

客户端采样策略对比:

策略实现位置优点缺点
Head-based应用 SDK决策前置、降低网络传输错误 Trace 可能被丢弃
Tail-basedOTel Collector保留所有错误/慢 TraceCollector 资源消耗大
Probabilistic应用 SDK / Collector简单、无状态长尾问题易丢失
Rate LimitingCollector控制总量突发流量可能丢失关键 Trace

OTel Collector Tail-based Sampling 配置:

# otel-collector-config.yaml
processors:
  tail_sampling:
    decision_wait: 10s              # 等待 10s 收集所有 Span
    num_traces: 50000               # 内存中维护的 Trace 数
    expected_new_traces_per_sec: 1000
    policies:
      # 错误请求 100% 保留
      - name: errors
        type: status_code
        status_code: {status_codes: [ERROR]}
      # P99 延迟 > 2s 的请求 100% 保留
      - name: slow-traces
        type: latency
        latency: {threshold_ms: 2000}
      # 健康请求采样 1%
      - name: health-sampled
        type: probabilistic
        probabilistic: {sampling_percentage: 1}

exporters:
  otlp/tempo:
    endpoint: tempo:4317
    tls: {insecure: true}

Tempo 接收端采样(Overhead Reduction):

# tempo.yaml - 接收端限制
distributor:
  receivers:
    otlp:
      protocols:
        grpc: {}
  # 限制单租户每秒 Span 数
  span_limits:
    max_recv_msg_size: 16777216  # 16MB

ingester:
  config:
    # 单个 Trace 最大 Span 数(防止单 Trace 撑爆存储)
    max_traces_per_user: 10000
    # 批处理周期
    flush_check_period: 10s
    trace_idle_period: 10s

Tempo + Pyroscope 集成(Continuous Profiling):

graph LR
  A[App + Pyroscope Agent] -->|CPU/Mem/Block Profile| P[Pyroscope]
  A -->|OTLP Traces| T[Tempo]
  P -->|标签: service.version, region| G[Grafana]
  T -->|标签: service.name, service.version| G
  G -->|关联展示| U[用户: Trace + Flamegraph]

Pyroscope 端配置(带 Trace 关联):

# pyroscope.yaml
pyroscopedb:
  data_path: /var/lib/pyroscope

# 启用 trace ingestion
tracing:
  enabled: true
  sampling_fraction: 0.1  # 10% Trace 携带 Profile

# Tempo 地址
otlp:
  endpoint: tempo:4317

应用端 Pyroscope Agent(Go 示例):

import (
    "github.com/grafana/pyroscope-go"
    "go.opentelemetry.io/otel"
)

func main() {
    // 启动 Pyroscope agent,集成 OTel Trace
    pyroscope.Start(pyroscope.Config{
        ApplicationName: "checkout-service",
        ServerAddress:   "http://pyroscope:4040",
        Tags: map[string]string{
            "service.version": "v1.2.3",
            "region":          "us-east-1",
        },
    })

    // 创建带 Pyroscope Profile 的 Span
    tracer := otel.Tracer("checkout")
    ctx, span := tracer.Start(context.Background(), "process-order")
    defer span.End()

    // 关联 Profile 到 Span
    pyroscope.TagWrapper(ctx, func(ctx context.Context) {
        // 业务逻辑,Pyroscope 自动捕获 CPU/Mem Profile
        processOrder(ctx)
    })
}

Grafana 联合查询体验:

# Grafana Explore 配置关联面板
panels:
  - title: "Slow Request Trace"
    type: trace
    datasource: {type: tempo, uid: tempo}
  - title: "CPU Flamegraph for /checkout"
    type: flamegraph
    datasource: {type: pyroscope, uid: pyroscope}
    targets:
      - query: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{service="checkout"}'

运维最佳实践:

  • Head + Tail 组合采样:客户端先按 10% 概率采样,Collector 再对错误/慢请求 100% 保留
  • Trace 与 Profile 标签一致:确保 service.nameservice.version 在 Tempo 和 Pyroscope 完全相同
  • 按服务设置采样率:核心服务 100%、边缘服务 1%
  • 存储分级:高频访问 Trace 保留 7 天,冷数据归档到 Glacier
5 如何将现有 Jaeger 部署迁移到 Tempo?迁移路径与数据兼容策略是什么?

答案:

从 Jaeger 迁移到 Tempo 的核心动机是 降低存储成本 + 统一可观测性栈。由于 OpenTelemetry 协议的标准化,迁移主要工作是 替换后端、保留客户端 API 兼容

迁移决策矩阵:

迁移触发条件建议方案
ES/Cassandra 成本过高迁移到 Tempo + S3
已有 Grafana 栈,希望统一迁移到 Tempo
强依赖按标签搜索 Jaeger UI保留 Jaeger / 评估 Jaeger v2
已采用 OTel SDK平滑迁移到 Tempo
仍在使用 Jaeger Thrift 协议升级到 OTLP 协议后迁移

数据流迁移(OTel Collector 作为统一接入层):

graph LR
  A[应用 1
OTel SDK] -->|OTLP| C[OTel Collector] B[应用 2
Jaeger Client] -->|Thrift/UDP| C D[应用 3
Zipkin Client] -->|HTTP| C C -->|OTLP gRPC| T[Tempo] C -->|OTLP gRPC| J[Jaeger
过渡期保留] T --> S3[(S3/GCS)] J --> ES[(ES/Cassandra
逐步退役)]

OTel Collector 多后端输出配置:

# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      grpc: {endpoint: 0.0.0.0:4317}
  jaeger:
    protocols:
      thrift_http: {endpoint: 0.0.0.0:14268}
      grpc: {endpoint: 0.0.0.0:14250}

processors:
  batch:
    timeout: 10s
    send_batch_size: 10000
  # 转换资源属性
  resource:
    attributes:
      - key: deployment.environment
        from_attribute: env
        action: insert

exporters:
  otlp/tempo:
    endpoint: tempo-distributor:4317
    tls: {insecure: true}
    sending_queue: {enabled: true, num_consumers: 10}
  otlp/jaeger-legacy:
    endpoint: jaeger-collector:14250
    tls: {insecure: true}

service:
  pipelines:
    traces:
      receivers: [otlp, jaeger]
      processors: [batch, resource]
      exporters: [otlp/tempo, otlp/jaeger-legacy]  # 双写过渡期

应用端渐进式迁移(无停机):

# 阶段 1: 双写(应用同时发送 Jaeger 和 OTel)
# OpenTelemetry SDK 配置 OTLP exporter
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

provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
)
trace.set_tracer_provider(provider)

# 阶段 2: 完全切换到 OTLP,移除 Jaeger Client
# 删除 jaeger-client 依赖,OTel SDK 自动处理

数据兼容性:

维度Jaeger ThriftOTLP gRPC迁移方式
Span 字段完全兼容完全兼容OTel Collector 自动转换
Tags字符串键值对字符串/数值/布尔自动转换
LogsKeyValue 数组KeyValue 数组自动转换
ReferencesChildOf/FollowsFromParent/Link自动转换
ProcessserviceName + tagsresource attributes自动映射

Tempo 接收 Jaeger 协议(直接模式,无需 OTel Collector):

# tempo.yaml
distributor:
  receivers:
    jaeger:
      protocols:
        thrift_http: {endpoint: 0.0.0.0:14268}
        grpc: {endpoint: 0.0.0.0:14250}
        thrift_compact: {endpoint: 0.0.0.0:6831}
        thrift_binary: {endpoint: 0.0.0.0:6832}
    zipkin:
      endpoint: 0.0.0.0:9411
    otlp:
      protocols:
        grpc: {endpoint: 0.0.0.0:4317}
        http: {endpoint: 0.0.0.0:4318}

Grafana 数据源切换:

# 阶段 1: 保留 Jaeger 数据源,新增加 Tempo
datasources:
  - name: Jaeger-Legacy
    type: jaeger
    url: http://jaeger-query:16686
  - name: Tempo
    type: tempo
    url: http://tempo:3100

# 阶段 2: 统一使用 Tempo,旧数据源通过 Tempo 的 search 功能查询历史

迁移验证清单:

  • 应用同时输出 Jaeger + OTLP,确认两个后端数据一致
  • Grafana 通过 Tempo 数据源查询新 Trace
  • 对比 Jaeger UI 与 Tempo UI 的同一 Trace 详情是否一致
  • 验证 Span 数量、Tags、Logs 完整无丢失
  • 性能基线对比(查询延迟、存储成本)
  • 历史数据保留策略:ES 数据保留 30 天后删除

Jaeger v2 替代方案: Jaeger v2 底层采用 OTel SDK,架构与 Tempo 趋同。若仍需 Jaeger UI 的搜索能力,可评估 Jaeger v2 + 后端接 S3。

6 TraceQL 高级查询:如何基于 Span 属性、时长、状态进行复杂过滤?

答案:

TraceQL 是 Tempo 专有的追踪查询语言,借鉴 PromQL / LogQL 设计理念,提供对 Trace、Span、属性、时长的结构化查询能力。TraceQL 2.0+ 支持 结构性查询聚合函数

TraceQL 语法基础:

# 基础结构
{ <span-set-filter> } [ <attribute-filter> ] [ <pipeline-filter> ]

# 示例
{ resource.service.name = "checkout" }                     # 过滤服务
{ span.http.status_code = 500 }                             # 过滤状态码
{ resource.service.name = "checkout" && duration > 2s }      # 组合条件
{ resource.service.name = "checkout" && span.http.method = "POST" && duration > 500ms }

核心操作符:

操作符含义示例
=等于span.http.method = "GET"
!=不等于span.http.status_code != 200
=~正则匹配span.http.url =~ "/api/.*"
!~正则不匹配resource.service.name !~ "test-.*"
>, <, >=, <=数值比较duration > 1s
&&, ||逻辑与/或status = error && duration > 500ms

内建字段:

字段含义类型
durationSpan 时长Duration(100ms, 2s, 1m
statusSpan 状态(ok / error / unsetEnum
nameSpan operation nameString
kindSpan kind(client/server/producer/consumer/internalEnum
parent父 SpanSpan
idSpan IDString

层级与结构化查询:

# parent 关系:查找所有父 Span 为 "POST /api" 的子 Span
{ parent.name = "POST /api" }

# descendant 关系:查找所有后裔 Span
{ descendant.name =~ "SELECT.*" }

# combine 关系:联合多个 Span 集
{ resource.service.name = "frontend" } && { resource.service.name = "backend" }

# 链式 Span 过滤:root 是 HTTP,descendant 包含 DB 查询
{ name = "HTTP GET /api" } >> { name =~ "SELECT.*" }

聚合与统计(TraceQL 2.0+):

# 计算 P99 延迟
{ resource.service.name = "checkout" } | quantile_over_time(duration, 0.99)
# 注:quantile_over_time 是 Tempo 支持的内置 TraceQL 聚合函数,非 PromQL

# 计算错误率
{ resource.service.name = "checkout" && status = error } | count() / count()

# 按服务名分组的错误数
{ status = error } | sum by (resource.service.name)(count())

# Top 10 最慢 Span
{ name = "db.query" } | topk(10, max(duration))

Pipeline 过滤器:

# 1. 先过滤再聚合
{ resource.service.name = "checkout" && status = error } | topk(5, max(duration))

# 2. select 投影
{ resource.service.name = "checkout" } | select(span.http.url, duration)

# 3. 条件过滤
{ resource.service.name = "checkout" } | status = error

生产实战查询示例:

# 1. 查找过去 1 小时所有超时请求的 Trace
{ name = "HTTP GET" && duration > 5s }

# 2. 查找包含 MySQL 慢查询的 Trace
{ descendant.name =~ "SELECT.*" && descendant.duration > 1s }

# 3. 查找特定用户 ID 的所有 Trace
{ span.user.id = "12345" }

# 4. 错误链路的服务拓扑分析
{ status = error && resource.service.name =~ ".*" } | sum by (resource.service.name)(count())

# 5. P95 延迟按服务名分组
{ resource.service.name =~ ".+" } | quantile_over_time(duration, 0.95) by (resource.service.name)

# 6. 查找跨服务调用的 root span
{ kind = server && parent = nil && duration > 1s }

Grafana TraceQL 查询面板:

{
  "datasource": { "type": "tempo", "uid": "tempo" },
  "targets": [{
    "queryType": "traceql",
    "query": "{ resource.service.name = \"checkout\" && status = error && duration > 1s }"
  }],
  "type": "traces"
}

TraceQL 与 PromQL 的设计对比:

维度PromQLTraceQL
数据模型时间序列Trace + Span 集合
核心单位样本点Span
聚合sum/rate/quantilecount/max/topk/quantile
结构化关系label selectorsparent/descendant/combine
时间过滤rate/range vectorTrace time range

TraceQL 性能特征:

  • 基于 TraceID 的精确查询:从对象存储拉取完整 Trace,毫秒级返回
  • 结构化过滤(如 parent.name):需在内存中重建 Span 树,对象存储拉取后过滤
  • TraceQL 搜索search 模式):需全表扫描,启用 block-search 后性能提升
  • 大量 Span 的 Trace:可能触发 max-spans-per-trace 限制
7 Tempo 的存储后端(S3/GCS/Azure Blob)如何配置?生产环境的最佳实践是什么?

答案:

Tempo 仅支持对象存储作为后端,这是其"低成本、高扩展"设计的核心。对象存储选择直接影响成本、性能、可靠性。

对象存储选型对比:

后端适用场景成本性能跨区域复制
AWS S3AWS 生态首选中等S3 Cross-Region Replication
GCSGCP 生态首选中等Multi-Regional Buckets
Azure BlobAzure 生态首选中等Geo-Redundant Storage
MinIO自建 / 边缘低(自有硬件)手动配置
Ceph RGW私有云手动配置

S3 存储配置:

# tempo.yaml
storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces-prod
      endpoint: s3.us-east-1.amazonaws.com
      region: us-east-1
      access_key: ${AWS_ACCESS_KEY_ID}
      secret_key: ${AWS_SECRET_ACCESS_KEY}
      # 生产环境关键配置
      forcepathstyle: false         # AWS S3 使用 virtual-hosted-style
      enable_dual_stack: true       # IPv4/IPv6 双栈
    # 块生命周期
    block:
      bloom_filter:
        enabled: true                # 启用 Bloom Filter 加速搜索
        false_positive_rate: 0.05   # 5% 误判率
      v2_in_buffer_bytes: 5242880    # 5MB 块大小
      v2_out_buffer_bytes: 10485760  # 10MB 写缓冲
      v2_prefetched_blocks: 10       # 预取块数
    wal:
      path: /var/tempo/wal           # 写前日志(WAL)路径
    pool:
      max_workers: 100               # 写入并发
      queue_depth: 10000             # 写队列深度

# 压缩器
compactor:
  compaction:
    block_retention: 336h           # 块保留 14 天
    compacted_block_retention: 168h # 压缩后保留 7 天
    max_compaction_objects: 1000000
  # 异步删除过期块
  deletion:
    marker_file_extension: .tombstone

GCS 存储配置:

storage:
  trace:
    backend: gcs
    gcs:
      bucket_name: tempo-traces-prod
      service_account: /var/secrets/gcs/key.json
      chunk_buffer_size: 10MiB
      enable_dual_stack: true

Azure Blob 存储配置:

storage:
  trace:
    backend: azure
    azure:
      container_name: tempo-traces
      storage_account_name: ${AZURE_STORAGE_ACCOUNT}
      storage_account_key: ${AZURE_STORAGE_KEY}
      endpoint_suffix: core.windows.net
      # 或者使用 managed identity
      # user_assigned_id: /subscriptions/.../userAssignedIdentities/...

生产环境最佳实践:

1. 存储分层:

# S3 生命周期策略(自动转储到 Glacier)
lifecycle:
  - id: tempo-tiering
    enabled: true
    rules:
      - status: enabled
        transitions:
          - days: 7
            storage_class: STANDARD_IA        # 7 天后转 Infrequent Access
          - days: 30
            storage_class: GLACIER_IR          # 30 天后转 Glacier Instant Retrieval
          - days: 90
            storage_class: DEEP_ARCHIVE        # 90 天后转 Deep Archive
        expiration:
          days: 365
        noncurrent_version_expiration:
          noncurrent_days: 30

2. 多租户隔离:

# tempo-multi-tenant.yaml
multitenancy:
  enabled: true
  tenant_settings:
    "tenant-a":
      usage_group: high-volume
    "tenant-b":
      usage_group: low-volume

# 每个租户独立 bucket
storage:
  trace:
    backend: s3
    s3:
      bucket: "tempo-${tenant_id}"   # 占位符

3. 跨区域复制(灾备):

# S3 跨区域复制
replication:
  role: arn:aws:iam::123456789012:role/tempo-replication
  rules:
    - id: tempo-dr
      status: enabled
      destination:
        bucket: arn:aws:s3:::tempo-traces-dr
        storage_class: STANDARD_IA
      filter:
        prefix: ""

4. 安全性:

# IAM 最小权限策略
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::tempo-traces-prod",
        "arn:aws:s3:::tempo-traces-prod/*"
      ]
    }
  ]
}

# 启用服务端加密
encryption:
  sse:
    type: aws:kms
    kms_key_id: arn:aws:kms:us-east-1:123456789012:key/abcd-1234

5. 监控告警:

# Prometheus 告警规则
groups:
  - name: tempo-storage
    rules:
      - alert: TempoIngesterWriteErrors
        expr: rate(tempo_ingester_traces_created_total[5m]) == 0
        for: 10m
        annotations:
          summary: "Tempo Ingester 写入失败"
      - alert: TempoCompactorLag
        expr: time() - tempo_compactor_marker_last_modified_timestamp_seconds > 3600
        for: 30m
        annotations:
          summary: "Compactor 压缩滞后超过 1 小时"
      - alert: TempoBlockSize
        expr: histogram_quantile(0.99, rate(tempo_querier_query_duration_seconds_bucket[5m])) > 5
        for: 15m
        annotations:
          summary: "P99 查询延迟超过 5s"

6. 容量规划:

规模Span 数/天存储/天月度 S3 成本(us-east-1)
小型1 亿50 GB$1.15
中型10 亿500 GB$11.5
大型100 亿5 TB$115
超大型1000 亿50 TB$1150

注: 上述成本为标准 S3 单价($0.023/GB/月),启用 IA + Glacier 后可降低 60-80%。

8 Tempo 的 Metrics Generator 工作原理是什么?如何从 Trace 派生服务依赖图与 RED 指标?

答案:

Metrics Generator 是 Tempo v1.4+ 引入的核心组件(非 v2.0+),从存储的 Trace 数据中提取服务间调用关系,自动生成 Service Graph 和 RED(Rate / Error / Duration)指标,回写 Prometheus / Mimir。

Metrics Generator 架构:

graph LR
  T[(Object Storage)] -->|Trace 块| MG[Metrics Generator]
  MG -->|解析 Span| P[Processor]
  P -->|生成 metrics| PROM[Prometheus Remote Write]
  PROM --> M[Mimir/Prometheus]
  M --> G[Grafana Service Map]

核心数据流:

  1. Metrics Generator 周期性扫描对象存储中的 Trace 块
  2. 解析每个 Trace 的 Span 关系(parent → child)
  3. 提取 (client_service, server_service, span_name, status_code, duration) 元组
  4. (client, server, span_name, status_code) 分组,生成 Prometheus 指标
  5. 通过 Remote Write 协议推送到 Prometheus / Mimir

Metrics Generator 配置:

# tempo.yaml
metrics_generator:
  registry:
    external_labels:
      source: tempo
      cluster: prod
  storage:
    path: /var/tempo/metrics
  # Remote Write 目标
  remote_write:
    - url: http://mimir:9009/api/v1/push
      headers:
        X-Scope-OrgID: tenant-1
      send_exemplars: true
  # 处理器配置
  processor:
    service_graphs:
      enable_grpc_histogram: true
      histogram_buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
    span_filters:
      - 'span.kind = server'  # 仅处理 server Span(避免重复计数)
    # 忽略内部服务
    dimensions:
      - service.name
      - span.http.method
      - span.http.status_code

  # 指标生成频率
  collection_interval: 30s
  # 启用注册中心(用于服务名解析)
  propagation:
    use_is_now: true

生成的指标类型:

指标名类型Labels含义
traces_service_graph_request_totalCounterclient, server, span_name, span_kind服务间请求总数
traces_service_graph_request_failed_totalCounterclient, server, span_name失败请求数
traces_service_graph_request_duration_seconds_bucketHistogramclient, server, span_name请求时长分布

Service Graph 生成原理:

# 伪代码:Metrics Generator 解析 Span 关系
def process_trace(trace):
    for span in trace.spans:
        # 提取父子关系
        parent_span = find_parent(span, trace.spans)
        if parent_span and parent_span.service != span.service:
            # 跨服务调用:生成 client → server 边
            edge = Edge(
                client=parent_span.service,
                server=span.service,
                span_name=span.name,
                duration=span.duration,
                status=span.status_code
            )
            edges.append(edge)
    return edges

Grafana Service Map 展示:

{
  "type": "nodeGraph",
  "title": "Service Map",
  "datasource": { "type": "prometheus", "uid": "prometheus" },
  "targets": [{
    "expr": "sum by (client, server) (rate(traces_service_graph_request_total[5m]))",
    "legendFormat": "{{client}} → {{server}}"
  }],
  "options": {
    "graph": {
      "enableHierarchicalLayout": true
    }
  }
}

RED 指标查询示例:

# Rate: 每秒请求数
sum by (server) (rate(traces_service_graph_request_total[5m]))

# Error: 错误率
sum by (server) (rate(traces_service_graph_request_failed_total[5m]))
  / sum by (server) (rate(traces_service_graph_request_total[5m]))

# Duration: P99 延迟
histogram_quantile(0.99,
  sum by (server, le) (
    rate(traces_service_graph_request_duration_seconds_bucket[5m])
  )
)

Exemplar 关联(Trace → Metric):

# Mimir / Prometheus 配置启用 exemplars
remote_write:
  - url: http://mimir:9009/api/v1/push
    send_exemplars: true

# Grafana 面板:点击指标的 exemplar 跳转到 Tempo
# 体验:发现 P99 异常 → 点击 exemplar → 查看具体 Trace → 定位 Span

生产实践:

1. 资源消耗控制:

metrics_generator:
  # 限制单个 Trace 处理的最大 Span 数
  max_spans_per_trace: 1000
  # 限制并发处理的 Trace 块数
  concurrent_processes: 4
  # 跳过特定服务的 Span
  ignore_services:
    - internal-health-check
    - kube-probe

2. 与现有 Prometheus 指标对齐:

# 维度对齐:确保 Trace 标签与指标标签一致
processors:
  resource:
    attributes:
      - key: service.namespace
        from_attribute: k8s_namespace
        action: insert
      - key: deployment.environment
        from_attribute: env
        action: insert

3. 存储成本优化:

  • Metrics Generator 生成的指标数据量约为 Trace 数据的 1-5%
  • 建议为这些指标设置 30-90 天 retention
  • 高基数标签(如 span.http.url)会导致指标爆炸,需谨慎
9 Tempo 在生产环境的故障案例:高负载下的 Ingester 数据丢失与 OOM 问题如何排查?

答案:

Tempo 在高吞吐量场景下常见故障包括 Ingester OOM、Trace 块写入失败、Compactor 滞后、查询超时等。系统性排查需结合指标、日志、Trace 本身。

典型故障模式:

故障现象根因分类影响范围
Ingester OOMKilledTrace 块过大 / 批处理窗口过长写入中断、数据丢失风险
Trace 找不到块未刷盘 / Wal 未恢复不可用
查询超时Trace 体积过大 / 网络带宽部分不可用
Compactor 滞后对象存储 IOPS 不足 / 块过多存储成本上升
Metrics Generator 漏算Span 解析失败 / 标签不匹配监控盲点

故障案例:电商大促期间 Ingester OOMKilled

故障时间线:

T+0:  黑色星期五开始,订单服务 Span 流量从 1万/s 飙升至 10万/s
T+5m: Ingester 内存使用从 4GB 升至 14GB(接近 16GB limit)
T+8m: Ingester Pod OOMKilled,Pod 自动重启
T+9m: 重启后 WAL 恢复,但内存再次迅速上升
T+12m: 第二个 Pod OOMKilled
T+15m: 写入路径中断,Distributor 缓冲区溢出,客户端报错
T+20m: 运维手动扩容 Ingester 至 6 副本,临时恢复

排查过程:

Step 1:检查 Ingester 指标

# 内存使用趋势
container_memory_working_set_bytes{pod=~"tempo-ingester.*"}

# 关键 Tempo 指标
tempo_ingester_bytes_received_total        # 接收字节数
tempo_ingester_traces_created_total         # 成功创建 Trace 数
tempo_ingester_live_traces                  # 内存中活跃 Trace 数
tempo_ingester_flush_queue_length           # 刷盘队列长度

Step 2:分析单 Trace 大小

# 通过 Tempo API 查询单 Trace 大小
curl -H "X-Scope-OrgID: tenant-1" \
  "http://tempo:3100/api/traces/abc123def456" | wc -c

# 检查超大 Trace
tempo_ingester_live_traces > 1000  # 活跃 Trace 数

根因定位:

# 1. 单个 Trace Span 数量爆炸
# 订单服务的 checkout 流程包含 500+ 个 DB Span(每条订单项一次 SQL)
# 10 万订单/分钟 × 500 Span/Trace = 5000 万 Span/分钟

# 2. Ingester 批处理窗口过长
ingester:
  config:
    trace_idle_period: 30s      # 等待 30s 才刷盘
    max_block_duration: 1h      # 块最大 1 小时
    # 内存累积量 = 30s × 10万/s × 平均 2KB/Span = 6GB

# 3. 缺少 Span 限制
ingester:
  config:
    max_traces_per_user: 10000  # 上限过低
    # 实际单租户有 20 万活跃 Trace

修复方案:

# 1. 调整 Ingester 配置
ingester:
  replicas: 6                    # 扩容到 6 副本
  resources:
    requests:
      memory: 16Gi
      cpu: 4
    limits:
      memory: 24Gi               # 提高内存上限
  config:
    trace_idle_period: 10s       # 缩短至 10s
    max_block_duration: 5m       # 缩短至 5 分钟
    max_traces_per_user: 50000   # 调高上限
    # 增加并发刷盘
    flush_check_period: 1s
    concurrent_flushes: 4

# 2. 客户端限制 Trace 体积
# OTel SDK 配置 BatchSpanProcessor
span_limits:
  max_attributes_per_span: 32
  max_events_per_span: 64
  max_links_per_span: 32
  attribute_count_limit: 1000
  max_span_attribute_length: 1024

# 3. 减少冗余 Span
# 在 OTel Collector 中过滤
processors:
  tail_sampling:
    policies:
      - name: drop-noisy-spans
        type: string_attribute
        string_attribute:
          key: span.name
          values: [health.check, db.ping]
          invert_match: false  # 丢弃这些 Span

Step 3:启用 WAL 持久化

ingester:
  config:
    wal:
      path: /var/tempo/wal
      # WAL 配置:OOm 后重启可恢复未刷盘数据
      max_block_duration: 1h
      flush_on_shutdown: true

Step 4:添加告警

# Prometheus 告警规则
groups:
  - name: tempo-ingester
    rules:
      - alert: TempoIngesterHighMemory
        expr: |
          container_memory_working_set_bytes{pod=~"tempo-ingester.*"}
          / container_spec_memory_limit_bytes{pod=~"tempo-ingester.*"} > 0.8
        for: 5m
        annotations:
          summary: "Ingester 内存使用超过 80%"
      - alert: TempoIngesterFlushLag
        expr: |
          time() - tempo_ingester_last_flushed_timestamp_seconds > 60
        for: 5m
        annotations:
          summary: "Ingester 刷盘滞后超过 1 分钟"
      - alert: TempoTraceDropRate
        expr: |
          rate(tempo_distributor_spans_rejected_total[5m]) > 0
        for: 1m
        annotations:
          summary: "Distributor 正在拒绝 Span"

Step 5:长期改进

  • 应用层优化:订单服务合并循环内的 DB 查询(500 Span → 1 Span)
  • 采样优化:健康请求采样率从 10% 降至 1%
  • 容量评估:按峰值 5× 设计资源
  • 混沌测试:定期使用 LitmusChaos 注入故障,验证 OOM 自动恢复
10 如何在 Grafana 中实现 Tempo 数据的可视化与告警?Service Map、Node Graph、Span Filter 实战配置是什么?

答案:

Grafana 与 Tempo 的集成涵盖 Trace 探索、Service Map、Node Graph、Exemplar 关联、告警 五大场景。合理的面板配置能将 Tempo 数据价值最大化。

核心可视化类型:

可视化类型用途数据源
Trace View瀑布图展示完整调用链Tempo
Service Map服务依赖图Prometheus(Service Graph 指标)
Node Graph跨服务调用拓扑Tempo
Span Filters按属性过滤 SpanTempo
Flame GraphTrace 时长火焰图Tempo(2.5+)
Exemplar指标上的 Trace 跳转Prometheus + Tempo

1. Trace 详情面板:

{
  "type": "traces",
  "title": "Slow Checkout Traces",
  "datasource": { "type": "tempo", "uid": "tempo" },
  "targets": [{
    "queryType": "traceql",
    "query": "{ resource.service.name = \"checkout\" && duration > 2s }",
    "limit": 20
  }],
  "options": {
    "showTraceActions": true,
    "showSpanFilterActions": true
  }
}

2. Service Map 配置:

# Tempo 数据源启用 Service Map
datasources:
  - name: Tempo
    type: tempo
    uid: tempo
    jsonData:
      serviceMap:
        datasourceUid: prometheus    # Service Graph 指标所在 Prometheus
        # Service Graph 指标名
        metricsQuery:
          rateQuery: 'sum by (client, server)(rate(traces_service_graph_request_total[$__rate_interval]))'
          errorRateQuery: |
            sum by (client, server)(
              rate(traces_service_graph_request_failed_total[$__rate_interval])
            ) / sum by (client, server)(
              rate(traces_service_graph_request_total[$__rate_interval])
            )
          latencyQuery: |
            histogram_quantile(0.95,
              sum by (client, server, le)(
                rate(traces_service_graph_request_duration_seconds_bucket[$__rate_interval])
              )
            )

3. Node Graph 配置:

{
  "type": "nodeGraph",
  "title": "Service Call Graph",
  "datasource": { "type": "tempo", "uid": "tempo" },
  "targets": [{
    "queryType": "traceql",
    "query": "{ resource.service.name = \"frontend\" && duration > 1s }"
  }],
  "options": {
    "graph": {
      "enableHierarchicalLayout": true,
      "showStats": true
    }
  }
}

4. Span Filter 实战:

# 查找所有包含 MySQL 慢查询的 Trace
{ resource.service.name =~ ".+" } >> { name =~ "SELECT.*" && duration > 500ms }

# 查找错误链路(任意 Span 报错)
{ status = error && resource.service.name = "checkout" }

# 查找 HTTP 5xx 错误
{ span.http.status_code >= 500 }

# 查找跨地域调用(region 标签差异)
{ resource.cloud.region = "us-east-1" } >> { resource.cloud.region = "eu-west-1" }

# 查找未捕获的异常
{ status = error && !span.error.type =~ "BusinessError.*" }

5. Exemplar 关联(指标 → Trace):

# Prometheus 数据源配置
datasources:
  - name: Prometheus
    type: prometheus
    uid: prometheus
    url: http://mimir:9009/prometheus
    jsonData:
      exemplarTraceIdDestinations:
        - name: traceID
          datasourceUid: tempo

# 指标面板启用 exemplar 显示
{
  "type": "timeseries",
  "targets": [{
    "expr": "histogram_quantile(0.99, sum by (le)(rate(http_request_duration_seconds_bucket[5m])))",
    "exemplar": true    # 启用 exemplar
  }],
  "options": {
    "showExemplars": true
  }
}

6. 告警规则(Grafana Unified Alerting):

# Tempo 告警规则
apiVersion: 1
groups:
  - orgId: 1
    name: tempo-alerts
    folder: Observability
    interval: 1m
    rules:
      - uid: tempo-high-error-rate
        title: "High Error Rate from Traces"
        condition: C
        data:
          - refId: A
            datasourceUid: mimir
            model:
              expr: |
                sum(rate(traces_service_graph_request_failed_total{
                  service="checkout"
                }[5m]))
                / sum(rate(traces_service_graph_request_total{
                  service="checkout"
                }[5m]))
              instant: true
          - refId: C
            datasourceUid: __expr__
            model:
              type: threshold
              expression: A
              conditions:
                - evaluator: {type: gt, params: [0.05]}
        for: 5m
        annotations:
          summary: "Checkout 服务错误率超过 5%"

      - uid: tempo-p99-latency
        title: "High P99 Latency"
        condition: C
        data:
          - refId: A
            datasourceUid: mimir
            model:
              expr: |
                histogram_quantile(0.99,
                  sum by (le)(
                    rate(traces_service_graph_request_duration_seconds_bucket{
                      service="checkout"
                    }[5m])
                  )
                )
          - refId: C
            datasourceUid: __expr__
            model:
              type: threshold
              expression: A
              conditions:
                - evaluator: {type: gt, params: [2]}
        for: 10m
        annotations:
          summary: "Checkout P99 延迟超过 2s"

7. 火焰图(Flame Graph)配置:

{
  "type": "flamegraph",
  "title": "Trace Duration Distribution",
  "datasource": { "type": "tempo", "uid": "tempo" },
  "targets": [{
    "queryType": "traceql",
    "query": "{ resource.service.name = \"checkout\" && duration > 1s }"
  }],
  "options": {
    "flamegraph": {
      "showAllFrames": true,
      "sortBy": "spanName"
    }
  }
}

8. 统一可观测性 Dashboard 设计:

# dashboard.json
{
  "title": "LGTM Observability Dashboard",
  "panels": [
    {
      "id": 1,
      "title": "Service Map",
      "type": "nodeGraph",
      "gridPos": {x: 0, y: 0, w: 24, h: 10}
    },
    {
      "id": 2,
      "title": "RED Metrics (Rate)",
      "type": "timeseries",
      "datasource": { "type": "prometheus", "uid": "prometheus" },
      "gridPos": {x: 0, y: 10, w: 12, h: 8}
    },
    {
      "id": 3,
      "title": "Error Logs",
      "type": "logs",
      "datasource": { "type": "loki", "uid": "loki" },
      "gridPos": {x: 12, y: 10, w: 12, h: 8}
    },
    {
      "id": 4,
      "title": "Recent Traces",
      "type": "traces",
      "datasource": { "type": "tempo", "uid": "tempo" },
      "gridPos": {x: 0, y: 18, w: 24, h: 10}
    }
  ]
}

最佳实践:

  • 关联面板联动:点击 Service Map 节点 → 自动过滤该服务的 Trace、Logs、Metrics
  • Exemplar 路径:从 Prometheus 指标异常点 → 点击 exemplar → 查看具体 Trace → 跳转 Loki 查看错误日志
  • TraceQL 搜索建议:为不同角色(前端、后端、QA)预置常用查询模板
  • 告警分级:P0(错误率 > 10%)、P1(P99 > 5s)、P2(特定 Span 失败率上升)分级路由
  • 数据采样与保留:Trace 数据 14 天(热存储),指标 90 天,日志 30 天