用于处理计量统计的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中,陷阱和通知就相当于这个。
有三个主要的优点。首先,它提供了方便性,用户可以随时随地通过网络购物。其次,它提供了更多的选择,人们可以从全球范围内的商家购买商品。最后,它通常比传统购物更便宜,因为在线商家可以减少中间环节和成本。
-
- 全体的な仕組み(特にInstrument側)が単純
- 事象の見逃しが起こりにくい
作为一个缺点,
-
- エラー処理が大変
Collector側の処理オーバーフロー、システムダウン等
Collector側の不具合がInstrument側に影響を及ぼす場合がある(送信エラーのリトライ等による余分な負荷等)
間にBrokerを置く等、階層化することによって軽減は可能
Instrument側がCollector側に依存することになる
Collector側の情報(宛先等)を知っている必要がある
拉动情节
这是一种以接收器端的信息获取需求作为触发器来获取值的方式。
Prometheus采用了这种方式。
在SNMP中,这称为轮询。
以下是利点的一些方面:
-
- 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シナリオで、その時の値を記録するためのもの
简单概括这个流程的话,
-
- 创建Meter对象
-
- 创建各种Instrument对象
-
- 触发事件
通过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也无法使用,因此需要注意它们的生存期。
乐器
成为所有计数器的基类。
作为公开属性拥有的包括:
有一个。
仪器 )
派生类用于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プロダクト