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


前言

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

最后歡迎掃碼關注我們的公眾號 【全球技術精選】,專注國外優秀博客的翻譯和開源項目分享。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM