.NET の EventCounters

この記事の対象: ✔️ .NET Core 3.0 SDK 以降のバージョン

EventCounters は、軽量でクロスプラットフォーム、かつ準リアルタイムのパフォーマンス メトリックの収集に使用される .NET API です。 EventCounters は、Windows 上の .NET Framework の "パフォーマンス カウンター" に代わるクロスプラットフォームとして追加されました。 この記事では、EventCounters の概要とその実装方法および使用方法について学習します。

.Net ランタイムといくつかの .NET ライブラリにより、.NET Core 3.0 以降の EventCounters を使用した、基本的な診断情報が公開されます。 .NET ランタイムによって提供される EventCounters とは別に、独自の EventCounters を実装することもできます。 EventCounters を使用して、さまざまなメトリックを追跡できます。 詳細については、「.NET の既知の EventCounter」を参照してください

EventCounters は EventSource の一部として存在し、定期的にリスナー ツールに自動的にプッシュされます。 EventSource の他のすべてのイベントと同様に、EventListenerEventPipe を介してインプロセスとアウトプロセスの両方で使用できます。 この記事では、EventCounters のクロスプラットフォーム機能に焦点を当てています。PerfView と ETW (Event Trace for Windows) は意図的に除外していますが、両方とも EventCounters で使用できます。

EventCounters のインプロセスとアウトプロセスの図のイメージ

EventCounter API の概要

EventCounter には主に 2 つのカテゴリがあります。 一部のカウンターは、例外の合計数、GC の合計数、要求の合計数などの "比率" の値用です。 その他のカウンターは、ヒープ使用率、CPU 使用率、ワーキング セット サイズなどの "スナップショット" の値です。 これらのカウンターの各カテゴリ内には、値の取得方法によって異なる 2 種類のカウンターがあります。 ポーリング カウンターによって、コールバックを介して値が取得され、ポーリング以外のカウンターの値は、カウンター インスタンスで直接設定されます。

これらのカウンターは、次の実装によって表されます。

イベント リスナーによって、測定間隔の長さが指定されます。 各間隔の最後に、各カウンターのリスナーに値が送信されます。 カウンターの実装によって、各間隔の値を生成するために使用される API と計算が決まります。

  • EventCounter によって、値のセットが記録されます。 EventCounter.WriteMetric メソッドによって、新しい値がセットに追加されます。 各間隔では、最小値、最大値、平均値など、セットの統計概要が計算されます。 dotnet-counters ツールによって、常に平均値が表示されます。 EventCounter は、個別の操作セットを記述する場合に役立ちます。 一般的な用途には、最近の IO 操作の平均サイズ (バイト単位) や、金融取引セットの平均額の監視が含まれる場合があります。

  • IncrementingEventCounter によって、各時間間隔の累計が記録されます。 IncrementingEventCounter.Increment メソッドによって、合計に加算されます。 たとえば、値が 12、および 5 に指定され、1 つの間隔で Increment() が 3 回呼び出された場合、8 の累計は、この間隔のカウンター値として報告されます。 dotnet-counters ツールにより、記録された合計/時間として比率が表示されます。 IncrementingEventCounter は、1 秒あたりに処理された要求の数など、アクションの発生頻度を測定するのに役立ちます。

  • PollingCounter によってコールバックが使用され、報告される値が決定されます。 各時間間隔で、ユーザーが指定したコールバック関数が呼び出され、戻り値がカウンター値として使用されます。 PollingCounter を使用すると、ディスク上の現在の空きバイト数を取得するなど、外部ソースからのメトリックに対してクエリを実行できます。 また、アプリケーションで要求時に計算できるカスタム統計を報告するために使用できます。 たとえば、最近の要求待機時間の 95 パーセンタイルや、キャッシュの現在のヒットまたはミス率の報告などです。

  • IncrementingPollingCounter によってコールバックが使用され、報告される増分値が決定されます。 各時間間隔で、コールバックが呼び出され、現在の呼び出しと最後の呼び出しの差が報告される値となります。 dotnet-counters ツールにより、比率 (報告された値/時間) として常に差が表示されます。 このカウンターは、発生のたびに API を呼び出すことができない場合に便利ですが、発生の合計回数に対してクエリを実行することもできます。 たとえば、バイトが書き込まれるたびに通知しなくても、1 秒あたりにファイルに書き込まれたバイト数を報告することができます。

EventSource を実装する

次のコードにより、名前付きの "Sample.EventCounter.Minimal" プロバイダーとして公開されるサンプルの EventSource が実装されます。 このソースには、要求の処理時間を表す EventCounter が含まれています。 このようなカウンターには、名前 (つまり、ソースのその一意の ID) と表示名があり、両方とも dotnet-counters などのリスナー ツールで使用されます。

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Minimal")]
public sealed class MinimalEventCounterSource : EventSource
{
    public static readonly MinimalEventCounterSource Log = new MinimalEventCounterSource();

    private EventCounter _requestCounter;

    private MinimalEventCounterSource() =>
        _requestCounter = new EventCounter("request-time", this)
        {
            DisplayName = "Request Processing Time",
            DisplayUnits = "ms"
        };

    public void Request(string url, long elapsedMilliseconds)
    {
        WriteEvent(1, url, elapsedMilliseconds);
        _requestCounter?.WriteMetric(elapsedMilliseconds);
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

監視できる .NET プロセスの一覧を表示するには、dotnet-counters ps を使用します。

dotnet-counters ps
   1398652 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399072 dotnet     C:\Program Files\dotnet\dotnet.exe
   1399112 dotnet     C:\Program Files\dotnet\dotnet.exe
   1401880 dotnet     C:\Program Files\dotnet\dotnet.exe
   1400180 sample-counters C:\sample-counters\bin\Debug\netcoreapp3.1\sample-counters.exe

カウンターの監視を開始するには、EventSource の名前を --counters オプションに渡します。

dotnet-counters monitor --process-id 1400180 --counters Sample.EventCounter.Minimal

次の例は監視出力を示しています。

Press p to pause, r to resume, q to quit.
    Status: Running

[Samples-EventCounterDemos-Minimal]
    Request Processing Time (ms)                            0.445

監視コマンドを停止するには、q キーを押します。

条件付きカウンター

EventSource を実装する場合、Command の値として EventCommand.Enable を指定して、EventSource.OnEventCommand メソッドが呼び出されると、格納先のカウンターを条件付きでインスタンス化できます。 null である場合にのみ、カウンター インスタンスを安全にインスタンス化するには、null 合体代入演算子を使用します。 さらに、カスタム メソッドで IsEnabled メソッドを評価して、現在のイベント ソースが有効になっているかどうかを判断できます。

using System.Diagnostics.Tracing;

[EventSource(Name = "Sample.EventCounter.Conditional")]
public sealed class ConditionalEventCounterSource : EventSource
{
    public static readonly ConditionalEventCounterSource Log = new ConditionalEventCounterSource();

    private EventCounter _requestCounter;

    private ConditionalEventCounterSource() { }

    protected override void OnEventCommand(EventCommandEventArgs args)
    {
        if (args.Command == EventCommand.Enable)
        {
            _requestCounter ??= new EventCounter("request-time", this)
            {
                DisplayName = "Request Processing Time",
                DisplayUnits = "ms"
            };
        }
    }

    public void Request(string url, float elapsedMilliseconds)
    {
        if (IsEnabled())
        {
            _requestCounter?.WriteMetric(elapsedMilliseconds);
        }
    }

    protected override void Dispose(bool disposing)
    {
        _requestCounter?.Dispose();
        _requestCounter = null;

        base.Dispose(disposing);
    }
}

ヒント

条件付きカウンターは、条件付きでインスタンス化 (マイクロ最適化) されるカウンターです。 ランタイムにより、わずかな時間を節約するために、通常はカウンターが使用されないシナリオでこのパターンが採用されます。

.NET Core ランタイムのカウンター例

.NET Core ランタイムには、多くの優れた実装例があります。 アプリケーションのワーキング セット サイズを追跡するカウンターのランタイム実装を以下に示します。

var workingSetCounter = new PollingCounter(
    "working-set",
    this,
    () => (double)(Environment.WorkingSet / 1_000_000))
{
    DisplayName = "Working Set",
    DisplayUnits = "MB"
};

PollingCounter により、アプリのプロセス (ワーキング セット) にマップされる物理メモリの現在の量が報告されます。これは、ある時点のメトリックがキャプチャされるためです。 値をポーリングするためのコールバックは、指定されたラムダ式であり、単に System.Environment.WorkingSet API の呼び出しです。 DisplayName および DisplayUnits は、カウンターのコンシューマー側でより明確に値を表示するのに役立つように設定できる省略可能なプロパティです。 たとえば、よりわかりやすいバージョンのカウンター名を表示するために、dotnet-counters によってこれらのプロパティが使用されます。

重要

DisplayName のプロパティはローカライズされません。

PollingCounter、および IncrementingPollingCounter では、他に何も行う必要はありません。 いずれの場合も、コンシューマーによって要求された間隔で値自体がポーリングされます。

IncrementingPollingCounter を使用して実装されたランタイム カウンターの例を以下に示します。

var monitorContentionCounter = new IncrementingPollingCounter(
    "monitor-lock-contention-count",
    this,
    () => Monitor.LockContentionCount
)
{
    DisplayName = "Monitor Lock Contention Count",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

ロック競合の合計数の増加を報告するために、IncrementingPollingCounter によって Monitor.LockContentionCount API が使用されます。 DisplayRateTimeScale プロパティは省略可能ですが、これを使用すると、カウンターが最適に表示される時間間隔に関するヒントを提供できます。 たとえば、ロック競合数は、"1 秒あたりの数" として最適に表示されるので、その DisplayRateTimeScale は 1 秒に設定されます。 表示率は、さまざまな種類の比率カウンターに合わせて調整できます。

注意

DisplayRateTimeScaledotnet-counters によって使用 "されません"。また、それを使用するためにイベント リスナーは必要ありません。

.NET ランタイム リポジトリで参照として使用するカウンター実装は他にもあります。

コンカレンシー

ヒント

EventCounters API によるスレッド セーフは保証されません。 PollingCounter または IncrementingPollingCounter インスタンスに渡された委任が複数のスレッドによって呼び出された場合、委任のスレッドセーフを保証するのはお客様の責任となります。

たとえば、要求を追跡する以下の EventSource について考えてみます。

using System;
using System.Diagnostics.Tracing;

public class RequestEventSource : EventSource
{
    public static readonly RequestEventSource Log = new RequestEventSource();

    private IncrementingPollingCounter _requestRateCounter;
    private long _requestCount = 0;

    private RequestEventSource() =>
        _requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => _requestCount)
        {
            DisplayName = "Request Rate",
            DisplayRateTimeScale = TimeSpan.FromSeconds(1)
        };

    public void AddRequest() => ++ _requestCount;

    protected override void Dispose(bool disposing)
    {
        _requestRateCounter?.Dispose();
        _requestRateCounter = null;

        base.Dispose(disposing);
    }
}

AddRequest() メソッドは要求ハンドラーから呼び出すことができ、カウンターのコンシューマーによって指定された間隔で、RequestRateCounter により値がポーリングされます。 しかし、AddRequest() メソッドは、一度に複数のスレッドによって呼び出すことができ、_requestCount で競合状態が発生します。 _requestCount を増分させるスレッドセーフな別の方法は、Interlocked.Increment を使用することです。

public void AddRequest() => Interlocked.Increment(ref _requestCount);

(32 ビット アーキテクチャでの) long-field _requestCount の読み取りの欠落を防ぐには、Interlocked.Read を使用します。

_requestRateCounter = new IncrementingPollingCounter("request-rate", this, () => Interlocked.Read(ref _requestCount))
{
    DisplayName = "Request Rate",
    DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};

EventCounters を使用する

EventCounters を使用する場合、インプロセスとアウトプロセスという 2 つの主な方法があります。 EventCounters の使用は、さまざまな使用テクノロジの 3 つの層に分類できます。

  • ETW または EventPipe を介した生のストリームでのイベントの転送:

    ETW API は Windows OS に付属しています。EventPipe には .NET API、または診断 IPC プロトコルとしてアクセスできます。

  • バイナリ イベント ストリームのイベントへのデコード:

    TraceEvent ライブラリにより、ETW と EventPipe の両方のストリーム形式が処理されます。

  • コマンド ラインと GUI ツール:

    PerfView (ETW または EventPipe)、dotnet-counters (EventPipe のみ)、dotnet-monitor (EventPipe のみ) などのツール。

アウトプロセスで使用する

EventCounters をアウトプロセスで使用することは、一般的な方法です。 dotnet-counters を使って、EventPipe を介してクロスプラットフォーム方式でそれらを使用することができます。 dotnet-counters ツールは、カウンターの値を監視するために使用できるクロスプラットフォームの dotnet CLI グローバル ツールです。 dotnet-counters を使用してカウンターを監視する方法を確認する場合は、「dotnet-counters」を参照するか、EventCounters を使用するパフォーマンスの測定に関するチュートリアルを実行してください。

dotnet-トレース

dotnet-trace ツールを使って、EventPipe を介してカウンター データを使用することができます。 カウンター データを収集するために dotnet-trace を使用する例を以下に示します。

dotnet-trace collect --process-id <pid> Sample.EventCounter.Minimal:0:0:EventCounterIntervalSec=1

経時的なカウンター値を収集する方法の詳細については、dotnet-trace に関するドキュメントを参照してください。

Azure Application Insights

EventCounters は Azure Monitor、つまり、Azure Application Insights で使用できます。 カウンターを追加したり、削除したりすることができます。また、カスタム カウンターや既知のカウンターを自由に指定することができます。 詳細については、「収集されるカウンターのカスタマイズ」を参照してください。

dotnet-monitor

この dotnet-monitor ツールを使用すると、リモートかつ自動化された方法で .NET プロセスから診断に簡単にアクセスできます。 トレースに加えて、メトリックの監視、メモリ ダンプの収集、および GC ダンプの収集を行うことができます。 CLI ツールと Docker イメージの両方として配布されます。 これによって REST API が公開され、診断アーティファクトの収集は REST 呼び出しを介して行われます。

詳細については、dotnet-monitor に関するページを参照してください。

インプロセスで使用する

EventListener API を介して、カウンター値を使用することができます。 EventListener は、アプリケーション内の EventSource のすべてのインスタンスによって書き込まれるすべてのイベントを使用するインプロセスの方法です。 EventListener API の使用方法の詳細については、EventListener に関するページを参照してください。

まず、カウンター値を生成する EventSource を有効にする必要があります。 EventSource が作成されたときに通知を受け取るように EventListener.OnEventSourceCreated メソッドをオーバーライドします。これが EventCounters に適切な EventSource である場合は、それに対して EventListener.EnableEvents を呼び出すことができます。 オーバーライドの例を以下に示します。

protected override void OnEventSourceCreated(EventSource source)
{
    if (!source.Name.Equals("System.Runtime"))
    {
        return;
    }

    EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
    {
        ["EventCounterIntervalSec"] = "1"
    });
}

サンプル コード

以下は、内部カウンター (System.Runtime) を 1 秒ごとに公開するために、.NET ランタイムの EventSource からすべてのカウンター名前と値を出力する EventListener クラスのサンプルです。

using System;
using System.Collections.Generic;
using System.Diagnostics.Tracing;

public class SimpleEventListener : EventListener
{
    public SimpleEventListener()
    {
    }

    protected override void OnEventSourceCreated(EventSource source)
    {
        if (!source.Name.Equals("System.Runtime"))
        {
            return;
        }

        EnableEvents(source, EventLevel.Verbose, EventKeywords.All, new Dictionary<string, string>()
        {
            ["EventCounterIntervalSec"] = "1"
        });
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (!eventData.EventName.Equals("EventCounters"))
        {
            return;
        }

        for (int i = 0; i < eventData.Payload.Count; ++ i)
        {
            if (eventData.Payload[i] is IDictionary<string, object> eventPayload)
            {
                var (counterName, counterValue) = GetRelevantMetric(eventPayload);
                Console.WriteLine($"{counterName} : {counterValue}");
            }
        }
    }

    private static (string counterName, string counterValue) GetRelevantMetric(
        IDictionary<string, object> eventPayload)
    {
        var counterName = "";
        var counterValue = "";

        if (eventPayload.TryGetValue("DisplayName", out object displayValue))
        {
            counterName = displayValue.ToString();
        }
        if (eventPayload.TryGetValue("Mean", out object value) ||
            eventPayload.TryGetValue("Increment", out value))
        {
            counterValue = value.ToString();
        }

        return (counterName, counterValue);
    }
}

前述のように、EnableEvents を呼び出す場合は、filterPayload 引数に "EventCounterIntervalSec" 引数が設定されていることを確認する "必要があります"。 そうしないと、カウンターによってフラッシュする必要がある間隔が把握されないため、値をフラッシュできなくなります。

関連項目