前言:
前面兩篇文章都介紹了.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()
,其值分別為 1
、2
和 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