最佳实践

了解使用 OpenTelemetry .NET 进行指标的最佳实践

遵循这些最佳实践,以充分利用 OpenTelemetry .NET 进行指标收集。

包版本

使用最新的稳定版本 System.Diagnostics.Metrics API,无论使用哪个 .NET 运行时版本,均来自 System.Diagnostics.DiagnosticSource 包的最新稳定版本

  • 如果您使用的是最新稳定版本的 OpenTelemetry .NET SDK,则无需担心 System.Diagnostics.DiagnosticSource 包的版本,因为它已通过包依赖关系为您处理好了。
  • .NET 运行时团队在 System.Diagnostics.DiagnosticSource 的向后兼容性方面设置了高标准,即使在主要版本更新期间也是如此,因此兼容性在此处不是问题。
  • 有关 System.Diagnostics.Metrics 的更多信息,请参阅 .NET 官方文档

指标 API

Meter

避免过于频繁地创建 System.Diagnostics.Metrics.MeterMeter 成本相当高,并且旨在在整个应用程序中重用。对于大多数应用程序,它可以建模为静态只读字段或通过依赖注入实现单例。

使用点分隔的 UpperCamelCase 作为 Meter.Name。在许多情况下,使用完全限定的类名可能是一个不错的选择。例如

static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0");

Instruments

理解并选择正确的仪表类型。

OpenTelemetry 规范.NET 仪表类型
Asynchronous CounterObservableCounter<T>
Asynchronous GaugeObservableGauge<T>
Asynchronous UpDownCounterObservableUpDownCounter<T>
CounterCounter<T>
GaugeGauge<T>
HistogramHistogram<T>
UpDownCounterUpDownCounter<T>

避免过于频繁地创建仪表(例如 Counter<T>)。仪表成本相当高,并且旨在在整个应用程序中重用。对于大多数应用程序,仪表可以建模为静态只读字段或通过依赖注入实现单例。

避免使用无效的仪表名称。

在报告测量值时,避免更改标签的顺序。例如

counter.Add(2, new("name", "apple"), new("color", "red"));
counter.Add(3, new("name", "lime"), new("color", "green"));
counter.Add(5, new("name", "lemon"), new("color", "yellow"));
counter.Add(8, new("color", "yellow"), new("name", "lemon")); // bad perf

正确使用 TagList 以获得最佳性能。有两种不同的方法可以将标签传递给仪表 API

  • 将标签直接传递给仪表 API

    counter.Add(100, new("Key1", "Value1"), new("Key2", "Value2"));
    
  • 使用 TagList

    var tags = new TagList
    {
        { "DimName1", "DimValue1" },
        { "DimName2", "DimValue2" },
        { "DimName3", "DimValue3" },
        { "DimName4", "DimValue4" },
    };
    
    counter.Add(100, tags);
    

一般而言:

  • 当报告带有 3 个或更少标签的测量值时,将标签直接传递给仪表 API。
  • 当报告带有 4 到 8 个标签(含)的测量值时,使用 TagList 可避免堆分配,如果避免 GC 压力是主要的性能目标。对于将减少 CPU 利用率视为比优化内存分配更重要的(例如,降低延迟、节省电池等)高性能代码,请使用性能分析器和压力测试来确定哪种方法更好。
  • 当报告带有超过 8 个标签的测量值时,这两种方法在 CPU 性能和堆分配方面非常相似。推荐使用 TagList,因为它具有更好的可读性和可维护性。

MeterProvider 管理

避免过于频繁地创建 MeterProvider 实例。MeterProvider 成本相当高,并且旨在在整个应用程序中重用。对于大多数应用程序,每个进程一个 MeterProvider 实例就足够了。例如

graph LR

subgraph Meter A
  InstrumentX
end

subgraph Meter B
  InstrumentY
  InstrumentZ
end

subgraph Meter Provider 2
  MetricReader2
  MetricExporter2
  MetricReader3
  MetricExporter3
end

subgraph Meter Provider 1
  MetricReader1
  MetricExporter1
end

InstrumentX --> | Measurements | MetricReader1
InstrumentY --> | Measurements | MetricReader1 --> MetricExporter1
InstrumentZ --> | Measurements | MetricReader2 --> MetricExporter2
InstrumentZ --> | Measurements | MetricReader3 --> MetricExporter3

如果您自己创建 MeterProvider 实例,请管理其生命周期。

一般而言:

内存管理

在 OpenTelemetry 中,测量值通过指标 API 报告。SDK 使用某些算法和内存管理策略来聚合指标,以实现良好的性能和效率。以下是 OpenTelemetry .NET 在实现指标聚合逻辑时遵循的规则:

  1. 预聚合:聚合发生在 SDK 内部。
  2. 基数限制:聚合逻辑遵守 基数限制,因此当发生基数爆炸时,SDK 不会占用无限的内存。
  3. 内存预分配:聚合逻辑使用的内存是在 SDK 初始化期间分配的,因此 SDK 无需即时分配内存。这是为了避免在热代码路径上触发垃圾回收。

示例

让我们以以下示例为例

  • 在时间范围 (T0, T1] 内
    • value = 1, name = apple, color = red
    • value = 2, name = lemon, color = yellow
  • 在时间范围 (T1, T2] 内
    • 未收到水果
  • 在时间范围 (T2, T3] 内
    • value = 5, name = apple, color = red
    • value = 2, name = apple, color = green
    • value = 4, name = lemon, color = yellow
    • value = 2, name = lemon, color = yellow
    • value = 1, name = lemon, color = yellow
    • value = 3, name = lemon, color = yellow

如果我们使用 Cumulative Aggregation Temporality 进行聚合和导出指标

  • (T0, T1]
    • attributes: {name = apple, color = red}, count: 1
    • attributes: {verb = lemon, color = yellow}, count: 2
  • (T0, T2]
    • attributes: {name = apple, color = red}, count: 1
    • attributes: {verb = lemon, color = yellow}, count: 2
  • (T0, T3]
    • attributes: {name = apple, color = red}, count: 6
    • attributes: {name = apple, color = green}, count: 2
    • attributes: {verb = lemon, color = yellow}, count: 12

如果我们使用 Delta Aggregation Temporality 进行聚合和导出指标

  • (T0, T1]
    • attributes: {name = apple, color = red}, count: 1
    • attributes: {verb = lemon, color = yellow}, count: 2
  • (T1, T2]
    • 什么都没有,因为我们没有收到任何测量值
  • (T2, T3]
    • attributes: {name = apple, color = red}, count: 5
    • attributes: {name = apple, color = green}, count: 2
    • attributes: {verb = lemon, color = yellow}, count: 10

预聚合

水果示例 为例,在 (T2, T3] 时间段内报告了 6 个测量值。SDK 不会导出每个单独的测量事件,而是将它们聚合起来,仅导出汇总结果。此方法(如下图所示)称为预聚合。

graph LR

subgraph SDK
  Instrument --> | Measurements | Pre-Aggregation[Pre-Aggregation]
end

subgraph Collector
  Aggregation
end

Pre-Aggregation --> | Metrics | Aggregation

预聚合带来了几个好处

  1. 虽然计算量保持不变,但使用预聚合可以显著减少传输的数据量,从而提高整体效率。
  2. 预聚合使得可以在 SDK 初始化期间应用 基数限制,结合 内存预分配,它们使指标数据收集行为更可预测(例如,遭受拒绝服务攻击的服务器仍然会产生恒定量的指标数据,而不是用大量测量事件淹没可观测性系统)。

在某些情况下,用户可能希望导出原始测量事件而不是使用预聚合,如下图所示。OpenTelemetry 目前不支持此场景,如果您感兴趣,请通过回复此 功能请求 加入讨论。

graph LR

subgraph SDK
  Instrument
end

subgraph Collector
  Aggregation
end

Instrument --> | Measurements | Aggregation

基数限制

属性的唯一组合数量称为基数。以 水果示例 为例,如果我们知道名称只能是 apple/lemon,颜色只能是 red/yellow/green,那么我们可以说基数是 6。无论我们有多少苹果和柠檬,我们始终可以使用下表来总结基于名称和颜色的水果总数。

名称颜色数量
苹果红色6
苹果黄色0
苹果绿色2
柠檬红色0
柠檬黄色12
柠檬绿色0

换句话说,我们知道收集和传输这些指标所需的存储和网络资源,而与流量模式无关。

在实际应用程序中,基数可能非常高。想象一下,如果我们有一个长期运行的服务,并且收集了 7 个属性的指标,每个属性可以有 30 个不同的值。我们最终可能需要记住所有 21,870,000,000 种组合的完整集合!这种基数爆炸是指标领域的一个众所周知的挑战。例如,它可能导致可观测性系统产生令人惊讶的高成本,甚至可能被黑客利用来发起拒绝服务攻击。

基数限制是一种限流机制,它允许指标收集系统在发生过度基数时(无论是由于恶意攻击还是开发人员在编写代码时出错),都保持可预测且可靠的行为。

OpenTelemetry 对每个指标的默认基数限制为 2000。此限制可以通过使用 View APIMetricStreamConfiguration.CardinalityLimit 设置在单个指标级别进行配置。

1.10.0 开始,一旦指标达到基数限制,任何无法独立聚合的新测量值都将使用 溢出属性 自动聚合。

1.10.0 开始,当使用 Delta Aggregation Temporality 时,可以选择较小的基数限制,因为 SDK 会回收未使用的指标点。

内存预分配

OpenTelemetry .NET SDK 旨在避免在热代码路径上进行内存分配。当与 正确使用 Metrics API 结合使用时,可以在热代码路径上避免堆分配。

您应该测量热代码路径上的内存分配,并尽量避免在使用指标 API 和 SDK 时进行任何堆分配,特别是在使用指标来衡量应用程序性能时(例如,您不希望在测量一个通常需要 10 毫秒的操作时花费 2 秒进行 垃圾回收)。

指标关联

在 OpenTelemetry 中,可以通过 示例 将指标与 跟踪 相关联。有关更多信息,请参阅 示例 教程。

指标增强

当收集指标时,它们通常存储在 时间序列数据库 中。从存储和消费的角度来看,指标可以是多维的。以 水果示例 为例,有两个维度——“name”和“color”。对于基本场景,所有维度都可以在 Metrics API 调用期间报告,但是,对于不太简单的场景,维度可以来自不同的来源:

一般而言:

  • 如果维度在整个进程生命周期中是静态的(例如,机器名称、数据中心名称)
    • 如果维度适用于所有指标,则将其建模为 Resource,甚至更好的是,如果可行,让收集器添加这些维度(例如,在同一数据中心运行的收集器应该知道数据中心的名称,而不是依赖/信任每个服务实例来报告数据中心名称)。
    • 如果维度适用于部分指标(例如,客户端库的版本),则将其建模为 meter 级别标签。
  • 如果维度值是动态的,则通过 Metrics API 报告。

导致指标丢失的常见问题

  • 用于创建仪表的 Meter 未添加到 MeterProvider。使用 AddMeter 方法来启用所需指标的处理。

最后修改日期:2025年8月14日: 添加其余dotnet内容 (#7543) (79e067c8)