作者:来自 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 中表示,并可跨服务边界保留。
概念性变化
重要的转变不是“ 多采样” 或“ 少采样”,而是:
- 保留概率性的 head-based 采样
- 在 trace 中传播概率元数据
- 让后端根据采样数据计算加权指标
这既保持了可控的采集成本,又恢复了正确的聚合分析。
实现流程
不同语言的 API 细节不同,但部署模式类似:
-
使用支持概率/组合规范行为的 SDK 采样器。
-
保持 W3C Trace Context 传播开启,使 tracestate 可跨服务传递。
-
在后端验证吞吐量/速率图表使用加权解释(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写代码
核心说明:
-
导入类:引入必要的 OpenTelemetry API 和采样器扩展。
-
设置采样比例:
ratio控制采样 trace 的比例(如 0.1 表示 10%)。 -
采样器配置:
CompositeSampler和ComposableSampler用于构建符合 OpenTelemetry composite sampler 规范的采样器,实现更精确的概率 head 采样。ComposableSampler.probability(ratio)指定采样比例。ComposableSampler.parentThreshold(...)确保遵循父 trace 的采样决策,保持 trace 上下文一致。CompositeSampler.wrap(...)包装成符合最新规范的采样器。
-
tracerProvider 和 OpenTelemetry 设置:将采样器附加到
SdkTracerProvider并构建成OpenTelemetrySdk实例。 -
使用采样器:创建 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写代码
核心说明
导入
- 从
@opentelemetry/sampler-composite引入三个函数:createCompositeSamplercreateComposableParentThresholdSamplercreateComposableTraceIDRatioBasedSampler
- 它们实现了符合规范的组合采样逻辑。
采样器配置
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写代码
环境变量设置
`
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%)。
核心说明
-
导入实验性 API
- 从
opentelemetry.sdk.trace._sampling_experimental构建组合采样器(composite sampler)。 - 支持在每个根 span 的
tracestate中编码采样概率,便于后端进行吞吐量校正。
- 从
-
ParentBasedCompositeSampler 类
-
可直接插入 SDK 使用。
-
构造函数读取采样概率参数(字符串形式),默认 1.0 = 100% 采样。
-
内部使用组合采样器实现:
-
根据概率采样根 span。
-
通过 parent-based threshold 逻辑传递根采样决策给子 span。
-
在
tracestate中嵌入并遵守 OTel 概率字段。
-
-
-
效果
-
启用标准化的概率采样与传播。
-
后端可基于
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_min和extrapolated_throughput_per_min,可验证采样概率元数据是否正确应用。
3. 验证要点
-
初期部署:先在单个服务启用采样感知配置,确保 tracestate 正确传播且后端指标准确。
-
逐步推广:确认无误后,再将配置逐步推广到其他服务。
-
监控与回归测试:建立吞吐量准确性的监控与自动化回归测试,以便在采样率或 SDK 更新时及时发现指标漂移。
总结
-
通过概率传播 (tracestate probability propagation),head-based sampling 变得安全且可预测。
-
团队无需在降低采样成本和保证可靠吞吐量计算之间做折中。
-
在启用之前,务必查阅对应 SDK 的 PR 与发行说明,确认使用的版本支持完整功能(Java、JavaScript、Python)。
这样可以在保持低成本的同时,确保运营指标可信、采样统计可推算真实吞吐量。