作者来自 Carson IpElastic 向 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并将它们分发到两个正在观测的相同 collectorCUO-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-Aconfig: 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-BCUO-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-completeCUO-A与使用 Pebble 磁盘存储的 span-ingestCUO-B。进程 RSS、Go 堆分配以及每个进程的 CPU 测量数据来自 OpenTelemetry Collector 内部的进程和运行时指标而容器工作集和容器 CPU 数据来自由 kubelet/cAdvisor 抓取的 Kubernetes cgroup 指标。内存时间窗口内的峰值指标cuo-acuo-bΔB 相比 A进程 RSS916.4 MiB442.7 MiB-51.7%Go 堆分配699.3 MiB241.7 MiB-65.4%容器工作集763.0 MiB282.9 MiB-62.9%CPU时间窗口内总量指标cuo-acuo-bΔB 相比 A每进程 CPU11.9 core-s22.7 core-s90.7%容器 CPU11.9 core-s22.7 core-s90.1%吞吐量时间窗口内总量指标ΔB 相比 Acuo-acuo-bΔB 相比 A接收的 span 数量257,804257,8040.0%发送的 span 数量2,4772,4770.0%尾部采样指标cuo-acuo-bΔB 相比 A内存中的追踪峰值29,28429,245-0.1%被 root_1pct 策略采样的追踪数量4964960.0%cuo-atrace-completecuo-bspan-ingest-pebble时间窗口15 分 39 秒t0:00开始t10:06开始排空t15:39排空结束结果显示在使用span-ingest搭配 pebbletailstorageextension 时内存使用量显著降低但代价是由于事件序列化和数据库开销导致 CPU 使用量增加。OpenTelemetry 尾部采样的下一步发展在撰写本文时sampling_strategy和 pebbletailstorageextension 都仍处于早期阶段。欢迎在 OpenTelemetry Collector Contrib 仓库中提供反馈和贡献。敬请期待更多改进。原文OpenTelemetry tail sampling: 65% less memory with disk storage — Elastic Observability Labs