前言
大家可能對診斷工具並不陌生,從大名鼎鼎的 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,覺得不錯的就給我點個贊吧!
最后歡迎掃碼關注我們的公眾號 【全球技術精選】,專注國外優秀博客的翻譯和開源項目分享。
