分布式追踪采样策略:Head-based 与 Tail-based 精度成本平衡
分布式追踪采样策略:Head-based 与 Tail-based 精度成本平衡
一、为什么 1% 采样率下找不到慢请求的根因
分布式追踪的经典困境:高流量系统全量采样成本太高(存储 + 传输),但低采样率下 P99 延迟的异常请求大概率被丢弃——你根本采不到想要的 trace。
这就是 Head-based Sampling(头部采样)的致命缺陷:在请求入口就决定是否采样,那时你还不知道这个请求是否会慢、是否会出错。如果一个请求最终耗时 10 秒,但在第 1 个服务就被"随机丢弃"了——这条关键 trace 永远丢失。
Tail-based Sampling(尾部采样)解决了这个问题:在所有 span 收集完毕后,根据整个 trace 的特征决定是否保留。但代价是更高的基础设施复杂度——你需要一个能缓存所有 span 直到 trace 完成的系统。
flowchart TD subgraph Head[Head-based 采样] A1[请求进入] --> A2{随机采样 1%?} A2 -->|是| A3[记录所有 span] A2 -->|否| A4[丢弃] A3 --> A5[存储] end subgraph Tail[Tail-based 采样] B1[请求进入] --> B2[100% 收集 span] B2 --> B3[span 缓存] B3 --> B4[trace 组装完成] B4 --> B5{采样策略} B5 -->|包含错误| B6[保留] B5 -->|延迟 > P95| B6 B5 -->|正常| B7[丢弃] B6 --> B8[存储] end style Head fill:#ffcccc style Tail fill:#ccffcc二、两种采样策略的深度对比
Head-based Sampling
- 原理:在 trace 的第一个 span(root span)生成时,用概率决定采样
- 优点:实现简单(一个随机数判断),资源消耗恒定
- 缺点:先验采样——不知道这个 trace 是否有价值
- 适用:QPS 极高、有大量健康请求,只关心"典型的"请求
Tail-based Sampling
- 原理:100% 采集所有 span,等 trace 完整后根据全局特征(延迟、错误)决定保留
- 优点:保留所有"有趣的"trace(慢、错、异常)
- 缺点:需要 span 缓存层,资源消耗高
- 适用:必须捕捉所有异常行为、P99 延迟排查
混合策略
- Head 层:随机 1-5% 的全量 trace,保证统计基线
- Tail 层:100% 采集,但只保留 5% 的"异常"trace
三、基于 OpenTelemetry Collector 的实现
# otelcol-config.yaml —— Tail-based 采样器配置 receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: # 第一步:内存限制 # 限制 span 在内存中的总数量——这是 tail-based 采样的关键约束 # 如果内存中 span 数量超过此值,新的 span 被丢弃 memory_limiter: check_interval: 1s limit_mib: 2048 spike_limit_mib: 512 # 第二步:Tail-based 采样策略 tail_sampling: # decision_wait: 等待多久才做采样决策 # 太短:trace 可能未完成,决策不准 # 太长:span 缓存在内存中,内存压力大 # 15 秒是一个平衡点——覆盖了大部分 trace 的完成时间 decision_wait: 15s # num_traces: 内存中缓存的 trace 数量上限 # 超过此数量时,最旧的 trace 被强制采样决策 num_traces: 50000 # expected_new_traces_per_sec: 用于预分配内存 expected_new_traces_per_sec: 1000 policies: # 策略 1: 错误采样——所有包含错误的 trace 100% 保留 - name: error-policy type: status_code status_code: status_codes: [ERROR] # 策略 2: 延迟采样——延迟超过 1s 的 trace 100% 保留 - name: latency-policy type: latency latency: threshold_ms: 1000 # P99 线 # 策略 3: 特定服务采样——关键服务的 trace 采样率提高 - name: critical-service-policy type: string_attribute string_attribute: key: service.name values: - payment-service - auth-service enabled_regex_matching: false # 策略 4: 概率采样——健康请求也保留 5%,建立基线 - name: baseline-policy type: probabilistic probabilistic: sampling_percentage: 5 # 策略 5: 速率限制——即使匹配上述策略,也限制最大采样速率 - name: rate-limiting type: rate_limiting rate_limiting: spans_per_second: 5000 # 第三步:批处理——减少下游写入次数 batch: timeout: 5s send_batch_size: 512 send_batch_max_size: 1024 exporters: # 导出到 Jaeger/Tempo otlp/jaeger: endpoint: jaeger-collector:4317 tls: insecure: true service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, tail_sampling, batch] exporters: [otlp/jaeger]四、Tail-based Sampling 的成本与边界
内存成本
Tail-based 采样需要在内存中缓存所有 span 直到 trace 完成。所需内存 =expected_new_traces_per_sec × decision_wait × avg_spans_per_trace × avg_span_size。
例:1000 QPS × 15s × 20 spans × 1KB = 300MB。在 2048MB 限制下绰绰有余。
但如果 QPS 突然飙升到 5000,内存需求变为 1.5GB,触发 memory limiter 丢弃新 span——被丢弃的 span 中有可能有重要错误 trace。
延迟敏感性
decision_wait: 15s意味着 trace 数据最多有 15 秒的延迟才被导出。对于实时告警,这不可接受。解决方案:并行跑 head-based 采样的快速通道和 tail-based 采样的完整通道。
不适用场景
- 超大规模系统(> 10 万 QPS):tail-based 的内存占用不可行
- 需要 < 1 秒延迟的实时告警
- 预算极有限的小团队(head-based 1% 采样 + 日志足够覆盖大部分需求)
五、总结
Tail-based Sampling 的核心洞察是:采样决策应该在 trace 完成之后做,而不是开始之前。这个简单的时间顺序倒置带来了巨大的价值——你不再随机丢弃 trace,而是精准保留"有趣的"trace。代价是更高的内存消耗和基础设施复杂度。对于 QPS < 5000 的系统,tail-based 的收益压倒成本;对于超大规模系统,hybrid 策略(head 做概率基线 + tail 做异常捕获)是最佳折中。
