作者:来自 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 使用量以及内存使用量。
基准测试设置示意图
markdown
`1. demo ns
2. +----------------------------------------+
3. | opentelemetry-demo |
4. | loadgenerator (locust) |
5. | services: frontend, cart, ... |
6. | demo-collector |
7. +----------------------------------------+
8. | OTLP/gRPC
9. v
10. chamber ns
11. +----------------------------------------+
12. | pipe-collector |
13. | receive once, fan out |
14. | exporters: [otlp/a, otlp/b] |
15. +----------------------------------------+
16. | OTLP | OTLP
17. v v
18. +----------------+ +----------------+
19. | CUO-A | | CUO-B |
20. | tail_sampling | | tail_sampling |
21. | (config A) | | (config B) |
22. +----------------+ +----------------+`AI写代码
尾部采样处理器配置
CUO-A
yaml
`
1. config:
2. processors:
3. tail_sampling:
4. sampling_strategy: trace-complete
5. decision_wait: 5m
6. num_traces: 5000000
7. block_on_overflow: true
8. decision_cache:
9. sampled_cache_size: 10000
10. non_sampled_cache_size: 200000
11. policies:
12. - name: root_1pct
13. type: and
14. and:
15. and_sub_policy:
16. - name: root_span_only
17. type: ottl_condition
18. ottl_condition:
19. error_mode: ignore
20. span:
21. - "IsRootSpan()"
22. - name: root_probabilistic
23. type: probabilistic
24. probabilistic:
25. sampling_percentage: 1.0
`AI写代码
CUO-B
CUO-B 使用与 CUO-A 相同的尾部采样处理器配置,唯一不同的是它设置了 sampling_strategy: span-ingest 和 tail_storage: pebble_tail_storage/main ,以及与之对应的 pebbletailstorageextension 配置。
markdown
`
1. extensions:
2. pebble_tail_storage/main:
3. directory: /var/lib/otelcol/pebble
`AI写代码
内存、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