EventSource的自定義實現

chaney1992發表於2021-05-23

前言:

 前面兩篇文章都介紹了.NET Core 效能診斷工具,其中診斷工具都用到了EventCounters來實時的收集伺服器效能指標。

 那麼收集指標能否自己定義呢?

一、What's EventCounters ?

 EventCounters 是一些 .NET Core API,用於輕量級、跨平臺、準實時效能指標收集。 EventCounters 新增為 Windows 上的 .NET 框架的“效能計數器”的跨平臺替代。

 EventCounters 作為 EventSource 的一部分實時自動定期推送到偵聽器工具。 與 EventSource 上所有其他事件一樣,可以通過 EventListener 和 EventPipe 在程式內和程式外使用它們。

 

  EventCounters 分為兩類計數器: 某些計數器用於計算“比率”的值,例如異常總數、GC 總數和請求總數。 其他計數器是“快照”值,例如堆使用情況、CPU 使用率和工作集大小。 在這兩個類別的計數器中,各有兩種型別的計數器,由獲取值的方式區分。 輪詢計數器通過回撥檢索其值,非輪詢計數器直接在計數器例項上設定其值。  

 計數器由以下實現表示:

  • EventCounter:記錄一組值。 EventCounter.WriteMetric 方法將新值新增到集。 在每個間隔中,將計算集的統計摘要,如最小值、最大值和平均值。

    EventCounter 用於描述一組離散的操作。 常見用法包括監視最近 IO 操作的平均大小(以位元組為單位)或一組金融交易的平均貨幣價值。

  • IncrementingEventCounter:記錄每個時間間隔的執行總計,使用 Increment 方法新增到總計。

   例如,如果在一段間隔內呼叫三次 Increment(),其值分別為 12 和 5,則此間隔的計數器值將報告執行總計 8。 用於測量操作發生的頻率,例如每秒處理的請求數。

  • IncrementingPollingCounter:使用回撥來確定報告的值。 在每個時間間隔中,呼叫使用者提供的回撥函式,然後返回值用作計數器值。

   例如獲取磁碟上的當前可用位元組。 它還可用於報告應用程式可按需計算的自定義統計資訊。 示例包括報告最近請求延遲的第 95 個百分位,或快取的當前命中或錯過比率。

  • PollingCounter:使用回撥來確定報告的增量值。 對於每個時間間隔,呼叫回撥,然後當前呼叫與最後一個呼叫之間的差值是報告的值。

   如果不可在每次發生事件時呼叫 API,但可以查詢事件總數,則此計數器很有用。 例如,可以報告每秒寫入檔案的位元組數,即使每次寫入位元組時沒有通知。

  瞭解各種計數器後,我們就可以開始實現自己的EventCounters來跟蹤各種指標,並在自己的程式中使用

二、自定義實現EventSource

 自定義實現一個獲取.NET Core Api專案的請求總數量、獲取當前程式記憶體佔用、請求數量、請求平均耗時情況的EventSource。

 1、新增:ApiEventCounterSource.cs 實現EventSource,並新增計數器用於獲取對應資料

using System;
using System.Diagnostics.Tracing;
using System.Threading;

namespace AuditLogDemo.EventSources
{
    /// <summary>
    /// Api.EventCounter 事件源
    /// </summary>
    [EventSource(Name = "Api.EventCounter")]
    public sealed class ApiEventCounterSource : EventSource
    {
        public static readonly ApiEventCounterSource Log = new ApiEventCounterSource();

        private EventCounter _requestCounter;

        private PollingCounter _workingSetCounter;

        private PollingCounter _totalRequestsCounter;

        private IncrementingPollingCounter _incrementingPollingCounter;

        private long _totalRequests;

        private ApiEventCounterSource()
        {
        }


        protected override void OnEventCommand(EventCommandEventArgs command)
        {
            if (command.Command == EventCommand.Enable)
            {
                //請求響應耗時
                _requestCounter = new EventCounter("request-time", this)
                {
                    DisplayName = "Request Processing Time",
                    DisplayUnits = "ms"
                };

                //記憶體佔用
                _workingSetCounter = new PollingCounter("working-set", this, () => (double)(Environment.WorkingSet / 1_000_000))
                {
                    DisplayName = "Working Set",
                    DisplayUnits = "MB"
                };

                //總請求量
                _totalRequestsCounter = new PollingCounter("total-requests", this, () => Volatile.Read(ref _totalRequests))
                {
                    DisplayName = "Total Requests",
                    DisplayUnits = ""
                };

                //單位時間請求速率
                _incrementingPollingCounter = new IncrementingPollingCounter("Request Rate", this, () =>
                {
                    return Volatile.Read(ref _totalRequests);
                })
                {
                    DisplayName = "Request Rate",
                    DisplayUnits = "次/s",
                    //時間間隔1s
                    DisplayRateTimeScale = new TimeSpan(0, 0, 1)
                };
            }
        }

        public void Request(string url, float elapsedMilliseconds)
        {
            //更新請求數量(保證執行緒安全)
            Interlocked.Increment(ref _totalRequests);

            //寫入指標值(請求處理耗時)
            _requestCounter?.WriteMetric(elapsedMilliseconds);

        }

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

            _workingSetCounter?.Dispose();
            _workingSetCounter = null;

            _totalRequestsCounter?.Dispose();
            _totalRequestsCounter = null;

            _incrementingPollingCounter?.Dispose();
            _incrementingPollingCounter = null;

            base.Dispose(disposing);
        }
    }
}

 2、新增過濾器:ApiRequestTimeFilter呼叫ApiEventCounterSource方法

public class ApiRequestTimeFilterAttribute : ActionFilterAttribute
{
    readonly Stopwatch _stopwatch = new Stopwatch();
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        base.OnActionExecuting(context);
        //開啟耗時記錄
        _stopwatch.Start();
    }
    public override void OnResultExecuted(ResultExecutedContext context)
    {
        //關閉耗時記錄
        _stopwatch.Stop();
        //呼叫方法記錄耗時 
     ApiEventCounterSource.Log.Request(context.HttpContext.Request.GetDisplayUrl(), _stopwatch.ElapsedMilliseconds);
} }

 3、啟用過濾器

services.AddControllers(options =>
{    
    options.Filters.Add(typeof(ApiRequestTimeFilterAttribute));
});

三、使用 EventCounters

 1、程式外使用:

  a) 使用全域性診斷工具:

   dotnet-counters、dotnet-trace、dotnet-monitor 使用大體相同

  • 使用 dotnet-counters ps 命令來顯示可監視的程式的列表:      
  • 使用命令獲取計數器實時結果:通過--counters引數指定計數器範圍
     dotnet-counters monitor --process-id 程式id --counters Api.EventCounter,System.Runtime[cpu-usage]

  執行結果如下:

    

  b) 自實現工具:

   1、新增一個:EventPipeProvider 指定名稱為:Api.EventCounter

var providers = new List<EventPipeProvider>()
{
    new EventPipeProvider("Api.EventCounter",EventLevel.Informational,(long)ClrTraceEventParser.Keywords.None,
                                new Dictionary<string, string>() {{ "EventCounterIntervalSec", "1" }})
};

   2、執行效果如下:

    

 2、程式內使用:

  實現EventListener,獲取計數器值

public class ApiEventListener : EventListener
{
    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (!eventSource.Name.Equals("Api.EventCounter"))
        {
            return;
        }

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

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (!eventData.EventName.Equals("Api.EventCounter"))
        {
            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}");
            }
        }
    }

    /// <summary>
    /// 計數器名稱和計數器值
    /// </summary>
    /// <param name="eventPayload"></param>
    /// <returns></returns>
    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);
    }
}

 

四、總結

 自定義實現EventSource可用於當前.dotnet 未提供的指標擴充套件、也可用於業務系統指定指標實現,等著去解鎖使用。

 當然dotnet 已經提供了多種效能計數器:供一般情況使用。已知EventCounters

相關文章