Tempo 架构与组件:核心设计理念

Grafana Tempo 的架构设计围绕“低成本、高可用、易扩展”三大目标展开。与传统的分布式追踪系统(如 Jaeger、Zipkin)不同,Tempo 不依赖应用程序注入特定 SDK,而是通过接收标准协议(OTLP、Jaeger、Zipkin 等)的 span 数据,实现与语言无关的追踪数据采集。其核心架构由五个关键组件构成:Distributor、Ingester、Querier、Compactor 以及可选的 Query Frontend。

架构要点:Tempo 采用“写时无索引、读时构建索引”的设计哲学。写入路径仅做简单哈希分发和临时存储,不构建全局索引;查询时通过 Querier 从后端存储(如 S3、GCS、Azure Blob)中按块扫描并重建 trace。这种设计大幅降低了写入开销,适合高吞吐量场景。

2.1 组件职责与数据流

下图展示了 Tempo 的核心组件及其数据流转关系。从 span 数据接收、分发、暂存、持久化到最终查询,每个组件承担明确的职责。

Tempo 核心组件与数据流 应用 / SDK Distributor Ingester 后端存储 Compactor Querier Query Frontend 数据流说明: 写入路径:应用 → Distributor(哈希分发)→ Ingester(内存暂存 + 块刷新)→ 后端存储(持久化) 查询路径:Grafana → Query Frontend(可选)→ Querier(扫描存储 + Ingester 内存)→ 返回完整 Trace

2.2 组件职责详解

组件 核心职责 关键特性
Distributor 接收来自 SDK 或采集器的 span 数据,按 trace ID 进行哈希分发 无状态、水平扩展、支持 gRPC/HTTP 协议
Ingester 将 span 数据暂存于内存中,按块(block)组织并定期刷新到后端存储 有状态、内存暂存、块刷新策略可配置
Querier 接收查询请求,从后端存储和 Ingester 内存中检索 span 数据并组装完整 trace 无状态、支持 TraceQL 查询语言
Compactor 合并后端存储中的小块为大块,删除重复数据,优化存储效率 后台运行、可配置合并策略
Query Frontend 可选组件,提供查询缓存、分片、合并等高级功能 提升大规模查询性能
性能瓶颈关注点:在实战中,Ingester 的内存使用率和块刷新频率是写入路径的常见瓶颈;Querier 的并发查询能力和后端存储的 IOPS 是查询路径的关键限制因素。Compactor 的合并速度会影响存储成本和查询效率。

2.3 部署模式对比

Tempo 支持两种部署模式:单二进制模式和微服务模式。选择哪种模式取决于你的规模、运维能力和性能需求。

单二进制 vs 微服务部署模式 单二进制模式 单一进程(所有组件合一) • 部署简单,适合开发/测试环境 • 资源隔离差,组件相互影响 • 无法独立扩缩容 • 适合日均 < 100GB 数据量 启动命令:tempo -config.file=tempo.yaml 微服务模式 Distributor Ingester Querier Compactor Query Frontend • 独立部署、独立扩缩容 • 资源隔离,故障域小 • 适合生产环境、大规模集群 • 运维复杂度高,需容器编排
对比维度 单二进制模式 微服务模式
部署复杂度 低,一个进程即可运行 高,需管理多个服务实例
资源隔离 差,组件共享内存/CPU 好,每个组件独立资源
扩缩容能力 只能整体扩缩 按组件独立扩缩(如 Ingester 可单独扩容)
故障影响 单进程崩溃导致全服务不可用 单个组件故障不影响其他组件
适用场景 开发测试、小规模(< 100GB/天) 生产环境、大规模(> 100GB/天)
配置方式 单一配置文件 每个组件独立配置,或共享配置
生产环境建议:对于日均追踪数据量超过 100GB 或对可用性要求较高的场景,强烈建议使用微服务模式。单二进制模式虽然部署简单,但在高负载下容易出现 OOM 或 GC 停顿,且无法针对瓶颈组件(如 Ingester)单独扩容。

2.4 数据流详细说明

理解数据在 Tempo 中的完整生命周期,是定位性能瓶颈的基础。下面按写入和查询两条路径详细拆解。

写入路径(Write Path)

  1. 应用发送 span:应用通过 OTLP、Jaeger 或 Zipkin 协议将 span 数据发送到 Distributor 的 gRPC 或 HTTP 端点。
  2. Distributor 哈希分发:Distributor 根据 trace ID 进行一致性哈希计算,将属于同一 trace 的所有 span 发送到同一个 Ingester 实例。这一步保证了 trace 的局部性。
  3. Ingester 内存暂存:Ingester 将接收到的 span 按 trace ID 组织成“块”(block),暂存在内存中。每个块包含多个 trace 的 span 数据。
  4. 块刷新(Flush):当块达到配置的大小或时间阈值时,Ingester 将块刷新到后端存储(如 S3、GCS)。刷新后的块变为不可变对象。
  5. Compactor 后台合并:Compactor 定期扫描后端存储中的小块,将它们合并为更大的块,并删除重复或过期的数据,优化存储布局。

查询路径(Read Path)

  1. Grafana 发起查询:用户通过 Grafana 的 TraceQL 查询界面或 API 发起 trace 查询请求。
  2. Query Frontend(可选):如果部署了 Query Frontend,它会将查询请求分片、缓存,并转发给 Querier。
  3. Querier 扫描存储:Querier 根据 trace ID 或查询条件,扫描后端存储中的所有相关块。同时,Querier 也会向所有 Ingester 实例查询内存中尚未刷新的 span 数据。
  4. 结果合并:Querier 将从存储和 Ingester 获取的 span 数据合并,组装成完整的 trace 结构,返回给 Grafana 展示。
性能瓶颈关键点:
  • 写入瓶颈:Ingester 的内存使用率过高会导致 OOM 或频繁 GC;Distributor 的哈希不均会导致 Ingester 负载倾斜。
  • 查询瓶颈:Querier 扫描大量小块(未合并)会导致查询延迟高;后端存储的 IOPS 不足会限制并发查询能力。
  • 存储瓶颈:Compactor 合并速度跟不上块生成速度,会导致存储中小块数量激增,影响查询效率。

2.5 组件配置示例

以下是一个微服务模式下各组件的最小配置示例,展示如何独立启动每个组件。

# Distributor 配置 (distributor.yaml)
distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318
  ring:
    kvstore:
      store: memberlist

# Ingester 配置 (ingester.yaml)
ingester:
  lifecycler:
    ring:
      kvstore:
        store: memberlist
      replication_factor: 3
  blocks_storage:
    backend: s3
    s3:
      bucket: tempo-traces
      endpoint: s3.amazonaws.com
    block:
      bloom_filter_false_positive: 0.05
      encoding: zstd

# Querier 配置 (querier.yaml)
querier:
  frontend_worker:
    frontend_address: query-frontend:9095
  search:
    external_hedge_requests_at: 2s
    external_hedge_requests_up_to: 3

# Compactor 配置 (compactor.yaml)
compactor:
  compaction:
    block_retention: 48h
    compacted_block_retention: 1h
    max_block_bytes: 107374182400  # 100GB
配置建议:在生产环境中,建议将 replication_factor 设置为 3 以保证数据高可用;block_retention 根据数据保留策略设置,通常为 24h-72h;max_block_bytes 控制单个块的最大大小,过大会增加查询扫描开销,过小会增加 Compactor 压力。