動(dòng)手實(shí)現(xiàn)一個(gè)適用于.NET Core 的診斷工具

前言
大家可能對(duì)診斷工具并不陌生,從大名鼎鼎的 dotTrace,到 .NET CLI 推出的一系列的高效診斷組件(dotnet trace,dotnet sos,dotnet dump)等, 這些工具提升了對(duì)程序Debug的能力和效率,可以讓開(kāi)發(fā)人員從更高層次的維度來(lái)發(fā)現(xiàn)程序中的問(wèn)題。
今天我們針對(duì)于.NET Core, 嘗試動(dòng)手實(shí)現(xiàn)一個(gè)簡(jiǎn)單的診斷工具,在保證對(duì)程序無(wú)侵入(不修改代碼和配置)的前提下,我們嘗試獲取程序的運(yùn)行信息,包括內(nèi)存,線程,垃圾回收,異常等。
這里可能會(huì)有小伙伴說(shuō),我可以用C++編寫然后利用Profiling API實(shí)現(xiàn),類似于OneAPM,Datadog 自動(dòng)探針的形式來(lái)收集數(shù)據(jù),當(dāng)然也可以,不過(guò)今天我們主要用到了?Microsoft.Diagnostics.NETCore.Client,運(yùn)行時(shí)團(tuán)隊(duì)給開(kāi)發(fā)人員提供了更簡(jiǎn)單和友好的組件。
初始化項(xiàng)目
首先,我們需要?jiǎng)?chuàng)建兩個(gè).NET Core 的項(xiàng)目,一個(gè)是C#的控制臺(tái)項(xiàng)目,名字叫ConsoleApp,這是我們的診斷程序,另一個(gè)是普通的WebAPI,我們需要對(duì)這個(gè)API項(xiàng)目進(jìn)行診斷分析。

然后在控制臺(tái)項(xiàng)目上通過(guò)Nuget引入診斷組件,分別是?Microsoft.Diagnostics.NETCore.Client,Microsoft.Diagnostics.Tracing.TraceEvent

1.獲取正在運(yùn)行的程序列表
在無(wú)侵入的情況下,我們首先需要獲取到運(yùn)行的dotnet程序,包括進(jìn)程的名字和PID,在多個(gè)dotnet項(xiàng)目中,我們后邊都會(huì)通過(guò)PID來(lái)對(duì)特定的程序進(jìn)行診斷。修改ConsoleApp的Program.cs如下,這里主要用到了 GetPublishedProcesses 方法。
class Program{static void Main(string[] args){if (args.Any()){switch (args[0]){case "ps": PrintProcessStatus(); break;}}}public static void PrintProcessStatus(){var processes = DiagnosticsClient.GetPublishedProcesses().Select(Process.GetProcessById).Where(process => process != null);foreach (var process in processes){Console.WriteLine($"ProcessId: {process.Id}");Console.WriteLine($"ProcessName: {process.ProcessName}");Console.WriteLine($"StartTime: {process.StartTime}");Console.WriteLine($"Threads: {process.Threads.Count}");Console.WriteLine();Console.WriteLine();}}}
修改完成后,我們用命令行啟動(dòng)項(xiàng)目,WebAPI 項(xiàng)目運(yùn)行dotnet run命令 , 啟動(dòng)之后,ConsoleApp 再運(yùn)行?dotnet run ps命令,ps 是我們傳入的參數(shù),我們可以在控制臺(tái)上看到正在運(yùn)行的進(jìn)程信息,我們主要會(huì)用到pid。

2.獲取 GC 信息
我們創(chuàng)建了一個(gè) DiagnosticsClient的實(shí)例,在構(gòu)造函數(shù)中傳入了processId進(jìn)程ID,然后開(kāi)啟了一個(gè)有關(guān)GC信息的會(huì)話,最后訂閱了CLR相關(guān)的事件回調(diào),輸出了事件名稱EventName到控制臺(tái)。
static void Main(string[] args){if (args.Any()){switch (args[0]){case "ps": PrintProcessStatus(); break;case "runtime": PrintRuntime(int.Parse(args[1])); break;}}}public static void PrintRuntime(int processId){var providers = new List() {new ("Microsoft-Windows-DotNETRuntime",EventLevel.Informational, (long)ClrTraceEventParser.Keywords.GC)};var client = new DiagnosticsClient(processId);using (var session = client.StartEventPipeSession(providers, false)){var source = new EventPipeEventSource(session.EventStream);source.Clr.All += (TraceEvent obj) =>{Console.WriteLine(obj.EventName);};try{source.Process();}catch (Exception e){Console.WriteLine(e.ToString());}}}
接下來(lái),我們修改一下WebAPI的代碼,在控制器中的方法中創(chuàng)建了一個(gè)集合,并且添加了很多數(shù)據(jù)。
[HttpGet]public IEnumerableGet() {Listlist = new (); for (int i = 0; i < 1000000; i++){list.Add(i.ToString());}var rng = new Random();return Enumerable.Range(1, 5).Select(index => new WeatherForecast{Date = DateTime.Now.AddDays(index),TemperatureC = rng.Next(-20, 55),Summary = Summaries[rng.Next(Summaries.Length)]}).ToArray();}
同樣,我們首先通過(guò)?dotnet run?命令啟動(dòng)WebAPI項(xiàng)目,然后?dotnet run ps?啟動(dòng)ConsoleApp項(xiàng)目,控制臺(tái)會(huì)輸出 webapi 項(xiàng)目的進(jìn)程信息,我這里的pid是3832

然后在控制臺(tái)項(xiàng)目中運(yùn)行?dotnet run runtime 3832, runtime 和 3832 都是我們傳入的參數(shù), 然后開(kāi)啟一個(gè)新的命令行窗口,通過(guò)curl訪問(wèn)幾次webapi的接口,當(dāng)然你也可以在瀏覽器中訪問(wèn),我們發(fā)現(xiàn),在右邊的控制臺(tái)項(xiàng)目輸出了GC的相關(guān)信息, 這里我們只輸出了事件名,實(shí)際上我們可以拿到更多的數(shù)據(jù)信息。

3.獲取異常信息
同樣的,我們先修改WebApi項(xiàng)目,手動(dòng)拋出一個(gè)異常。
[HttpGet]public IEnumerableGet() {throw new Exception("error");var rng = new Random();return Enumerable.Range(1, 5).Select(index => new WeatherForecast{Date = DateTime.Now.AddDays(index),TemperatureC = rng.Next(-20, 55),Summary = Summaries[rng.Next(Summaries.Length)]}).ToArray();}
在控制臺(tái)項(xiàng)目中,我們只需要改動(dòng)一個(gè)Keywords 枚舉,就是把?ClrTraceEventParser.Keywords.GC?改成?ClrTraceEventParser.Keywords.Exception,當(dāng)然這里支持了其他更多的類型。

修改完成后,我們先啟動(dòng) WebApi 項(xiàng)目,然后在ConsoleApp中先運(yùn)行?dotnet run ps,查看webapi的進(jìn)程id,然后再運(yùn)行?dotnet run runtime 13600, 最后我們通過(guò) curl 命令或者瀏覽器訪問(wèn)webapi的接口,同樣,在右邊的ConsoleApp中,輸出了異常的相關(guān)事件信息。

在上面的代碼中,我手動(dòng)拋出一個(gè)異常,我們的診斷工具ConsoleApp是可以獲取到相關(guān)的異常信息,那我用try,catch 把異常吃掉呢?它還能捕獲到異常嗎?
[HttpGet]public IEnumerableGet() {try{Convert.ToInt32("sss");}catch (Exception ex){Console.WriteLine(ex.ToString());}var rng = new Random();return Enumerable.Range(1, 5).Select(index => new WeatherForecast{Date = DateTime.Now.AddDays(index),TemperatureC = rng.Next(-20, 55),Summary = Summaries[rng.Next(Summaries.Length)]}).ToArray();}
修改代碼后,我們重新運(yùn)行webapi和診斷工具ConsoleApp,訪問(wèn)api接口時(shí),你會(huì)發(fā)現(xiàn),就算我們用try,catch 吃掉了異常,它仍然會(huì)輸出異常信息。

4. 生成Dump文件
通過(guò)?Microsoft.Diagnostics.NETCore.Client?組件,我們可以很方便的為程序生生成Dump文件,然后可以用 windbg 工具來(lái)進(jìn)行分析。
修改控制臺(tái)項(xiàng)目ConsoleApp的Program.cs如下:
static void Main(string[] args){if (args.Any()){switch (args[0]){case "ps": PrintProcessStatus(); break;case "runtime": PrintRuntime(int.Parse(args[1])); break;case "dump": Dump(int.Parse(args[1])); break;}}}public static void Dump(int processId){var client = new DiagnosticsClient(processId);client.WriteDump(DumpType.Normal, @"mydump.dmp", false);}
修改完成后,啟動(dòng)webapi項(xiàng)目和控制臺(tái)項(xiàng)目,在控制臺(tái)項(xiàng)目中運(yùn)行?dotnet run dump 13288?命令,它會(huì)在webapi的目錄下,生成程序的dump文件

5.生成 Trace 文件
同樣,我們可以很方便的生成 Trace 文件,它可以分析到CPU的函數(shù)執(zhí)行耗時(shí)情況,它的格式是.nettrace, 你可以直接用VS 2017及以上或者 PerfView 工具打開(kāi)。
修改控制臺(tái)項(xiàng)目ConsoleApp的Program.cs如下:
static void Main(string[] args){if (args.Any()){switch (args[0]){case "ps": PrintProcessStatus(); break;case "runtime": PrintRuntime(int.Parse(args[1])); break;case "dump": Dump(int.Parse(args[1])); break;case "trace": Trace(int.Parse(args[1])); break;}}}public static void Trace(int processId){var cpuProviders = new List() {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.Run(async () =>{using (FileStream fs = new FileStream(@"mytrace.nettrace", FileMode.Create, FileAccess.Write)){await traceSession.EventStream.CopyToAsync(fs);}}).Wait(10 * 1000);traceSession.Stop();}}
修改完成后,啟動(dòng)webapi項(xiàng)目和控制臺(tái)項(xiàng)目,在控制臺(tái)項(xiàng)目中運(yùn)行?dotnet run trace 13288命令,trace和13288都是參數(shù),它會(huì)在控制臺(tái)項(xiàng)目的目錄下,生成 mytrace.nettrace文件

我們可以使用VS或者 PerfView 打開(kāi)它

總結(jié)
其實(shí)在.NET Core CLI 中,已經(jīng)提供了高度可用的一系列診斷工具,dotnet-trace,dotnet-dump 等等,Microsoft.Diagnostics.NETCore.Client?提供了非常友好和高層次的API,不僅僅是文中這些, 我們可以用C#代碼,來(lái)完成對(duì)CLR層面的一些操作,來(lái)幫助我們發(fā)掘?qū)Τ绦蛟\斷的更多可能性。
示例代碼都已經(jīng)上傳到 https://github.com/SpringLeee/DiagnosticDemo,覺(jué)得不錯(cuò)的就給我點(diǎn)個(gè)贊吧!
