采样 · 头部还是尾部

追踪的成本随流量线性增长,所以必须采样。这一章讲清头部采样与尾部采样的根本取舍——前者便宜但会漏掉出问题的那条,后者能「只留有意思的」但代价高昂,以及为什么尾采样必须在 gateway 且必然有状态。

作者 David更新于 第 44 篇(共 52 篇)

采样 · 头部还是尾部

第 0 部分讲成本模型时说过:追踪的数据量随流量线性增长。10 万 QPS 全量采,一天几百亿个 span——没人付得起。

所以必须采样。而“怎么采”是追踪工程里最重要的决策。

两种时机,两个世界

区别只有一个:在哪个时刻决定“这条 trace 要不要留”。

头部采样(head)尾部采样(tail)
决定时刻trace 一开始(第一个 span)trace 结束后(所有 span 都到齐)
依据只有随机数 / trace_id整条 trace 的全貌(慢不慢?错没错?)
成本极低(没被采的根本不产生)高(所有 span 都要先收上来、缓存住)
能“只留有意思的”吗❌ 不能✅ 能
在哪做SDK(应用里)必须在 gateway
有状态吗无状态有状态(要缓存 span 等齐)

这张表的每一行都是同一个根本矛盾的投影:你要在“决定得早(便宜)”和“决定得对(信息全)”之间选。

头部采样:便宜,但会漏掉重要的

网关收到请求 → 掷骰子:1% 概率 → 决定"采"
             → 把决定写进 traceparent 的[采样位](/manual/observability/traces-basics)
             → 下游所有服务照做(不采的连 span 都不产生)

头部采样最大的优点是便宜**:没被采中的 trace,span 压根不会被创建——不占 CPU、不占内存、不占网络。这是“零成本”级别的便宜。**

但它有个致命缺陷:决定的时候,你还什么都不知道。

1% 采样率下:
- 100 个正常请求 → 采到 1 个 → 挺好
- 1 个超慢的请求(5 秒!)→ 99% 概率不采 → 你永远不知道它发生过

而“那个出问题的请求”恰恰是你唯一想看的。 你付钱存下了 99 个“一切正常”的 trace,却漏掉了那个唯一有价值的。

这就是头部采样的根本问题:它对“有趣”和“无聊”一视同仁——因为在决定的那一刻,它还不知道这条 trace 会不会出问题。这和“未知的未知”是同一类困境:你没法预先知道该留什么。

缓解手段(都不完美):

  • 对错误请求提高采样率 —— 但错误是在最后才知道的,而决定在最开始。有些 SDK 支持“事后补救”(先记录、延迟决定),但那已经在往尾采样靠了。
  • 按路径分层采样 —— 重要接口 100%、健康检查 0.01%。这是最实用的头部采样优化,而且不需要任何额外基础设施。

尾部采样:能只留有意思的,但很贵

所有 span 都发到 Collector gateway
  → gateway 把同一 trace_id 的 span 缓存起来(等!)
  → 等到 trace "看起来完整了"(或超时)
  → 现在看全貌:有错误吗?超过 1s 吗?
  → 决定留还是丢

尾部采样的价值是你可以按结果决定**——这是质的飞跃:**

# tail_sampling processor 的典型策略
policies:
  - name: errors                  # 所有错误的 trace,100% 留
    type: status_code
    status_code: { status_codes: [ERROR] }
  - name: slow                    # 所有慢的,100% 留
    type: latency
    latency: { threshold_ms: 1000 }
  - name: normal                  # 正常的,只留 1%
    type: probabilistic
    probabilistic: { sampling_percentage: 1 }

“错的全留、慢的全留、正常的采样”——这几乎就是你想要的一切。 存储成本大降,而信息价值几乎没损失(那 99% 被丢掉的正常 trace,本来也没人看)。

代价极其现实,三条都很痛:

① 所有 span 都必须先发出来 —— 尾采样是在 gateway 决定的,所以应用必须先把 100% 的 span 发给它。你省的是“后端存储”,没省“应用的埋点开销 + 网络传输”。 这和头部采样“压根不产生”的便宜完全不是一回事。

② 必须缓存等齐,于是有状态 —— gateway 要把一条 trace 的所有 span 攒在内存里,直到它“完整”。这带来:

  • 内存开销 —— 攒的是所有 trace(不只是最后会留下的那 1%)。
  • decision_wait 的两难 —— 等多久算“完整”?等太短会把慢 trace 拦腰截断(讽刺的是:慢 trace 恰恰是你最想留的,却最可能因为超时被截断);等太长内存吃不消。
  • 有状态就不能随便扩副本 —— 这是最大的坑,见下。

③ 必须保证“同一 trace 的 span 落到同一个 gateway 实例”

这是尾采样部署时最容易搞砸的一点。如果 gateway 有 3 个副本、前面挂个普通负载均衡:

span A(trace=abc)→ 副本 1
span B(trace=abc)→ 副本 2     ← 灾难!
span C(trace=abc)→ 副本 3

每个副本都只看到 trace 的一部分,谁也没法做出正确决定。 结果是:要么各自瞎决定(trace 被撕碎),要么全都留(等于没采样)。

解法:必须按 trace_id 做一致性路由——用 loadbalancing exporter(它按 trace_id 哈希路由到固定副本)组成两层 gateway:第一层只负责按 trace_id 路由,第二层做尾采样。

这就是上一部分说“尾采样必须在 gateway”的完整含义——不只是“要集中”,而是“要按 trace_id 一致性地集中”。很多团队第一次上尾采样都栽在这里:配好了策略,发现 trace 全是碎的。

怎么选

图表加载中…

几条实战经验:

  • 最好的采样是不采样 —— 如果你的流量不大,全量存。别为了“最佳实践”给自己加复杂度。采样是被成本逼出来的,不是美德。
  • 头部采样 + 按路径分层,能解决 80% 的问题 —— 而且零基础设施成本。先做这个,不够了再上尾采样。
  • 上尾采样前,先确认你真的需要它 —— 它带来一整套有状态服务的运维负担(扩缩容、一致性路由、内存调优)。如果你只是想“别漏掉错误的 trace”,很多 SDK 支持“错误请求提高采样率”,可能就够了。
  • exemplar 是个被低估的中间道路 —— 下一章讲:它让你从指标曲线直接跳到那一刻的某条真实 trace,而不需要“存下所有 trace”。用聚合的便宜,换到了个案的入口。

小结与下一章

追踪成本随流量线性增长,必须采样。根本矛盾:决定得早(便宜) vs 决定得对(信息全)。 头部采样(trace 一开始就掷骰子,靠 traceparent 的采样位传播):极便宜(没采中的 span 压根不产生),但致命缺陷是决定时什么都不知道——1% 采样下,那个 5 秒的慢请求 99% 会被漏掉,而它恰恰是你唯一想看的;最实用的优化是按路径分层(重要接口 100%、健康检查 0.01%),零基础设施成本。尾部采样(等 trace 齐了看全貌再决定):能做到“错的全留、慢的全留、正常的 1%”——信息价值几乎无损。但三条代价:①所有 span 仍要先发出来(省的是存储,不是埋点和网络);②必须缓存等齐 → 有状态;decision_wait 两难(等太短会截断慢 trace,而慢 trace 恰恰最该留);③必须按 trace_id 一致性路由(否则副本各看一半,trace 全碎)——要用 loadbalancing exporter 做两层 gateway,很多团队第一次上尾采样都栽在这。选型:最好的采样是不采样(采样是被成本逼的,不是美德)→ 不够就头部+分层 → 再不够才上尾采样。

有没有办法“用聚合的便宜,换个案的入口”?有——下一章讲那座最巧妙的桥。👉 exemplar。

参考资料

本页目录