前言
大家可能對診斷工具並不陌生,從大名鼎鼎的 dotTrace,到 .NET CLI 推出的一系列的高效診斷元件(dotnet trace,dotnet sos,dotnet dump)等, 這些工具提升了對程式Debug的能力和效率,可以讓開發人員從更高層次的維度來發現程式中的問題。
今天我們針對於.NET Core, 嘗試動手實現一個簡單的診斷工具,在保證對程式無侵入(不修改程式碼和配置)的前提下,我們嘗試獲取程式的執行資訊,包括記憶體,執行緒,垃圾回收,異常等。
這裡可能會有小夥伴說,我可以用C++編寫然後利用Profiling API實現,類似於OneAPM,Datadog 自動探針的形式來收集資料,當然也可以,不過今天我們主要用到了 Microsoft.Diagnostics.NETCore.Client
,執行時團隊給開發人員提供了更簡單和友好的元件。
初始化專案
首先,我們需要建立兩個.NET Core 的專案,一個是C#的控制檯專案,名字叫ConsoleApp,這是我們的診斷程式,另一個是普通的WebAPI,我們需要對這個API專案進行診斷分析。
然後在控制檯專案上通過Nuget引入診斷元件,分別是 Microsoft.Diagnostics.NETCore.Client
,Microsoft.Diagnostics.Tracing.TraceEvent
1.獲取正在執行的程式列表
在無侵入的情況下,我們首先需要獲取到執行的dotnet程式,包括程式的名字和PID,在多個dotnet專案中,我們後邊都會通過PID來對特定的程式進行診斷。 修改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();
}
}
}
修改完成後,我們用命令列啟動專案,WebAPI 專案執行dotnet run
命令 , 啟動之後,ConsoleApp 再執行 dotnet run ps
命令,ps 是我們傳入的引數,我們可以在控制檯上看到正在執行的程式資訊,我們主要會用到pid。
2.獲取 GC 資訊
我們建立了一個 DiagnosticsClient的例項,在建構函式中傳入了processId程式ID,然後開啟了一個有關GC資訊的會話,最後訂閱了CLR相關的事件回撥,輸出了事件名稱EventName到控制檯。
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<EventPipeProvider>()
{
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());
}
}
}
接下來,我們修改一下WebAPI的程式碼,在控制器中的方法中建立了一個集合,並且新增了很多資料。
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
List<string> list = 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();
}
同樣,我們首先通過 dotnet run
命令啟動WebAPI專案,然後 dotnet run ps
啟動ConsoleApp專案,控制檯會輸出 webapi 專案的程式資訊,我這裡的pid是3832
然後在控制檯專案中執行 dotnet run runtime 3832
, runtime 和 3832 都是我們傳入的引數, 然後開啟一個新的命令列視窗,通過curl訪問幾次webapi的介面,當然你也可以在瀏覽器中訪問,我們發現,在右邊的控制檯專案輸出了GC的相關資訊, 這裡我們只輸出了事件名,實際上我們可以拿到更多的資料資訊。
3.獲取異常資訊
同樣的,我們先修改WebApi專案,手動丟擲一個異常。
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
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();
}
在控制檯專案中,我們只需要改動一個Keywords 列舉,就是把 ClrTraceEventParser.Keywords.GC
改成 ClrTraceEventParser.Keywords.Exception
,當然這裡支援了其他更多的型別。
修改完成後,我們先啟動 WebApi 專案,然後在ConsoleApp中先執行 dotnet run ps
,檢視webapi的程式id,然後再執行 dotnet run runtime 13600
, 最後我們通過 curl 命令或者瀏覽器訪問webapi的介面,同樣,在右邊的ConsoleApp中,輸出了異常的相關事件資訊。
在上面的程式碼中,我手動丟擲一個異常,我們的診斷工具ConsoleApp是可以獲取到相關的異常資訊,那我用try,catch 把異常吃掉呢?它還能捕獲到異常嗎?
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
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();
}
修改程式碼後,我們重新執行webapi和診斷工具ConsoleApp,訪問api介面時,你會發現,就算我們用try,catch 吃掉了異常,它仍然會輸出異常資訊。
4. 生成Dump檔案
通過 Microsoft.Diagnostics.NETCore.Client
元件,我們可以很方便的為程式生生成Dump檔案,然後可以用 windbg 工具來進行分析。
修改控制檯專案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);
}
修改完成後,啟動webapi專案和控制檯專案,在控制檯專案中執行 dotnet run dump 13288
命令,它會在webapi的目錄下,生成程式的dump檔案
5.生成 Trace 檔案
同樣,我們可以很方便的生成 Trace 檔案,它可以分析到CPU的函式執行耗時情況,它的格式是.nettrace, 你可以直接用VS 2017及以上或者 PerfView 工具開啟。
修改控制檯專案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<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.Run(async () =>
{
using (FileStream fs = new FileStream(@"mytrace.nettrace", FileMode.Create, FileAccess.Write))
{
await traceSession.EventStream.CopyToAsync(fs);
}
}).Wait(10 * 1000);
traceSession.Stop();
}
}
修改完成後,啟動webapi專案和控制檯專案,在控制檯專案中執行 dotnet run trace 13288
命令,trace和13288都是引數,它會在控制檯專案的目錄下,生成 mytrace.nettrace檔案
我們可以使用VS或者 PerfView 開啟它
總結
其實在.NET Core CLI 中,已經提供了高度可用的一系列診斷工具,dotnet-trace,dotnet-dump 等等,Microsoft.Diagnostics.NETCore.Client
提供了非常友好和高層次的API,不僅僅是文中這些, 我們可以用C#程式碼,來完成對CLR層面的一些操作,來幫助我們發掘對程式診斷的更多可能性。
示例程式碼都已經上傳到 https://github.com/SpringLeee/DiagnosticDemo,覺得不錯的就給我點個贊吧!
最後歡迎掃碼關注我們的公眾號 【全球技術精選】,專注國外優秀部落格的翻譯和開源專案分享。