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_waitnum_tracesexpected_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.routeuser.tierrpc.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%”这套最小方案开始,跑一周,再看数据说话。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐