Grafana Tempo 与 Loki、Mimir 同属 Grafana 可观测性套件,Tempo 专注分布式链路追踪,最大的不同是它只保存 trace 数据本身,不建立 span 级别的全文索引。查询时靠 TraceQL 从对象存储中按需扫描,这让它的写入成本大幅低于 Jaeger 和 Elastic APM 这类方案。对于本地开发、演示或者小型集群,用 Docker 启动一个单机 Tempo 完全够用,不需要额外部署 Cassandra 或 Elasticsearch。不过单机部署的坑往往集中在 backend 选型和数据目录权限上,本文会把这些步骤串成一条可复现的链路。

一、Tempo 单机版需要理解哪些关键配置
与 Jaeger 全家桶不同,Tempo 将数据写入与查询解耦。distributor 负责接收不同协议的 trace,切分成 batch 后交给 ingester;ingester 先写 WAL 再按时间窗口刷成 block;compactor 合并和保留历史 block;querier 与 query-frontend 处理 TraceQL 和按 trace ID 查询。单机部署时这些组件在同一个进程内运行,配置却仍然按模块划分,因此看 tempo.yaml 时会发现顶层同时出现 server、distributor、ingester、compactor、storage 等字段。
对 Docker 场景影响最大的是 storage.trace.backend。可选值有 local、s3、gcs、azure、bos 等。local 只适合单实例,因为 block 会落在本地文件系统,无法被多个 Tempo 实例共享。如果以后要横向扩展,必须迁移到 S3 或 MinIO。本文选择 local,路径指向容器内 /var/tempo/traces,并挂载到宿主机 ./tempo-data,这样即使容器被删除,本地磁盘上仍然保留 trace 数据。
另一个容易忽略的点是接收器。Tempo 的 OTLP gRPC 默认端口 4317、OTLP HTTP 4318、Jaeger thrift HTTP 14268、Zipkin 9411。Docker 映射端口时要与 tempo.yaml 中 distributor.receivers 保持一致,否则应用 SDK 能连上端口但 Tempo 不认协议。端口冲突很常见,尤其是在已经跑着 Jaeger 或 Prometheus 的机器上,启动前最好先检查一下监听状态。
二、编写 docker-compose.yml 与 tempo.yaml
下面给出完整可用的编排文件。先把 Tempo 和 Grafana 放进同一个自定义网络,Grafana 通过服务名 tempo 访问查询接口,避免宿主机 IP 变动带来的数据源失效。Tempo 镜像统一使用 grafana/tempo:latest,挂载配置文件与数据目录,并把常用的接收器端口暴露到宿主机。
services:
tempo:
image: grafana/tempo:latest
container_name: tempo
command: ["-config.file=/etc/tempo.yaml"]
ports:
- "3200:3200"
- "4317:4317"
- "4318:4318"
- "14268:14268"
- "9411:9411"
volumes:
- ./tempo.yaml:/etc/tempo.yaml:ro
- ./tempo-data:/var/tempo
networks:
- tracing
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana-provisioning:/etc/grafana/provisioning
networks:
- tracing
networks:
tracing:
driver: bridge
上面的 compose 文件把 Tempo 的 5 个端口绑定到宿主机。其中 3200 是 HTTP 查询接口,Grafana 数据源走容器网络 http://tempo:3200 即可;4317 和 4318 分别给 OpenTelemetry SDK 使用;14268 是 Jaeger 客户端通过 HTTP 上报;9411 是 Zipkin 接收器,后文验证时会用到。生产环境不一定需要暴露全部端口,但本地调试保留完整协议会比较方便。
接着创建 tempo.yaml。注意缩进必须是两个空格,不能使用 Tab,否则 YAML 解析会直接失败。该配置启用 OTLP、Jaeger、Zipkin 三套接收器,存储后端使用 local,WAL 和 block 均写入 /var/tempo 下。
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
grpc:
endpoint: 0.0.0.0:14250
zipkin:
endpoint: 0.0.0.0:9411
ingester:
trace_idle_period: 10s
max_block_duration: 5m
complete_block_timeout: 10m
flush_check_period: 5s
compactor:
compaction:
block_retention: 1h
compaction_window: 1h
storage:
trace:
backend: local
local:
path: /var/tempo/traces
wal:
path: /var/tempo/wal
querier:
query_timeout: 30s
这里有几个参数值得单独说明。trace_idle_period 控制一个 trace 超过多久没有新 span 就认为可结束,设置 10 秒是为了本地验证能更快看到数据。max_block_duration 决定 ingester 最多累积多久就切一个新 block,设 5 分钟可以降低 block 碎片。compaction_window 是指压缩时按 1 小时窗口聚合,block_retention 为 1 小时,意味着超过 1 小时的 trace block 会被清理。这些值在 demo 环境尽量调小,生产环境则需要根据流量放大。
三、启动并验证 trace 写入与查询
在 docker-compose.yml 所在目录执行 docker compose up -d。第一次启动会先拉取镜像。执行 docker compose ps 确认两个容器都是 Up 状态,再执行 docker logs tempo 查看是否有 level=error 的日志。常见错误是宿主机目录权限不足,因为 Tempo 官方镜像默认使用 UID 10001 运行,如果 ./tempo-data 的所有者是 root,ingester 无法创建 WAL 目录。可通过 sudo chown -R 10001:10001 ./tempo-data 修复。
验证最简单的方式是使用 Zipkin 接收器。Tempo 原生支持 Zipkin v2 JSON,我们不需要在本地安装 OpenTelemetry SDK,直接用 curl 发送一个包含单个 span 的 trace 即可。示例中的 traceId 和 id 都是 16 位十六进制字符串,需要保证长度正确,否则接收器会返回 400。
curl -s -X POST http://localhost:9411/api/v2/spans \
-H 'Content-Type: application/json' \
-d '[
{
"traceId": "4d1e00c0db9010db86154a4ba6e91385",
"id": "86154a4ba6e91385",
"name": "docker-tempo-check",
"timestamp": 1710000000000000,
"duration": 100000,
"localEndpoint": {
"serviceName": "demo-service"
},
"tags": {
"http.method": "GET",
"component": "docker-tempo"
}
}
]'
若命令返回空且 HTTP 状态为 202,说明数据已经被 Zipkin 接收器受理。接下来等待 10 到 20 秒,让 ingester 完成 trace_idle_period 判断并产生可查询 block。然后打开 Grafana 左侧 Explore,数据源选择 Tempo,将查询类型切换为 TraceQL,输入 { resource.service.name = "demo-service" } 并执行。只能看到一条最近 1 小时内的 trace。也可以直接用 curl 查询 API:curl -s http://localhost:3200/api/traces/4d1e00c0db9010db86154a4ba6e91385。如果返回 JSON 中包含 batches 和 resource 信息,说明从写入到查询整条链路已经打通。
这里有一个容易混淆的点:Grafana 里数据源 URL 要写 http://tempo:3200,而不是 http://localhost:3200。因为 Grafana 运行在 Docker 网络内部,localhost 指向它自己的容器,不是宿主机。只有在宿主机上测试 Tempo API 时才使用 localhost。
四、常见问题与调优建议
问题一:trace 查询不到但写入无报错。先确认 span 时间戳是否在当前时间附近,Tempo 默认只查询最近 2 小时,过期数据即便存在也不会返回。再检查 ingester 是否已经 flush。可以 docker exec -it tempo ls /var/tempo/traces 查看是否有 block 目录,如果只有 WAL 没有 block,可以把 trace_idle_period 和 flush_check_period 进一步调小观察。
问题二:接收器端口连接拒绝。容器端口映射和 Tempo 配置必须同时存在,仅靠 Dockerfile EXPOSE 不会生效。执行 docker port tempo 可以快速看到映射结果,但容器内进程没监听时依然无法访问。此时先在容器内执行 wget -qO- http://127.0.0.1:3200/ready 检查就绪状态。若返回 503,通常是配置解析失败,需要查看完整启动日志。
问题三:磁盘增长过快。local backend 不会自动清理未压缩的小 block,需要 compactor 正常运行。可以注意 compactor 日志中的 compacted 信息。若长期运行,建议把 block_retention 设为合理天数,并按计划备份或迁移到 S3。对于生产环境,不要把 local backend 当作长期方案,因为容器重建后宿主机挂载目录虽然能恢复数据,但无法水平扩展。
最后,如果只是做本地演示,这些参数已经足够;如果要接入真正的 OpenTelemetry SDK,推荐优先使用 OTLP gRPC 或 HTTP 上报,并沿用官方推荐的 resource 属性 organization、service.name 和 deployment.environment。TraceQL 查询时先用 resource.service.name 过滤比直接按 span name 扫更高效。
DockerGrafana Tempo分布式追踪修改时间:2026-09-18 23:36:58