Grafana Tempo 架构深度解析:写入路径、Parquet 列式存储与 TraceQL 查询的组件体系
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
Grafana Tempo 是一个面向高吞吐、低依赖场景的分布式链路追踪后端。本文基于仓库中的 Tempo architecture 文档,系统讲解 Tempo 如何完成"接入链路数据 → 写入对象存储 → 按 TraceID 或 TraceQL 检索"的全过程,并结合源码剖析各组件职责、单体/微服务两种部署模式以及存储设计。读完本文,你将掌握 Tempo 的写入与读取生命周期、每个组件的运行方式与适用模式,以及如何通过-target参数组织实际部署。
设计目标:塑造 Tempo 架构的四个原则
Tempo 的架构围绕以下四个目标展开,它们直接决定了数据如何被接入、存放和查询:
- 低成本的存储:所有链路(trace)数据全部存放在对象存储(object storage)中,不依赖自建的高成本本地存储集群。
- 读写独立伸缩:在微服务模式下,Tempo 将写入路径(write path)与读取路径(read path)彻底分离,可分别按需扩缩容。
- 无需额外复制的持久性:微服务模式下,一个 Kafka 兼容的消息队列充当写入预写日志(write-ahead log, WAL)。只要 Kafka 确认写入,数据即视为持久化,因此 Tempo 在写入路径上不做跨实例复制,可以以复制因子 1(RF1)运行,显著降低成本。
- 高效的属性查询:数据块以 Apache Parquet 列式格式存储,查询时只读取所需列,而非扫描整条 trace,从而加速基于属性的搜索。
这四个目标在源码中都有对应体现:PushSpansToKafka、ConsumeFromKafka等开关决定了数据在进程间是走 Kafka 还是进程内直推(见下文源码分析),而 tempodb/encoding/vparquet5 目录则承载了 Parquet 编码的具体实现。
部署模式:一个二进制,两种运行形态
Tempo 的所有组件都被编译进同一个二进制文件,通过-target参数(命令行形式--target)决定当前进程运行哪些组件。这带来了两种部署形态:
- 单体模式(Monolithic):
-target=all(默认值),所有必需组件运行在单个进程中,无需 Kafka;distributor 通过进程内调用把数据直接推送给 live-store 与 metrics-generator。 - 微服务模式(Microservices):每个组件以独立进程、独立
-target运行,需要 Kafka 兼容系统,是生产环境推荐模式。
无论哪种模式,生产环境都建议使用对象存储;本地文件系统后端仅用于开发与测试。
从 cmd/tempo/app/modules.go 可以看到这一设计的直接实现:文件顶部定义了全部目标常量(modules.go),包括all、distributor、metrics-generator、querier、query-frontend、block-builder、backend-scheduler、backend-worker、live-store等;模块管理器(setupModuleManager)随后注册了每个模块及其依赖关系,SingleBinary目标的依赖列表把BackendScheduler、BackendWorker、QueryFrontend、Querier、Distributor、MetricsGenerator、LiveStore全部串起来(modules.go),对应"一个进程跑所有组件"的单体形态。
-target的具体取值与说明汇总如下(来源:command-line-flags.md):
| Target | 说明 |
|---|---|
all | 单体模式,单进程运行全部组件(默认) |
distributor | 接收并分发 trace 数据给下游组件 |
metrics-generator | 从摄入的 trace 数据生成指标 |
querier | 查询后端存储中的 trace 与指标 |
query-frontend | 提供搜索 API,并把查询拆分为并行任务 |
block-builder | 从 Kafka 消费数据并写入后端存储块 |
backend-scheduler | 在多个 worker 间调度、协调后端查询任务 |
backend-worker | 执行 backend scheduler 分配的任务 |
live-store | 消费 Kafka 中近期数据,服务实时查询 |
注意:Tempo 3.0 移除了旧的
ingester、compactor与scalable-single-binary(SSB)目标,此前被称为 scalable monolithic mode 的形态已不再存在,写入与压缩职责分别由 live-store、block-builder 和 backend scheduler/worker 接管。
单体模式:何时使用与注意事项
单体模式适合快速上手、开发环境以及中低 trace 量的场景,此时运维简单性优先于独立伸缩。它的局限在于:所有组件共享同一资源池,查询负载的突增会影响写入吞吐,反之亦然;随着数据量增大,live-store 与 querier 等组件的内存压力集中在一台实例上,可能引发 OOM。部署时需为实例预留足够内存,以同时容纳 live-store 的内存 trace 缓冲、querier 的并发任务执行以及 backend worker 合并块时的内存开销。
微服务模式:独立伸缩与故障隔离
微服务模式适用于生产环境、高 trace 量、对高可用有要求的场景。其优势包括:block-builder、querier、live-store 等组件可独立扩缩容;故障域相互隔离(querier OOM 不影响数据接入,block-builder 重启不影响查询可用性);live-store 可跨可用区部署以提升高可用性;每个组件按需分配资源,避免单体模式的过度供给。
各组件的扩展策略参考如下(来源:deployment-modes.md):
| 组件 | 扩展策略 | 说明 |
|---|---|---|
| Distributor | 水平 | 无状态,按摄入速率扩展 |
| Block-builder | 水平 | 受 Kafka 分区数约束,按数据量扩展 |
| Live-store | 水平 | 受 Kafka 分区数约束,按近期数据查询量与内存扩展 |
| Query frontend | 垂直 | 保持 2 个副本,优先提升 CPU/RAM 而非加副本 |
| Querier | 水平 | 按查询并发与延迟要求扩展 |
| Backend worker | 水平 | 处理压缩与保留,按块数量与压缩滞后扩展 |
| Metrics-generator | 水平 | 按 trace 量与生成的序列数量扩展 |
在微服务模式中,Kafka 是写入路径的主干通信通道:distributor 写入 Kafka,block-builder、live-store、metrics-generator 各自独立消费;querier 通过 gRPC 访问 live-store 获取近期数据,所有组件则统一访问对象存储获取块数据。新增或下线任一组件实例都无需重配其他组件(除 Kafka 分区管理外)。从单体迁移到微服务时,只需为各组件指定相应-target、让所有组件指向同一套 Kafka、对象存储与 memberlist,然后缩容单体实例即可;由于 Kafka 已保证持久性,迁移过程不丢数据,live-store 启动时从 Kafka 重放,block-builder 从上次提交的偏移量继续消费。
两种模式下的组件差异速查
| 组件 | 配置块 | 单体模式 | 微服务模式 |
|---|---|---|---|
| Distributor | distributor | 进程内直推 live-store 与 metrics-generator | 写入 Kafka |
| Ingest | ingest | 不使用 | 写入路径的 Kafka 连接设置 |
| Block-builder | block_builder | 不使用 | 消费 Kafka,构建 Parquet 块并刷入对象存储 |
| Live-store | live_store | 直接从 distributor 接收数据 | 从 Kafka 消费 |
| Live-store client | live_store_client | querier 到 live-store 的客户端(进程内) | querier 到 live-store 的 gRPC 客户端 |
| Query frontend | query_frontend | 进程内运行 | 独立进程 |
| Querier | querier | 进程内运行 | 独立进程 |
| Backend scheduler / worker | backend_scheduler/backend_worker | 进程内运行 | 独立进程 |
| Metrics-generator | metrics_generator | 可选,进程内运行 | 可选,独立进程 |
| Storage | storage | trace 数据存储后端 | trace 数据的对象存储 |
数据流转:写入路径与读取路径
Tempo 将"把数据写入存储"的写入路径与"服务查询"的读取路径明确分离,两条路径的生命周期如下。
一条写入的生命周期
以下步骤描述微服务模式的写入路径(单体模式下,distributor 以进程内方式把数据推给 live-store 与 metrics-generator,而非写入 Kafka):
- 追踪管线(如 OpenTelemetry Collector、Grafana Alloy)把 trace 数据发送给distributor。
- distributor 校验请求,按 trace ID 对 trace 分片,并写入Kafka。
- Kafka 确认写入后,distributor 向客户端返回响应。此后的下游消费都是异步的:
- Live-store从 Kafka 消费数据,使近期数据可被查询。
- Block-builder从 Kafka 消费数据,为长期对象存储构建数据块。
- Metrics-generator(可选)从 Kafka 消费数据,从 trace 派生指标。
源码印证:在 initDistributor 中,t.cfg.Distributor.PushSpansToKafka = !singleBinary(modules.go),即只有非单体模式才写 Kafka;单体模式下则注册两个进程内目标函数localPushTargets.Generator与localPushTargets.LiveStore,把数据直接 push 给同进程的 metrics-generator 与 live-store。类似地,initLiveStore 中t.cfg.LiveStore.ConsumeFromKafka = !IsSingleBinary(t.cfg.Target)(modules.go)决定 live-store 是否从 Kafka 消费,initGenerator 中ConsumeFromKafka同理控制 metrics-generator。
一条读取的生命周期
- Query frontend接收查询,将其拆分为"近期数据"与"长期存储"两类任务。
- Querier从live-store获取近期数据。
- 对更早的数据,querier 从对象存储拉取数据块。
- 结果聚合后返回给用户。
读取路径在模块依赖图中同样清晰:Querier模块依赖{Common, Store, LiveStoreRing, PartitionRing},而QueryFrontend依赖{Common, Store, OverridesAPI}(modules.go);initQuerier 还会在单体模式下自动把 worker 地址指向本进程 gRPC 端口(modules.go),保证单体部署开箱即用。
组件总览
| 组件 | 职责 | 使用模式 |
|---|---|---|
| Distributor | 接收并校验 span,路由到写入路径 | 两种模式 |
| Kafka 兼容队列 | distributor 与下游消费者之间的持久化 WAL | 微服务模式 |
| Block-builder | 构建 Apache Parquet 块并刷入对象存储 | 微服务模式 |
| Live-store | 服务近期 trace 数据的查询 | 两种模式 |
| Query frontend | 接收查询并拆分为并行任务 | 两种模式 |
| Querier | 针对 live-store 与对象存储执行查询任务 | 两种模式 |
| Backend scheduler 与 worker | 处理压缩、保留与块列表维护 | 两种模式 |
| Metrics-generator | 可选;从 trace 派生 RED 指标与服务图 | 两种模式 |
| 对象存储 | 所有 trace 数据的长期存储 | 两种模式 |
组件详解
Distributor
distributor 是 trace 数据的入口。它接收 OTLP(推荐)、Jaeger 和 Zipkin 三种格式的 span,并按照每租户的接入限制(per-tenant ingestion limits)进行校验。微服务模式下,它按 trace ID 分片并写入 Kafka;单体模式下则进程内推送给 live-store 与 metrics-generator。其实现位于 modules/distributor/distributor.go,分片与转发逻辑可以追溯到PushSpansToKafka与LocalPushTargets的分支处理。
Kafka 兼容队列
微服务模式下,Tempo 使用 Kafka 兼容系统(如 Apache Kafka 或 WarpStream)作为 distributor 与下游消费者之间的持久化 WAL。由于 Kafka 在确认写入后即保证持久性,Tempo 可以以复制因子 1(RF1)运行,无需在写入路径做额外复制。单体模式不使用 Kafka。Kafka 的接入配置在ingest配置块下,可参考 configure-kafka 了解完整设置。
Block-builder
block-builder 从 Kafka 消费 trace 数据,把 span 组织成 Apache Parquet 数据块,并刷入对象存储长期保留。它只在微服务模式运行;单体模式下由 live-store 直接向对象存储刷块。对应实现见 modules/blockbuilder/blockbuilder.go,其块配置与 WAL 版本始终取自storage.trace.block(见 initBlockBuilder)。
Live-store
live-store 服务近期 trace 数据:trace 保存在内存和本地 WAL 中,因此在摄入后数秒内即可查询。微服务模式下它从 Kafka 消费数据;单体模式下直接从 distributor 接收。为保障高可用,live-store 可以跨可用区部署。实现位于 modules/livestore/live_store.go,它对外以 gRPC 暴露 Querier 与 Metrics 服务(modules.go)。
Query frontend
query frontend 是查询入口:接收 TraceQL 查询与 trace ID 查询,把每条查询拆分为并行任务分发给 querier,再合并结果返回最终响应。它在 modules/frontend/frontend.go 中实现;initQueryFrontend 为其注册了 trace by ID、search、search tags、metrics query、MCP 等一系列 HTTP 端点。
Querier
querier 执行 query frontend 分发的任务:从 live-store 获取近期数据,从对象存储获取历史数据,然后返回给 query frontend 合并。实现见 modules/querier/querier.go,其注册的 HTTP 处理器覆盖 TraceByID、Search、SearchTags、SearchTagValues、QueryRange 等(modules.go)。
Backend scheduler 与 worker
backend scheduler 与 backend worker 共同负责对象存储数据的压缩(compaction)、保留(retention)与块列表(blocklist)维护:scheduler 创建任务并分派给 worker,worker 把小块压缩成更大的块,并按保留期过期数据。两者取代了旧版的 compactor。相关代码见 modules/backendscheduler/backendscheduler.go 与 modules/backendworker/backendworker.go。保留策略的具体过期逻辑可参考 tempodb/retention.go。
Metrics-generator
metrics-generator 是可选组件,它从 trace 派生速率(rate)、错误(error)、耗时(duration)指标与服务图(service graphs),并通过 remote write 写入 Prometheus 或 Grafana Mimir 等指标后端。实现位于 modules/generator/generator.go,支持span-metrics、service-graphs等处理器。
对象存储
对象存储是所有 trace 数据的长期存储层。Tempo 支持三种主流对象存储 API,并提供本地文件系统后端用于开发与测试:
- Amazon S3(以及 MinIO 等 S3 兼容系统)
- Google Cloud Storage(GCS)
- Microsoft Azure Blob Storage
- 本地文件系统(开发/测试)
各后端实现分别位于 tempodb/backend/s3、tempodb/backend/gcs、tempodb/backend/azure、tempodb/backend/local。后端 worker 在保留期过后使对象存储中的数据过期,从而强制执行保留策略。
存储:Parquet 列式块与保留
Tempo 把全部 trace 数据存放于对象存储,以 Apache Parquet 列式格式组织成数据块。列式存储的关键收益是:查询时只读取所需属性对应的列,而不是扫描整条 trace。Tempo 当前的 Parquet 编码版本为 vParquet5,实现位于 tempodb/encoding/vparquet5。保留策略通过 backend worker 在对象存储中过期数据实现。
你能查询什么
Tempo 回答两类读取请求:按 trace ID 查询特定 trace,以及使用 TraceQL(Tempo 的 trace 查询语言)跨 trace 搜索。TraceQL 的解析器、AST 与执行引擎位于 pkg/traceql。此外,你还可以直接基于 trace 数据计算 TraceQL 指标(例如 span 速率与延迟分位数),并使用可选的 metrics-generator 产出 RED 指标与服务图。
接入方面,Tempo 支持 OpenTelemetry(OTLP)、Jaeger 与 Zipkin 三种格式。同时 Tempo 是多租户的:trace 数据在存储层按租户隔离。
命令行与配置实操
关键命令行标志
Tempo 的全局与部署相关标志(完整参考见 command-line-flags.md):
| Flag | 说明 | 默认值 |
|---|---|---|
--config.file | 要加载的配置文件 | |
--config.expand-env | 在配置文件中展开环境变量 | false |
--config.verify | 校验配置后退出 | false |
--target | 要运行的目标模块 | all |
--multitenancy.enabled | 启用多租户 | false |
--http-api-prefix | 所有 HTTP API 端点的字符串前缀 | "" |
--shutdown-delay | SIGTERM 与关闭之间的等待时间 | 0 |
--log.level | 日志级别:debug/info/warn/error | info |
--log.format | 日志格式:logfmt/json | logfmt |
--server.http-listen-port | HTTP 监听端口 | 3200 |
--server.grpc-listen-port | gRPC 监听端口 | 9095 |
--health | 对/ready端点执行健康检查后退出 | false |
--health.url | 健康检查 URL | http://localhost:3200/ready |
常见用法示例:
# 以配置文件启动(默认单体模式) tempo --config.file=/etc/tempo/config.yaml # 以指定目标启动(微服务模式下的 distributor) tempo --target=distributor --config.file=/etc/tempo/config.yaml # 只校验配置、不启动 tempo --config.file=/etc/tempo/config.yaml --config.verify # 打印版本信息 tempo --version # 在配置文件中展开环境变量(便于注入密钥与环境相关值) tempo --config.file=/etc/tempo/config.yaml --config.expand-env # 微服务部署中,为 distributor 指定自定义 HTTP 端口并以 JSON 格式输出日志 tempo --target=distributor \ --config.file=/etc/tempo/config.yaml \ --server.http-listen-port=3200 \ --log.format=json # 单体模式启用多租户并设置 30 秒优雅关闭延迟 tempo --config.file=/etc/tempo/config.yaml \ --multitenancy.enabled \ --shutdown-delay=30s--health标志专为无 shell 的 distroless 容器镜像设计:由于镜像内没有curl/wget,可直接用它编写 Dockerfile 健康检查:HEALTHCHECK CMD ["/tempo", "--health"]。Kubernetes 用户通常改用 httpGet 探针直接探测/ready端点。
配置加载与单体模式默认行为
从 cmd/tempo/main.go 的loadConfig(main.go)可以看出配置加载顺序:先解析命令行标志,注册默认值,再叠加配置文件(YAML,严格解析),最后用 CLI 覆盖。有趣的是,当目标为单体模式时,代码会强制把 Generator、LiveStore 的 ring KVStore 设为inmemory、地址设为127.0.0.1,并清空 BackendWorker 的 ring KVStore 以进入"无分片"模式(main.go)——这正是单体模式无需额外 KV 存储即可运行的原因。
单二进制(单体)模式配置示例
仓库中的 example/docker-compose/single-binary/tempo.yaml 是一个完整的单体模式示例(默认-target=all,故未显式写出 target):
stream_over_http_enabled: true server: http_listen_port: 3200 log_level: info distributor: receivers: otlp: protocols: grpc: endpoint: "tempo:4317" http: endpoint: "tempo:4318" #log_received_spans: # enabled: true # log_discarded_spans: # enabled: true metrics_generator: registry: external_labels: source: tempo cluster: docker-compose storage: path: /var/tempo/generator/wal remote_write: - url: http://prometheus:9090/api/v1/write send_exemplars: true query_frontend: mcp_server: enabled: true storage: trace: backend: local wal: path: /var/tempo/wal # where to store the wal locally local: path: /var/tempo/blocks overrides: defaults: metrics_generator: processors: ["span-metrics", "service-graphs"] generate_native_histograms: both usage_report: reporting_enabled: false该示例使用local本地文件系统后端(适合开发/测试,生产请改用对象存储),配置了 OTLP gRPC/HTTP 接收器、metrics-generator 及其到 Prometheus 的 remote write、以及 query frontend 的 MCP server。微服务模式的完整多组件示例可参考仓库中的 example/docker-compose/distributed 目录。
三条必须记住的事实
关于 trace 数据,有三点值得特别留意:
- trace 没有"结束"概念:一条 trace 可以从任何一个携带全新 trace ID 的 span 开始,未来任意时刻都可能有新 span 追加进来。
- 按 trace ID 查询返回的是完整关系图:Tempo 返回当前已存储/接入的所有属于该 trace 的 span,并以映射后的响应呈现(即 span 之间的父子/兄弟关系图),例如 Grafana 可以据此对 trace 做可视化渲染。
- TraceQL 查询不保证时间序:返回的是所有匹配过滤条件的 trace,但可能不是按时间顺序——因为某些 querier 返回结果比其他 querier 更快。当 TraceQL 查询达到最大 trace 数限制时,query frontend 会先返回已收集到的 trace 与 span,并停止等待尚未返回的 querier。
下一步
- 规划与部署 Tempo:参见 set-up-for-tracing 下的部署文档。
- 深入了解部署模式细节:见 Deployment modes。
- 学习 span 的组成部分与可查询字段:见 Trace structure。
- 掌握完整配置项:见 configuration 文档。
- 深入源码:模块装配逻辑见 cmd/tempo/app/modules.go,各组件实现位于 modules 目录,Parquet 编码在 tempodb/encoding/vparquet5,TraceQL 引擎在 pkg/traceql。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考