TraceState:概率采样

状态: 开发中

概述

采样是降低收集和处理遥测数据相关成本的重要手段。它使您能够从总体中选择一组代表性的项目。

追踪数据的采样有两个关键方面。首先,采样决策可以为追踪中的每个 span 独立做出。其次,采样决策可以在遥测管道中的多个点进行。例如,在 span 创建时对 span 的采样决策可能是保留该 span,而在稍后阶段(例如在数据收集管道的外部进程中)对相同 span 的下游采样决策可能是丢弃它。

对于上述每个方面,如果我们不做出一致的采样决策,我们将得到无法使用且不包含一组连贯 span 的追踪,因为独立的采样决策。相反,我们希望以一致的方式做出采样决策,以便我们能够有效地推理追踪。

本规范描述了如何通过一种称为一致概率采样的机制来实现一致的采样决策。为此,它使用了两个关键构建块。第一个是所有参与者都可以使用的通用随机源(R),包括一组追踪器和收集器。这可以是 OpenTelemetry TraceState rv 子键中表示的显式随机值,也可以是从 TraceID 的最后 7 个字节中获取。第二个是拒绝阈值(T)的概念。这直接源自参与者的采样率,并针对已采样的 span 在 OpenTelemetry TraceState th 子键中表示。本提案描述了这些值应如何传播以及参与者应如何使用它们来做出采样决策。

有关此规范的更多详细信息,请参阅 OTEP 235OTEP 250

有关将概率采样信息编码到 OpenTelemetry 追踪数据中的详细信息,特别是 OpenTelemetry TraceState rvth 值,请参阅 TraceState 处理。此后,我们在进行采样决策时使用变量 RT 来表示这些数量。

定义

采样概率

采样概率是一个 span 被保留的可能性。每个参与者都可以为每个 span 选择不同的采样概率。例如,如果采样概率为 0.25,则大约 25% 的 span 将被保留。

在 OpenTelemetry 中,采样概率的有效范围是 2^-56 到 1。此表达式中出现的 56 值对应于 W3C Trace Context Level 2 TraceID 指定的 7 字节随机性(即 56 位)。请注意,零值未定义,“从不”采样不是概率采样的一种形式。

一致的采样决策

一致的采样决策意味着,对于某个 span 以概率 p1 做出的正面采样决策,如果另一个 span 属于同一追踪且以概率 p2 >= p1 进行采样,则必然也做出正面采样决策。

拒绝阈值(T

这是一个 56 位的值,直接源自采样概率。一种思考方式是,这是在考虑的 2^56 个 span 中被丢弃的 span 的数量。

您可以按如下方式从采样概率派生拒绝阈值

Rejection_Threshold = (1 - Sampling_Probability) * 2^56.

例如,如果采样概率是 100%(保留所有 span),则拒绝阈值为 0。

同样,如果采样概率是 1%(丢弃 99% 的 span),则具有 5 位精度的拒绝阈值将是 (1-0.01) * 2^56 ≈ 71337018784743424 = 0xfd70a400000000。

我们将此拒绝阈值称为 T。当采样 Span 或 Context 时,采样器的有效 T 会编码在 OpenTelemetry TraceState th 子键中,以指示其采样概率。在这种情况下,以 1% 采样的 span 将具有 OpenTelemetry TraceState 值 ot=th:fd70a4,因为在编码中删除了尾随零。

有关更多示例,请参阅 tracestate 处理

随机值(R

一个通用的随机值(已知或传播给所有参与者)是实现一致概率采样的主要成分。每个参与者都可以将此值(R)与其拒绝阈值(T)进行比较,从而在整个追踪(甚至在一组追踪)中做出一致的采样决策。

此提案支持两种随机源

  • 显式随机源:OpenTelemetry 支持一个称为显式追踪随机性的 56 位随机(或伪随机)值。这可以通过 OpenTelemetry TraceState rv 子键进行传播,并存储在 Span 的 TraceState 字段中。
  • 使用 TraceID 作为随机源:OpenTelemetry 支持使用 TraceID 的最低有效 56 位作为随机源,如 W3C Trace Context Level 2 中所述。如果根 Span 的 Trace SDK 知道 TraceID 是以随机或伪随机方式生成的,则可以这样做。

有关编码随机值的详细信息,请参阅 tracestate 处理

方法和术语

决策算法

鉴于上述构建块,让我们看看参与者如何做出一致的采样决策。为此,必须存在两个值

  1. SpanContext 中,通用的随机源:56 位随机值(R)。
  2. 来自采样器配置的拒绝阈值:56 位阈值(T)。

如果 R >= T,则保留 span,否则丢弃 span。

采样阶段

概述中提到的两个采样方面被称为采样策略的阶段

  1. 父/子采样。这些决策是在追踪生命周期中,在 SDK 内部,同步地为 span 做出的。这些决策基于实时 Context。
  2. 下游采样。这些采样决策发生在收集路径上的 span,在它们完成后,并且可能发生在收集管道中的多个位置。这些决策基于历史 Context。

我们将这些阶段视为采样决策的两个维度。在父/子采样中,沿着因果关系的方向,从父级到子级,向前发展。在下游采样中,从收集器到收集器,向前发展。考虑一个完成的 span 在某个时刻,它将由一个父/子采样器采样一次,并由一个或多个下游采样器采样零次或多次。

在这两种情况下,“前一个”采样器指的是在时间上在其之前发生采样的阶段(即父采样器或上游采样器)。

graph TD
    subgraph "parent/child samplers"
        direction LR
        Root["root span"] --> Child1["child span"]
        Child1 --> Child2["child span"]
    end

    subgraph "downstream samplers"
        Root --> LC1["frontend agent"]
        LC1 --> LC2["frontend gateway"]

        Child1 --> RC1["backend agent"]
        Child2 --> RC1
        RC1 --> RC2["backend gateway"]
    end

    LC2 --> FC["destination service"]
    RC2 --> FC

    classDef span fill:#a7a,stroke:#333,stroke-width:2px;
    classDef collector fill:#77a,stroke:#33c,stroke-width:1px;

    class Root,Child1,Child2 span;
    class LC1,LC2,RC1,RC2,FC collector;

采样基本情况

CompositeSampler 旨在通过内置的 ComposableSamplers 作为基本情况来组合多个采样规则。

在父/子采样类别中,这些是主要的基本情况

  • 根:根采样决策是两个采样维度中的第一个决策,无需先前的阈值。根采样决策是唯一允许修改 Context 的显式追踪随机值的情况。
  • 基于父级的:当在子级使用基于父级的采样时,我们期望子级的决策与父级的决策匹配。父级和子级将具有匹配的阈值。
  • 一致概率:概率采样器(例如 TraceIDRatio)做出独立的采样决策。一致概率采样决策会忽略父级的采样阈值(如果有)。这种情况涵盖了始终开启和始终关闭的采样行为。

有些术语不是关于某个特定采样器,而是关于方法

  • 头部:警告:此术语有多种用途。这可能指 SDK 在启动新 span 时做出的采样决策,例如“头部采样决策只能考虑 span 创建期间存在的信息”(来自 Span Link 的 API 规范。这也可以指分布式追踪设置,其中所有子采样器都使用基于父级的采样器,例如“大多数分布式追踪配置都使用头部采样”。
  • 尾部:这种采样模式意味着下游采样器参与了决策。有时这指的是收集路径上单个 span 的采样(称为“中间”采样),但通常它指的是在将多个 span 组合成一个数据集后,在下游做出的整个追踪采样决策。尾部采样当然可以与头部采样结合,并且不一定会在追踪中的 span 之间贡献不相等的概率。

术语调整计数指的是采样概率的数学逆(倒数),该项应该被计数的预期次数,以便代表总体。调整计数有用作估计没有采样时的 span 数量。

术语span-to-metrics指的是在遥测系统中,使用调整计数来根据调整计数信息从追踪 span 中派生指标。处理 tracestate 和管理采样器中阈值信息的规范确保调整计数主要在为此目的可靠。

TraceState 处理要求

这是一个补充指南。有关每个内置采样器的具体要求,请参阅 SDK 规范

拒绝阈值表示在所有先前的一致采样阶段(包括父/子和下游采样器)所应用的最大阈值。

有关可能将显式追踪随机性插入 Context 的信息,请参阅 SDK 对追踪随机性的要求

有关 thrv 如何编码的示例,请参阅 TraceState 处理

通用要求

所有修改有效采样阈值的采样阶段都必须通过重新编码 OpenTelemetry TraceState 来更新采样阈值,以保持正确的统计解释。

为了使条件采样阶段(例如基于规则的采样器)保持一致,它们不得基于随机值或依赖的随机源来条件化提供的阈值(可以使用独立随机性)。

产生具有未知采样概率的 span 的采样阶段,包括当它们遇到没有父阈值的 Context 时基于父级的采样器,必须在其输出中清除 OpenTelemetry 阈值。

采样阶段应在测试简单时检查一致性,并在阈值明显不一致时将其清除。例如,一个接收并传播了已采样 Context 和阈值的基于父级的采样器,应检查采样标志是否与表达式 rv >= th 匹配,否则应清除阈值。

如果随机性是从显式随机值派生的,则相同的 rv 值必须设置在传出的 OpenTelemetry TraceState 中。

父/子阈值

父/子采样器初始化一致概率阈值。这可以通过使用内置采样器之一来实现,无论是作为基本情况还是使用 ComposableSampler 的逻辑。

独立父/子阈值

由于没有父级阈值,根采样器始终做出独立决策。子采样器可以配置独立采样器,尽管这可能导致不完整的追踪。

独立父/子采样器基本情况(即 AlwaysOn、TraceIDRatio)会根据其采样概率编码一个固定阈值。

基于父级的阈值

基于父级的采样器会将传入的阈值作为传出的阈值进行传播,但需遵守通用要求

  • 如果采样的传入阈值明显不一致,则清除它
  • 如果采样的传入阈值缺失,请确保没有传出阈值。

下游阈值

下游采样器能够提高所应用的阈值,但不能降低。下游采样器降低阈值相当于追溯性地提高先前阶段的采样概率。换句话说,下游采样器被允许降低采样概率,但提高采样概率会引入统计误差。

接下来将讨论两种下游概率采样(具有标准术语)。

均衡下游采样器

均衡下游采样器旨在使所有 span 在通过该阶段后具有相等的阈值。当早期阶段的选择性较低时,此阶段的选择性会更高,从而产生具有相等概率的 span。

当一个配置了阈值 T_d 的均衡下游采样器考虑一个具有随机值 R 和阈值 T_s 的 span 时

  • 如果 T_s > T_d,则出站阈值不变,等于 T_s。均衡概率采样器在这种情况下无法降低阈值,因此也无法均衡概率。
  • 如果 R >= T_d,则使用出站阈值 T_d 选择 span。
  • 否则,R < T_d 表示 span 未被采样。

比例下游采样器

比例下游采样器旨在将传入概率乘以一个配置的乘数。通过统一提高阈值,此阶段的采样器承诺尽管有传入的阈值,但仍能将流量减少固定量。

当一个配置了概率 p 的比例下游采样器考虑一个具有阈值 T_s 和随机值 R 的 span 时,它首先计算乘积阈值 T_o

T_o = ProbabilityToThreshold(p * ThresholdToProbability(T_s))
  • 如果 T_o 小于最小概率阈值,则 span 未被采样。
  • 如果 R >= T_o,则使用出站阈值 T_o 选择 span。
  • 否则,R < T_o 表示 span 未被采样。

迁移到一致的概率采样器

OpenTelemetry 规范的 TraceIdRatioBased 采样器直到 SDK 规范被宣布稳定后才完成,并且该采样器的确切行为仍未定义。当前规范解决了此行为。

随着 OpenTelemetry TraceIdRatioBased 采样器定义的变化,用户必须考虑如何在旧逻辑和新逻辑之间的过渡期间避免因采样不一致而导致不完整的追踪。

原始 TraceIdRatioBased 采样器规范为未明确定义的行为提供了一个变通方法,即对于根 span 使用它是安全的:“建议仅将此采样器算法用于根 span(与 ParentBased 结合使用),因为不同的语言 SDK 甚至同一语言 SDK 的不同版本可能会为相同的输入产生不一致的结果。”

为了在此过渡期间避免不一致,用户应遵循此指导,直到系统中的所有 Trace SDK 都已升级到基于 W3C Trace Context Level 2 的现代 Trace 随机性要求。用户可以通过检查其系统中的所有 Span 是否都设置了 Trace 随机标志(在 Span 标志中)来验证所有 Trace SDK 是否已升级。为协助此次迁移,TraceIdRatioBased Sampler 在首次假设 TraceID 随机性但未设置 Trace 随机标志的 Context 时,会发出警告。

算法

TR 值可以根据处理器的功能和实现的需要,以多种形式表示和操作。作为 56 位值,它们与字节数组和 64 位整数兼容,也可以使用 64 位浮点数进行操作,精度损失极小。

以下示例使用 Python3。它们仅用于说明目的,并非建议的实现。

将浮点概率转换为阈值

阈值会删除尾随零进行编码,这允许可变精度。这可以通过四舍五入来实现,并且有几种实用的方法可以使用内置字符串格式化库来完成。

利用高达 56 位的精度,使用内置浮点数支持的实现将受到底层数字支持精度的限制。一种编码阈值的方法是将 IEEE 754-2008 标准的十六进制浮点表示法作为一种简单的解决方案。

import math

# ProbabilityToThresholdWithPrecision assumes the probability value is in the range
# [2^-56, 1] and precision is in the range [1, 13], which is the maximum for a
# IEEE-754 double-width float value.
def probability_to_threshold_with_precision(probability, precision):
    if probability == 1:
        # Special case
        return "0"

    # Raise precision by the number of leading 'f' digits.
    _, exp = math.frexp(probability)
    # Precision is limited to 12 so that there is at least one digit of precision
    # in the final [:precision] statement below.
    precision = max(1, min(12, precision + exp // -4))

    # Change the probability to 1 + rejection probability = 1 + 1 - probability,
    # i.e., modify the range (0, 1] into the range [1, 2).
    rejection_prob = 2 - probability

    # To ensure rounding correctly below, add an offset equal to half of the
    # final digit of precision in the corresponding representation.
    rejection_prob += math.ldexp(0.5, -4 * precision)

    # The expression above technically can't produce a number >= 2.0 because
    # of the compensation for leading Fs. This gives additional safety for
    # the hex_str[4:][:-3] expression below which blindly drops the exponent.
    if rejection_prob >= 2.0:
        digits = "fffffffffffff"
    else:
        # Use float.hex() to get hexadecimal representation
        hex_str = rejection_prob.hex()

        # The hex representation for values between 1 and 2 looks like '0x1.xxxxxxxp+0'
        # Extract the part after '0x1.' (4 bytes) and before 'p' (3 bytes)
        digits = hex_str[4:][:-3]

    assert len(digits) == 13
    # Remove trailing zeros
    return digits[:precision].rstrip('0')

请注意,使用 math.frexp(probability) 来调整概率的底为 2 的指数。这使得配置的精度应用于接近零的概率的有效数字。请注意,对于接近单位概率的值,没有进行对称调整,因为我们认为没有实际用途可以非常精确地采样接近 100% 的值。

要直接从浮点概率转换为 56 位无符号整数表示,并使用 math.Round() 和移位操作,请参阅 OpenTelemetry Collector-Contrib pkg/sampling。该包演示了如何直接从概率计算整数阈值。

OpenTelemetry SDK 默认建议使用 4 位精度。下表显示了使用上述方法计算的 1-in-N 概率采样值,精度分别为 3、4 和 5。

1-in-N输入概率阈值(精度 3、4、5)实际概率(精度 3、4、5)精确调整计数(精度 3、4、5)
110
0
0
1
1
1
1
1
1
20.58
8
8
0.5
0.5
0.5
2
2
2
30.3333333333333333aab
aaab
aaaab
0.333251953125
0.3333282470703125
0.33333301544189453
3.0007326007326007
3.00004577706569
3.0000028610256777
40.25c
c
c
0.25
0.25
0.25
4
4
4
50.2ccd
cccd
ccccd
0.199951171875
0.1999969482421875
0.19999980926513672
5.001221001221001
5.0000762951094835
5.0000047683761295
80.125e
e
e
0.125
0.125
0.125
8
8
8
100.1e66
e666
e6666
0.10009765625
0.100006103515625
0.10000038146972656
9.990243902439024
9.99938968568813
9.999961853172863
160.0625f
f
f
0.0625
0.0625
0.0625
16
16
16
1000.01fd71
fd70a
fd70a4
0.0099945068359375
0.010000228881835938
0.009999990463256836
100.05496183206107
99.99771123402633
100.00009536752259
10000.001ffbe7
ffbe77
ffbe76d
0.0010004043579101562
0.0009999871253967285
0.000999998301267624
999.5958055290753
1000.012874769029
1000.0016987352618
100000.0001fff972
fff9724
fff97247
0.00010001659393310547
0.00010000169277191162
0.00010000006295740604
9998.340882002383
9999.830725674266
9999.99370426336
1000000.00001ffff584
ffff583a
ffff583a5
9.998679161071777e-06
1.00000761449337e-05
1.0000003385357559e-05
100013.21013412817
99999.238556461
99999.96614643588
10000000.000001ffffef4
ffffef39
ffffef391
9.98377799987793e-07
1.00000761449337e-06
9.999930625781417e-07
1.0016248358208955e+06
999992.38556461
1.0000069374699865e+06

将整数阈值转换为 T

要将 56 位整数拒绝阈值转换为 T 表示,请将其输出为十六进制值(不带前导“0x”),可选择省略尾随零。

if tvalue == 0:
  add_otel_trace_state('th:0')
else:
  h = hex(tvalue).rstrip('0')
  # remove leading 0x
  add_otel_trace_state('th:'+h[2:])

随机性与阈值的测试

给定随机性和阈值作为 64 位整数,如果随机性大于或等于阈值,则应进行采样。

shouldSample = (randomness >= threshold)

将阈值转换为采样概率

采样概率是从 0.0 到 1.0 的值,可以通过除以 2^56 来用浮点数计算。

# embedded _ in numbers for clarity (permitted by Python3)
maxth = 0x100_0000_0000_0000  # 2^56
prob = float(maxth - threshold) / maxth

将阈值转换为调整计数(采样率)

调整计数表示此样本代表的人口数量的近似值。它等于 1/probability

maxth = 0x100_0000_0000_0000  # 2^56
adjCount = maxth / float(maxth - threshold)

对于通过非概率采样获得的 span(没有 th 值的已采样 span),调整计数未定义。