Elastic 如何利用磁盘支持的追踪存储将 OpenTelemetry 尾部采样内存占用降低 65%
作者:来自 Carson Ip
Elastic 向 OTel Collector 的尾部采样处理器上游贡献了两项功能。span-ingest策略让采样决策能够更早发生,而Pebble尾部存储则将追踪缓冲区迁移到磁盘。虽然这样会消耗更多 CPU,但运维人员可以提高decision_wait和num_traces,而无需担心因内存耗尽( OOM )而导致进程被终止。
Elastic 向 OpenTelemetry Collector 的尾部采样处理器( tailsamplingprocessor )上游贡献了两项改进,最多可将内存使用量降低 65%。sampling_strategy: span-ingest让采样决策能够在数据摄入时发生,在decision_wait到期之前释放追踪数据。pebbletailstorageextension 将追踪缓冲移动到磁盘上的 Pebble LSM 数据库中,使存储能力由磁盘容量决定,而不是由 RAM 决定。这意味着运维人员可以增加decision_wait和num_traces,而无需担心因内存耗尽( OOM )导致进程被终止。代价是大约增加 2 倍 CPU 消耗。
什么是尾部采样?
分布式追踪对于调试非常有用,但在生产规模下,它会带来处理开销和存储成本,此时采样成为一种自然的方法,可以在控制成本的同时保持追踪的价值。基于尾部的采样( tail-based sampling ),也称为尾部采样,是一种在稍后阶段根据条件做出采样决策的技术,因此错误或慢事务等高价值追踪更有可能被采样。与之相反的是头部采样( head sampling ),它在追踪开始时就做出决策,此时还无法获得这些信息。
尾部采样处理器如何工作?
尾部采样处理器会缓冲 100% 的输入追踪数据(或 span,两者可以互换使用),然后在应用采样策略后转发被采样的子集。缓冲是内存使用的重要来源,并且它会随着 span 数量增长而按比例增加,这是社区中众所周知的痛点。
内存使用量受到decision_wait和num_traces等配置参数限制。将decision_wait设置为 1 分钟意味着系统会在 1 分钟后对一个追踪做出采样决策,在此期间,该追踪的所有 span 都应该已经到达。如果一个追踪耗时超过 1 分钟,那么做出决策时会缺少部分 span。
补充说明一下,扩展尾部采样部署规模需要使用 loadbalancingexporter,以满足一个要求:同一个追踪中的所有 span 必须被路由到同一个 collector。这会引入一些运维复杂性,并且在 collector 重启期间可能导致数据丢失。但本文关注的是单个尾部采样处理器实例的内存使用情况,而不考虑水平扩展。
为什么尾部采样会导致内存压力?
这些参数在数据丢失和内存使用之间引入了权衡,并且它们需要基于对追踪形态的假设:追踪可能持续多慢、包含多少 span、每个 span 有多大。随着埋点方式不断演进,这些假设可能会逐渐失效。
为了限制内存使用,可以接受多少数据丢失?这种权衡是否可以进一步优化?下面介绍的两个贡献旨在为运维人员提供更大的灵活性。
span-ingest 如何通过提前释放 span 降低尾部采样内存使用
sampling_strategy是尾部采样处理器中新增加的配置选项,加入于 v0.149.0。
sampling_strategy默认值为trace-complete,这与原始行为一致:只有当decision_wait时间结束后,采样策略才会被评估,此时追踪被认为已经完成。(还有一个类似的配置decision_wait_after_root_received用于优化,但为了简单起见,本文不展开讨论。)这意味着无论是否能够更早做出决策,所有 span 都会在内存中缓冲大约decision_wait时间,然后才被释放。例如,应该始终被丢弃的健康检查 span 仍然会一直保存在内存中,直到策略评估时间到达。
另一种方式是将sampling_strategy设置为span-ingest,此时 span 会在摄入时被单独评估。这允许更早做出终止决策,特别是drop或sampled决策,通过在decision_wait到期之前丢弃或导出该追踪目前已经缓冲的所有 span 来释放内存。在健康检查示例中,可以配置一个策略:只要根 span 属于健康检查,就立即丢弃整个追踪。需要注意的是,与明确的drop不同,unsampled决策不是终止性的,因为它可能被同一个追踪中另一个 span 的sampled或drop决策覆盖,因此unsampled追踪无法提前释放。
从trace-complete切换到span-ingest需要调整策略,因为策略不能再假设评估时所有 span 都已经可用。此外,并非所有策略类型都支持span-ingest策略。
使用 Pebble 的磁盘支持尾部采样存储
即使使用span-ingest,所有 span 仍然会被缓存在内存中。当增加decision_wait以适应慢速追踪,并增加num_traces以减少数据丢失时,collector 最终仍然会达到内存限制并被 OOM 终止,从而导致进一步的数据丢失。
如果追踪数据改为缓存在磁盘上会怎样?磁盘通常具有高出一个数量级的容量。主要缺点是性能:即使使用 SSD,磁盘吞吐量和延迟也至少比内存慢一个数量级,因此磁盘写入必须非常高效。出于这个原因,选择了 LSM 数据库 Pebble 作为存储后端,因为它具有快速写入性能。当sampling_strategy设置为span-ingest时,读取只会发生在被采样的追踪子集上,因此读取性能的重要性较低。
该实现引入了用于追踪存储操作的TailStorage接口,并在 v0.150.0 中新增了tail_storage选项(通过功能开关processor.tailsamplingprocessor.tailstorageextension启用),用于配置存储后端。默认的内存存储行为保持不变,但现在可以替换为其他存储后端,例如由 Elastic 贡献给 Collector Contrib 的新 pebbletailstorageextension。
尾部采样内存基准测试:使用 Pebble 的 trace-complete 与 span-ingest 对比
基准测试设置:带扇出的 OpenTelemetry Demo
下面的基准测试通过运行 OpenTelemetry Demo,并增加负载到一个管道 collector 来生成。该管道 collector 接收所有 span,并将它们分发到两个正在观测的相同 collector(CUO-A和CUO-B),两者唯一不同之处是尾部采样配置不同。测量指标包括管道 collector 吞吐量、接收的 span 数量、发送的 span 数量(已采样)、CPU 使用量以及内存使用量。
基准测试设置示意图
demo ns +----------------------------------------+ | opentelemetry-demo | | loadgenerator (locust) | | services: frontend, cart, ... | | demo-collector | +----------------------------------------+ | OTLP/gRPC v chamber ns +----------------------------------------+ | pipe-collector | | receive once, fan out | | exporters: [otlp/a, otlp/b] | +----------------------------------------+ | OTLP | OTLP v v +----------------+ +----------------+ | CUO-A | | CUO-B | | tail_sampling | | tail_sampling | | (config A) | | (config B) | +----------------+ +----------------+尾部采样处理器配置
CUO-A
config: processors: tail_sampling: sampling_strategy: trace-complete decision_wait: 5m num_traces: 5000000 block_on_overflow: true decision_cache: sampled_cache_size: 10000 non_sampled_cache_size: 200000 policies: - name: root_1pct type: and and: and_sub_policy: - name: root_span_only type: ottl_condition ottl_condition: error_mode: ignore span: - "IsRootSpan()" - name: root_probabilistic type: probabilistic probabilistic: sampling_percentage: 1.0CUO-B
CUO-B使用与CUO-A相同的尾部采样处理器配置,唯一不同的是它设置了sampling_strategy: span-ingest和tail_storage: pebble_tail_storage/main,以及与之对应的 pebbletailstorageextension 配置。
extensions: pebble_tail_storage/main: directory: /var/lib/otelcol/pebble内存、CPU 和吞吐量结果
下面的表格从内存、CPU 和吞吐量三个方面,对比了 trace-complete(CUO-A)与使用 Pebble 磁盘存储的 span-ingest(CUO-B)。进程 RSS、Go 堆分配以及每个进程的 CPU 测量数据来自 OpenTelemetry Collector 内部的进程和运行时指标,而容器工作集和容器 CPU 数据来自由 kubelet/cAdvisor 抓取的 Kubernetes cgroup 指标。
内存(时间窗口内的峰值)
| 指标cuo-acuo-bΔ(B 相比 A) | |||
|---|---|---|---|
| 进程 RSS | 916.4 MiB | 442.7 MiB | -51.7% |
| Go 堆分配 | 699.3 MiB | 241.7 MiB | -65.4% |
| 容器工作集 | 763.0 MiB | 282.9 MiB | -62.9% |
CPU(时间窗口内总量)
| 指标 | cuo-a | cuo-b | Δ(B 相比 A) |
|---|---|---|---|
| 每进程 CPU | 11.9 core-s | 22.7 core-s | +90.7% |
| 容器 CPU | 11.9 core-s | 22.7 core-s | +90.1% |
吞吐量(时间窗口内总量)
| 指标Δ(B 相比 A) | cuo-a | cuo-b | Δ(B 相比 A) |
|---|---|---|---|
| 接收的 span 数量 | 257,804 | 257,804 | 0.0% |
| 发送的 span 数量 | 2,477 | 2,477 | 0.0% |
尾部采样
| 指标 | cuo-a | cuo-b | Δ(B 相比 A) |
|---|---|---|---|
| 内存中的追踪峰值 | 29,284 | 29,245 | -0.1% |
| 被 root_1pct 策略采样的追踪数量 | 496 | 496 | 0.0% |
cuo-a=trace-complete,cuo-b=span-ingest-pebble时间窗口:15 分 39 秒(
t+0:00开始,t+10:06开始排空,t+15:39排空结束)
结果显示,在使用span-ingest搭配 pebbletailstorageextension 时,内存使用量显著降低,但代价是由于事件序列化和数据库开销导致 CPU 使用量增加。
OpenTelemetry 尾部采样的下一步发展
在撰写本文时,sampling_strategy和 pebbletailstorageextension 都仍处于早期阶段。欢迎在 OpenTelemetry Collector Contrib 仓库中提供反馈和贡献。敬请期待更多改进。
原文:OpenTelemetry tail sampling: 65% less memory with disk storage — Elastic Observability Labs