
如果你维护过几个微服务恐怕你逃不掉和分布式追踪打交道。两年多前我接手一套每天百万级请求的业务集群链路追踪一直处于“有工具、没数据、没法用”的状态Jaeger 部署着可大部分时候控制台里只有零星几条 trace开发同学也不知道该不该全量上报更不敢把采样率调高怕把后端集群直接“压垮”。直到我把 Grafana Tempo 接进已有的 Prometheus Loki Grafana 技术栈追踪数据才终于从“偶尔看一眼”变成了“随时可查、日常能搜”的排障手段。这篇文章不是官方文档的翻译而是我从调研、部署、压测到落地排查的真实全流程会讲清楚 Tempo 的架构取舍、一套最小可复用的部署方案、TraceQL 查询经验以及我踩过的几个坑希望对同样被分布式追踪折磨的人有实际帮助。1. 分布式追踪的旧困境采样、存储与工具割裂1.1 为什么很多人被传统追踪后端劝退做后端的人基本都认识 Jaeger 和 Zipkin它们确实把“追踪”这个概念普及了。但一提到自建维护大家的表情就复杂了。我见过不少团队Jaeger 部署完以后追查问题照样费劲不是因为追踪本身没用而是后端存储被设计成了“通用检索系统”需要 Elasticsearch 或 Cassandra 来存索引需要较大的内存和磁盘还要花精力调分片、调性能。这里有一个关键矛盾分布式追踪的日常诉求其实主要是“我在日志和指标里看到一个 traceID现在想把这条链路的完整经过拉出来看一眼”。它更像一个按键式查询而不是一个通用搜索引擎。但传统追踪后端天然倾向于建立全局索引结果就是我在生产环境看到的现象即便采样率只有 5%一小撮节点的磁盘和 CPU 依然被索引写入吃掉了不少保留周期一拉长ES 集群的运维成本立刻翻倍。很多团队最后只能把采样率砍到 1% 以下于是真正出问题时偏偏抓不到那一条关键 trace。1.2 采样是双刃剑漏掉根因还是吞下全部成本采样率低根因线索可能被漏掉采样率高存储后端先撑不住。这是过去做分布式追踪绕不开的跷跷板。更麻烦的是传统采样大多在客户端完成也就是 head sampling进入后端之前就把大部分 span 丢了。这在“链路比较短、问题比较稳定”的场景下还能接受一旦遇到偶发的高延迟、慢查询、零星报错头部采样很难精准命中问题 trace。尾部采样能解决一部分问题但它需要把所有 span 先缓冲起来再统一决策基础设施和延迟开销都不小。所以以前很多团队的选择其实是“两害相权取其轻”要么全量采后端爆掉要么低比例采问题查不到。Grafana Tempo 之所以能破局核心并不是采样算法突然变聪明了而是它把追踪数据当作日志一样对待放进对象存储让“保存更多 trace 数据”这件事的成本大幅下降采样率的决策空间一下子被打开了。2. 从诞生到架构Tempo 是靠什么站稳脚跟的2.1 没有全局索引为什么反而更快Grafana Labs 在 2020 年开源 Tempo 时思路就很鲜明不要试图为所有 span 字段建立通用索引而是把 trace 当作一个整体对象按 traceID 去定位。也就是说Tempo 的默认追求不是“我帮你搜出所有满足条件的 span”而是“你给我一个 traceID我还你一条完整链路”。这听起来像降级实际却非常聪明。对象存储S3、GCS、MinIO很便宜扩容也简单不需要一个庞大的 ES 集群持续维护索引CPU 和内存压力自然小很多。Tempo 在存储侧做了两层加速block 里写入时会带上 bloomfilter用来快速判断某个 traceID 是否可能存在于这个 block查询时按时间范围扫 block用 filter 过滤掉明显不匹配的文件再把命中的 block 加载出来。对“按 traceID 找链路”这个高频场景来说它甚至比传统后端更轻更快。2.2 一条 span 从接收到查询的完整旅程我第一次把 Tempo 的读写路径理顺之后对它有了一种“原来如此”的感觉。整条链路大致是这样应用通过 OTLP 协议把 span 发给 Tempo 的 distributordistributor 根据 traceID 做路由把同一链路的 span 打到对应的 ingester 上ingester 先在内存中攒成小块写入本地 WAL 保证崩溃恢复再定期把块刷到对象存储此后 compactor 会不断把小块合并成大块减少文件数量提高查询效率。查询路径则是另一个方向。Grafana 发起查询请求后querier 会同时看两块数据还在 ingester 内存里的热数据以及已经落到对象存储里的冷数据。对于冷数据querier 依靠 block 的索引和 bloomfilter 快速过滤再用列式格式读取需要的字段。整个过程中Tempo 没有维护“字段 A 等于 xxx 的所有 span 在哪里”这种反向索引所以写入时不需要承担昂贵的索引更新成本。这也是它敢把保留期设置到几十天甚至更久的原因。注意Tempo 并不是用来做“全局复杂分析”的万能后端。如果你想做“过去一小时内所有 status500 的 span 的聚合指标”那是 Prometheus 和 span metrics 的活Tempo 更适合把某一条具体的 trace 从头到尾挖出来。理解这个边界部署起来才不会拧巴。2.3 TraceQL面向 trace 的专用查询语言如果 Tempo 只能按 traceID 查那还是不够好用。Grafana 团队给出的答案是 TraceQL一种专为追踪数据设计的结构化查询语言。它介于 PromQL 和日志查询之间能按 span 和 resource 的 attribute 做过滤也能表达链路上的父子关系。比如我想找 checkout 服务里所有 HTTP 状态码大于等于 500 的 span可以写{ resource.service.name checkout span.http.status_code 500 }大括号里描述的是 span 自身的条件如果我想表达“这个 span 下面还挂了其他调用”可以用这类结构操作符继续往下钻。TraceQL 是在 Tempo 2.x 里逐步成熟起来的它对标的是“既不想背 traceID又不想用一堆复杂查询语句”的中间场景。我实际用下来定位慢接口、错误链路、跨服务调用问题比过去在 Jaeger UI 里靠时间轴手动翻块要直观得多。3. 落地实操从最小化部署到打通全链路3.1 一套足够生产参考的组件选型很多教程默认你已经有 Kubernetes、Prometheus、对象存储但本地验证和生产预研阶段最顺手的还是 Docker Compose。我落地时选型比较克制Tempo 负责追踪MinIO 充当 S3 兼容对象存储Grafana 做查询界面和最终呈现Prometheus 用来接收 Tempo metrics_generator 吐出的指标。先跑通逻辑再平移上生产环境。下面这个 compose 文件精简掉了部分健康检查但核心组件和端口都保留了services: minio: image: minio/minio:latest command: server /data --console-address :9001 environment: MINIO_ROOT_USER: tempo MINIO_ROOT_PASSWORD: tempo123 ports: - 9000:9000 - 9001:9001 createbuckets: image: minio/mc:latest depends_on: - minio entrypoint: sh -c mc alias set local http://minio:9000 tempo tempo123 mc mb --ignore-existing local/tempo tempo: image: grafana/tempo:2.6.1 command: [-config.file/etc/tempo.yaml] volumes: - ./tempo.yaml:/etc/tempo.yaml - tempo-data:/var/tempo ports: - 3200:3200 - 4317:4317 - 4318:4318 depends_on: - minio grafana: image: grafana/grafana:11.1.0 ports: - 3000:3000 environment: GF_SECURITY_ADMIN_PASSWORD: admin volumes: - grafana-data:/var/lib/grafana volumes: tempo-data: grafana-data:3.2 tempo.yaml 配置逐行拆解Tempo 的配置文件乍看字段多但只要抓住几条主线就不乱接收端、存储端、压缩端、指标生成端。下面是我验证过的精简配置server: http_listen_port: 3200 distributor: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 storage: trace: backend: s3 s3: bucket: tempo endpoint: minio:9000 access_key: tempo secret_key: tempo123 insecure: true wal: path: /var/tempo/wal local: path: /var/tempo/traces block: version: vParquet4 compactor: compaction: block_retention: 336h metrics_generator: processors: [service-graphs, span-metrics] storage: path: /var/tempo/metrics remote_write: - url: http://prometheus:9090/api/v1/write这里的storage.trace.backend: s3是指后端存储类型对象存储选 MinIO 或云厂商 S3 都行endpoint指向 MinIOinsecure: true表示走 HTTP。wal.path是 ingester 的本地预写日志目录必须给够磁盘空间。block.version: vParquet4是列式存储格式查询时过滤效率更好建议新环境直接用它。block_retention控制保留时长我设了 14 天你可以按业务需要调整。metrics_generator 不是必选项但我强烈建议开启。它会在 Tempo 内部对 trace 数据做聚合分析生成 service graphs 和 span metrics然后 remote_write 到 Prometheus。这样你既能保留追踪细节又能用指标做告警和总览不必为了看个错误率就全量查 trace。3.3 打通数据链路应用埋点到 Grafana 查询Tempo 的 OTLP 接收端可以直接作为应用追踪数据的入口。在测试环境里我建议先用 OpenTelemetry SDK 自带的 exporter 指向 Tempo:4317gRPC或 Tempo:4318HTTP确认整条链路通了再考虑引入 OpenTelemetry Collector 做统一采样和转发。很多语言的自动埋点都支持 OTLP exporter比如 Java 的 agent、Go 的 SDK、Python 的 instrumentation配置量不大。Grafana 这边的动作更简单在 Data Sources 里添加 TempoURL 填http://tempo:3200保存即可。然后在 Explore 页面切到 TraceQL 模式输入{ resource.service.name checkout }就能看到 checkout 服务的 span 列表。点开任意一条 trace左边是服务名和时间线右边能看到 span 的 tag、日志、状态码点击某个 span 还能联动查看对应的日志和指标。对于多语言微服务架构Tempo 最大的好处就是这里你不再需要为“每个服务自己搞一套追踪可视化”而头疼一套 Grafana 就能把 metrics、logs、traces 串起来。4. 常见问题与排查技巧实录4.1 查不到 Trace从哪一步开始排查这是群友问得最多的问题。按照我的经验如果应用配好了 OTLPGrafana 数据源也建好了但 Explore 里始终查不到数据可以按下面顺序排查先确认应用侧确实有 span 在发。最简单的办法是在本地用 curl 往 Tempo 的 OTLP HTTP 端口打一条测试数据比如访问http://localhost:4318/v1/traces看返回是否 200。查看 Tempo 自身指标。Tempo 暴露了/metrics重点看tempo_distributor_spans_received_total有没有增长、tempo_ingester_blocks_flushed_total是否持续增加。如果 received 是 0问题在应用侧如果 received 有增长但 flushed 不动问题可能在对象存储配置上。检查 MinIO 或云端 bucket 里有没有 block 文件。如果里面有meta.json、index、bloom这类文件说明写路径正常。检查 Grafana 数据源的时间范围。Tempo 默认查询时间窗口有限如果数据点恰好在时间范围外UI 上就会显示“没找到”这是最容易忽略的原因之一。最后用 traceID 直接查一次。如果在 Explore 的 TraceID tab 里粘贴一个已知 traceID 能查出来那说明存储没问题问题多半出在 TraceQL 语句或时间范围上。4.2 TraceQL 匹配不到数据的几个隐秘原因TraceQL 看起来像“条件查询”但它的字段定位比想象中严格。最常见的问题是把 resource 上的属性写成了 span 属性。比如服务名通常在resource.service.name里而不是span.service.nameHTTP 状态码通常在 span 属性里写法是span.http.status_code。写错层级查询结果当然为空。还有一个坑是匹配大小写和类型。TraceQL 对于字符串值默认严格匹配如果你知道值是大写但查询时用了小写可能就查不到需要用正则匹配时可以写成~ .*error.*。另外如果集群里同时存在升级前和升级后写入的不同存储格式 block某些老格式的 block 在新查询引擎下可能扫不出来稳妥做法是升级后等老 block 自然过期或者用官方工具重写 block让格式统一。最后别忽略查询时间范围。Tempo 的冷数据块存放在对象存储里查询时是“按时间窗口找 block”如果你把时间范围拉得特别大扫描的数据量会激增某些查询可能因为超时被截断。这种情况我一般会先把时间范围缩小到 30 分钟或 1 小时确认小范围能查到后再逐渐扩大。4.3 资源占用与存储增长的边界Tempo 比传统追踪后端省钱但不代表可以完全不管资源。storage.trace.wal目录是本地磁盘热点写入量大时它短期占用可能很高尤其内存块还没刷到对象存储的时候。如果磁盘很小很容易被 WAL 撑满。我建议至少给 WAL 独立挂一块 SSD 或高 IOPS 云盘容量按预期半小时写入量估算。对象存储的增长则主要取决于采样率和保留时长。举一个大概的参考值如果日活千万级请求的集群按 10% 采样率每天产生约 50GB 压缩后的 trace 数据保留 14 天就是 700GB。这个体量对 S3/OSS 来说很轻松但如果你用本地 MinIO 跑在普通磁盘上就要提前规划容量。metrics_generator 也会吃一部分 CPU 和内存特别是 service-graphs 需要在内存里聚合拓扑关系流量大时建议独立评估资源不要把它和 Tempo 主流程挤在一个小容器里。5. 从能跑到跑好采样策略与团队协作5.1 采样率的正确姿势头部采样与尾部采样配合Tempo 降低了存储成本但并不意味着可以无脑全量采样——链路一旦非常深、调用量非常大全量成本仍然可观。我现在的做法是两层配合在 OpenTelemetry Collector 里对常规请求做头部采样比如统一采样 10%同时开启尾部采样处理器把状态码为 5xx、耗时超过 P99 的 span 保留 100%确保那些真正需要排查的异常链路不被丢掉。这套策略的本质是可用性指标和告警依赖 Prometheus日常问题定位靠那 10% 的常规 trace而严重故障排查则靠 100% 的错误和慢链路 trace。Tempo 因为是对象存储允许你保留比传统后端大得多的数据量所以哪怕把错误 trace 全量保留两周成本也完全可以接受。我建议团队在制定采样率时不要拍脑袋定一个 1% 就完事而是先看一下每日 span 总量和对象存储账单再把“错误链路的全量保留”作为硬性要求。5.2 用 span metrics 与服务拓扑图补足全链路视角Tempo 的价值如果只体现在“查具体某条 trace”上那它对你的帮助仍然有限。真正让我觉得它值得推广的是跟 Prometheus 联动后的组合拳。开启 metrics_generator 之后Tempo 会根据收到的 span 计算出服务维度的 RED 指标每秒请求数、错误率、耗时分布写到 PrometheusGrafana 里就可以直接做服务大盘看到每个服务的请求量、错误数、P50/P95/P99 时延。同时service-graphs 会生成服务间调用关系相当于一张实时更新的拓扑图。以前我们为了画服务依赖图得专门找人埋点或者靠人肉梳理现在追踪数据本身就能攒出这张图来。排查问题的时候我通常先看大盘定位哪个服务异常再看拓扑确认调用链上下级最后点进一条 trace 追到具体代码逻辑。5.3 最后分享几个我自己的习惯Tempo 落地大半年后我最深的感受是分布式追踪从来不是一个独立工具能解决的它和数据采样、存储成本、团队排障习惯深度绑定。如果你也在评估 Tempo我建议不要一上来就全量迁移先在旁边并行部署一周拿几个真实故障案例对比一下旧方案和新方案的排查体验再决定要不要彻底切换。我个人后来养成的习惯是把 traceID 主动打进业务日志里这样出现线上问题时日志、指标、追踪三个入口能立刻对上号遇到慢查询和偶发错误先去 Grafana 里搜对应服务的 TraceQL而不是像以前那样对着日志猜半天。Tempo 给我的最大改变是追踪终于从“大故障时才会被想起的武器”变成了“日常随手就能用的工具”。