找回密码
 立即注册
首页 业界区 业界 .NET 高级开发 | 开发 .NET 诊断工具、链路追踪原理 ...

.NET 高级开发 | 开发 .NET 诊断工具、链路追踪原理

靛尊 15 小时前
系列教程地址:https://docs.whuanle.cn/

目录

  • 开发 .NET 诊断工具



      • System.Diagnostics、Microsoft.Diagnostics

        • Debug、Trace
        • EventSource、EventListener

          • 自定义 EventSource 、DiagnosticCounter
          • 编写收集器
          • 编写诊断工具





开发 .NET 诊断工具

System.Diagnostics、Microsoft.Diagnostics

在 System.Diagnostics Microsoft.Diagnostics 命名空间中的接口用于诊断 .NET 程序,里面涉及到很多诊断技术,由于个人技术水平限制以及篇幅原因,笔者只介绍比较常用的几种诊断方法,不深入探讨原理。

Debug、Trace

在 System.Diagnostics 命名空间中有 Debug、Trace 两个类型,用来追踪代码的执行Debug、Trace 可以打印调试信息并使用断点检查逻辑,使代码更可靠,而不会影响发运程序的性能。System.Diagnostics.Debug 只在 Debug 环境下起作用,在 Release 环境下会失效,除此之外两者的接口几乎一样。

下面示例代码,当 sum 的值在 100 以内时程序正常执行,当 sum 的值大于等于 100时会触发断点,IDE 会跳转到该位置,此时会引起我们的注意。
  1. static void Main()
  2. {
  3.         List<int> ls = new List<int> { 30, 40, 50 };
  4.         Sum(ls);
  5. }
  6. static int Sum(List<int> ls)
  7. {
  8.         var sum = 0;
  9.         foreach (var item in ls)
  10.         {
  11.                 sum += item;
  12.                 // 当条件为否时触发
  13.                 // Debug.Assert(condition: sum < 100);
  14.                 Debug.Assert(condition: sum < 100, message: "数据量有点大");
  15.         }
  16.         return sum;
  17. }
复制代码
1.png


.Assert()  会触发断点同时打印信息。.NET Runtime 源代码中就大量地使用了 Debug.Assert() ,笔者个人也常常在项目中使用 Debug.Assert,比如某个条件分支很少情况下会执行,如果该分支被执行,需要引起开发者关注。一方面 Debug 只在调试模式下有效,不会干扰正式发布的项目运行。IDE 断点正在当前设备环境中起效,不能跟其它设备共享断点位置,而 Debug 在代码中,所有人都可以使用。

此外通过 Debug、Trace 打印信息,方法有  Write 、WriteLine 、 WriteIf 、 WriteLineIf 、Print 等,默认打印到 IDE 的调试输出。
  1. int value = -1;
  2. Debug.Assert(value != -1, "值不应该为 -1.");
  3. Debug.WriteLineIf(value == -1, "当前值居然为 -1.");
复制代码
  1. ---- DEBUG ASSERTION FAILED ----
  2. ---- Assert Short Message ----  <ItemGroup>
  3.     <PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.442301" />
  4.     <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.5" />
  5.   </ItemGroup>      
  6. 值不应该为 -1.
  7. ---- Assert Long Message ----
  8.    at Demo2.Diagnostics.Program.Main(String[] args) in E:\demo\Program.cs:line 14
  9. 当前值居然为 -1.
复制代码
也可以通过监听器将信息打印到控制台或文件中,如需将调试信息打印到控制台,可以注册相关的侦听器:
  1. Trace.Listeners.Add(new ConsoleTraceListener();
  2. // 或者 Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
复制代码
注意,  Debug 没有 Listeners 属性,因为 Debug 使用的是 Trace 的侦听器,即给 Trace 配置之后,Debug 也会生效。
2.png


.NET 中主要有以下监听器 DefaultTraceListener、TextWriterTraceListener、ConsoleTraceListener、DelimitedListTraceListener、EventLogTraceListener 等。
如果需要输出到文件中,可以自行继承 TextWriterTraceListener ,编写文件流输出,也可以使用 DelimitedListTraceListener。
示例:
  1. // TraceListener listener = new TextWriterTraceListener(new FileStream(@"C:\debugfile.txt", FileMode.OpenOrCreate));
  2. TraceListener listener = new DelimitedListTraceListener(@"D:\debugfile.txt");
  3. Debug.Listeners.Add(listener);
  4. Debug.WriteLine("打印调试信息");
复制代码
为了格式化输出流,可以使用相关属性控制排版:
属性说明AutoFlush获取或设置一个值,通过该值指示每次写入后是否应在 Flush() 上调用 Listeners。IndentLevel获取或设置缩进级别。IndentSize获取或设置缩进的空格数。
  1. Debug.WriteLine("One");
  2. // 缩进
  3. Debug.Indent();
  4. Debug.WriteLine("Two");
  5. Debug.WriteLine("Three");
  6. // 结束缩进
  7. Debug.Unindent();
  8. Debug.WriteLine("Four");
复制代码
  1. One
  2.     Two
  3.     Three
  4. Four
复制代码
EventSource、EventListener

System.Diagnostics.Tracing 命名空间下的 EventSource 、DiagnosticCounter 都是抽象类,EventSource 称为事件源,用于定义事件和记录日志,运行时本身会有很多事件,比如 GC 回收事件、线程退出事件等,可以使用工具进行监听和分析。DiagnosticCounter 称为计数器,用于收集各种类型的性能指标,比如内存使用量、GC 触发次数。

从技术角度出发,我们需要关注两个部分,自定义 EventSource 、DiagnosticCounter ,以及如何监听  EventSource 、DiagnosticCounter 。

自定义 EventSource 、DiagnosticCounter

我们可以在程序中自定义事件源,然后通过代码监听或 .NET 中的诊断工具收集这些事件。
示例项目在 Demo2.ES 中,下面的代码定义了一个事件源 MyEventSource 类,其内部使用了一个计数器。其功能非常简单,每次循环时,触发 MyEventSource 内的计数器自动递增 1。
  1. internal class Program
  2. {
  3.         private static readonly MyEventSource EventSource = new MyEventSource();
  4.         public static void Main(string[] args)
  5.         {
  6.                 int number = 0;
  7.                 while (true)
  8.                 {
  9.                         number++;
  10.                         EventSource.LogEvent("测试", number);
  11.                         Thread.Sleep(1000);
  12.                 }
  13.         }
  14. }
  15. // MyEvent 是事件的名称
  16. [EventSource(Name = "MyEvent")]
  17. public class MyEventSource : EventSource
  18. {
  19.         // 计数器
  20.         private readonly IncrementingEventCounter _incrementingEventCounter;
  21.         public MyEventSource()
  22.         {
  23.                 _incrementingEventCounter = new IncrementingEventCounter("MyEvent", this);
  24.         }
  25.         [Event(eventId: 1)]
  26.         public void LogEvent(string message, int favoriteNumber)
  27.         {
  28.                 _incrementingEventCounter.Increment();
  29.                 WriteEvent(1, message, favoriteNumber);
  30.         }
  31. }
复制代码
然后我们通过 dotnet-trace 工具收集事件信息。
我们在一个空目录中启动命令行工具,然后执行下面的命令。
安装 dotnet-trace 工具:
  1. dotnet tool install -g dotnet-trace
复制代码
列出系统中的 .NET 程序及其进程 id:
  1. $> dotnet-trace ps
  2. 18064  Demo2.ES ...
复制代码
收集进程 18064 中的事件以及指定事件名称包括 MyEvent:
  1. dotnet-trace collect --process-id 18064 --providers MyEvent
复制代码
一段时间之后按下回车键或 Ctrl+C,在目录中可以找到一个 .nettrace 文件,使用 Visual Studio 打开 .nettrace 文件。
然后点击表格头部 “提供程序名称/事件名称” 右侧的图标,选中 MyEvent 对事件进行筛选。
3.png


然后点击具体的事件可以观察到该事件的信息,文本列显示的是使用 WriteEvent(1, message, favoriteNumber); 记录的事件的信息,时间戳列记录了事件发生的时间,右侧属性面板显示了事件的详细信息。
4.png


自定义事件源需要继承 EventSource ,EventSource 中包含很多记录事件发生的函数,如 WriteEvent 方法,然后使用 dotnet-trace 工具可以捕获程序中发生的事件,获取事件记录的信息。

由于在自定义事件源中我们添加了计数器,所以我们还可以使用 dotnet-counters 工具收集计数信息。
  1. dotnet-counters monitor  --process-id 18064  --counters MyEvent
复制代码
5.png


如上图所示,dotnet-counters 统计了 MyEvent 计数器在一秒钟之内触发的次数。

当然,在不指定计数器名称时, dotnet-counters 可以显示 CLR 中很多的信息,在 ASP.NET Core 中可以显示流量速率、并发量等。
  1. dotnet-counters monitor  --process-id 18064
复制代码
6.png


我们也可以给计数器设置一些属性:
  1. _incrementingEventCounter = new IncrementingEventCounter("MyEvent", this)
  2. {
  3.         // 以下两项只能从构造函数传入
  4.         // EventSource = this,
  5.         // Name = "MyEvent"
  6.         // 显示的名称
  7.         DisplayName = "MyEvent",
  8.         // 时间间隔
  9.         DisplayRateTimeScale = TimeSpan.FromSeconds(1),
  10.         // 单位名称
  11.         DisplayUnits = "count"
  12. };
复制代码
除了 IncrementingEventCounter ,还存在其它类型的计数器:

  • EventCounter:事件计数器
  • IncrementingEventCounter :递增事件计数器
  • PollingCounter :轮询计数器
  • IncrementingPollingCounter :递增轮询计数器

通过以上的例子,我们可以看到 dotnet-trace、dotnet-counters 两个工具收集的信息是基于 EventSource、EventCounter 的,.NET CLI 诊断工具通过 .NET 内置或自定义的事件或计数器收集信息。
在 .NET 中本身提供一些事件源,线程池、类型系统、异常、运行时方法等事件,比如在 .NET Runtime 中主要提供以下两种事件:

  • Microsoft-Windows-DotNETRuntime 提供运行时发出的各种事件,如 GC、JIT、异常等事件;
  • Microsoft-DotNETCore-SampleProfiler 提供托管线程堆栈的快照;

.NET 还有其他内置的事件,读者感兴趣的话可以通过官方文档了解更多,这里就不再赘述。

编写收集器

上一节中,我们使用了 dotnet-trace、dotnet-counters 两个工具捕获程序中的事件和计数器,在本节中,笔者将介绍如何使用 EventListener 捕获程序内发生的事件。

在 System.Net.Http 包中,有着跟 http 请求相关的接口,例如 HttpClient ,System.Net.Http 也内置了一些事件,记录 HTTP 请求信息,列举部分 Http 事件如下:
事件名称说明RequestStartHTTP 请求已启动。RequestStopHTTP 请求已完成。RequestFailedHTTP 请求失败。ConnectionEstablishedHTTP 连接已建立。ConnectionClosedHTTP 连接已关闭。
那么,我们编写一个 HttpClient 程序,然后编写一个监听器监听程序发出的所有 Http 请求并记录状态码。
示例代码在 Demo2.ESTrace 中。
  1. public static class Program
  2. {
  3.         public static async Task Main(string[] args)
  4.         {
  5.                 // 由 CLR 自动调用
  6.                 HttpClientEventListener listener = new();
  7.                 Console.WriteLine("活动ID ---- 事件名称 ---- 请求地址 ---- 协议");
  8.                 while (true)
  9.                 {
  10.                         await GetAsync();
  11.                         await Task.Delay(1000);
  12.                 }
  13.         }
  14.         static async Task GetAsync()
  15.         {
  16.                 await new HttpClient().GetAsync("https://www.baidu.com");
  17.         }
  18. }
  19. // 只监听 System.Net.Http 事件源的监听器
  20. sealed class HttpClientEventListener : EventListener
  21. {
  22.         protected override void OnEventSourceCreated(EventSource eventSource)
  23.         {
  24.                 switch (eventSource.Name)
  25.                 {
  26.                         case "System.Net.Http":
  27.                                 EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All);
  28.                                 break;
  29.                 }
  30.                 base.OnEventSourceCreated(eventSource);
  31.         }
  32.         protected override void OnEventWritten(EventWrittenEventArgs eventData)
  33.         {
  34.                 // RequestStart 事件
  35.                 if (eventData.EventId == 1)
  36.                 {
  37.                         var scheme = (string)eventData.Payload[0];
  38.                         var host = (string)eventData.Payload[1];
  39.                         var port = (int)eventData.Payload[2];
  40.                         var pathAndQuery = (string)eventData.Payload[3];
  41.                         var versionMajor = (byte)eventData.Payload[4];
  42.                         var versionMinor = (byte)eventData.Payload[5];
  43.                         var policy = (HttpVersionPolicy)eventData.Payload[6];
  44.                         Console.WriteLine($"{eventData.ActivityId} {eventData.EventName} {scheme}://{host}:{port}{pathAndQuery} HTTP/{versionMajor}.{versionMinor}");
  45.                 }
  46.                 // RequestStop 事件
  47.                 else if (eventData.EventId == 2)
  48.                 {
  49.                         Console.WriteLine($"{eventData.ActivityId} {eventData.EventName} 状态码:{eventData.Payload[0]}");
  50.                 }
  51.         }
  52. }
复制代码
运行之后在控制台中可以看到事件信息。
7.png


此外,通过 Visual Studio 的诊断工具也可以看到相关的事件,或者使用 dotnet-trace 工具进行收集。
8.png


当然,当前使用的监听器还只能收集自身进程内的事件,在前面我们使用的 dotnet-trace、dotnet-counters 是如何通过跨进程收集的呢?接下来我们学习如何编写一个跨进程收集信息的诊断工具。
编写诊断工具

dotnet CLI 工具很多,除了前面提到的 dotnet-trace、dotnet-counters ,还有 dotnet-dump、dotnet-gcdump 等 CLI 工具,都可以通过跨进程的方式收集程序的信息。在本小节中,我们通过诊断工具包实现跨进程收集信息,实现 类似的工具。

写一个简单的控制台程序并启动:
  1. private static readonly HttpClient Http = new();
  2. public static async Task Main(string[] args)
  3. {
  4.         while (true)
  5.         {
  6.                 await Http.GetAsync("https://www.baidu.com");
  7.                 await Task.Delay(1000);
  8.                 GC.Collect();
  9.         }
  10. }
复制代码
然后编写一个诊断工具,示例项目在 Demo2.Diagnostics 中。
创建一个控制台,引入两个包:
  1.   <ItemGroup>
  2.     <PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.442301" />
  3.     <PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.5" />
  4.   </ItemGroup>
复制代码
编写一个订阅事件信息的程序:
  1. internal class Program
  2. {
  3.         static void Main(string[] args)
  4.         {
  5.                 // 获取所有 .NET 进程
  6.                 var processes = DiagnosticsClient.GetPublishedProcesses()
  7.                         .Select(Process.GetProcessById)
  8.                         .Where(process => process != null);
  9.                 Console.WriteLine("请输入进程 id");
  10.                 foreach (var item in processes)
  11.                 {
  12.                         Console.WriteLine($"{item.Id} ------ {item.ProcessName}");
  13.                 }
  14.                 var read = Console.ReadLine();
  15.                 ArgumentNullException.ThrowIfNullOrEmpty(read);
  16.                 var pid = int.Parse(read);
  17.                 var providers = new List<EventPipeProvider>()
  18.                         {
  19.                                 new ("Microsoft-Windows-DotNETRuntime", EventLevel.Informational, (long)ClrTraceEventParser.Keywords.GC),
  20.                         };
  21.                 var client = new DiagnosticsClient(pid);
  22.                 using var session = client.StartEventPipeSession(providers: providers, requestRundown: false, circularBufferMB: 256);
  23.                 var source = new EventPipeEventSource(session.EventStream);
  24.                 // CLR 事件
  25.                 source.Clr.All += (TraceEvent obj) =>
  26.                 {
  27.                         Console.WriteLine(obj.ToString());
  28.                 };
  29.                 // 订阅 providers 中监听的所有事件
  30.                 // 如果想订阅全部事件,则应该则使用 Dynamic.All
  31.                 //source.AllEvents += (TraceEvent obj) =>
  32.                 //{
  33.                 //    Console.WriteLine(obj.ToString());
  34.                 //};
  35.                 // 内核事件
  36.                 //source.Kernel.All += (TraceEvent obj) =>
  37.                 //{
  38.                 //    Console.WriteLine(obj.ToString());
  39.                 //};
  40.                 // 动态处理所有事件
  41.                 //source.Dynamic.All += (TraceEvent obj) =>
  42.                 //{
  43.                 //    Console.WriteLine(obj.ToString());
  44.                 //};
  45.                 // 通常在 Debug 下使用,
  46.                 // 当一个事件没有被订阅处理时,将会使用此事件处理
  47.                 //source.UnhandledEvents += (TraceEvent obj) =>
  48.                 //{
  49.                 //    Console.WriteLine(obj.ToString());
  50.                 //};
  51.                 try
  52.                 {
  53.                         // 监听进程
  54.                         source.Process();
  55.                 }
  56.                 catch (Exception e)
  57.                 {
  58.                         Console.WriteLine(e.ToString());
  59.                 }
  60.         }
  61. }
复制代码
接着启动 Demo2.Diagnostics,输入控制台的进程号,即可考察到监听的进程 GC 事件。
9.png


我们也可以做一个像 dotnet-dump 的工具,截取进程快照。
  1. static async Task Main()
  2. {
  3.         var processes = DiagnosticsClient.GetPublishedProcesses()
  4.         .Select(Process.GetProcessById)
  5.         .Where(process => process != null);
  6.         Console.WriteLine("请输入进程 id");
  7.         foreach (var item in processes)
  8.         {
  9.                 Console.WriteLine($"{item.Id} ------ {item.ProcessName}");
  10.         }
  11.         var read = Console.ReadLine();
  12.         ArgumentNullException.ThrowIfNullOrEmpty(read);
  13.         var pid = int.Parse(read);
  14.         var client = new DiagnosticsClient(pid);
  15.         await client.WriteDumpAsync(
  16.                 dumpType: DumpType.Full,
  17.                 dumpPath: $"D:/{pid}_{DateTime.Now.Ticks}.dmp",
  18.                 logDumpGeneration: true,
  19.                 token: CancellationToken.None
  20.         );
  21. }
复制代码
然后使用 Visual Studio 打开 .dmp 文件,可以看到很多快照信息。
10.png

已经介绍了 System.Diagnostics 中的接口,以及介绍了部分 .NET CLI 工具的使用方法,因此不单独介绍 dotnet-gcdump、dotnet-dump 等诊断工具,读者可根据需要阅读官方文档。
https://learn.microsoft.com/en-us/dotnet/core/diagnostics/microsoft-diagnostics-netcore-client

目前,很多 C# 语言编写的可观测性框架是基于 System.Diagnostics、Microsoft.Diagnostics 的,由于本书不涉及微服务,因此对于这类框架在 C# 程序中的原理不再赘述,请参考官方文档。
也可以参考笔者的另一个 MQ 项目:https://mmq.whuanle.cn/7.opentelemetry.html

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册