最佳实践
遵循这些最佳实践,以充分利用 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.Meter。Meter 成本相当高,并且旨在在整个应用程序中重用。对于大多数应用程序,它可以建模为静态只读字段或通过依赖注入实现单例。
使用点分隔的 UpperCamelCase 作为 Meter.Name。在许多情况下,使用完全限定的类名可能是一个不错的选择。例如
static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0");
Instruments
理解并选择正确的仪表类型。
.NET 运行时已根据 OpenTelemetry 规范提供了几种仪表类型。为您的用例选择正确的仪表类型对于确保正确的语义和性能至关重要。有关更多信息,请参阅补充指南中的 仪表选择 部分。
避免过于频繁地创建仪表(例如 Counter<T>)。仪表成本相当高,并且旨在在整个应用程序中重用。对于大多数应用程序,仪表可以建模为静态只读字段或通过依赖注入实现单例。
避免使用无效的仪表名称。
OpenTelemetry 不会收集使用无效名称的仪表指标。有关有效语法,请参阅 OpenTelemetry 规范。
在报告测量值时,避免更改标签的顺序。例如
最后一行代码性能很差,因为标签的顺序不一致
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"));使用
TagListvar 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,因为它具有更好的可读性和可维护性。
当报告带有超过 8 个标签的测量值时,API 会在热代码路径上分配内存。您应该尝试将标签数量保持在 8 个或更少。如果超过此数量,请检查是否可以将某些标签建模为 Resource,如 此处所示。
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 实例,请管理其生命周期。
一般而言:
- 如果您正在构建一个使用 依赖注入 (DI) 的应用程序(例如 ASP.NET Core 和 .NET Worker),在大多数情况下,您应该创建
MeterProvider实例并让 DI 管理其生命周期。有关更多信息,请参阅 在 5 分钟内开始使用 OpenTelemetry .NET 指标 - ASP.NET Core 应用程序 教程。 - 如果您正在构建一个不使用 DI 的应用程序,请创建
MeterProvider实例并显式管理其生命周期。有关更多信息,请参阅 在 5 分钟内开始使用 OpenTelemetry .NET 指标 - 控制台应用程序 教程。 - 如果您忘记在应用程序结束前处置
MeterProvider实例,指标可能会因缺少适当的刷新而丢失。 - 如果您过早处置
MeterProvider实例,任何后续的测量值都将不会被收集。
内存管理
在 OpenTelemetry 中,测量值通过指标 API 报告。SDK 使用某些算法和内存管理策略来聚合指标,以实现良好的性能和效率。以下是 OpenTelemetry .NET 在实现指标聚合逻辑时遵循的规则:
- 预聚合:聚合发生在 SDK 内部。
- 基数限制:聚合逻辑遵守 基数限制,因此当发生基数爆炸时,SDK 不会占用无限的内存。
- 内存预分配:聚合逻辑使用的内存是在 SDK 初始化期间分配的,因此 SDK 无需即时分配内存。这是为了避免在热代码路径上触发垃圾回收。
示例
让我们以以下示例为例
- 在时间范围 (T0, T1] 内
- value = 1, name =
apple, color =red - value = 2, name =
lemon, color =yellow
- value = 1, name =
- 在时间范围 (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
- value = 5, name =
如果我们使用 Cumulative Aggregation Temporality 进行聚合和导出指标
- (T0, T1]
- attributes: {name =
apple, color =red}, count:1 - attributes: {verb =
lemon, color =yellow}, count:2
- attributes: {name =
- (T0, T2]
- attributes: {name =
apple, color =red}, count:1 - attributes: {verb =
lemon, color =yellow}, count:2
- attributes: {name =
- (T0, T3]
- attributes: {name =
apple, color =red}, count:6 - attributes: {name =
apple, color =green}, count:2 - attributes: {verb =
lemon, color =yellow}, count:12
- attributes: {name =
如果我们使用 Delta Aggregation Temporality 进行聚合和导出指标
- (T0, T1]
- attributes: {name =
apple, color =red}, count:1 - attributes: {verb =
lemon, color =yellow}, count:2
- attributes: {name =
- (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
- attributes: {name =
预聚合
以 水果示例 为例,在 (T2, T3] 时间段内报告了 6 个测量值。SDK 不会导出每个单独的测量事件,而是将它们聚合起来,仅导出汇总结果。此方法(如下图所示)称为预聚合。
graph LR subgraph SDK Instrument --> | Measurements | Pre-Aggregation[Pre-Aggregation] end subgraph Collector Aggregation end Pre-Aggregation --> | Metrics | Aggregation
预聚合带来了几个好处
- 虽然计算量保持不变,但使用预聚合可以显著减少传输的数据量,从而提高整体效率。
- 预聚合使得可以在 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 API 和 MetricStreamConfiguration.CardinalityLimit 设置在单个指标级别进行配置。
从 1.10.0 开始,一旦指标达到基数限制,任何无法独立聚合的新测量值都将使用 溢出属性 自动聚合。
在 SDK 版本 1.6.0 - 1.9.0 中,溢出属性是一项实验性功能,可以通过设置环境变量 OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE=true 来启用。
从 1.10.0 开始,当使用 Delta Aggregation Temporality 时,可以选择较小的基数限制,因为 SDK 会回收未使用的指标点。
在 SDK 版本 1.7.0 - 1.9.0 中,指标点回收是一项实验性功能,可以通过设置环境变量 OTEL_DOTNET_EXPERIMENTAL_METRICS_RECLAIM_UNUSED_METRIC_POINTS=true 来启用。
内存预分配
OpenTelemetry .NET SDK 旨在避免在热代码路径上进行内存分配。当与 正确使用 Metrics API 结合使用时,可以在热代码路径上避免堆分配。
您应该测量热代码路径上的内存分配,并尽量避免在使用指标 API 和 SDK 时进行任何堆分配,特别是在使用指标来衡量应用程序性能时(例如,您不希望在测量一个通常需要 10 毫秒的操作时花费 2 秒进行 垃圾回收)。
指标关联
在 OpenTelemetry 中,可以通过 示例 将指标与 跟踪 相关联。有关更多信息,请参阅 示例 教程。
指标增强
当收集指标时,它们通常存储在 时间序列数据库 中。从存储和消费的角度来看,指标可以是多维的。以 水果示例 为例,有两个维度——“name”和“color”。对于基本场景,所有维度都可以在 Metrics API 调用期间报告,但是,对于不太简单的场景,维度可以来自不同的来源:
- 通过 Metrics API 报告的 测量值。
- 在仪表创建时提供的额外标签。例如,
Meter.CreateCounter<T>(name, unit, description, tags)重载。 - 在 meter 创建时提供的额外标签。例如,
Meter(name, version, tags, scope)重载。 - 在
MeterProvider级别配置的 资源。 - 由导出器或收集器提供的额外属性。例如,Prometheus 中的 作业和实例。
Instrument level tags support is not yet implemented in OpenTelemetry .NET since the OpenTelemetry Specification does not support it.
一般而言:
- 如果维度在整个进程生命周期中是静态的(例如,机器名称、数据中心名称)
- 如果维度适用于所有指标,则将其建模为 Resource,甚至更好的是,如果可行,让收集器添加这些维度(例如,在同一数据中心运行的收集器应该知道数据中心的名称,而不是依赖/信任每个服务实例来报告数据中心名称)。
- 如果维度适用于部分指标(例如,客户端库的版本),则将其建模为 meter 级别标签。
- 如果维度值是动态的,则通过 Metrics API 报告。
过去曾有关于添加一个名为 MeasurementProcessor 的新概念的讨论,它允许动态地向测量值添加/删除维度。由于复杂性和性能影响,这个想法没有得到推动,有关更多上下文,请参阅此 拉取请求。
导致指标丢失的常见问题
- 用于创建仪表的
Meter未添加到MeterProvider。使用AddMeter方法来启用所需指标的处理。