.NET 中的 EventCounters

本文適用於: ✔️ .NET Core 3.0 SDK 與更新版本

EventCounters 是 .NET API,適用於輕量、跨平台和近乎即時效能的計量集合。 新增 EventCounters 之目的,是為了作為 Windows 上 .NET Framework「效能計數器」的跨平台替代方案。 在本文中,您將了解什麼是 EventCounters、如何實作 EventCounters,以及如何進行取用。

從 .NET Core 3.0 開始,.NET 執行階段和一些 .NET 程式庫,會使用 EventCounters 發佈基本診斷資訊。 除了 .NET 執行階段所提供的 EventCounters 之外,您還可以選擇實作自己的 EventCounters。 EventCounters 可用來追蹤各種計量。 如需深入了解,請參閱 .NET 中已知的 EventCounters

EventCounters 是 EventSource 的一部分,會定期自動推送至接聽程式工具。 和 EventSource 上的所有其他事件一樣,都可以透過 EventListenerEventPipe,在處理序內和處理序外,取用它們。 本文著重於 EventCounters 的跨平台功能,刻意排除了 PerfView 和 ETW (Windows 的事件追蹤) - 雖然這兩者都可以與 EventCounters 搭配使用。

EventCounters 處理序內和處理序外圖表影像

EventCounter API 概觀

EventCounters 有兩個主要類別。 某些計數器適用於「比率」值,例如例外狀況總數、GC 總數和要求總數。 其他計數器則為「快照集」值,例如堆積使用量、CPU 使用量和工作集大小。 在這些計數器類別中,有兩種類型的計數器,會因取得其值的方式而有所不同。 輪詢計數器會透過回呼擷取其值,而非輪詢計數器則會直接在計數器執行個體上設定其值。

計數器以下列實作表示:

事件接聽程式會指定測量間隔的時間長度。 每個間隔時間結束時,就會將值傳輸至每個計數器的接聽程式。 計數器的實作,會決定要使用哪些 API 和計算,來產生每個間隔的值。

  • EventCounter 會記錄一組值。 EventCounter.WriteMetric 方法會將新的值新增至該集合中。 每個間隔都會計算該集合的統計摘要,例如最小值、最大值和平均值。 dotnet-counters 工具一律會顯示平均值。 EventCounter 很適合用來描述一組離散的運算。 一般用法包含監視最近 IO 作業的位元組平均大小,或一組財務交易的平均貨幣價值。

  • IncrementingEventCounter 會記錄每個時間間隔的總計。 IncrementingEventCounter.Increment 方法會新增至總計。 例如,如果在一個間隔期間呼叫了三次 Increment(),且值為 125,則會回報總數 8 作此間隔的計數器值。 dotnet-counters 工具會將比率顯示為所記錄的「總計/時間」。 IncrementingEventCounter 可適合用來測量動作發生的頻率,例如每秒處理的要求數目。

  • PollingCounter 會使用回呼,來判斷所回報的值。 每經過一次時間間隔,都會叫用使用者提供的回呼函式,並使用傳回值作為計數器值。 PollingCounter 可用於查詢來自外部來源的計量,例如取得磁碟上目前可用的位元組。 也可以用來回報可由應用程式依需求所計算的自訂統計資料。 範例包括回報最近要求延遲的第 95 個百分位數,或快取的目前命中率或失敗率。

  • IncrementingPollingCounter 使用回呼來判斷回報的增量值。 每經過一次時間間隔,都會叫用回呼,然後叫用目前叫用之間的差異,最後叫用回報的值。 dotnet-counters 工具一律會將差異顯示為比率,也就是回報的「值/時間」。 無法每次發生就呼叫 API,但可以查詢發生的總次數時,就很適合使用此計數器。 例如,即使每次寫入位元組時都沒有通知,也可以回報告每秒寫入檔案的位元組數目。

實作 EventSource

下列程式碼會實作公開為名稱是 "Sample.EventCounter.Minimal" 提供者的範例 EventSource。 此來源包含代表要求處理時間的 EventCounter。 這類計數器的名稱 (也就是來源中的唯一識別碼) 和顯示名稱,會由像是 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);
    }
}

您可以使用 dotnet-counters ps 來顯示可監視的 .NET 處理序清單:

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 時,若呼叫 EventSource.OnEventCommand 方法時,Command 的值為 EventCommand.Enable,可以有條件地具現化包含計數器。 若要只有在計數器執行個體為 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 會回報目前對應至應用程式處理序 (工作集) 的實體記憶體數量,因為它會在某個時間點擷取計量。 輪詢值的回呼,是提供的 Lambda 運算式,只是對 System.Environment.WorkingSet API 的呼叫而已。 DisplayNameDisplayUnits 是選用屬性,設定此屬性可協助計數器的取用者端,能更清楚地顯示值。 例如,dotnet-counters 會使用這些屬性,讓計數器名稱以較容易顯示的方式呈現。

重要

屬性 DisplayName 並未當地語系化。

若為 PollingCounterIncrementingPollingCounter,則不需要執行任何動作。 這兩者都會以取用者要求的間隔,輪詢值本身。

以下是使用 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 屬性為選用屬性,但使用它可以提供一個提示,指示最適合以什麼時間間隔,顯示計數器。 例如,鎖定競爭計數最好的顯示間隔為 每秒多少個,因此其 DisplayRateTimeScale 設定為一秒。 針對不同類型的比率計數器,可以調整顯示率。

注意

dotnet-counters「不會」 使用 DisplayRateTimeScale,而且使用它時不需要事件接聽程式。

.NET 執行階段存放庫中,有更多計數器實作可用來作為參考。

並行

提示

EventCounters API 不保證執行緒安全性。 當多個執行緒呼叫傳遞至 PollingCounterIncrementingPollingCounter 執行個體的委派時,您必須負責保證委派的執行緒安全性。

例如,請考慮下列 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);

為避免在 long-field _requestCount 發生未鎖定讀取 (torn read) (32 位元架構),請使用 Interlocked.Read

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

取用 EventCounters

取用 EventCounters 有兩種主要方式:處理序內和處理序外。 取用 EventCounters 可以區分為三個層級的不同取用技術。

  • 透過 ETW 或 EventPipe 在原始串流中傳輸事件:

    Windows OS 隨附 ETW API,而 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-trace

dotnet-trace 工具可用於透過 EventPipe,取用計數器資料。 以下是使用 dotnet-trace 收集計數器資料的範例。

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

如需有關如何收集一段時間計數器值的詳細資訊,請參閱 dotnet-trace 文件。

Azure Application Insights

Azure 監視器可以取用EventCounters,特別是 Azure Application Insights。 您可以新增和移除計數器,而且可以自由指定自訂計數器或已知的計數器。 如需詳細資訊,請參閱自訂要收集的計數器

dotnet-monitor

您可利用 dotnet-monitor 工具,更輕鬆地以遠端和自動化的方式,從 .NET 處理序存取診斷。 除了追蹤之外,還可以監視計量、收集記憶體傾印,以及收集 GC 傾印。 其以 CLI 工具和 Docker 映像的形式散發。 其提供 REST API,且可透過 REST 呼叫,收集所發生的診斷成品。

如需詳細資訊,請參閱 dotnet-monitor

在同處理序內取用

您可以透過 EventListener API,取用計數器的值。 EventListener 是一種處理序內取用的方式,其取用應用程式內所有 EventSource 執行個體所寫入的任何事件。 如需如何使用 EventListener API 的詳細資訊,請參閱 EventListener

首先,必須要啟用產生該計數器值的 EventSource。 再覆寫 EventListener.OnEventSourceCreated 方法,在建立 EventSource 時取得通知,而如果對您的 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"
    });
}

範例指令碼

以下是範例 EventListener 類別,其會列印來自 .NET Runtime 的 EventSource 的所有計數器名稱和值,以每秒發佈其內部計數器 (System.Runtime)。

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" 引數。 否則計數器將無法清除值,因為它不知道應以什麼間隔進行清除。

另請參閱