.NET Core 服務診斷工具

chaney1992發表於2021-05-16

前言:

 前一篇文中介紹了.NET Core-全域性效能診斷工具 的使用方法,那麼接下來自己實現一個簡單.NET Core的診斷工具。

 該工具主要包括:.NET Core 程式程式資訊檢視、效能計數器結果獲取、Dump抓取、Trace 檔案生成等一些基本功能

 本文主要採用:Microsoft.Diagnostics.NETCore.Client 庫來實現相關功能

一、Microsoft.Diagnostics.NETCore.Client 介紹:

 簡介:

  Microsoft.Diagnostics.NETCore.Client(也稱為Diagnostics客戶端庫)是一個託管庫,可讓您與.NET Core執行時(Core CLR)進行互動以執行各種與診斷相關的任務,

  例如:跟蹤,請求轉儲或附加ICorProfiler 。使用此庫,您可以編寫針對特定情況定製的自己的診斷工具。

 安裝方式:

Install-Package Microsoft.Diagnostics.NETCore.Client

二、工具實現:

 1、建立專案:DiagnosticsTools(.NET 5.0 Winform專案)

  新增包引用:

Install-Package Microsoft.Diagnostics.NETCore.Client
Install-Package Microsoft.Diagnostics.Tracing.TraceEvent 

  專案結構:

   

  調整窗體介面如下:

   

 2、獲取當前所有.Net Core 3.0及以上的程式列表

/// <summary>
/// 獲取程式狀態:.Net Core 3.0及以上程式
/// </summary>
private void PrintProcessStatus()
{
   //定位上次記錄
int row = dgvPros.CurrentCell == null ? 0 : dgvPros.CurrentCell.RowIndex; int col = dgvPros.CurrentCell == null ? 0 : dgvPros.CurrentCell.ColumnIndex; var data = DiagnosticsClient.GetPublishedProcesses() .Select(Process.GetProcessById) .Where(process => process != null) .Select(o => { return new { o.Id, o.ProcessName, o.StartTime, o.Threads.Count }; }); dgvPros.DataSource = data.ToList(); if (dgvPros.Rows.Count > row) dgvPros.CurrentCell = dgvPros.Rows[row].Cells[col]; }

 3、獲取當前程式基本資訊:

private string GetProInfo(Process info)
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("程式影像名:" + info.ProcessName + "\r\n");
    stringBuilder.Append("程式ID:" + info.Id + "\r\n");
    stringBuilder.Append("啟動執行緒樹:" + info.Threads.Count.ToString() + "\r\n");
    stringBuilder.Append("CPU佔用時間:" + info.TotalProcessorTime.ToString() + "\r\n");
    stringBuilder.Append("執行緒優先順序:" + info.PriorityClass.ToString() + "\r\n");
    stringBuilder.Append("啟動時間:" + info.StartTime.ToLongTimeString() + "\r\n");
    stringBuilder.Append("專用記憶體:" + (info.PrivateMemorySize64 / 1024).ToString() + "K" + "\r\n");
    stringBuilder.Append("峰值虛擬記憶體:" + (info.PeakVirtualMemorySize64 / 1024).ToString() + "K" + "\r\n");
    stringBuilder.Append("峰值分頁記憶體:" + (info.PeakPagedMemorySize64 / 1024).ToString() + "K" + "\r\n");
    stringBuilder.Append("分頁系統記憶體:" + (info.PagedSystemMemorySize64 / 1024).ToString() + "K" + "\r\n");
    stringBuilder.Append("分頁記憶體:" + (info.PagedMemorySize64 / 1024).ToString() + "K" + "\r\n");
    stringBuilder.Append("未分頁系統記憶體:" + (info.NonpagedSystemMemorySize64 / 1024).ToString() + "K" + "\r\n");
    stringBuilder.Append("實體記憶體:" + (info.WorkingSet64 / 1024).ToString() + "K" + "\r\n");
    stringBuilder.Append("虛擬記憶體:" + (info.VirtualMemorySize64 / 1024).ToString() + "K");
    return stringBuilder.ToString();
}

 4、監聽程式相關事件(CLR、Dynamic、Kernel等):

/// <summary>
/// 程式執行事件輸出:CLR、效能計數器、動態處理(cpu使用率超過90%則抓取dump)
/// </summary>
/// <param name="processId">程式id</param>
/// <param name="threshold">cpu使用率</param>
private void PrintRuntime(int processId, int threshold = 90)
{
    if (!diagnosticsCache.ContainsKey(processId))
    {
        var providers = new List<EventPipeProvider>()
        {
           new EventPipeProvider("Microsoft-Windows-DotNETRuntime",EventLevel.Informational, (long)ClrTraceEventParser.Keywords.Default),
           //效能計數器:間隔時間為1s
           new EventPipeProvider("System.Runtime",EventLevel.Informational,(long)ClrTraceEventParser.Keywords.None,
                                new Dictionary<string, string>() {{ "EventCounterIntervalSec", "1" }})
         };

        DiagnosticsClient client = new DiagnosticsClient(processId);
        diagnosticsCache[processId] = client;
        using (EventPipeSession session = client.StartEventPipeSession(providers, false))
        {
            var source = new EventPipeEventSource(session.EventStream);

            source.Clr.All += (TraceEvent obj) =>
            {
                if (dgvPros.CurrentRow != null && obj.ProcessID.Equals(dgvPros.CurrentRow.Cells[0].Value))
                {
                    string msg = $"Clr-{obj.EventName}-";
                    if (obj.PayloadNames.Length > 0)
                    {
                        foreach (var item in obj.PayloadNames)
                            msg += $"{item}:{ obj.PayloadStringByName(item)}-";
                    }
                    TextAppendLine(msg);
                }
            };
            source.Dynamic.All += (TraceEvent obj) =>
            {
                if (dgvPros.CurrentRow != null && obj.ProcessID.Equals(dgvPros.CurrentRow.Cells[0].Value))
                {
                    string msg = $"Dynamic-{obj.EventName}-{string.Join("|", obj.PayloadNames)}";
            //效能計數器事件
            if (obj.EventName.Equals("EventCounters"))
                    {
                        var payloadFields = (IDictionary<string, object>)(obj.PayloadByName(""));
                        if (payloadFields != null)
                            payloadFields = payloadFields["Payload"] as IDictionary<string, object>;

                        if (payloadFields != null)
                        {
                            msg = $"Dynamic-{obj.EventName}-{payloadFields["DisplayName"]}:{payloadFields["Mean"]}{payloadFields["DisplayUnits"]}";
                            TextAppendLine(msg);
                        }
                //如果CPU使用率超過90%抓取dump
                if (payloadFields != null && payloadFields["Name"].ToString().Equals("cpu-usage"))
                        {
                            double cpuUsage = Double.Parse(payloadFields["Mean"].ToString());
                            if (cpuUsage > (double)threshold)
                            {
                                client.WriteDump(DumpType.Normal, "/tmp/minidump.dmp");
                            }
                        }
                    }
                    else
                    {
                        if (obj.PayloadNames.Length > 0)
                        {
                            foreach (var item in obj.PayloadNames)
                                msg += $"{item}:{ obj.PayloadStringByName(item)}-";
                        }
                        TextAppendLine(msg);
                    }
                }
            };
            source.Kernel.All += (TraceEvent obj) =>
            {
                if (dgvPros.CurrentRow != null && obj.ProcessID.Equals(dgvPros.CurrentRow.Cells[0].Value))
                {
                    string msg = $"Kernel-{obj.EventName}-{string.Join("|", obj.PayloadNames)}";
                    TextAppendLine(msg);
                }
            };

            try
            {
                source.Process();
            }
            catch (Exception e)
            {
                string errorMsg = $"錯誤:{e}";
                TextAppendLine(errorMsg);
            }
        }
    }
}

 5、Dump抓取功能:

/// <summary>
/// 抓取Dmp檔案
/// </summary>
/// <param name="processId"></param>
private void TriggerCoreDump(int processId)
{
    saveFileDialog1.Filter = "Dump檔案|*.dmp";
    if (saveFileDialog1.ShowDialog() == DialogResult.OK)
    {
        var client = new DiagnosticsClient(processId);
        //Normal = 1,WithHeap = 2,Triage = 3,Full = 4
        client.WriteDump(DumpType.Normal, saveFileDialog1.FileName, false);
    }
}

  引數說明:

轉儲型別。

  • Normal:僅包括捕獲程式中所有現有執行緒的所有現有跟蹤的堆疊跟蹤所需的資訊。有限的GC堆記憶體和資訊。
  • WithHeap:包括GC堆和捕獲程式中所有現有執行緒的堆疊跟蹤所必需的資訊。
  • Triage:僅包括捕獲程式中所有現有執行緒的所有現有跟蹤的堆疊跟蹤所需的資訊。有限的GC堆記憶體和資訊。
  • Full:在此過程中包括所有可訪問的記憶體。原始記憶體資料包含在末尾,因此可以直接對映初始結構,而無需原始記憶體資訊。此選項可能會導致非常大的轉儲檔案。

 6、生成程式指定事件內Trace檔案

/// <summary>
/// 寫入Trace檔案
/// </summary>
/// <param name="processId">程式ID</param>
/// <param name="duration">指定時間範圍(單位s)</param>
private void TraceProcessForDuration(int processId, int duration)
{
    saveFileDialog1.Filter = "Nettrace檔案|*.nettrace";
    if (saveFileDialog1.ShowDialog() == DialogResult.OK)
    {
        var cpuProviders = new List<EventPipeProvider>()
        {
            new EventPipeProvider("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, (long)ClrTraceEventParser.Keywords.Default),
            new EventPipeProvider("Microsoft-DotNETCore-SampleProfiler", EventLevel.Informational, (long)ClrTraceEventParser.Keywords.None)
        };
        var client = new DiagnosticsClient(processId);
        using (var traceSession = client.StartEventPipeSession(cpuProviders))
        {
            Task copyTask = Task.Run(async () =>
            {
                using (FileStream fs = new FileStream(saveFileDialog1.FileName, FileMode.Create, FileAccess.Write))
                {
                    await traceSession.EventStream.CopyToAsync(fs);
                }
            });

            copyTask.Wait(duration * 1000);
            traceSession.Stop();
        }
    }
}

三、執行效果

 執行效果如下圖:實現相關效果

  

 總結:

 通過微軟提供的 Microsoft.Diagnostics.NETCore.Client 比較簡單的就實現了這些功能,當然註冊的事件裡面還有很多資訊分析等著去解鎖。

 這只是走出了簡單的第一步,後續還任重而道遠

其他:

 參考:https://github.com/dotnet/diagnostics 

 原始碼:https://github.com/cwsheng/DiagnosticsTools

 

相關文章