用于处理计量统计的System.Diagnostics.Metrics API

首先

从.NET 6.0 Preview5开始,新增了System.Diagnostics.Metrics命名空间及其相关的API。
因为我觉得以后可能会用到,所以打算解释一下这个内容。
在撰写本文时,最新版本是dotnet-6.0pre7,我将以此版本为基础进行解释。

这是为了做什么的?

简而言之,我们应该称之为“处理程序中的度量统计信息的机制”。这里所指的度量统计信息包括“平均CPU使用率”、“平均每秒请求数”、“每秒IO写入数据量”等可以用数字表示的统计信息。

对于处理Prometheus或Mackerel之类的概念的人来说,我认为他们会感到非常熟悉。
如果是Windows用户,可以将其类比为可以在性能监视器(perfmon.exe)中收集的信息,这样可能更容易理解。

从概念上来说,OpenTelemetry作为一个基础设施,实现了在dotnet中使用Metrics。
实际上,OpenTelemetry的dotnet库也开始使用Prometheus的Exporter来进行指标的导出,版本为1.2.0-alpha1及以后。
此外,从这个版本开始,ConsoleExporter也支持Metrics。

希望确定的情节

在登场人物中,大致可以分为信息发布者(Instrument、Publisher)和信息接收并处理者(Collector)。

推动的情境

假设这里是指仪器向收集器发送信息。
其机制相当简单。
在SNMP中,陷阱和通知就相当于这个。

push-sequence.png

有三个主要的优点。首先,它提供了方便性,用户可以随时随地通过网络购物。其次,它提供了更多的选择,人们可以从全球范围内的商家购买商品。最后,它通常比传统购物更便宜,因为在线商家可以减少中间环节和成本。

    • 全体的な仕組み(特にInstrument側)が単純

 

    事象の見逃しが起こりにくい

作为一个缺点,

    • エラー処理が大変

Collector側の処理オーバーフロー、システムダウン等
Collector側の不具合がInstrument側に影響を及ぼす場合がある(送信エラーのリトライ等による余分な負荷等)
間にBrokerを置く等、階層化することによって軽減は可能

Instrument側がCollector側に依存することになる

Collector側の情報(宛先等)を知っている必要がある

拉动情节

这是一种以接收器端的信息获取需求作为触发器来获取值的方式。
Prometheus采用了这种方式。
在SNMP中,这称为轮询。

pull-sequence.png

以下是利点的一些方面:

    • Collector側の負荷制御がしやすい

問い合わせに来なければInstrument側の負荷は無い

エラー処理が比較的容易
Instrument側がCollector側に依存する割合が低くなる

Instrument側はCollector側の詳細を知る必要がない

作为欠点

    • 一つのやり取りだけ見ればInstrument側のオーバーヘッドはpush型よりも大きくなりがち

普通Collectorは高頻度で問い合わせはしないので、多くの場合全体的な負荷は減る

Instrument側の仕組みが複雑になりがち

Instrument側にCollectorを受け入れる口を作る必要がある(prometheusのexporterはHttpListenerかASP.NET Coreを使って口を作るようにしている)

pullタイミングによっては事象を見逃す可能性がある

CPUのスパイク現象等

Collector側が自重しないと結局Instrument側が過負荷になりやすい

与EventCounter的相关性

实现这种类似机制的是.NET Framework时代就存在的EventCounter(根据pull场景而定,还有PollingCounter,需要netstandard2.1或更高版本)。
然而,这个仅与EventSource强相关的机制,在实现opentelemetry的机制时存在一些不便之处。
因此,我们纯粹地使用托管代码进行重新设计和实现,以便更容易地与opentelemetry集成,这就是System.Diagnostics.Metrics。

此外,有一种名为MetricsEventSource的方法可以使用EventSource来处理Metrics事件。由于在本文中进行详细描述会变得冗长,因此本次将省略详细说明。

在MetricsEventSource中,需要注意各个仪器的处理方式不同,如对Counter和ObservableCounter进行了集计处理。

与DiagnosticSource的关联

就像兄弟一样占据的立场,其实在DiagnosticSource中也可以实现push场景。

    • DiagnosticSourceで扱うと想定されるオブジェクトはより汎用的なオブジェクトで、メトリックとして使うにはオーバーヘッドが大きくなることがある

 

    pullシナリオは実現が難しい

因为有这样的情况,Metrics被实施了。

引入

如果目标框架为net6.0,则无需额外添加包。如果希望在net5.0或更低版本中使用,则需要添加”6.0.0-preview.5.21301.5″或更高版本的System.Diagnostics.DiagnosticSource,以启用System.Diagnostics.Metrics中的功能。

乐器旁的流动

在进行信息传播的一方,以下都是出现在System.Diagnostics.Metrics下的。

Meter

Instrumentの親となるオブジェクト

Counter

pushシナリオで、増分を記録するためのもの

Histogram

pushシナリオで、その時の値を記録するためのもの

ObservableCounter

pullシナリオで、増分を記録するためのもの

ObservableGauge

pullシナリオで、その時の値を記録するためのもの

简单概括这个流程的话,

    1. 创建Meter对象

 

    1. 创建各种Instrument对象

 

    1. 触发事件

通过Meter.Dispose销毁各种Instrument对象

销毁并非必需。

创建一个Meter对象

使用 new System.Diagnostics.Metrics.Meter(string name, string? version) 构建一个基本的 Meter 对象。
需要注意的是,由于它会被注册到全局列表中,在大多数情况下应该将其设置为 static readonly,以免造成大量生成。
在使用完毕后,如果调用 Dispose 方法,会将其从全局列表中移除(如果它的生命周期与应用程序生命周期相同,则不一定非得调用 Dispose 方法)

仪器的制作

使用前面创建的Meter对象来创建各种仪表。
可在6.0时点创建的有以下选项:

    • Counter where T: struct

 

    • Histogram where T: struct

 

    • ObservableCounter where T: struct

 

    ObservableGauge where T: struct

这个类型是从Instrument派生而来的。

在T中可以选择的类型如下:

    • byte

 

    • short

 

    • int

 

    • long

 

    • float

 

    • double

 

    decimal

基本上,如果使用其他类型,而不是基本数值类型,那么在创建时会引发InvalidOperationException异常。关于每种仪器的说明将在稍后提供。

在中文中引述以下的句子(只需一個選項):
无论什么情况下都可以这样说,由于在Create的时候程序会在内部触发创建事件,因此最好使用单例模式来运行。
此外,一旦Meter被Dispose,Instrument也无法使用,因此需要注意它们的生存期。

乐器

成为所有计数器的基类。
作为公开属性拥有的包括:

名前型説明Namestring名前Descriptionstring説明(ユーザー任意)Enabledbool監視しているリスナーがいるかどうかIsObservableboolObservableかどうか(pullシナリオ用かどうか)MeterMeter親となるMeterのインスタンスUnitstring単位を表す文字列(req/s,KB等)

有一个。

仪器 )

派生类用于push场景的基于Instrument类的派生类。
新增了用于触发事件的受保护方法RecordMeasurement。
该方法的第一个参数用于传入值,其后可以传入KeyValuePair<string, object?>类型的标签数据。

计数器

从Instrument派生而来。
这是一个用于记录增量的度量类(如总请求数)。我公开了void Add(T delta)方法和一个带有额外元数据的重载方法。
尽管称之为Counter,但请注意它并不单独保存总计值。
期望将增量值放入自上次发送的值中。

他正在看电视。
答: 他正在观看电视。

class C1
{
    static readonly Meter _M1 = new Meter("m1");
    // unitとdescriptionは必須ではない
    static readonly Counter<int> _C1 = _M1.CreateCounter<int>("c1", "unit", "description");
    public void Method1()
    {
       // processing
       if(_C1.Enabled)
       {
          // 有効ならば適当な値を入れる
          _C1.Add(1);
       }
    }
}

T的直方图

继承自Instrument。
这是一个用于记录非单调递增数值(如req/s等)的度量类。它公开了void Record(T measurement)方法和一个附加元数据的重载方法。
你可能会问Counter和Metric之间有什么区别,但从单个角度来看,两者的区别仅仅是公开方法的命名不同。
然而,在后面提到的MetricsEventSource中,它们被视为不同的事件,因此最好根据需要选择使用。

我喜欢看电影。

class C1
{
    static readonly Meter _M1 = new Meter("m1");
    // unitとdescriptionは必須ではない
    static readonly Histogram<int> _H1 = _M1.CreateHistogram<int>("h1", "unit", "description");
    public void Method1()
    {
       // processing
       if(_H1.Enabled)
       {
          // 有効ならば適当な値を入れる
          _H1.Record(10);
       }
    }
}

可观察仪器 (Kě )

派生类用于Pull场景,派生自Instrument类。
除了Instrument.IsObservable变为true之外,还定义了protected abstract IEnumerable<Measurement> Observe()方法。
Measurement是由T Value和ReadonlySpan Tags组成的结构体。

ObservableCounter 观察计数器

可观察仪器 T 的派生类。这是一个度量类,用于记录增量。通过 Meter.CreateObservableCounter() 创建,需要指定一个回调函数来返回值。根据我的理解,它与 Counter 不同,似乎期望返回当前的总计值,而不是增量,这是我根据 MetricsEventSource 观察得出的理解。

请问一下,餐厅里有没有素食选项?

class C1
{
    static readonly Meter _M1 = new Meter("m1");
    static int _CachedValue = 0;
    // unitとdescriptionは必須ではない
    static readonly ObservableCounter<int> _OC1 = _M1.CreateObservableCounter<int>("oc1", () => _CachedValue, "unit", "description");
    public void Method1()
    {
       // メソッド呼び出し回数を想定
       Interlocked.Increment(ref _CachedValue);
    }
}

可观察的测量表

通过ObservableInstrument创建派生类。
用于记录观察时刻的值的度量类。通过Meter.CreateObservableGauge()创建,并指定回调函数以返回该值。

与Counter和Histogram的关系相同,虽然在结构上有所不同,但在MetricsEventSource中被视为不同的事件处理方式只是名称的区别。

I have a lot of homework to do tonight, so I won’t be able to go out with friends.

class C1
{
    static readonly Meter _M1 = new Meter("m1");
    static int _CachedValue = 0;
    static readonly Random _r = new Random();
    // unitとdescriptionは必須ではない
    static readonly ObservableGauge<int> _OG1 = _M1.CreateObservableCounter<int>("og1", () => _CachedValue, "unit", "description");
    public void Method1()
    {
       // ランダムな値を入れると想定
       _CachedValue = _r.Next(100));
    }
}

推荐的使用方法

    • Meter及び各種Instrumentの名前は一意に

後述するCollector側で監視対象を判別するため

Prometheusのガイドライン等、有名どころのドキュメントを参考にするのが吉

Instrumentオブジェクトはprivateないしinternalに

外部から勝手にイベントが追加されるのを防ぐため

pushシナリオの場合、イベントを発生させる前に必ずEnabledをチェックする

オーバーヘッド、過負荷の軽減のため

收藏家方面的趋势

以下为描述Collector流程的内容。
由于此处描述的是进程内的情况,
需要注意实际上可以考虑从Collector流向其他Collector的数据传送。

生成 MeterListener

使用新的 System.Diagnostics.Metrics.MeterListener() 语句创建一个监听器对象。
由于在接下来的 Start 时刻,MeterListener 将被注册到程序的全局列表中,因此最好使用单例模式进行管理。

设置要监视的仪器

首先,我们进行设置,确定要监视的仪器。
由于MeterListener中有一个名为InstrumentPublished的成员,类型为Action<Instrument, MeterListener>,所以如果要将其作为监视对象,我们需要执行传入的MeterListener的EnableMeasurementEvents(Instrument, object?)方法。
如果不加入,则不执行任何操作。
第二个参数用于指定在事件发生时要传递的回调对象(可为null)。

请你帮我找出手机在哪里。(Can you help me find my phone?)

var listener = new MeterListener();
listener.InstrumentPublished = (inst, l) =>
{
   if(inst.Name == "Abc" && inst.Meter.Name == "Meter1")
   {
      // 外側のlistenerは使わない
      l.EnableMeasurementEvents(inst, null);
   }
};

设置事件发生时的处理方式

要设置处理事件发生时的操作,可以使用MeterListener的MeterListener.SetMeasurementEventCallback(MeasurementCallback)方法。
MeasurementCallback的类型为void MeasurementCallback(Instrument inst, T measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)。
每个参数的含义如下:

Instrument inst: イベントを発生させたInstrumentインスタンス

T measurement: イベント発生時で指定された値(Counterならdelta等)

ReadOnlySpan<KeyValuePair<string, object?>> tags: イベント発生時に指定されたメタデータ

object? state: EnableMeasurementEventsで指定されたstate

他正在跑步。

using var listener = new MeterListener();
listener.SetMeasurementEventCallback<long>((inst, measurement, tags, state) =>
                                           Console.WriteLine($"{inst.Name}: {measurement}"));

如果在Observable的回调中返回IEnumerable<Measurement>,则会多次调用回调函数。

在指定SetMeasurementEventCallback时,必须严格匹配Instrument侧的T类型和要求设置的T类型。如果不匹配,将会忽略回调函数。

解除监视时的设置

如果希望在Collector停止监视时执行某种回调操作,可以将其设置为Action <Instrument,object?> MeterListener.MeasurementsCompleted。
第一个参数是之前监视的Instrument实例,第二个参数是在启用时传递的状态实例。

开始监视

只是进行SetMeasurementEventCallback不会开始监视。
要启动监视,需要执行MeterListener.Start()。
在这时,如果有注册的Instrument对象,将调用在InstrumentPublished中设置的回调函数,
在其中调用EnableMeasurementEvents,监视将开始。

实际上,即使不使用Start()直接调用EnableMeasurementEvents,监视也会开始,但通常情况下,Instrument通常是私有或内部对象,所以预计的使用方式是通过从Start()的回调函数中进行设置。

获取ObservableInstrument系列(拉取场景)

ObservableCounter和ObservableGauge在默认情况下不会发生值获取事件。
那么该怎么办呢?你需要在MeterListener端执行void RecordObservableInstruments()。
当执行此操作时,会调用在创建Observable时指定的回调函数,并根据返回的值执行通过SetMeasurementEventCallback指定的处理程序。

他喜欢吃汉堡。
选项: 他爱吃汉堡。

using var m1 = new Meter("Meter1");
using var listener = new MeterListener();
listener.SetMeasurementEventCallback<int>((inst, measurement, tags, state) =>
                                          Console.WriteLine($"{inst.Name}: {measurement}"));
// Instrumentの設定等
var oc1 = m1.CreateObservableCounter<int>("observablecounter1", () => 1);
// RecordObservableInstrumentsが呼ばれると、"() => 1"が呼ばれ、
// SetMeasurementEventCallbackで設定されたイベントが発生し、"observablecounter1: 1"が出力される
listener.RecordObservableInstruments();

停止监视

如果想要停止监视的话,

MeterListener.DisableMeasurementEvents(Instrument)を呼ぶ
MeterListenerをDisposeする

有两种方法,但为了禁用,需要Instrument对象,所以通常会使用Dispose。
与Instrument不同,收集器通常需要执行一些结束处理(例如断开连接或关闭各种句柄),因此在这里最好预先确定生存期。

当停止监视时,将对每个仪器调用在MeasurementsCompleted中设置的回调函数。

总结

有关指标的总结。
我个人想要注意的是什么。

    • Instrument側もCollector側も両方シングルトンで動かす

 

    • 名前は一意に

 

    • Instrument側でpullシナリオかpushシナリオか決める

 

    扱う値の型を確定させておく

可能在附近吧。

虽然这次省略了对MetricsEventSource的说明,但我想在另一篇文章中可以写到。可能在使用dotnet-counter等观测工具时会需要它。另外,如果心情好的话,也许会写一些关于与促成API引入的opentelemetry的协作的内容。

请提供参考链接。

OpenTelemetryにおけるMetricsについての仕様

概念レベルでよくわからなくなってきたらここ

OpenTelemetryのdotnet実装

Metricsを使うようになったのは1.2.0-alpha1から

dotnet/runtimeのソース

Prometheus

pullシナリオをサポートする代表的OSSプロダクト

广告
将在 10 秒后关闭
bannerAds