使用 OpenTelemetry 进行尾采样:为何有用,如何实现,以及要考虑的事项
尾采样在节省可观察性成本的同时,有助于识别分布式系统中的问题。在本文中,你将学习如何使用 OpenTelemetry Collector 实现尾采样。我还将分享一些一般性和 OpenTelemetry 特定的问题,供你在开发采样策略时参考。
什么是采样?为什么要采样?
使用分布式追踪,你可以观察请求在分布式系统中的传递过程。出于各种原因,比如理解服务之间的连接,诊断延迟问题等,这非常实用。如果分布式追踪是你的新话题,请确保阅读本文的分布式追踪和采样。
然而,如果大部分请求都是成功的 200 状态码,并且在没有延迟或错误的情况下完成,你真的需要那么多数据吗?事实是,你并不总是需要大量数据来找到正确的洞察力。你只需正确采样数据。
采样的想法是控制你发送到可观察性后端的 span,从而降低摄入成本。不同的组织将有其自己的原因,不仅包括_为什么_他们希望进行采样,还包括_他们_希望采样_什么_。你可能希望根据以下目标自定义采样策略:
- 管理成本:如果你导出和存储所有的 span,你可能会面临来自相关云提供商或供应商的严重收费风险。
- 关注感兴趣的追踪:例如,你的前端团队可能只想看到带有特定用户属性的追踪。
- 过滤噪声:例如,你可能希望过滤掉健康检查。
什么是尾部采样?
尾部采样是在请求的所有 span 完成后,才会做出采样决策的一种方式。这与头部采样相反,头部采样是在根 span 开始处理时就做出决策。尾部采样允许你根据特定的条件来筛选你的追踪,而头部采样并不提供这个选项。
尾部采样可让你仅查看你感兴趣的追踪。由于你只导出预先确定的追踪子集,还减少了数据摄取和存储成本。例如,作为一个应用程序开发人员,我可能只对带有错误或延迟的追踪感兴趣,用于调试目的。
如何在 OpenTelemetry Collector 中实现尾采样
要在 OpenTelemetry 中使用尾采样,你需要实现一个称为 tail sampling processor 的组件。该组件根据一组策略对追踪进行采样,你可以从中选择并定义策略。首先,为了确保捕获所有 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% 的不含错误的追踪进行随机采样。
下图是你在实现此示例配置后可能在 backend 中看到的示例。
右侧的蓝色点和矩形表示采样决策发生在某个请求的所有 span 完成之后。绿色点表示采样的 span,灰色点表示未采样的 span。最后,红色点表示检测到错误的 span。使用此配置,你将获取所有带有错误的追踪,以及我们配置的速率下的其他一部分追踪的随机采样。
如果你只想基于特定过滤器(如错误)进行采样,你可以删除概率策略。但是,随机采样所有其他追踪可以帮助发现其他问题,并帮助你更全面地了解软件的性能和行为。这是仅定义了状态码策略时的示例。
你还可以根据需要添加其他策略。以下是几个示例:
always_sample
:采样所有追踪。latency
:根据追踪的持续时间进行采样。例如,你可以对超过 5 秒的所有追踪进行采样。string_attribute
:根据字符串属性值进行采样,支持精确值和正则表达式匹配。例如,你可以根据特定的自定义属性值进行采样。
尾采样的潜在问题
- 可能无法预测的成本:尽管采样通常有助于管理数据摄入和存储成本,但你可能偶尔遇到活动激增的情况。例如,如果你对带有延迟的追踪进行采样,并且遇到严重的网络拥塞,你的追踪解决方案将导出大量带有延迟问题的追踪,从而导致成本在此期间意外增加。然而,这也是为什么尾采样存在的—因此我们可以快速发现并解决这类问题。
- 性能问题:如果你将遥测数据存储在本地,你需要存储跨度,直到采样决策完成。如果存储在本地,这可能会消耗你的应用程序的资源,如果没有存储在本地,则可能会消耗额外的网络带宽。
- 确定正确的策略:此外,尽管你并不一定需要大量数据来获得正确的洞察力,但你需要正确的采样,而确定这一点可能具有挑战性。你将不得不问很多问题,比如:一个健康请求的基准是什么样的?你能为尾采样分配多少资源?否则,你可能会无意间过滤掉本应揭示系统问题的请求,或者消耗比你最初预期的更多资源。
- 为尾采样建立等待时间:尾采样的另一个挑战是很难预测何时追踪实际上将完成。由于追踪本质上是一个 span 的图,其中子 span 引用它们的父 span,因此新的 span 可以在任何给定时间添加。为解决此问题,你可以建立一个可接受的等待时间,在做出采样决策之前等待。这里的假设是一个追踪应该在配置的时间内完成,但这意味着你有可能在该时间窗口之外完成的有趣 span 丢失,这可能导致片段化的追踪。片段化的追踪发生在 span 缺失时,会导致可见性出现间隙。
OpenTelemetry 的局限性
在使用 OpenTelemetry 时,还有一些与之相关的局限性。请注意,其中一些限制不仅适用于 OpenTelemetry,还适用于任何客户端托管的尾采样解决方案。
首先,你需要部署一个 collector。虽然 collector 在集中配置和处理数据方面非常实用,但它是系统中需要实现和维护的另一个组件。此外,要使尾采样起作用,特定追踪的所有 span 必须在同一个 collector 中处理,这带来了可扩展性挑战。
对于简单的设置,一个 collector 就足够了,不需要负载平衡。然而,越多的请求存储在内存中,你就需要越多的内存,并且你还需要额外的处理和计算资源来查看每个 span 的属性。随着系统的增长,你不能只依赖一个单独的 collector 来完成所有的任务,这意味着你必须考虑你的 collector 部署模式 和负载平衡。
由于一个 collector 不足,你需要实现一个两层设置,其中 collector 在代理-collector 配置中部署。你还需要每个 collector 具有对其接收到的追踪的完整视图。这意味着具有相同追踪 ID 的所有 span 需要发送到同一个 collector 实例,否则你将得到片段化的追踪。如果你使用了带有尾采样处理器的多个代理/collector 实例,你可以使用 负载均衡导出器。这个导出器确保具有相同追踪 ID 的所有 span 最终进入同一个代理/collector。但是,实施这个导出器可能会带来其他开销,例如配置它。
另一个需要考虑的限制是,由于 OpenTelemetry 不传播能让后端重新加权计数的元数据(例如 P95、P99 和总事件数)- 例如,你已将采样器配置为保留 25% 的所有追踪。如果后端不知道它仅在操作 25% 的所有数据,它生成的任何测量结果都将是不准确的。解决这个问题的一种方法是将元数据附加到 span 中,告诉后端采样率是多少,这将使后端能够准确地测量一段时间内的总跨度计数等指标。Sampling SIG 目前正在开发这个概念。
最后,由于 OpenTelemetry 仍然是一个不断发展的项目,许多组件仍在积极开发中,包括尾采样处理器和 collector。对于尾采样处理器,目前在 collector-contrib 存储库 中有一个 开放问题 来讨论该处理器的未来,其中重点是将其替换为单独的处理器,以便事件链是明确定义和理解的。社区正在努力解决的主要问题之一是,使用单独的处理器来进行尾采样是否更高效。需要注意的是,不会在没有向后兼容的解决方案的情况下发布任何内容。对于 collector,有限的选项来监视 collector,这对疑难解答至关重要。请参阅 此处 获取更多信息。
此文章的原始版本在 New Relic 博客上发布。