• 动手实现一个适用于.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,觉得不错的就给我点个赞吧!

    最后欢迎扫码关注我们的公众号 【全球技术精选】,专注国外优秀博客的翻译和开源项目分享。

  • 相关阅读:
    printcap
    browser-ua
    PHP 开发 APP 接口 学习笔记与总结
    Java实现 LeetCode 72 编辑距离
    Java实现 LeetCode 72 编辑距离
    Java实现 LeetCode 72 编辑距离
    Java实现 LeetCode 71 简化路径
    Java实现 LeetCode 71 简化路径
    Java实现 LeetCode 71 简化路径
    Java实现 LeetCode70 爬楼梯
  • 原文地址:https://www.cnblogs.com/myshowtime/p/14743456.html
Copyright © 2020-2023  润新知