一句话总结
全量存储每一条追踪既昂贵也无必要,采样是追踪落地的首要工程问题--尤其是"尾部采样"能够确保仅保留出错和延迟异常的请求.Jaeger 是经典的追踪系统,而 Tempo 采用了与 Loki 相同的策略--"仅按 ID 索引,成本极低"--并与指标和日志深度联动.
前置回顾
第 06 篇阐述了追踪原理:Span 组成 Trace,Context 传播串联链路,OTel 统一采集.篇末留下两个工程问题:海量请求中应采样哪些?数据存储在何处,如何查询,如何联动? 本篇即回答这些问题,其中将看到第 04 篇"保留异常,稀释正常"的采样策略在追踪领域的升级版本.
为什么必须采样:全量存储的不可行性
先做一个定量估算.一个中等流量的服务每秒处理 1 万个请求,每个请求产生几十个 Span:
|
全量存储每日 26TB 的追踪数据,成本极高,且 99% 为低价值的正常请求追踪.因此采样(Sampling)--仅保留一部分追踪--不是优化选项,而是必选项.问题的关键在于:如何智能地选择保留哪些追踪数据.
两种采样策略:头部采样与尾部采样
采样的时机决定了其智能程度.这是本篇的核心知识点.
头部采样(Head-based Sampling):在请求入口做出决策
在请求刚进入系统时即决定是否追踪该请求--例如"随机保留 1%".决策在"头部"(链路入口)做出,此后整条链路要么全量追踪,要么完全不追踪.
- 优点:实现简单,开销极低(早期决策使后续服务无需缓存数据)
- 关键缺陷:决策时尚不知道此请求是否会出错或变慢.结果是--最需要查看的那条 2 秒超时的请求,有 99% 的概率恰好未被采到,待需要排查时该追踪数据根本不存在.
尾部采样(Tail-based Sampling):在请求完成后做出决策
等待一条追踪的所有 Span 全部完成后,基于"本次请求的完整信息"再决定保留与否--例如"出错的全部保留,延迟高的全部保留,正常的仅保留 1%".决策在"尾部"(链路结束)做出.
- 优点:能够精准保留有价值的追踪数据--出错的,延迟高的一条不漏,正常的大比例稀释.这正符合排障需求.
- 代价:需将一条 trace 的所有 Span 暂存至请求结束(因为 Span 来自不同服务,陆续到达),需要更多内存和更复杂的组件(通常由 OTel Collector 承担).
回顾第 04 篇日志采样的基本原则--"保留异常,稀释正常,ERROR 级别绝不采样".尾部采样将相同策略应用到追踪领域,且实现得更为彻底:因为它等待请求结束并获取完整结果后才做出决策,所以能够精确地"仅保留出错和延迟异常的请求".
**这是可观测性应对海量数据的通用思想在不同支柱上的同一种体现.**在条件允许的情况下应优先使用尾部采样,这是生产级追踪的推荐做法.
|
Jaeger:经典的追踪系统
Jaeger(Uber 开源,CNCF 项目)是追踪领域长期使用的主力方案,提供完整的功能栈:Span 接收,存储,查询,以及经典的瀑布图 UI.
- UI 成熟:瀑布图,服务依赖拓扑图,Trace 对比,排障体验完善
- 存储可替换:后端可对接 Cassandra,Elasticsearch 等
- 原生支持 OTel:直接接收 OTLP 数据(见第 06 篇),无需厂商专用 SDK
第 06 篇练习中"在 Jaeger UI 查看瀑布图",即是 Jaeger 将收到的 Span 按 trace_id 组装为树,绘制为时间线的结果.
Jaeger 的存储问题与 Elasticsearch 同源
Jaeger 常用 Elasticsearch 作为存储后端--于是它继承了第 05 篇 EFK 方案的老问题:为支持"按各种标签搜索追踪",需要建立大量索引,存储和运维成本高昂.当追踪数据量增长到一定规模,这一成本问题与 EFK 存储日志的困境相同.这为下文的 Tempo 方案提供了对比背景--又是一次"全文/多维索引 vs 极简索引"的架构取舍.
Tempo:将 Loki 的低成本策略应用于追踪
Tempo(Grafana 出品)对追踪数据的处理方式与 Loki 对日志的处理方式完全一致,设计理念一脉相承:
查询追踪时,绝大多数场景是通过已知的 trace_id 直接查询(从指标的异常点,从日志的某条记录获取 trace_id,然后查看其完整链路).既然如此,无需为追踪的各类字段建立索引.仅按 trace_id 建立索引,Span 数据整体压缩后存入对象存储(S3)即可.
| Jaeger(配 ES) | Tempo | |
|---|---|---|
| 索引 | 对多个字段建立索引,支持按标签搜索 trace | 仅按 trace_id 索引,正文存入对象存储 |
| 成本 | 高(索引体积大) | 极低(几乎仅支付对象存储费用) |
| 查询方式 | 可按 service/tag 等自由搜索 trace | 主要通过已知 trace_id 直接拉取(也支持 TraceQL 按属性查询) |
| 定位思路 | 在追踪系统内部搜索 | 从指标/日志处获取 trace_id,再到此查询 |
Jaeger + ES 如同为图书馆的每本书按书名,作者,主题均编制了检索卡片,支持多维度检索,但维护卡片系统耗费大量人力.
Tempo 如同书籍仅按借阅号归档:日常操作并非"按主题浏览书架",而是手持特定借阅号来取对应的一本--如此则仅按号码归档最为高效,成本最低.
Tempo 的设计前提:trace_id 始终可从其他来源获取
Tempo 敢于"仅按 trace_id 建立索引",其依据来自整个可观测性栈的协同:很少需要"在追踪系统中凭空搜索一条 trace",而总是从一个指标异常(携带 exemplar,见第 08 篇)或一条日志(携带 trace_id,见第 04 篇)出发,已拿到 trace_id,再到 Tempo 获取完整链路.
这一前提成立,Tempo 的极简索引策略即成立--它本质上将"如何找到该查看哪条 trace"这一职责,交由指标和日志系统承担.这也解释了为什么 Grafana 体系(Prometheus + Loki + Tempo)如此强调三者联动.
实战:一次慢请求的完整定位
将第 06,07 篇的内容串联起来,重新走一遍第 01 篇凌晨的排障流程--这次有了完整的追踪能力.以下第 2 步用到 exemplar:它是附加在指标数据点上的一个"样例 trace_id",使我们能够从一个聚合的指标点跳转到一条具体的代表性追踪(原理在第 08 篇详细说明,此处先理解"指标点上可附带一个 trace_id"即可).
|
注意这条路径中,trace_id 是贯穿始终的线索:从指标的 exemplar 获取它,用它取追踪,查日志.三个独立的系统(Prometheus/Tempo/Loki),通过这一个 ID 串联为一次连贯的排障流程.这就是第 08 篇将要系统论述的"关联",而追踪在其中承担"定位到哪个调用跳"的关键角色.
这是采样固有的风险,也是尾部采样优于头部采样的实际意义所在:使用尾部采样时,"出错,延迟高"的请求被规则强制保留,因此从告警追查的那条慢 trace,大概率已被存储.若使用头部随机采样,则很可能无法找到.
这也是为什么:追踪和日志应采用同一套"保留异常"的采样策略,否则在日志中看到了 trace_id,在 Tempo 却查无此 trace--两个支柱的采样策略不一致,关联即告中断.
快速回顾
- 采样是追踪的必选项:全量存储追踪数据成本极高且大部分无价值(99% 为正常请求),关键在于如何智能选择
- 头部采样:请求入口处随机决策,实现简单,开销低,但决策时不知结果,最需查看的错误请求大概率未被保留
- 尾部采样:等待请求完成,查看完整结果后再决策,能精准"保留异常,稀释正常",代价是需暂存 Span(通常由 OTel Collector 承担);为生产环境推荐方案
- 尾部采样 = 第 04 篇"保留异常"策略的升级,可观测性应对海量数据的通用方法论
- Jaeger:经典追踪系统,UI 成熟,原生支持 OTel;搭配 ES 存储则继承 EFK 的高成本问题
- Tempo:效仿 Loki,仅按 trace_id 索引,正文存入对象存储,成本极低;前提是通过指标/日志获取 trace_id 后再来查询--依赖三支柱联动
- 实战定位:指标 exemplar → trace_id → Tempo 查链路 → Loki 查日志,trace_id 为贯穿全过程的线索
既然尾部采样效果更好,为什么仍有场景使用头部采样?
因为尾部采样存在实际代价.它需要在内存中暂存每条进行中 trace 的所有 Span,直至请求结束才能决策--这对超高流量系统构成不小的内存和架构负担,且 Span 来自多个服务,到达有先后,暂存与聚合的逻辑也更为复杂.头部采样虽"粗放"但极轻量,易于部署.实践中常组合使用:头部先做第一道粗筛(过滤掉明显过量的流量),尾部再做精筛(在剩余流量中保留异常).没有绝对的最优方案,应依据流量规模和预算进行选择.
Jaeger 与 Tempo 应如何选择?
取决于"查找 trace 的方式"和对成本的敏感程度.如果需要且习惯"在追踪系统内按 service,tag 自由搜索 trace",Jaeger(配 ES)的检索能力更直接.如果排障路径为"从指标/日志获取 trace_id 再查看链路",且关注成本,Tempo 更经济且运维更简单--尤其是已经使用 Grafana + Prometheus + Loki 的团队,Tempo 可无缝融入同一界面和标签体系.云原生新项目,成本敏感场景,Tempo 是当前较为流行的选择;这与第 05 篇 Loki vs EFK 的决策逻辑高度一致.
动手练习
- 查看瀑布图:将第 06 篇的 demo 接入本地 Jaeger(或 Tempo),在 UI 中查看完整瀑布图,找出耗时最长的那个 Span.
- 配置头部采样:配置一个简单的头部采样(如采样率 10%),发送 100 个请求,统计实际存储的 trace 数量.
- 配置尾部采样:在 OTel Collector 中配置尾部采样规则:"错误请求 100% 保留,正常请求 10%",制造一些错误请求,验证错误请求全部被保留.
- 串联全栈:在一个 Grafana 中同时接入 Prometheus,Loki,Tempo,从一条慢请求的指标出发,跳转到 Tempo 查看链路,再使用 trace_id 跳转到 Loki 查看日志--亲自实践"指标→追踪→日志"的定位路径.
- 对比存储成本:对比 Jaeger(配 ES)与 Tempo 存储等量追踪数据时的资源占用,用自己的语言阐述这一差异的来源(对照第 05 篇 Loki vs ES).