Tempo 面试题
10 道题- 分类
- 可观测性
- 子分类
- trace
- 题目数
- 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 的本质差异:
| 维度 | Jaeger | Tempo |
|---|---|---|
| 索引 | 索引 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
共享对象存储的实现机制:
| 维度 | Loki | Tempo |
|---|---|---|
| 存储后端 | S3 / GCS / Azure / MinIO | S3 / GCS / Azure / MinIO |
| 数据组织 | Chunks(压缩日志块) | Trace Blocks(压缩 Trace 块) |
| 生命周期 | retention + compaction | block-retention + compactor |
| 多租户 | tenant_id 前缀 | tenant-id 前缀(per-tenant blocks) |
| 压缩格式 | gzip / snappy | snappy / 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 Logs | Tempo Span → Loki 日志 | Span 含 traceID + spanID,Loki 日志含相同 ID |
| Logs to Traces | Loki 日志 → Tempo Trace | 日志提取 traceID 字段,点击跳转 Tempo |
| Traces to Metrics | Tempo Span → Prometheus 指标 | 提取 Span 标签作为 PromQL 变量 |
| Metrics to Traces | Prometheus Exemplar → Tempo | Prometheus 指标含 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-based | OTel Collector | 保留所有错误/慢 Trace | Collector 资源消耗大 |
| Probabilistic | 应用 SDK / Collector | 简单、无状态 | 长尾问题易丢失 |
| Rate Limiting | Collector | 控制总量 | 突发流量可能丢失关键 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.name、service.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 Thrift | OTLP gRPC | 迁移方式 |
|---|---|---|---|
| Span 字段 | 完全兼容 | 完全兼容 | OTel Collector 自动转换 |
| Tags | 字符串键值对 | 字符串/数值/布尔 | 自动转换 |
| Logs | KeyValue 数组 | KeyValue 数组 | 自动转换 |
| References | ChildOf/FollowsFrom | Parent/Link | 自动转换 |
| Process | serviceName + tags | resource 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 |
内建字段:
| 字段 | 含义 | 类型 |
|---|---|---|
duration | Span 时长 | Duration(100ms, 2s, 1m) |
status | Span 状态(ok / error / unset) | Enum |
name | Span operation name | String |
kind | Span kind(client/server/producer/consumer/internal) | Enum |
parent | 父 Span | Span |
id | Span ID | String |
层级与结构化查询:
# 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 的设计对比:
| 维度 | PromQL | TraceQL |
|---|---|---|
| 数据模型 | 时间序列 | Trace + Span 集合 |
| 核心单位 | 样本点 | Span |
| 聚合 | sum/rate/quantile | count/max/topk/quantile |
| 结构化关系 | label selectors | parent/descendant/combine |
| 时间过滤 | rate/range vector | Trace 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 S3 | AWS 生态首选 | 中等 | 高 | S3 Cross-Region Replication |
| GCS | GCP 生态首选 | 中等 | 高 | Multi-Regional Buckets |
| Azure Blob | Azure 生态首选 | 中等 | 中 | 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]
核心数据流:
- Metrics Generator 周期性扫描对象存储中的 Trace 块
- 解析每个 Trace 的 Span 关系(parent → child)
- 提取
(client_service, server_service, span_name, status_code, duration)元组 - 按
(client, server, span_name, status_code)分组,生成 Prometheus 指标 - 通过 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_total | Counter | client, server, span_name, span_kind | 服务间请求总数 |
traces_service_graph_request_failed_total | Counter | client, server, span_name | 失败请求数 |
traces_service_graph_request_duration_seconds_bucket | Histogram | client, 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 OOMKilled | Trace 块过大 / 批处理窗口过长 | 写入中断、数据丢失风险 |
| 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 | 按属性过滤 Span | Tempo |
| Flame Graph | Trace 时长火焰图 | 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 天