我们如何修复 OpenTelemetry 中基于 head 的采样

0 阅读11分钟

作者:来自 Elastic Alexander Wert 及 Anuraag Agrawal

如果没有采样元数据,基于 head 的采样可能会破坏吞吐量图表。了解 OpenTelemetry 如何在 Java、JS 和 Python 中通过 tracestate 概率字段修复此问题。

OpenTelemetry 中的基于 head 的采样既便宜又实用,但过去它会带来一个重大分析问题:采样后的 trace 会减少原始 span 数量,从而导致后端吞吐量图表不准确。解决方法是在 tracestate 中携带采样概率,这样后端就可以估算每个采样 trace 代表多少原始 trace。本文解释了问题、规范,以及我们如何在 OpenTelemetry Java、JavaScript 和 Python 中实现该修复。

采样为何会造成吞吐量问题

大多数生产系统对 trace 进行采样,因为发送每个 span 都很昂贵。两种常见方法是:

  • 基于 head 的采样:在 trace 开始时决定保留或丢弃 trace。
  • 基于 tail 的采样:稍后决定,在看到更多或全部 span 后决定。

基于 head 的采样速度快且成本低,因为它早期就做出决定。但如果后端只看到 10% 的 trace,那么基于接收 trace 构建的简单吞吐量图表可能会低估真实流量 10 倍。

换句话说,如果没有额外元数据,采样的遥测数据会丢失重建基于流量的指标所需的上下文。

解决方案的 OpenTelemetry 规范

OpenTelemetry 规范定义了一种在 tracestate 中编码概率采样信息的方法:

总体上,采样器会在 tracestate 中写入足够的信息,使下游系统能够理解 trace 的有效采样概率。当这些元数据存在且正确传播时,基于吞吐量和速率的分析仍能保持准确,同时仍享受基于 head 采样的成本优势。Elastic Observability 开箱即支持该规范和行为。如果使用 Elastic 的分发版 (EDOT) SDK,或者按照下文配置上游 OpenTelemetry SDK,Elastic 就可以从采样数据估算原始吞吐量指标。

例如,一个采样的 span 可能携带如下条目:

`tracestate: ot=th:fd70a4;rv:fe123456789abc` AI写代码

按照规范规则:

  • th 是拒绝阈值 (T),去掉尾随零。
  • rv 是 56 位随机值 (R)。
  • 当 R >= T 时,参与者保留该 span。

这里,th:fd70a4 展开为 T = 0xfd70a400000000,rv 给出 R = 0xfe123456789abc,所以因为 R >= T,该 span 被保留。

用十进制表示:

  • T = 0xfd70a400000000 = 71,337,018,784,743,424
  • R = 0xfe123456789abc = 71,514,660,082,850,492
  • 2^56 = 72,057,594,037,927,936

由于 71,514,660,082,850,492 >= 71,337,018,784,743,424,该 trace 被采样。

后端可以将 T 转换为代表性计数(adjusted_count):

`

1.  probability = (2^56 - T) / 2^56
2.  adjusted_count = 1 / probability = 2^56 / (2^56 - T)

`AI写代码

代入数值:

`

1.  probability = (72,057,594,037,927,936 - 71,337,018,784,743,424) / 72,057,594,037,927,936
2.              = 720,575,253,184,512 / 72,057,594,037,927,936
3.              ~= 0.01

5.  adjusted_count = 1 / 0.01 = 100

`AI写代码

因此,实际上这是约 1% 的采样率,每个采样的 span 代表大约 100 个原始 span。

这意味着后端可以进行加权计算,例如:

`extrapolated_throughput = sampled_throughput * adjusted_count` AI写代码

之前缺失的内容

几个月前,OpenTelemetry SDK 用户有规范,但主要 SDK 中没有开箱实现。因此,后端接收到的 span 没有携带采样元数据,无法估算原始 trace 流量。实际上,这意味着采用基于 head 的采样的团队选择有限:

  • 接受偏差的吞吐量数字,
  • 构建自定义采样器逻辑,或
  • 切换到更复杂的采样设置。

对于许多团队来说,这使得标准的基于 head 的采样远不如应有的有用。

修复方案:在 Java、JavaScript 和 Python 中的实现

我们在三种 SDK 中实现了与规范一致的行为,让团队可以使用标准化的采样元数据,而不是依赖自定义解决方案。

  • Java:open-telemetry/opentelemetry-java#7626
  • JavaScript:open-telemetry/opentelemetry-js#5839
  • Python:open-telemetry/opentelemetry-python#4714

这三个 PR 都实现了组合/概率采样(composite/probability sampling)行为,使根采样决策在 tracestate 中表示,并可跨服务边界保留。

概念性变化

重要的转变不是“ 多采样” 或“ 少采样”,而是:

  1. 保留概率性的 head-based 采样
  2. 在 trace 中传播概率元数据
  3. 让后端根据采样数据计算加权指标

这既保持了可控的采集成本,又恢复了正确的聚合分析。

实现流程

不同语言的 API 细节不同,但部署模式类似:

  1. 使用支持概率/组合规范行为的 SDK 采样器。

  2. 保持 W3C Trace Context 传播开启,使 tracestate 可跨服务传递。

  3. 在后端验证吞吐量/速率图表使用加权解释(weighted interpretation)处理采样元数据。

Java 实例

如果你使用 Elastic Distribution for OpenTelemetry Java (EDOT Java)

  • 采样器默认已配置为概率/组合规范行为。
  • 默认情况下,EDOT 对所有 traces 使用 100% 采样率。
  • 可以通过 central configuration 或 Java 系统属性 / 环境变量 otel.traces.sampler.arg 来修改采样率。

使用上游 OTel Java SDK,可以这样配置采样器:

`

1.  import io.opentelemetry.api.OpenTelemetry;
2.  import io.opentelemetry.api.trace.Tracer;
3.  import io.opentelemetry.sdk.OpenTelemetrySdk;
4.  import io.opentelemetry.sdk.extension.incubator.trace.samplers.ComposableSampler;
5.  import io.opentelemetry.sdk.extension.incubator.trace.samplers.CompositeSampler;
6.  import io.opentelemetry.sdk.trace.SdkTracerProvider;

8.  // 采样比例,例如 10%
9.  double ratio = 0.1;

11.  SdkTracerProvider tracerProvider =
12.      SdkTracerProvider.builder()
13.          .setSampler(
14.              CompositeSampler.wrap(
15.                  ComposableSampler.parentThreshold(
16.                      ComposableSampler.probability(ratio)
17.                  )
18.              )
19.          )
20.          .build();

22.  OpenTelemetry openTelemetry =
23.      OpenTelemetrySdk.builder()
24.          .setTracerProvider(tracerProvider)
25.          .build();

27.  Tracer tracer = openTelemetry.getTracer("my-instrumentation-library");

29.  // 开始 span
30.  tracer.spanBuilder("example-span").startSpan();

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)
核心说明:
  1. 导入类:引入必要的 OpenTelemetry API 和采样器扩展。

  2. 设置采样比例ratio 控制采样 trace 的比例(如 0.1 表示 10%)。

  3. 采样器配置

    1. CompositeSamplerComposableSampler 用于构建符合 OpenTelemetry composite sampler 规范的采样器,实现更精确的概率 head 采样。
    2. ComposableSampler.probability(ratio) 指定采样比例。
    3. ComposableSampler.parentThreshold(...) 确保遵循父 trace 的采样决策,保持 trace 上下文一致。
    4. CompositeSampler.wrap(...) 包装成符合最新规范的采样器。
  4. tracerProvider 和 OpenTelemetry 设置:将采样器附加到 SdkTracerProvider 并构建成 OpenTelemetrySdk 实例。

  5. 使用采样器:创建 span 时,SDK 会根据配置应用采样策略。

通过这种方式,head-based 采样既能控制成本(只采样部分 traces),又能携带和保留采样元数据,使下游后端(如 Elastic 或任何 OTel 兼容后端)能够正确计算吞吐量/体量指标,提供更准确的运维度量。

Node.js 实现

如果你使用 Elastic Distribution for OpenTelemetry Node.js (EDOT Node)

  • 默认情况下,采样器已配置为使用概率/组合规范行为。
  • EDOT 默认对所有 traces 使用 100% 采样率。
  • 你可以通过 central configuration 或设置环境变量 OTEL_TRACES_SAMPLER_ARG 来修改采样率。

使用上游 OTel JavaScript SDK 配置采样器示例如下:

`

1.  const { NodeSDK } = require('@opentelemetry/sdk-node');
2.  const {
3.    createCompositeSampler,
4.    createComposableParentThresholdSampler,
5.    createComposableTraceIDRatioBasedSampler,
6.  } = require('@opentelemetry/sampler-composite');

8.  // 示例:采样 10% 的新根 trace,同时保留父采样决策
9.  const sampler = createCompositeSampler(
10.    createComposableParentThresholdSampler(
11.      createComposableTraceIDRatioBasedSampler(0.1)
12.    )
13.  );

15.  const sdk = new NodeSDK({ sampler });
16.  sdk.start();

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

核心说明

导入

  • @opentelemetry/sampler-composite 引入三个函数:
    • createCompositeSampler
    • createComposableParentThresholdSampler
    • createComposableTraceIDRatioBasedSampler
  • 它们实现了符合规范的组合采样逻辑。

采样器配置

  • createComposableTraceIDRatioBasedSampler(0.1):对所有根 trace 采样 10%。
  • createComposableParentThresholdSampler(...):确保采样遵循上游父 span 的决策(保持分布式 trace 上下文)。
  • createCompositeSampler(...):包装成 OTel SDK 预期的采样器形式。

使用

  • 将配置好的 sampler 传给 NodeSDK,启动 SDK 后,应用内创建的所有 span 都遵循此采样逻辑。

效果

  • 实现了准确的 head-based 概率采样。
  • 按配置比例采样 trace。
  • 将采样元数据(tracestate)传播到下游服务和后端(如 Elastic、Jaeger)。
  • 支持体量调整、准确指标计算,并在分布式追踪环境中控制成本。

Python 实现

如果你使用 Elastic Distribution for OpenTelemetry Python (EDOT Python)

  • 默认情况下,采样器已配置为使用概率/组合规范行为。
  • EDOT 默认对所有 traces 使用 100% 采样率。
  • 你可以通过 central configuration 或设置环境变量 OTEL_TRACES_SAMPLER_ARG 来修改采样率。

对于上游 OTel Python SDK,可以通过注册自定义 sampler 并将 OTEL_TRACES_SAMPLER 指向它来实现:

配置示例

pyproject.toml

`

1.  [project.entry-points.opentelemetry_traces_sampler]
2.  parentbased_composite = "your_package.sampling:ParentBasedCompositeSampler"

`AI写代码

your_package/sampling.py

`

1.  from __future__ import annotations
2.  from typing import Sequence
3.  from opentelemetry.context import Context
4.  from opentelemetry.sdk.trace._sampling_experimental import (
5.      composable_parent_threshold,
6.      composable_traceid_ratio_based,
7.      composite_sampler,
8.  )
9.  from opentelemetry.sdk.trace.sampling import Sampler, SamplingResult
10.  from opentelemetry.trace import Link, SpanKind, TraceState
11.  from opentelemetry.util.types import Attributes

14.  class ParentBasedCompositeSampler(Sampler):
15.      # SDK 会将 OTEL_TRACES_SAMPLER_ARG 作为构造参数传入
16.      def __init__(self, ratio_str: str | None):
17.          try:
18.              ratio = float(ratio_str) if ratio_str else 1.0
19.          except ValueError:
20.              ratio = 1.0
21.          self._delegate = composite_sampler(
22.              composable_parent_threshold(composable_traceid_ratio_based(ratio))
23.          )

25.      def should_sample(
26.          self,
27.          parent_context: Context | None,
28.          trace_id: int,
29.          name: str,
30.          kind: SpanKind | None = None,
31.          attributes: Attributes | None = None,
32.          links: Sequence[Link] | None = None,
33.          trace_state: TraceState | None = None,
34.      ) -> SamplingResult:
35.          return self._delegate.should_sample(
36.              parent_context,
37.              trace_id,
38.              name,
39.              kind,
40.              attributes,
41.              links,
42.              trace_state,
43.          )

45.      def get_description(self) -> str:
46.          return self._delegate.get_description()



`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)
环境变量设置
`

1.  export OTEL_TRACES_SAMPLER=parentbased_composite
2.  export OTEL_TRACES_SAMPLER_ARG=0.10

`AI写代码
  • OTEL_TRACES_SAMPLER 指定使用自定义采样器。
  • OTEL_TRACES_SAMPLER_ARG 指定采样比例(这里 0.10 = 10%)。

核心说明

  1. 导入实验性 API

    1. opentelemetry.sdk.trace._sampling_experimental 构建组合采样器(composite sampler)。
    2. 支持在每个根 span 的 tracestate 中编码采样概率,便于后端进行吞吐量校正。
  2. ParentBasedCompositeSampler 类

    • 可直接插入 SDK 使用。

    • 构造函数读取采样概率参数(字符串形式),默认 1.0 = 100% 采样。

    • 内部使用组合采样器实现:

      • 根据概率采样根 span。

      • 通过 parent-based threshold 逻辑传递根采样决策给子 span。

      • tracestate 中嵌入并遵守 OTel 概率字段。

  3. 效果

    • 启用标准化的概率采样与传播。

    • 后端可基于 tracestate 元数据准确估算吞吐量指标。

    • 保持 head-based sampling 低成本,同时保证指标准确。

验证(Validation)

要验证你的设置并确保 head-based sampling 下的吞吐量指标准确,可按以下步骤操作:

1. 部署环境
  • 在可控环境中部署 SDK,以便管理负载和采样率。

  • 生成稳定且可预测的负载,确保已知真实吞吐量。

  • 为 traces 设置固定采样率。

  • 将生成的遥测数据发送到 Elastic Observability

2. 检查指标
原始吞吐量(Raw throughput)

使用 ES|QL 统计采样 traces 的数量:

`

1.  FROM traces-* 
2.  | WHERE service.name == "your-service"
3.  | WHERE transaction.name IS NOT NULL
4.  | STATS count_transactions = COUNT(*),
5.      time_range = DATE_DIFF("minute", MIN(@timestamp), MAX(@timestamp))
6.  | EVAL raw_throughput_per_min = count_transactions::double / time_range

`AI写代码
  • raw_throughput_per_min 表示从采样 trace 直接计算出的每分钟吞吐量。

  • 这个值通常接近你设置的采样率比例。


推算吞吐量(Extrapolated throughput)

使用 metrics- 数据表计算根据采样概率推算的真实吞吐量:

`

1.  FROM metrics-*
2.  | WHERE service.name == "your-service"
3.  | WHERE metricset.name == "service_transaction" AND metricset.interval == "1m"
4.  | STATS count_transactions = COUNT(transaction.duration.summary),
5.      time_range = DATE_DIFF("minute", MIN(@timestamp), MAX(@timestamp))
6.  | EVAL extrapolated_throughput_per_min = count_transactions::double / time_range

`AI写代码
  • extrapolated_throughput_per_min 应接近真实吞吐量。

  • 对比 raw_throughput_per_minextrapolated_throughput_per_min,可验证采样概率元数据是否正确应用。

3. 验证要点
  1. 初期部署:先在单个服务启用采样感知配置,确保 tracestate 正确传播且后端指标准确。

  2. 逐步推广:确认无误后,再将配置逐步推广到其他服务。

  3. 监控与回归测试:建立吞吐量准确性的监控与自动化回归测试,以便在采样率或 SDK 更新时及时发现指标漂移。

总结

  • 通过概率传播 (tracestate probability propagation),head-based sampling 变得安全且可预测。

  • 团队无需在降低采样成本和保证可靠吞吐量计算之间做折中。

  • 在启用之前,务必查阅对应 SDK 的 PR 与发行说明,确认使用的版本支持完整功能(Java、JavaScript、Python)。

这样可以在保持低成本的同时,确保运营指标可信、采样统计可推算真实吞吐量。

原文:www.elastic.co/observabili…