使用 OpenTelemetry 进行尾部采样:它为何有用、如何实现以及需要考虑的事项

博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。

尾部采样有助于识别分布式系统中的问题,同时节省可观测性成本。在本文中,您将学习如何使用 OpenTelemetry Collector 实现尾部采样。我还将分享一些在制定采样策略时需要考虑的通用和 OpenTelemetry 特定的注意事项。

什么是采样,为什么要做采样?

通过分布式跟踪,您可以观察请求在分布式系统中从一个服务移动到另一个服务的过程。它在许多方面都非常实用,例如了解服务连接和诊断延迟问题,以及许多其他好处。如果分布式跟踪对您来说是一个新话题,请务必阅读这篇关于分布式跟踪和采样文章。

然而,如果大多数请求都是成功的 200 响应,并且在没有延迟或错误的情况下完成,您真的需要所有这些数据吗?关键在于——您不总是需要大量数据来找到正确的洞察。*您只需要正确采样的数据。*

Illustration shows that not all data needs to be traced, and that a sample of data is sufficient.

采样的目的是控制您发送到可观测性后端(observability backend)的 span,从而降低摄取成本。不同的组织会有他们自己不只是*为什么*要采样,还有*要采样什么*的原因。您可能希望自定义采样策略以

  • 管理成本:如果您导出和存储所有 span,您可能会产生相关的云提供商或供应商的高额费用。
  • 关注有趣的跟踪:例如,您的前端团队可能只希望查看具有特定用户属性的跟踪。
  • 过滤掉噪音:例如,您可能希望过滤掉健康检查。

什么是尾部采样?

尾部采样是在请求中的所有 span 都完成后,才做出采样决策。这与头部采样(head-based sampling)相反,头部采样是在请求开始时,当根 span 开始处理时做出决策。尾部采样允许您根据特定标准过滤跟踪,而这是头部采样无法做到的。

Illustration shows how spans originate from a root span. After the spans are complete, the tail sampling processor makes a sampling decision.

尾部采样让您只能看到您感兴趣的跟踪。由于您只导出预定数量的跟踪子集,因此您还可以降低数据摄取和存储成本。例如,作为一名应用程序开发人员,我可能只对带有错误或延迟的跟踪感兴趣,以便进行调试。

如何在 OpenTelemetry Collector 中实现尾部采样

要在 OpenTelemetry 中使用尾部采样,您需要实现一个名为尾部采样处理器的组件。该组件根据您选择和定义的策略对跟踪进行采样。*首先,为了确保您捕获所有 span,请在 SDK 中使用默认采样器或 AlwaysOn 采样器。*

现在,让我们通过一个尾部采样处理器配置示例来演示,该处理器恰好位于 collector 配置文件中的 processors 部分。

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100
    expected_new_traces_per_sec: 10
    policies:
      [
        {
          name: errors-policy,
          type: status_code,
          status_code: { status_codes: [ERROR] },
        },
        {
          name: randomized-policy,
          type: probabilistic,
          probabilistic: { sampling_percentage: 25 },
        },
      ]
  • tail_sampling 是您将用于实现尾部采样的处理器名称。
  • 前三行是可选的可配置设置。
    • decision_wait 是在创建跟踪的第一个 span 之后,做出采样决策之前的时间(以秒为单位)。默认值为 30 秒。
    • num_traces 是要保留在内存中的跟踪数量。默认值为 50,000。
    • expected_new_traces_per_sec 是新跟踪的预期数量,有助于分配数据结构。默认值为 0。
  • policies 是您定义采样策略的地方。没有默认值,这是处理器配置中唯一必需的部分。在本例中,定义了两个策略:
    • status_code,名为 errors-policy,因为此示例将过滤掉状态码为 ERROR 的跟踪。
    • probabilistic,名为 randomized-policy。除了过滤掉所有带有错误信息的跟踪外,还将对没有错误的跟踪进行 25% 的随机采样。

下图是您在后端实现此示例配置后可能看到的景象。

Illustration shows sampling based on both traces that have errors as well as randomly selected traces.

右侧的蓝色圆点和矩形表示采样决策发生在跟踪的末尾,当给定请求的所有 span 都完成时。绿色圆点表示被采样的 span,而灰色圆点表示未被采样的 span。最后,红色圆点表示检测到错误的 span。通过此配置,您将获得所有带错误的跟踪,以及基于我们配置的速率对其他跟踪进行的随机采样。

如果您只想基于特定过滤器(如错误)进行采样,可以移除 probabilistic 策略。但对所有其他跟踪进行随机采样有助于发现其他问题,并为您提供更广泛的软件性能和行为视图。以下是仅定义状态码策略时看到的效果。

Illustration shows an example of tail sampling based on errors only, and not including random traces.

您还可以灵活地添加其他策略。以下是一些示例:

  • always_sample:采样所有跟踪。
  • latency:基于跟踪的持续时间进行采样。例如,您可以采样所有耗时超过 5 秒的跟踪。
  • string_attribute:基于字符串属性值进行采样,支持精确匹配和正则表达式匹配。例如,您可以基于特定的自定义属性值进行采样。

尾部采样可能遇到的问题

  • 潜在不可预测的成本:虽然采样通常有助于管理数据摄取和存储成本,但您偶尔可能会出现活动峰值。例如,如果您正在对带有延迟的跟踪进行采样,并且遇到了严重的网络拥塞,您的跟踪解决方案将导出大量带有延迟问题的跟踪,导致在此期间成本意外飙升。然而,这也是尾部采样存在的原因——以便我们能够快速发现这类问题并采取行动。
  • 性能:如果您将遥测数据本地存储,您将需要将 span 存储到做出采样决策。如果 span 本地存储,这可能会消耗应用程序的资源,或者如果不是本地存储,则会消耗额外的网络带宽。
  • 确定合适的策略:同样,虽然您不一定需要大量数据来获得正确的洞察,但您确实需要正确的采样,而确定这一点可能具有挑战性。您必须提出许多问题,例如:健康请求的基线是什么样的?您有多少资源可以用于尾部采样?否则,您可能会无意中过滤掉可能揭示您系统中问题的请求,或者消耗比您最初预期的更多的资源。
  • 建立尾部采样的等待期:尾部采样的另一个挑战是很难预测跟踪何时会真正完成。由于跟踪本质上是 span 的图,其中子 span 引用其父 span,因此可以在任何给定时间添加新的 span。为了解决这个问题,您可以建立一个可接受的时间段,在做出采样决策之前等待。这里的假设是,跟踪应在配置的时间段内完成,但这意味着您可能会丢失在时间窗口之外完成的有趣 span 的风险,这可能导致跟踪碎片化。跟踪碎片化发生在 span 丢失时,并且可能导致可见性中断。

OpenTelemetry 的局限性

还有一些与 OpenTelemetry 相关的局限性需要考虑。请注意,其中一些局限性也更广泛地适用于任何客户端托管的尾部采样解决方案,而不仅仅是 OpenTelemetry。

首先,您必须自行部署一个 collector。虽然 collector 在集中配置和处理数据方面最终可能非常实用,但它是在您的系统中又一个需要实现和维护的组件。此外,为了使尾部采样正常工作,*特定跟踪的所有 span 都必须在同一个 collector 中进行处理*,这导致了可伸缩性挑战。

对于简单的设置,一个 collector 就足够了,并且不需要负载均衡。但是,需要将越来越多的请求保存在内存中,您将需要更多的内存,并且还需要额外的处理和计算能力来查看每个 span 属性。*随着系统的增长,仅靠一个 collector 无法完成所有这些工作,这意味着您必须考虑您的collector 部署模式和负载均衡。*

由于一个 collector 不足,您必须实现一个两层设置,其中 collector 以代理-collector(agent-collector)配置部署。您还需要每个 collector 都拥有对其接收到的跟踪的完整视图。这意味着所有具有相同 trace ID 的 span 都需要发送到同一个 collector 实例,否则您将得到碎片化的跟踪。如果您运行多个带有尾部采样处理器的代理/collector 实例,可以使用负载均衡导出器。此导出器可确保所有具有相同 trace ID 的 span 都最终进入同一个代理/collector。但是,实现此导出器可能会带来额外的开销,例如配置它。

另一个需要考虑的局限性是,由于 OpenTelemetry 不会传播允许后端重新加权计数的元数据——例如 P95、P99 和总事件——*您只能看到采样数据的测量结果,而无法获得关于*所有*数据的准确测量结果*。假设您已将采样器配置为保留所有跟踪的 25%。如果后端不知道它仅处理 25% 的数据,它生成的任何测量结果都将不准确。一种解决此问题的方法是将元数据附加到您的 span,告诉后端采样率是多少,这将允许后端准确地测量给定时间段内的总 span 计数等内容。Sampling SIG 目前正在研究这个概念。

最后,由于 OpenTelemetry 仍然是一个不断发展的项目,许多组件都处于积极开发中,包括尾部采样处理器和 collector。对于尾部采样处理器,目前在collector-contrib 仓库有一个开放 issue,讨论该处理器的未来,其核心是将其替换为单独的处理器,以便事件链能够得到明确的定义和理解。社区正在努力解决的一个主要问题是,使用单独的处理器进行尾部采样是否会比仅使用尾部采样处理器更高效。需要注意的是,在没有向后兼容的解决方案的情况下,不会发布任何内容。对于 collector,用于监控 collector 的选项有限,而这对于故障排除至关重要。有关更多信息,请参阅Collector Troubleshooting

本文的一个版本最初发布在 New Relic 博客上。

最后修改日期:2025 年 5 月 27 日:[chore] Accessible links 5 (#6053) (860f5141)