zl程序教程

您现在的位置是:首页 >  其他

当前栏目

创建指标

2023-04-18 17:00:56 时间

本文适用范围:✔️ .NET Core 3.1 及更高版本 ✔️ .NET Framework 4.6.1 及更高版本

可以使用 System.Diagnostics.Metrics API 来检测 .NET 应用程序以跟踪重要指标。 一些指标包括在标准 .NET 库中,但可能需要添加与应用程序和库相关的新的自定义指标。 在本教程中,你将添加新的指标并了解可用的指标类型。

备注

.NET 有一些较旧的指标 API,即 EventCounters 和 System.Diagnostics.PerformanceCounter,此处不会介绍这些 API。 若要详细了解这些选项,请参阅比较指标 API。

创建自定义指标

先决条件:.NET Core 3.1 SDK或更高版本

创建引用 System.Diagnostics.DiagnosticSource NuGet 包版本 6 或更高版本的新控制台应用程序。 默认情况下,面向 .NET 6 及更高版本的应用程序包括此引用。 然后,更新 Program.cs 中的代码以匹配:

> dotnet new console
> dotnet add package System.Diagnostics.DiagnosticSource
using System;
using System.Diagnostics.Metrics;
using System.Threading;
class Program
{
    static Meter s_meter = new Meter("HatCo.HatStore", "1.0.0");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hats-sold");
    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each second that sells 4 hats
            Thread.Sleep(1000);
            s_hatsSold.Add(4);
        }
    }
}

System.Diagnostics.Metrics.Meter 类型是库创建指定检测组的入口点。 检测记录计算指标所需的数值度量值。 这里我们使用 CreateCounter 来创建名为“hats-sold”的 Counter 检测。 在每次虚拟的交易中,代码调用 Add 来记录售出帽子的数量,在本例中为 4。 “hats-sold”检测隐式定义了一些指标,可通过这些度量计算这些指标,例如售出的帽子总计数或每秒售出的帽子数。最终由指标集合工具确定要计算哪些指标,以及如何执行这些计算,但每个检测都有一些能体现开发人员意图的默认约定。 对于 Counter 检测,约定是集合工具显示总计数和/或计数增加的速率。

Counter<int> 和 CreateCounter<int>(...) 上的泛型参数 int 定义该计数器必须能够存储到 Int32.MaxValue 的值。 可以使用 byte、short、int、long、float、double 或decimal 中的任何一个,具体取决于需要存储的数据大小以及是否需要小数值。

运行应用并使其保持运行状态。 接下来,我们将查看指标。

> dotnet run
Press any key to exit

最佳做法

创建一次计量,将它存储在静态变量或 DI 容器中,并根据需要使用相应实例。 每个库或库子组件都可以(并且通常应该)创建自己的 Meter。 如果预期应用开发人员希望能够单独启用和禁用指标组,请考虑创建新计量,而不是重复使用现有计量。

传递给 Meter 构造函数的名称必须是唯一的,以免与其他任何计量发生冲突。 使用包含程序集名称和子组件名称(可选)的点分层次结构名称。 如果程序集在第二个独立程序集中添加代码检测,则名称应基于定义计量的程序集,而不是要检测其代码的程序集。

Meter 构造函数的版本参数是可选的。 建议在发布库的多个版本时提供一个版本并更改检测。

.NET 不会强制实施任何指标命名方案,但按照约定,所有 .NET 运行时库名称都采用“-”(如果需要分隔符)。 其他指标生态系统建议使用“.”或“_”作为分隔符。 Microsoft 建议在代码中使用“-”,并让指标使用方(如 OpenTelemetry 或 Prometheus)在需要时转换为备用分隔符。

用于创建检测和记录度量值的 API 是线程安全的。 在 .NET 库中,大多数实例方法在从多个线程的同一对象上进行调用时都需要同步,但在这种情况下不需要。

用于记录度量值的检测 API(在本例中为Add)在没有收集数据时通常运行在小于 10 纳秒内,而在高性能集合库或工具收集度量值时则运行在数十到数百纳秒。 这允许在大多数情况下随意地使用这些 API,但是要注意那些对性能非常敏感的代码。

查看新指标

有很多选项可用于存储和查看指标。 本教程使用 dotnet-counters 工具,此工具适用于即席分析。 还可以查看指标集合教程,了解其他替代方法。 如果尚未安装 dotnet-counters 工具,请使用 SDK 进行安装:

> dotnet tool update -g dotnet-counters
You can invoke the tool using the following command: dotnet-counters
Tool 'dotnet-counters' (version '5.0.251802') was successfully installed.

让示例应用保持运行,在第二个 shell 中列出正在运行的进程,以确定进程 ID:

> dotnet-counters ps
     10180 dotnet     C:Program Filesdotnetdotnet.exe
     19964 metric-instr E:	empmetric-instrinDebug
etcoreapp3.1metric-instr.exe

查找与示例应用匹配的进程名称的 ID,并让 dotnet-counters 监视新计数器:

> dotnet-counters monitor -p 19964 HatCo.HatStore
Press p to pause, r to resume, q to quit.
    Status: Running
[HatCo.HatStore]
    hats-sold (Count / 1 sec)                          4

按照预期,可以看到,HatCo 商店每秒稳定地售出 4 个帽子。

检测类型

在上面的示例中,我们只演示了一个 Counter<T> 检测,但可用的检测类型还有很多。 可从两个方面区分这些检测:

默认指标计算 - 收集和分析检测度量值的工具会根据不同的检测计算不同的默认指标。

存储聚合数据 - 最有用的度量值需要通过多个度量值聚合数据。 一种选择是调用方在任意时间提供单独的度量值,再由集合工具管理聚合。 或者,调用方可以管理聚合度量值,并在回调中按需提供它们。

当前可用的检测类型:

Counter (CreateCounter) - 此检测在概念上跟踪随时间增加的值,并且调用方使用 Add 来报告增量。 大多数工具将计算总计数和总计数中的变化率。 对于仅显示一项内容的工具,建议显示变化率。 例如,假定调用方每秒调用一次 Add(),使用的值依次为 1、2、4、5、4、3。 如果集合工具每三秒钟更新一次,则三秒后的总计数为 1 + 2 + 4 = 7,六秒后的总计数为 1 + 2 + 4 + 5 + 4 + 3 = 19。 变化率是 (current_total - previous_total),因此在三秒后,该工具报告 7-0 = 7,而在六秒钟后,该工具会报告 19-7 = 12。

ObservableCounter (CreateObservableCounter) - 此检测类似于 Counter,只不过调用方现在负责维护聚合的总计数。 当创建 ObservableCounter 时,调用方会提供回调委托,并在每次工具需要观察当前总计数时调用回调。 例如,如果集合工具每三秒钟更新一次,则会每三秒调用一次回调。 大多数工具都提供总计数以及总计数中的变化率。 如果只能显示一个,则建议显示变化率。 如果回调在 0、3 和 6 秒分别返回 0、7 和 19,那么工具将报告这些值作为总计数。 对于变化率,此工具将在三秒钟后显示 7 - 0 = 7,并在六秒钟后显示 19 -7 = 12。

ObservableGauge (CreateObservableGauge) - 此检测允许调用方提供一个回调,其中将度量值直接作为指标传递。 每次集合工具更新时,都会调用回调,并且回调返回的任何值都会显示在该工具中。

Histogram (CreateHistogram) - 此检测跟踪度量值的分布情况。 并没有单一的规范方法来描述一组测量,但建议使用直方图或计算百分比工具。 例如,假设调用方调用 Record 来在集合工具的更新间隔期间记录这些度量值:1、5、2、3、10、9、7、4、6、8。 集合工具可能会报告这些度量值的 50%、90% 和 95%分别为 5、9 和 9。

选择检测类型时的最佳做法

针对事物计数或在一段时间内简单增加的任何其他值,请使用 Counter 或 ObservableCounter。 要在 Counter 和 ObservableCounter 之间进行选择,具体要考虑其中哪一个更容易添加到现有代码中:是对每个增量操作的 API 调用,还是从代码维护的变量中读取当前总计数的回调。 在性能非常重要的极热代码路径中,使用 Add 会为每个线程每秒创建超过一百万个调用,使用 ObservableCounter可能会更有机会进行优化。

对于涉及计时的情况,通常首选的是 Histogram。 通常,了解这些分布(90%、95% 和 99%)的尾部值比了解平均值或总计数更有用。

其他常见的情况(例如业务指标、物理传感器、缓存命中率或缓存大小、队列和文件)则一般适合 ObservableGauge。

备注

OpenTelemetry 还定义了 .NET API 中当前没有的 UpDownCounter。 通常可以通过定义一个变量来替换 ObservableGauge,以存储运行总计数并报告 ObservableGauge 回调中该变量的值。

不同检测类型的示例

停止前面启动的示例进程,并将 Program.cs 中的示例代码替换为:

using System;
using System.Diagnostics.Metrics;
using System.Threading;
class Program
{
    static Meter s_meter = new Meter("HatCo.HatStore", "1.0.0");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hats-sold");
    static Histogram<int> s_orderProcessingTimeMs = s_meter.CreateHistogram<int>("order-processing-time");
    static int s_coatsSold;
    static int s_ordersPending;
    static Random s_rand = new Random();
    static void Main(string[] args)
    {
        s_meter.CreateObservableCounter<int>("coats-sold", () => s_coatsSold);
        s_meter.CreateObservableGauge<int>("orders-pending", () => s_ordersPending);
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms that each sell 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);
            // Pretend we also sold 3 coats. For an ObservableCounter we track the value in our variable and report it
            // on demand in the callback
            s_coatsSold += 3;
            // Pretend we have some queue of orders that varies over time. The callback for the "orders-pending" gauge will report
            // this value on-demand.
            s_ordersPending = s_rand.Next(0, 20);
            // Last we pretend that we measured how long it took to do the transaction (for example we could time it with Stopwatch)
            s_orderProcessingTimeMs.Record(s_rand.Next(5, 15));
        }
    }
}

运行新进程,并在第二个 shell 中使用 dotnet-counters 以查看指标:

> dotnet-counters ps
      2992 dotnet     C:Program Filesdotnetdotnet.exe
     20508 metric-instr E:	empmetric-instrinDebug
etcoreapp3.1metric-instr.exe
> dotnet-counters monitor -p 20508 HatCo.HatStore
Press p to pause, r to resume, q to quit.
    Status: Running
[HatCo.HatStore]
    coats-sold (Count / 1 sec)                        30
    hats-sold (Count / 1 sec)                         40
    order-processing-time
        Percentile=50                                125
        Percentile=95                                146
        Percentile=99                                146
    orders-pending                                     3

此示例使用一些随机生成的数字,因此这些值会有所不同。 可以看到 hats-sold(即 Counter)和coats-sold(即 ObservableCounter)都显示为变化率。 ObservableGauge orders-pending 以绝对值形式显示。 Dotnet-counters 将 Histogram 检测呈现为三个百分比统计信息(50%、95% 和 99%),但是其他工具可能会以不同的方式汇总分布情况,或提供更多配置选项。

最佳做法

直方图在内存中存储的数据比其他指标类型要多得多,但具体的内存使用情况取决于所使用的集合工具。

如果要定义大量 (>100) Histogram 指标,则可能需要指导用户不要同时启用所有指标,或者将其工具配置为通过降低精准率来节省内存。 部分集合工具可能对它们将监视的并发 Histogram 数量有硬性限制,目的是防止过度使用内存。

将按顺序调用所有可观察检测的回调,因此需要较长时间的任何回调都可能会延迟或阻止收集所有指标。 优先选择快速读取缓存值、不返回度量值或者在执行任何可能长时间运行或阻止操作的回调时引发异常。

CreateObservableGauge 和 CreateObservableCounter 函数确实返回检测对象,但在大多数情况下,不需要将其保存在变量中,因为无需进一步与该对象进行交互。 因为 C# 静态初始化是推迟的,并且通常不会引用变量,所以将其分配给一个静态变量(就像我们在其他检测中所做的那样)是合法的,但容易出错。 下面是此问题的示例:

using System;
using System.Diagnostics.Metrics;
class Program
{
    // BEWARE! Static initializers only run when code in a running method refers to a static variable.
    // These statics will never be initialized because none of them were referenced in Main().
    //
    static Meter s_meter = new Meter("HatCo.HatStore", "1.0.0");
    static ObservableCounter<int> s_coatsSold = s_meter.CreateObservableCounter<int>("coats-sold", () => s_rand.Next(1,10));
    static Random s_rand = new Random();
    static void Main(string[] args)
    {
        Console.ReadLine();
    }
}

说明和单位

检测可以指定可选说明和单位。 这些值对于所有指标计算都是不透明的,但可以在集合工具 UI 中显示,以帮助工程师了解如何解释数据。 停止前面启动的示例进程,并将 Program.cs 中的示例代码替换为:

using System;
using System.Diagnostics.Metrics;
using System.Threading;
class Program
{
    static Meter s_meter = new Meter("HatCo.HatStore", "1.0.0");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(name: "hats-sold",
                                                                unit: "Hats",
                                                                description: "The number of hats sold in our store");
    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each 100ms that sells 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);
        }
    }
}

运行新进程,并在第二个 shell 中使用 dotnet-counters 以查看指标:

Press p to pause, r to resume, q to quit.
    Status: Running
[HatCo.HatStore]
    hats-sold (Hats / 1 sec)                           40

dotnet-counters 当前不在 UI 中使用说明文本,但它在提供时会显示单位。 在本例中,可以看到“Hats”已替换在之前的说明中可见的一般名称“Count”。

最佳做法

构造函数中指定的单位应描述适用于各个度量值的单位。 这有时与最终指标中的单位不同。 此示例中,每个度量值都是一定数量的帽子,因此“Hats”是构造函数中要传递的适当单位。 集合工具计算了变化率,并自行派生出计算指标的适当单位为 Hats/sec。

多维指标

度量值还可以与被称为标记的键值对相关联,从而能对数据进行分类以进行分析。 例如,HatCo 不仅想要记录售出的帽子数量,还想要记录它们的大小和颜色。 在稍后分析数据时,HatCo 工程师可以按大小、颜色或两者的任意组合来对总计数进行分解。

Counter 和 Histogram 标记可以在采用一个或多个 KeyValuePair 参数的 Add 和 Record 的重载中指定。 例如:

s_hatsSold.Add(2,
               new KeyValuePair<string, object>("Color", "Red"),
               new KeyValuePair<string, object>("Size", 12));

替换 Program.cs 的代码,并像以前一样重新运行应用和 dotnet-counters:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;
class Program
{
    static Meter s_meter = new Meter("HatCo.HatStore", "1.0.0");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hats-sold");
    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each 100ms that sells 2 red size 12 hats and 1 blue size 19 hat.
            Thread.Sleep(100);
            s_hatsSold.Add(2,
                           new KeyValuePair<string,object>("Color", "Red"),
                           new KeyValuePair<string,object>("Size", 12));
            s_hatsSold.Add(1,
                           new KeyValuePair<string,object>("Color", "Blue"),
                           new KeyValuePair<string,object>("Size", 19));
        }
    }
}

Dotnet-counters 现在显示基本分类:

Press p to pause, r to resume, q to quit.
    Status: Running
[HatCo.HatStore]
    hats-sold (Count / 1 sec)
        Color=Blue,Size=19                             9
        Color=Red,Size=12                             18

对于 ObservableCounter 和 ObservableGauge,可以在传递给构造函数的回调中提供带标记的度量值:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;
class Program
{
    static Meter s_meter = new Meter("HatCo.HatStore", "1.0.0");
    static void Main(string[] args)
    {
        s_meter.CreateObservableGauge<int>("orders-pending", GetOrdersPending);
        Console.WriteLine("Press any key to exit");
        Console.ReadLine();
    }
    static IEnumerable<Measurement<int>> GetOrdersPending()
    {
        return new Measurement<int>[]
        {
            // pretend these measurements were read from a real queue somewhere
            new Measurement<int>(6, new KeyValuePair<string,object>("Country", "Italy")),
            new Measurement<int>(3, new KeyValuePair<string,object>("Country", "Spain")),
            new Measurement<int>(1, new KeyValuePair<string,object>("Country", "Mexico")),
        };
    }
}

在像以前一样使用 dotnet-counters 运行时,结果为:

Press p to pause, r to resume, q to quit.
    Status: Running
[HatCo.HatStore]
    orders-pending
        Country=Italy                                  6
        Country=Mexico                                 1
        Country=Spain                                  3

最佳做法

尽管 API 允许将任何对象用作标记值,但集合工具预期使用的是数值类型和字符串。 某个给定的集合工具不一定支持其他类型。

请注意在实际操作中记录的标记值的组合非常大或不受限的情况。 尽管 .NET API 实现可以处理它,但集合工具可能会为与每个标记组合关联的指标数据分配存储,这可能会变得非常大。 例如,假设 HatCo 有 10 种不同的帽子颜色和 25 种帽子的尺寸,也就是要跟踪的销售总计数是 10*25=250 个,这很正常。但是,如果 HatCo 添加了第三个标记,该标记是销售的 CustomerID,并且向全球 1 亿客户销售产品,就可能会记录数十亿个不同的标记组合。 大多数指标集合工具会丢弃数据以保持在技术限制范围内,或者可能会花费大量的货币成本来支撑数据存储和处理。 每个集合工具的实现将确定其限制,但对于一个检测而言,组合低于 1000 个应该是安全的。 超过 1000 个组合的任何内容都需要集合工具应用筛选,或者通过设计来以大规模运行。

Histogram 实现使用的内存往往远多于其他指标,因此安全限制可能低 10-100 倍。 如果预计会存在大量的唯一标记组合,则日志、事务数据库或大数据处理系统可能是更合适的解决方案,可以按所需的规模运行。

对于将具有大量标记组合的检测,建议使用较小的存储类型来帮助降低内存开销。 例如,为 Counter<short> 存储 short每个标记组合只占用 2 个字节,而为 Counter<double> 存储 double,每个标记组合占用 8 个字节。

推荐集合工具优化代码,为每个调用指定顺序相同的相同标记名称集来记录同一检测的度量值。 对于需要频繁调用 Add 和 Record 的高性能代码,建议对每次调用使用相同的标记名称序列。

.NET API 经过优化,对于单独指定三个或更少标记的 Add 和 Record 调用,可以实现无分配。 若要避免带有大量标记的分配,请使用 TagList。 一般情况下,这些调用的性能开销会随着使用更多标记而增加。

备注

OpenTelemetry 将标记引用为“特性”。 虽然名字不同,但功能是一样的。