分布式追踪采样实战:从头采样到尾采样,把成本压下来又不丢异常
分布式追踪一旦铺开,第一个让你肉疼的不是接入难度,而是成本。每来一个请求就生成几十条甚至上百条 span,全量上报到后端,存储和流量费用按月跳着涨。更要命的是,绝大多数 span 是健康请求的正常路径,你其实并不需要它们全部。采样就是回答一个问题:在海量 span 里,留下哪些才既看得清系统、又不被账单压垮。
但采样最怕两件事:一是把偶发的报错链路采没了,出事时追踪里查无此链;二是采得支离破碎,父 span 在、子 span 不在,图谱拼不起来。这篇文章把头采样、尾采样和各种采样策略讲透,并给你一套能直接落地的取舍原则。
1. 为什么必须采样,不能全量
先算笔账。一个中等流量服务,QPS 2000,每请求平均 30 条 span,一天就是约 52 亿条 span。全量存下来,光存储就要按 TB 计,查询也慢。而其中 95% 以上是正常的、重复的、没分析价值的路径。追踪的本质目的是"在异常时定位问题",不是"给每根健康请求建档"。
所以行业共识是:对高流量系统,采样不是可选项,是必选项。难点在于怎么采得"聪明"——把采样率压到个位数百分比,同时让错误、慢、异常的链路几乎不丢。
2. 头采样:在起点就决定
头采样(head-based sampling)在 span 被创建的那一刻就决定这条链路采不采,通常是抛个硬币(概率采样)或看固定规则。它的好处很直接:
- 零缓冲、零额外内存,决定立刻生效。
- 不引入决策延迟,对延迟敏感的服务友好。
- 实现简单,SDK 里一个
sampler配置就完事。
坏处也明显:它"没看到结局"。一条最终 500 错误的链路,在采样点可能恰好被概率丢掉;一条跨十个服务的慢链路,头采样无法预知它后面会变慢。所以头采样适合流量低、或者只关心"有没有"而非"具体哪条错了"的场景。
3. 尾采样:看完全程再拍板
尾采样(tail-based sampling)反过来:先把一条链路相关的 span 收齐,等请求跑完(甚至等多久看配置),再根据"最终表现"决定保留与否。它可以基于这些信号决策:
- 这条链路最终报错了吗(HTTP 5xx、gRPC error、异常)?
- 耗时是否超过某阈值(P99 之外)?
- 这是不是罕见路径(低频接口、特定用户)?
- 是否触发了自定义业务规则(如金额大于某值)?
于是你能做到"错误和慢链路全留、正常链路按比例抽",在极低总采样率下保住最大价值。代价是要有一层组件把 span 缓冲起来、占用内存、引入一点点决策延迟。OpenTelemetry Collector 的 tail_sampling 处理器就是干这个的。
4. 常见采样策略拆解
单一策略不够用,实战里是组合拳:
- 概率采样:每条链路按固定比例(如 1%)保留。简单、流量平稳时够用,但会随机丢掉偶发错误。
- 限流采样(rate-limited):每秒最多保留 N 条,超出丢弃。控制总量上限很有效,适合防突发。
- 自适应采样:根据当前流量和错误率动态调采样率,错误率高时自动调高。需要额外实现或平台支持。
- 父级采样:下游跟随上游的采样决定,保证链路完整性(见 FAQ)。
- 基于属性的采样:按服务名、接口、用户标签等做不同采样率,核心业务高、旁路低。
生产环境通常是"尾采样 + 错误/慢强制保留 + 普通链路概率/限流"的组合。
5. OpenTelemetry Collector 尾采样配置
下面是 tail_sampling 处理器的骨架,逻辑是:先看是否错误、是否够慢,命中就 100% 保留;否则走概率采样把总量压住:
processors:
tail_sampling:
decision_wait: 10s # 等整条链路到齐的窗口
num_traces: 50000 # 内存里最多攒多少条链路
policies:
- name: errors
type: status_code
status_code:
status_codes: [ERROR]
- name: slow
type: latency
latency:
threshold_ms: 500
- name: probabilistic
type: probabilistic
probabilistic:
sampling_percentage: 5
注意顺序:前面的 policy 命中即保留,未命中的才落到概率策略。再配一个 batch 处理器和导出器即可。这套配置下,错误和慢链路几乎不丢,整体采样率只有约 5%,成本直接砍掉九成多。
6. 内存与等待窗口的权衡
尾采样的命门是内存。它在决策前要把 span 缓存在 Collector 里,所以两个参数要盯紧:
decision_wait:等太久,内存堆得多、延迟大;等太短,长链路没到齐就被迫决策,慢请求被误丢。从 2 秒起,跨多服务调到 5-10 秒。num_traces:并发缓存的链路数上限,防内存爆。超了会触发淘汰,可能丢还在等待的链路。
经验法则:链路平均跨度越长、服务越多,decision_wait 越大;Collector 内存越小,num_traces 越要收。上线后监控 Collector 的内存和"被淘汰未决策链路数",这是尾采样最该看的指标。
7. 父级采样保证链路完整
微服务里如果每个服务独立抛硬币采样,会出现"父 span 没采、子 span 采了"的残缺链路,追踪图谱全是断点。父级采样让下游跟随上游决定:上游说采,下游就采;上游说不采,下游也不采。在 OTel 里默认开启,和头采样组合使用时尤其重要。别为了"更细粒度"把它关掉,否则你会得到一堆拼不起来的碎片。
8. 怎么验证采样没丢关键链路
采样率数字好看不等于没丢东西。建议三条验证线:
- 监控尾采样各 policy 的命中率,特别是
errors/slow的命中,确认异常链路确实进了保留池。 - 对核心业务(支付、登录)走独立 100% 采样通道,不依赖通用采样,确保这部分永远可查。
- 周期性用已知故障做探针:故意触发一次错误,确认能在追踪后端查到完整链路。
把"关键链路可查到率"当成 SLO 来盯,比盯"总采样率"更有意义。
9. 上线后的巡检与防退化纪律
采样配置也会悄悄退化:一次 Collector 配置回滚可能让尾采样变头采样、一次内存超限可能让 num_traces 被调小丢链路。建立纪律:
- 每周:看尾采样各 policy 命中率趋势,错误链路保留率应稳定接近 100%;若骤降,查是不是规则被改。
- 每周:核对 Collector 内存与"淘汰未决策链路数",内存持续高位说明
decision_wait/num_traces该调或该扩容。 - 每次变更前:在测试环境用已知错误链路验证"采到率",再上生产;变更后立刻复核。
- 红线:禁止"为省成本把错误链路也纳入概率采样"。错误和慢链路必须强制保留,这是采样配置不可动的底线。
- 每次扩服务:新接入的服务默认继承统一采样策略,别让某个服务"裸奔"全量上报拉高成本。
FAQ
wait_for)。它决定 Collector 收到第一个 span 后最多等多久才对整条链路做决策。窗口太短,跨服务的长链路还没到齐就被迫决策,慢请求会被误判为"普通"而丢掉;窗口太长,内存里堆的 span 越多、延迟越大。经验值从 2 秒起步,跨多服务或长事务的系统调到 5-10 秒,同时配合 num_traces 上限防止内存爆。这是个"内存 vs 保真度"的权衡,按你的链路长度实测调。