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

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

  • 相关阅读:
    单用户模式启动SQL Server实例总结
    MySQL下perror工具查看System Error Code信息
    ERROR 1050 (42S01): Table xxx already exists
    RMAN-06172 Troubleshooting
    [翻译]LVM中逻辑卷的最大大小限制
    如何定位那些SQL产生了大量的redo日志
    MySQL的自动提交模式
    MySQL服务读取参数文件my.cnf的规律研究探索
    SQL Server等待事件—RESOURCE_SEMAPHORE_QUERY_COMPILE
    Azure SQL Virtual Machine报Login failed for user 'NT ServiceSqlIaaSExtension'. Reason: Could not find a login matching the name provided
  • 原文地址:https://www.cnblogs.com/myshowtime/p/14743456.html
Copyright © 2011-2022 走看看