動手實現一個適用於.NET Core 的診斷工具

SpringLeee發表於2021-05-08

前言

大家可能對診斷工具並不陌生,從大名鼎鼎的 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,覺得不錯的就給我點個贊吧!

最後歡迎掃碼關注我們的公眾號 【全球技術精選】,專注國外優秀部落格的翻譯和開源專案分享。

動手實現一個適用於.NET Core 的診斷工具

相關文章