tail-based sampling 实战:关键请求保留,普通请求自动降噪
tail-based sampling 实战:关键请求保留,普通请求自动降噪
说实话,我前段时间一度想把 trace 先关掉。
不是链路追踪没用,恰恰是它太有用了:流量一上来,什么请求都采、什么 span 都留,存储打满得比告警还快。等真出问题的时候,工程师盯着一堆健康请求发呆,真正该看的那几条异常链路反而被埋了。
后来我把策略换成了 tail-based sampling。简单讲,不再在请求刚进来时立刻决定“要不要采”,而是等一条链路快结束、关键信息都出来之后,再决定留还是丢。结果很直接:错误请求基本都保住了,普通 200 请求大幅降噪,存储和查询压力也肉眼可见地下来了。
这篇我不聊概念课,直接讲一套能落地的做法:怎么定规则、怎么在 OpenTelemetry Collector 里配、怎么验证策略没把关键请求漏掉,以及我踩过的几个坑。
为什么 head sampling 在生产里经常不够用
很多团队第一次上链路追踪,都会先配一个固定比例的 head sampling,比如 10%、20%。这一步不是错,胜在简单,而且部署成本低。
问题是,head sampling 决策太早了。请求刚进来时,你还不知道它后面会不会超时、会不会打爆下游、会不会在第 6 个 span 才冒出真正的异常。采样器在入口处一刀切,只能靠概率赌。
如果你的系统平时错误率只有千分之几,那固定比例采样很容易出现一种很烦的情况:正常请求留了一大堆,真正的异常链路刚好没抽中。
我后来复盘过一次支付回调延迟问题,根因其实在第三方接口抖动。入口网关打进来的请求都长得差不多,只有当下游 span 出现 http.status_code=504、或者整体耗时超过 2 秒时,这条 trace 才有分析价值。你让 head sampling 在第一个 span 就拍板,它根本看不到这些信息。
tail-based sampling 适合解决什么问题
tail-based sampling 最值钱的地方就一个:它允许你根据“整条链路的结果”做决策。
这意味着你可以很自然地写出下面这些规则:
- 只要 trace 里出现 error,就 100% 保留
- 只要总耗时超过 1500ms,就保留
- 某些支付、下单、登录请求,即使成功也提高采样率
- 健康检查、静态资源、低价值内部探活请求,尽量丢掉
这种策略特别适合三个场景。
第一种是请求量很大,但真正值得看的异常请求比例很低。比如网关、订单、推荐、埋点这些服务。
第二种是链路跨服务太多,问题往往出在中后段。像 RPC 调用串很长、外部依赖多、异步任务穿插的系统,晚一点做决策会更稳。
第三种是你的 trace 存储已经有成本压力。别硬扛,全量追踪在大多数团队里都不是长期方案。
我的策略不是“全错全留”,而是分三层
一开始我也走过弯路,最早的配置只有一句:status_code = ERROR 就保留。结果线上看起来挺省,实际排查还是不够用。
因为很多真正麻烦的问题,未必会把 span 状态打成 error。最典型的是慢查询、超时重试、队列积压、下游雪崩前的抖动期。这些请求最后可能还是 200,但体验已经坏了。
后来我把规则拆成三层,效果稳定很多。
第一层:硬保留异常链路
这层不用省。
只要 trace 里出现 error、5xx、panic、exception 之类的信号,我建议直接 100% 保留。异常链路本来就少,省这点量没意义,反而会把排障能力打折。
常见保留条件可以包括:
status_code = ERROR- span attribute 里有
exception.type - HTTP 状态码
>= 500 - gRPC 状态非
OK
第二层:保留高延迟链路
这是我后来最看重的一层。
很多性能问题都不是“挂了”,而是“越来越慢”。如果你只保留异常请求,就会错过一大批用户已经感知到卡顿,但服务端监控还没红的链路。
我现在通常会给不同业务线配不同阈值,比如:
- 核心交易链路:
latency >= 800ms - 普通接口:
latency >= 1500ms - 异步消费任务:
latency >= 3000ms
阈值别拍脑袋,最好参考自己过去 7 天或 14 天的 P95/P99。要的是“保住异常慢”,不是“把一半边缘慢请求全捞上来”。
第三层:给高价值请求留预算
有些请求就算成功、耗时也正常,我还是希望能看到一部分样本。
比如支付确认、库存扣减、登录鉴权、发布流程这类关键路径。如果这些链路完全不留样本,平时做容量评估、依赖排查、发布回归时会很被动。
我的做法通常是:
- 对关键 route 或 service 保留
20% ~ 50% - 对普通 200 请求只保留
1% ~ 5% - 对健康检查、metrics、内部探活直接 drop
这样做的核心不是“更复杂”,而是把有限的 trace 预算花到最值得看的地方。
OpenTelemetry Collector 配置怎么落地
下面这份配置是我现在比较常用的一版,逻辑不花哨,但足够稳。
receivers:
otlp:
protocols:
grpc:
http:
processors:
memory_limiter:
check_interval: 1s
limit_mib: 1024
spike_limit_mib: 256
batch:
timeout: 2s
send_batch_size: 1024
tail_sampling:
decision_wait: 15s
num_traces: 50000
expected_new_traces_per_sec: 3000
policies:
- name: keep-errors
type: status_code
status_code:
status_codes: [ERROR]
- name: keep-http-5xx
type: numeric_attribute
numeric_attribute:
key: http.response.status_code
min_value: 500
max_value: 599
- name: keep-slow-traces
type: latency
latency:
threshold_ms: 1500
- name: keep-payment-routes
type: string_attribute
string_attribute:
key: http.route
values: [/api/pay/confirm, /api/order/submit, /api/refund/apply]
enabled_regex_matching: false
- name: sample-normal-traffic
type: probabilistic
probabilistic:
sampling_percentage: 3
exporters:
otlp:
endpoint: tempo-gateway:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp]
这份配置里,我最建议你盯紧 3 个参数:decision_wait、num_traces、expected_new_traces_per_sec。
这 3 个参数配错了,collector 会先出事
很多人第一次配 tail-based sampling,规则本身没问题,Collector 却先扛不住了。原因通常就在这几个参数。
decision_wait 不是越长越好
它决定 Collector 等多久再对一条 trace 做采样决策。
等得太短,后面的关键 span 还没到,Collector 就提前拍板了,结果慢请求、异常请求被错判成普通流量。
等得太长,内存里积压的 trace 数就会变多,Collector 自己先被撑胖。
我的经验是:
- 普通同步 HTTP 服务:
10s ~ 15s差不多够用 - 链路里有消息队列、异步回调:可以拉到
20s ~ 30s - 再高就要非常谨慎,先看内存和 trace 完整率
别一上来就配 60 秒,那不是稳,是给自己埋炸弹。
num_traces 决定你的缓冲池够不够大
tail sampling 不是白来的,它要在内存里先攒一会儿 trace 再判断。
如果并发高、num_traces 又太小,就会出现老 trace 被挤掉的情况。表现出来通常是:明明规则写得没错,但你总感觉 trace 丢得很玄学。
我一般会先按下面这个思路估个起点:
num_traces ≈ 峰值每秒新 trace 数 × decision_wait 秒数 × 1.2
比如高峰期每秒 3000 条新 trace,decision_wait=15s,那起步就得准备 54000 左右的容量。你配个 5000,基本就是让 Collector 凭运气工作。
expected_new_traces_per_sec 别乱填
这个参数会影响内部资源预估。填得太低,Collector 在高峰期容易抖;填得太高,又会让资源占用偏保守。
最简单的办法就是先从网关入口、APM、Tempo/Jaeger 接入侧估一个峰值,再留 20% 左右余量。别追求一次配准,先跑起来,再看指标修。
我怎么验证“关键请求真的保住了”
很多团队做完采样配置就结束了,这一步其实最危险。
因为策略写出来不等于策略有效。你要验证的不是 Collector 没报错,而是那些你真正关心的 trace,是否真的被留下来了。
我通常会做 3 轮验证。
第一轮:主动造错误请求
最直接。
在测试环境或预发环境里,手动打几类请求:
- 正常 200 请求
- 人工制造 500 的请求
- 人工 sleep 让接口超过慢阈值
- 命中关键 route 的成功请求
然后去后端 trace 存储里查这几类请求的保留情况。目标不是“全都看到”,而是:
- 500 请求应该接近 100% 留存
- 超慢请求应该稳定被保留
- 关键 route 的成功请求要能看到样本
- 普通 200 请求数量明显下降
第二轮:看 Collector 自己的指标
只盯业务 trace 不够,Collector 本身也要看。
我至少会把下面这些指标挂到面板上:
otelcol_processor_tail_sampling_sampling_decision_total
otelcol_processor_tail_sampling_global_count_traces_sampled
otelcol_processor_tail_sampling_global_count_traces_dropped
otelcol_processor_tail_sampling_count_traces_in_memory
process_resident_memory_bytes
如果你发现 traces_in_memory 长时间贴着上限跑,或者 Collector 内存跟着流量尖峰剧烈抬升,那不是业务有问题,是采样器缓冲池顶不住了。
第三轮:对比采样前后的查询体验
这个很现实。
如果你把采样配上之后,存储压力确实降了,但工程师查一条关键链路反而更难,那这套策略就是失败的。
我一般会拉两组数据对比:
- trace 写入量下降了多少
- 错误链路的命中率有没有下降
- P99 慢请求的样本覆盖率怎么样
- 查询一个典型问题的平均耗时有没有改善
以前我做过一次调优,普通成功请求采样从 20% 降到 3% 后,trace 写入量降了接近 70%,但 5xx 和高延迟链路的覆盖率几乎没掉。这个结果就很值。
三个特别容易踩的坑
这部分我建议你认真看,比背配置更重要。
坑一:只按错误保留,结果慢请求全漏了
这个坑我前面提过,但值得再说一次。
很多线上事故真正的前兆不是 error,而是延迟爬升、重试变多、下游抖动。你如果只保留错误 trace,会错过最有诊断价值的“临界状态”。
别省这点阈值规则。
坑二:关键属性打得太晚,导致规则匹配不上
比如你想按 http.route、user.tier、rpc.service 去做采样,但这些 attribute 是在中间件后面、或者业务处理后半段才补上的。
如果 decision_wait 太短,或者某些 span 到得慢,采样器看到的 trace 信息其实是不完整的。最后表现出来就是:规则看起来没错,命中率却不稳定。
我的建议是:用于采样决策的属性,尽量在入口 span 或关键早期 span 就打上。
坑三:Collector 和业务服务混部,互相拖累
这个也挺常见。
tail-based sampling 本身就比 head sampling 更吃内存。如果 Collector 跟高负载业务实例混在一台机器上,流量一波动,两边会互相抢资源。你最后看到的现象很可能是:业务 RT 变差了,Collector 也开始掉链路。
能拆就拆,至少把 Collector 作为独立工作负载部署,给清晰的 CPU / Memory limit。
如果你现在就要上线,我建议按这个顺序来
别想着一步到位,把规则堆满。
我更推荐这个渐进式方案:
第一步:先保错误 + 高延迟
先把最有价值的两类链路保住。
这时候你已经能把“错误留存率”和“存储压降”都跑出来了,而且出问题也容易定位。
第二步:再给关键业务路由开白名单
把支付、登录、下单、发布这类关键路径补进来,给一点固定预算。
这一步做完,你的线上排障和日常巡检体验会明显好很多。
第三步:最后再处理低价值噪声
像健康检查、探活、静态资源、内部低价值调用,放在最后做 drop 或超低比例采样。
因为这一步最容易误杀。先把“该保的都保住”,再谈“该丢的尽量丢”。
写在最后
我现在越来越不信“全量采集才安全”这套说法了。
真实生产环境里,数据不是越多越好,关键是有用的数据能不能在你需要的时候立刻找到。tail-based sampling 的价值,就在于它把链路预算从“平均分给所有请求”,改成了“优先留给真正值得分析的请求”。
如果你现在也在被 trace 成本、查询噪声、排障效率这三件事一起折腾,不妨先别急着扩容存储,先把采样策略做对。很多时候,问题不在你采得不够多,而在你留错了东西。
如果你愿意,我建议先从“错误 100% 保留 + 慢请求保留 + 普通请求 3%”这套最小方案开始,跑一周,再看数据说话。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)