Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9747

計量的な統計を扱うためのSystem.Diagnostics.Metrics API

$
0
0
はじめに dotnet-6.0 preview5より、System.Diagnostics.Metrics名前空間と、配下にAPIが追加された。 今後使うことになりそうかなあと思ったので、これについて解説しようと思う。 記事執筆時点での最新版はdotnet-6.0pre7なので、これをベースに解説する。 何をするためのものか 短く言うと、"プログラムにおける計量的な統計情報を扱うための仕組み"というべきだろうか。 ここでいう計量的な統計情報とは、"平均CPU使用率"とか、"平均秒間リクエスト数"とか、"秒間IO書き込みデータ量"とか、 とにかく数字で表せる統計情報を指す。 概念的には、PrometheusやMackerel的なものを扱っている人であれば、馴染みは深いと思う。 Windowsユーザーであれば、パフォーマンスモニター(perfmon.exe)で収集できるような情報と言えばわかりやすいだろうか。 概念的には、OpenTelemetryのMetricsをdotnetの中で実現するための基盤となる。 実際、OpenTelemetryのdotnetライブラリも、PrometheusのExporterは1.2.0-alpha1以降こちらを使うようになっている。 また、ConsoleExporterもこのバージョンからMetricsに対応している。 想定するシナリオ 登場人物としては、大雑把に言うと情報を発信する側(Instrument、Publisher)と、情報を受け取って処理する側(Collector)が存在する。 pushシナリオ こちらは、Instrumentが、Collectorに情報を発信するということを想定する。 仕組みとしては割と単純になる。 利点としては、 全体的な仕組み(特にInstrument側)が単純 事象の見逃しが起こりにくい 欠点としては、 エラー処理が大変 Collector側の処理オーバーフロー、システムダウン等 Collector側の不具合がInstrument側に影響を及ぼす場合がある(送信エラーのリトライ等による余分な負荷等) 間にBrokerを置く等、階層化することによって軽減は可能 pullシナリオ こちらは、 Collector側の情報取得の要求をトリガーとして値を取得する という方式である。 Prometheusはこの方式である。 利点としては Collector側の負荷制御がしやすい 問い合わせに来なければInstrument側の負荷は無い エラー処理が比較的容易 欠点としては 一つのやり取りだけ見れば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となる。 また、MetricsのイベントをEventSourceで取り扱うための MetricsEventSource というものがあるが、 この記事で記述すると長大になってしまうため、今回は詳しい説明は省略する。 MetricsEventSourceの中では、Counter<T>、ObservableCounter<T>の集計を行っている等、各Instrumentの扱いが異なるので注意 DiagnosticSourceとの関連 立ち位置的には兄弟のようなもので、pushシナリオは実はDiagnosticSourceでも実現可能。 しかし、 DiagnosticSourceで扱うと想定されるオブジェクトはより汎用的なオブジェクトで、メトリックとして使うにはオーバーヘッドが大きくなることがある pullシナリオは実現が難しい という事情があるため、Metricsが実装された。 導入 対象とするTargetFrameworkがnet6.0であれば、特に追加パッケージは必要ない。 net5.0あるいはそれ以下で使いたい場合は、System.Diagnostics.DiagnosticSourceの"6.0.0-preview.5.21301.5"以降を追加すれば、System.Diagnostics.Metrics以下が使えるようになる。 Instrument側の流れ 情報発信を行う側で登場するのは以下で、全てSystem.Diagnostics.Metrics配下に存在する。 Meter Instrumentの親となるオブジェクト Counter<T> pushシナリオで、増分を記録するためのもの Histogram<T> pushシナリオで、その時の値を記録するためのもの ObservableCounter<T> pullシナリオで、増分を記録するためのもの ObservableGauge<T> pullシナリオで、その時の値を記録するためのもの 簡単に流れを書くと、 Meterオブジェクトの作成 各種Instrumentの作成 イベントの発生 Meter.Disposeで各種Instrumentオブジェクトの破棄 破棄は必須ではない Meterオブジェクトの作成 new System.Diagnostics.Metrics.Meter(string name, string? version)で、大元となるMeterオブジェクトを作成する。 注意点として、グローバルなリストに登録されるため、多くの場合でstatic readonlyにして、大量生成されないようにする 使い終わったらDisposeすれば、グローバルなリストからは外される(生存期間がアプリケーションのライフタイムと一緒ならば、Disposeは必ずしもしなくていい) Instrumentの作成 前項で作成したMeterオブジェクトから各種Instrumentを作成する。 6.0時点で作成可能なものは以下の通り Counter<T> where T: struct Histogram<T> where T: struct ObservableCounter<T> where T: struct ObservableGauge<T> where T: struct 上記の型は、Instrument<T> から派生している。 Tで取り得る型は以下の通り byte short int long float double decimal 要するに基本の数値型で、他の型を使おうとすると、Create時にInvalidOperationExceptionが発生する。 それぞれのInstrumentについての説明は後述する。 全てに共通して言えることだが、Createした時点で、プログラム内に作成イベントが通知されるため、シングルトンで運用するのが望ましい。 また、MeterがDisposeされた段階で、Instrumentも使えなくなるので、お互いの生存期間には注意すること。 Instrument 全てのカウンターのベースクラスとなる。 持っている公開プロパティとしては、 名前 型 説明 Name string 名前 Description string 説明(ユーザー任意) Enabled bool 監視しているリスナーがいるかどうか IsObservable bool Observableかどうか(pullシナリオ用かどうか) Meter Meter 親となるMeterのインスタンス Unit string 単位を表す文字列(req/s,KB等) がある。 Instrument<T> Instrumentクラスから派生した、pushシナリオ用の派生クラス。 イベント発生をさせるためのprotectedメソッドであるRecordMeasurementが追加されている。 このメソッドでは、第一引数に値を入れるが、それ以降はKeyValuePair<string, object?>なタグデータを入れることができる。 Counter<T> Instrument<T>から派生。 増分を記録していくためのメトリッククラス(総リクエスト数とか)。void Add(T measurement)と、追加でメタデータを与えるオーバーライドが公開されている。 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); } } } Histogram<T> Instrument<T>から派生。 単調増加ではない数値(req/sとか)を記録していくためのメトリッククラス。void Record(T measurement)と、追加でメタデータを与えるオーバーライドが公開されている。 Counterと何が違うのかという疑問を持つかもしれないが、単体で見ると、違いは公開メソッドの名前位である。 しかし、後述する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); } } } ObservableInstrument<T> Instrumentクラスから派生した、pullシナリオ用の派生クラス。 Instrument.IsObservableがtrueになる他、protected abstract IEnumerable<Measurement<T>> Observe()が定義されている。 Measurement<T>は、T ValueとReadonlySpan<T> Tagsで構成される構造体となる。 ObservableCounter<T> ObservableInstrument<T>から派生。 増分を記録しておくためのメトリッククラス。Meter.CreateObservableCounter<T>()で作成され、この時値を返すためのコールバックを指定する。 こちらもCounter<T>同様、総計値を保存してくれるみたいなことはないので注意。 例: 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); } } ObservableGauge<T> ObservableInstrument<T>から派生。 観測時点の値を記録するためのメトリッククラス。Meter.CreateObservableGauge<T>()で作成され、この時値を返すためのコールバックを指定する。 Counter<T>とHistogram<T>の関係と同じく、構造上異なる点は名前位なものだが、MetricsEventSourceでは異なるイベント扱いされる。 例: 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から更に他のCollectorにデータを流すことは十分に考えられることに注意。 MetricListenerの生成 new System.Diagnostics.Metrics.MetricListener()でリスナーオブジェクトを作成する。 MetricListenerも、後述するStart時点でプログラムグローバルなリストに登録されるため、シングルトンで管理するのが望ましい。 どのInstrumentを監視するかの設定 最初に、どのInstrumentを監視対象に入れるかの設定を行う。 MetricListenerにはAction<Instrument, MetricListener> InstrumentPublishedというメンバがあるので、 ここで監視対象に入れるならば、引数として渡ってきたMetricListenerのEnableMeasurementEvents(Instrument, object?)を実行する。 入れない場合はそのまま何もしないようにする。 第二引数には、イベント発生時のコールバックで渡したいオブジェクトを指定する(null可)。 例: var listener = new MetricListener(); listener.InstrumentPublished = (inst, l) => { if(inst.Name == "Abc" && inst.Meter.Name == "Meter1") { // 外側のlistenerは使わない l.EnableMeasurementEvents(inst, null); } }; イベント発生時の処理の仕方の設定 実際にイベントが来た時の処理を設定するには、MetricListenerのMetricListener.SetMeasurementEventCallback<T>(MeasurementCallback<T>)を使う。 MeasurementCallback<T>の型は、void MeasurementCallback<T>(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 MetricListener(); listener.SetMeasurementEventCallback<long>((inst, measurement, tags, state) => Console.WriteLine($"{inst.Name}: {measurement}")); Observable*のコールバックでIEnumerable<Measurement<T>>を返している場合は、コールバックが複数呼ばれる。 注意点として、Instrument側のTの型と、SetMeasurementEventCallbackで指定させるTの型は完全に一致させなければならない。 一致しない場合はコールバックが無視される。 監視解除時の設定 Collectorが監視を停止したときに何らかのコールバックを行いたい場合は、 Action<Instrument, object?> MetricListener.MeasurementsCompleted に設定する。 第一引数は監視していたInstrumentインスタンスで、第二引数はEnableしたときに渡したstateインスタンスになる。 監視の開始 SetMeasurementEventCallbackしただけでは監視は始まらない。 監視をスタートさせるには、MetricListener.Start()を行う必要がある。 この時、登録されたInstrumentオブジェクトがあると、InstrumentPublishedで設定したコールバックが呼ばれ、 その中でEnableMeasurementEventsすると監視が開始される。 実際は、Start()しないで直接EnableMeasurementEventsしても監視は開始されるが、通常Instrumentはprivateないしはinternalな オブジェクトで運用することが多いので、Startからのコールバックで設定、というのが想定する使われ方と思われる。 ObservableInstrument系の取得(pullシナリオ) ObservableCounter<T>やObservableGauge<T>は、そのままでは値取得イベントは発生しない。 ではどうすればいいかというと、MetricListener側でvoid RecordObservableInstruments()を実行する。 これを実行すると、Observable生成時に指定したコールバックが呼ばれ、返された値を元にしてSetMeasurementEventCallbackで指定した処理が実行される。 例: using var m1 = new Meter("Meter1"); using var listener = new MetricListener(); 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(); 監視の停止 監視を停止したい場合は、 MetricListener.DisableMeasurementEvents(Instrument)を呼ぶ MetricListenerをDisposeする の二種類の方法があるが、DisableするにはInstrumentオブジェクトが必要なので、普通はDisposeを使うことになるだろう。 Insturment側とは違い、Collector側は何らかの終了処理を行いたい場合が多い(接続の解除や各種ハンドルのクローズ等)ので、こちらは生存期間をなるべく 決めておいた方が良いと思う。 監視を停止すると、MeasurementsCompletedで設定したコールバックがInstrumentごとに呼ばれる。 まとめ Metricsについてまとめた。 個人的に気を付けたいのは Instrument側もCollector側も両方シングルトンで動かす 名前は一意に Instrument側でpullシナリオかpushシナリオか決める 扱う値の型を確定させておく 辺りだろうか。 MetricsEventSourceについては今回説明を省略したが、別の記事で書ければいいかなと思う。 dotnet-counterとかで観測するために多分必要になってくるし。 後、このAPI導入のきっかけとなったopentelemetryとの連携についても、気が向けば書くかもしれない。 参考リンク OpenTelemetryにおけるMetricsについての仕様 概念レベルでよくわからなくなってきたらここ OpenTelemetryのdotnet実装 Metricsを使うようになったのは1.2.0-alpha1から dotnet/runtimeのソース Prometheus pullシナリオをサポートする代表的OSSプロダクト

Viewing all articles
Browse latest Browse all 9747

Trending Articles