tail-based sampling 实战:关键请求保留,普通请求自动降噪
我以前也以为链路追踪这件事,采得越多越安心。
直到有一次值班,Jaeger 查一条慢请求要转十几秒,ES 磁盘还在疯狂涨。那天我才彻底接受一件事:全量追踪不是安全感,很多时候只是把噪声原封不动地存了下来。
这篇想聊一个更实用的思路:别再只靠固定比例采样了,直接把 tail-based sampling 用起来。核心目标不是“少存点数据”这么简单,而是把真正该留下来的关键请求保住,把普通请求自动降噪。
为什么固定比例采样经常不够用
很多团队上采样的第一步,都是把 head sampling 设成 10% 或 20%。这样做当然能立刻降成本,但问题也很明显:请求一进系统就被决定命运了。
如果一条请求刚好没被采到,后面哪怕它超时、重试、报错,整条 trace 还是看不到。真正出事故时,最糟糕的不是数据太多,而是关键请求偏偏没留住。
我后来把问题拆得更直白一点:普通成功请求可以少看,但异常请求、慢请求、关键业务请求,必须尽量保住。只要目标变成这个,tail sampling 就比单纯的概率采样更顺手。
tail-based sampling 到底解决了什么
tail sampling 的思路很像“看完整张卷子再打分”。Collector 不会在请求刚进来时立刻决定保不保留,而是先等一小段时间,把整条 trace 尽量收齐,再根据结果做判断。
这样你就能按结果留数据,而不是按运气留数据。
比如:
- 状态码是 5xx 的请求,全留
- 耗时超过 1.5 秒的请求,全留
- 支付、下单、登录这类关键链路,高比例保留
- 健康检查、静态资源、低价值轮询,直接过滤或低比例采样
这个差别看起来只是“决策时机”变了,实际效果却很大。以前是先丢再后悔,现在是先观察再筛选。
我更推荐的落地方式:head sampling + tail sampling 配合用
只开 tail sampling 也不是万能的。因为它要先缓存一段时间的 trace,如果入口流量本来就特别大,Collector 的内存和队列压力会先上来。
所以我更推荐两段式做法。
第一层,在入口先做基础过滤。像/healthz、/metrics、内部心跳、静态资源这类低价值流量,能不进追踪系统就别进。
第二层,对普通在线请求做一层温和的 head sampling,先把洪峰削下来。
第三层,再用 tail sampling 专门兜住异常、慢请求和高价值业务请求。
这个思路的好处很现实:不是把所有流量都塞给 tail sampling 硬扛,而是先把确定没价值的噪声挡掉,再把真正重要的样本挑出来。
一套比较稳的 Collector 配置思路
下面这份配置不一定适合所有团队,但很适合作为第一版起点:
processors:filter/drop-noisy-spans:error_mode:ignoretraces:span:-'attributes["http.target"] == "/healthz"'-'attributes["http.target"] == "/metrics"'probabilistic_sampler/default:hash_seed:22sampling_percentage:20tail_sampling:decision_wait:10snum_traces:50000expected_new_traces_per_sec:3000policies:-name:keep-errorstype:status_codestatus_code:status_codes:[ERROR]-name:keep-slow-requeststype:latencylatency:threshold_ms:1500-name:keep-core-servicestype:string_attributestring_attribute:key:service.namevalues:[payment-service,order-service,auth-service]-name:sample-the-resttype:probabilisticprobabilistic:sampling_percentage:5这里最关键的不是参数本身,而是顺序。先过滤噪声,再做基础削峰,最后保关键样本。只要顺序反过来,系统压力和样本质量通常都会变差。
这几个参数最容易配错
1.decision_wait太短
如果链路里有异步任务、消息队列或者跨多个服务的调用,3 秒、5 秒这种配置经常不够。Collector 还没等到完整 trace,就已经提前做决定了,结果是慢请求明明很关键,却被当成普通请求放掉。
我的经验是,先按真实链路时长给 8 到 15 秒,再看内存和命中率慢慢调。别一上来追求极限。
2.num_traces太小
这个参数太保守,高峰期就会把旧 trace 挤掉。表面上策略开着,实际关键链路留不住。
简单估算方法可以先按“峰值每秒新 trace 数 ×decision_wait秒数”来抓一个大致量级,再结合 Collector 内存曲线验证。
3. 只盯报错,不盯慢请求
很多线上问题不会直接 500,而是先慢、再重试、最后级联放大。如果策略里只有 error,没有 latency,排障窗口会直接少一半。
我怎么看一套采样策略有没有真的跑稳
我现在不会只看“存储降了多少”,而是同时看三组结果。
第一组是成本:每天 trace 存储量、索引增长速度、Collector 导出吞吐。
第二组是体验:Jaeger 查询耗时、trace 搜索成功率、Collector 队列堆积。
第三组最重要,是故障复盘时能不能稳定找到关键 trace。
如果成本降下来了,但真正出事时 trace 老是缺关键 span,那这套策略只是省钱,不算可用。反过来,如果异常请求和慢请求都能稳定保住,普通噪声又显著下降,这才说明策略开始有工程价值。
写在最后
我后来越来越少说“全量追踪更保险”这种话了。
真正靠谱的可观测性,不是把所有请求都留着,而是在你最需要证据的时候,最快看到最该看的那一批请求。tail-based sampling 的价值,就在这里。
如果你现在也被 trace 成本、查询变慢、样本噪声太多这些问题困住,真可以先别纠结采样率数字,先把“哪些请求必须留下来”这件事定义清楚。后面的策略,反而会简单很多。