前言:
前面兩篇文章都介紹了.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