OpenTelemetry OpenTelemetry

用采样控制成本:SDK 与 Collector 配置

决定何时在 SDK 中采样,以及何时在两层 Collector 中使用尾部采样。

15 min 高级

保留每一条追踪很快就会变得昂贵,而大多数追踪都是例行请求,没有人会打开它们。采样保留一个有代表性的或有趣的子集。OpenTelemetry 提供两个做决定的地方:追踪开始时在 SDK 中(头部采样),以及追踪完成之后在 Collector 中(尾部采样)。两者都是纯配置。本指南展示每一种,以及如何选择。

SDK 中的头部采样

决定在根 span 创建时做出,所以它很便宜,不需要额外的基础设施。它无法知道请求是否会失败或变慢,因为那还没有发生。

export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

规范定义的采样器名称包括 always_on、always_off、traceidratio、parentbased_always_on(默认)、parentbased_always_off 和 parentbased_traceidratio。请使用 parentbased_ 变体:它们遵循传入请求的父级的采样决定,所以一条追踪要么整体被保留,要么整体被丢弃,跨越各个服务。在每个服务中使用普通的 traceidratio 会做出相互独立的决定,并产生破碎的追踪。

在声明式配置文件中,同样的想法看起来像这样:

tracer_provider:
  sampler:
    parent_based:
      root:
        trace_id_ratio_based:
          ratio: 0.1

请查看你的 SDK 文档,了解它在文件格式中支持的采样器名称。

Collector 中的尾部采样

尾部采样会等到追踪完成,然后应用策略。它让你保留每一个错误和每一个慢追踪,外加少量其余的,这是头部采样做不到的。代价是更多的基础设施和内存。

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    expected_new_traces_per_sec: 100
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: keep-slow
        type: latency
        latency:
          threshold_ms: 2000
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

工作原理:

  • decision_wait 是该 processor 在看到某条追踪的第一个 span 之后,等待多久再做决定。它必须比你最慢追踪的持续时间更长,否则你会基于不完整的追踪做出决定。
  • num_traces 是等待期间在内存中保存多少条追踪。请把它定为大约你的追踪到达速率乘以 decision_wait,并留有余量。
  • policies 针对每条追踪进行评估。只要任何一条策略说保留,该追踪就会被保留,所以上面的三条策略意味着”保留错误,保留慢的,并且还保留所有追踪中的 5%”。
  • 其他策略类型包括 string_attribute(匹配某个值,比如客户等级)、numeric_attribute、always_sample、and(组合策略)和 composite(在各策略之间分配速率)。

tail_sampling 在 contrib 发行版中,它的选项名称在不断演进,所以请查看你所用版本的 README。

陷阱:一条追踪必须到达同一个 Collector

尾部采样只有在一条追踪的所有 span 都到达同一个 Collector 实例时才有效。在普通负载均衡器后面有多个副本时,同一条追踪的 span 会分散,决定只能基于片段做出。标准的解决方案是两层:

  1. 第 1 层(负载均衡层) 使用 loadbalancing exporter,按 trace ID 把 span 路由到一个一致的第 2 层实例。
  2. 第 2 层(采样层) 运行 tail_sampling 并导出被保留的追踪。

第 1 层:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

exporters:
  loadbalancing:
    routing_key: traceID
    protocol:
      otlp:
        tls:
          insecure: true
    resolver:
      dns:
        hostname: otel-sampler-headless.observability.svc.cluster.local
        port: 4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [loadbalancing]

第 2 层:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
  batch:

exporters:
  otlp/backend:
    endpoint: tempo:4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/backend]

使用 headless Service,这样 DNS 会列出第 2 层的 Pod,并注意当 Pod 扩容或缩容时,路由会发生变化,少数在途的追踪可能会基于不完整的数据做出决定。让各层保持分离也隔离了故障:采样中的问题不会阻塞摄取。

组合头部与尾部

一种常见且合理的配置:

  • SDK 中一个适度的头部采样器(例如 50%),为流量非常大的服务在源头削减体量。
  • Collector 中的尾部采样,以保证错误和慢请求得以保留。

请小心:在头部被丢弃的任何东西,都无法在尾部恢复。如果你必须保留所有错误,就不要对这些服务进行激进的头部采样。

如果你先派生指标,指标可以在采样中幸存

丢弃追踪也会丢掉你本可以从中计算出的数字,比如请求速率和延迟百分位。请在采样步骤之前,用 spanmetrics connector 生成指标,这样仪表板就能保持准确:

connectors:
  spanmetrics:

service:
  pipelines:
    traces/all:
      receivers: [otlp]
      exporters: [spanmetrics, loadbalancing]
    metrics/derived:
      receivers: [spanmetrics]
      processors: [batch]
      exporters: [otlphttp/prometheus]

这里每个 span 都馈入 connector,而只有被采样的集合继续进入存储。

选择

目标方法
简单地削减成本,无需新的基础设施使用 parentbased_traceidratio 的头部采样
保留所有错误和慢请求两层 Collector 中的尾部采样
保持准确的速率和百分位在采样之前使用 spanmetrics
服务较少,流量较低不采样;等成本变得重要时再重新考虑

验证

  • 在第 2 层中,使用 Collector 自己的指标(8888 端口)比较进出的 span 数量,确认比例与你的策略匹配。
  • 搜索一个已知的失败请求,并确保它的整条追踪都存在。
  • 观察第 2 层的内存,因为它会在 decision_wait 期间保存追踪。如果它增长,请降低 num_traces 或增加副本,并让 memory_limiter 保持在前面。

热门指南