前言
前文中介绍了,cpu 使用率和三个方面有关,一个是中断、一个是用户时间、一个是系统时间。
那么当我们排查问题时候,查到是用户时间占用比较多的话,那么该如何排查呢?
正文
首先我们想到的是,cpu高嘛,查看火焰图对吧。 火焰图可以看到哪个函数消耗cpu比较多?
似乎这个问题好像马上能查看到,如果火焰图调入gc函数消耗的cpu比较多的话,那么就直接定位了。
然而事情好像没有这么简单。
这个我们来看一下火焰图原理:
火焰图是一种性能分析工具,它通过采样CPU调用栈来生成可视化的性能分析图表。火焰图能够显示:
1.CPU时间消耗最多的函数调用路径
2. 调用栈的深度和宽度
3. 热点代码的位置
GC在火焰图中的表现
- GC线程活动:如果GC线程占用大量CPU时间,会在火焰图中显示为GC相关的调用栈
- GC暂停时间:虽然火焰图主要显示CPU时间,但GC暂停期间的CPU使用也会被记录
- 内存分配热点:频繁的内存分配会导致GC压力,这些分配点会在火焰图中显示
可能观察不到的情况:
- 后台GC:.NET的Server GC模式使用后台线程进行垃圾回收,这些线程的活动可能不会在火焰图中明显显示
- GC暂停:GC暂停期间应用程序线程被挂起,火焰图可能显示为"空闲"时间
也就是说火焰图可能能看出一些gc问题,但是有些可能不明显。
那么从简单的说,内存分配热点,也就是说内存频繁的分配,那么可以查看出。
这个其实占用cpu的话,经过测试,这个其实一般来说占用cpu不会太多,很难看出来。
比如说:- using System;
- using System.Collections.Generic;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Text;
- namespace RealAllocationHotspot
- {
- class Program
- {
- static void Main(string[] args)
- {
- Console.WriteLine("Real Memory Allocation Hotspot Demo");
- Console.WriteLine("This will show actual allocation hotspots in flame graphs");
-
- var cts = new CancellationTokenSource();
- var tasks = new List<Task>();
- // 任务1:CPU密集型的分配热点
- tasks.Add(Task.Run(() => CpuIntensiveAllocation(cts.Token)));
- // 任务2:字符串操作热点
- tasks.Add(Task.Run(() => StringAllocationHotspot(cts.Token)));
- // 任务3:集合操作热点
- tasks.Add(Task.Run(() => CollectionAllocationHotspot(cts.Token)));
- // 任务4:复杂对象分配热点
- tasks.Add(Task.Run(() => ComplexObjectAllocation(cts.Token)));
- Console.WriteLine("Press any key to stop...");
- Console.ReadKey();
- cts.Cancel();
- Task.WaitAll(tasks.ToArray());
- }
- // CPU密集型的分配热点 - 会在火焰图中显示
- static void CpuIntensiveAllocation(CancellationToken token)
- {
- while (!token.IsCancellationRequested)
- {
- // 这些操作会消耗大量CPU时间
- for (int i = 0; i < 10000; i++)
- {
- // 热点1:大量字符串操作
- var result = "";
- for (int j = 0; j < 100; j++)
- {
- result += $"String_{i}_{j}_"; // 字符串连接
- result += DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
- }
- // 热点2:字符串格式化
- var formatted = string.Format("Complex string: {0}, {1}, {2}, {3}, {4}",
- Guid.NewGuid(), DateTime.Now, Environment.TickCount,
- i);
- // 热点3:数组操作
- var array = new byte[1000];
- for (int k = 0; k < array.Length; k++)
- {
- array[k] = (byte)(i + k);
- }
- }
- }
- }
- // 字符串分配热点 - 会显示为CPU热点
- static void StringAllocationHotspot(CancellationToken token)
- {
- while (!token.IsCancellationRequested)
- {
- // 这些操作会消耗CPU时间进行字符串处理
- for (int i = 0; i < 5000; i++)
- {
- // 热点:字符串连接操作
- var sb = new StringBuilder();
- for (int j = 0; j < 50; j++)
- {
- sb.Append($"String_{i}_{j}_");
- sb.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
- sb.Append("_");
- }
- var result = sb.ToString();
- // 热点:字符串分割和重组
- var parts = result.Split('_');
- var reconstructed = string.Join("|", parts);
- }
- }
- }
- // 集合分配热点 - 会显示为CPU热点
- static void CollectionAllocationHotspot(CancellationToken token)
- {
- while (!token.IsCancellationRequested)
- {
- // 这些操作会消耗CPU时间进行集合操作
- for (int i = 0; i < 1000; i++)
- {
- // 热点:列表操作
- var list = new List<string>();
- for (int j = 0; j < 100; j++)
- {
- list.Add($"Item_{i}_{j}");
- }
- // 热点:字典操作
- var dict = new Dictionary<string, object>();
- for (int j = 0; j < 50; j++)
- {
- dict[$"Key_{i}_{j}"] = new byte[100];
- }
- // 热点:集合操作
- var set = new HashSet<string>();
- for (int j = 0; j < 100; j++)
- {
- set.Add($"SetItem_{i}_{j}");
- }
- }
- }
- }
- // 复杂对象分配热点 - 会显示为CPU热点
- static void ComplexObjectAllocation(CancellationToken token)
- {
- while (!token.IsCancellationRequested)
- {
- // 这些操作会消耗CPU时间进行对象创建和初始化
- for (int i = 0; i < 1000; i++)
- {
- // 热点:复杂对象创建
- var complexObject = new ComplexObject
- {
- Id = i,
- Name = $"ComplexObject_{i}",
- Data = new byte[500],
- Tags = new List<string> { "tag1", "tag2", "tag3" },
- Properties = new Dictionary<string, object>
- {
- ["prop1"] = Guid.NewGuid(),
- ["prop2"] = DateTime.Now,
- ["prop3"] = Environment.TickCount
- }
- };
- // 热点:对象序列化/反序列化模拟
- var serialized = complexObject.ToString();
- var deserialized = ComplexObject.FromString(serialized);
- }
- }
- }
- }
- // 复杂对象 - 用于演示分配热点
- class ComplexObject
- {
- public int Id { get; set; }
- public string Name { get; set; }
- public byte[] Data { get; set; }
- public List<string> Tags { get; set; }
- public Dictionary<string, object> Properties { get; set; }
- public override string ToString()
- {
- return $"ComplexObject{{Id={Id}, Name={Name}, DataLength={Data?.Length ?? 0}}}";
- }
- public static ComplexObject FromString(string str)
- {
- // 模拟反序列化
- return new ComplexObject { Id = 0, Name = str };
- }
- }
- }
复制代码 那么来看一下cpu火焰图:
可以看到一个datetime的now,后面都是大量的扩容创建对象啥的。
GC暂停时间:虽然火焰图主要显示CPU时间,但GC暂停期间的CPU使用也会被记录。
这个怎么说呢?
// .NET中有两种GC模式:
// 1. 工作站GC (Workstation GC) - 前台GC,会暂停所有线程
// 2. 服务器GC (Server GC) - 后台GC,使用专用线程
然后这两种工作状态呢?- // GC暂停期间,应用程序线程的状态:
- // 1. 前台GC:所有线程被挂起
- // 2. 后台GC:应用程序线程继续运行,GC线程在后台工作
复制代码 这里说的是gc暂停时间呢?说的是前台gc,所有的线程被挂起的情况。
前台gc暂停:
明显可见:前台GC会暂停所有用户线程,在火焰图中表现为明显的"热点"
具体表现:
GC.Collect() 调用会显示为CPU热点
GC.WaitForPendingFinalizers() 会显示为等待时间
用户线程在GC期间会显示为 WaitHandle.WaitOne 或 Thread.Sleep
GC.Collect() 和 GC.WaitForPendingFinalizers() 是gc回收的函数。
[code]using System;using System.Collections.Generic;using System.Diagnostics;using System.Threading;using System.Threading.Tasks;namespace GCPauseFlameDemo{ /// /// 演示GC暂停在火焰图中的不同表现 /// public class GCFlameGraphDemo { public static void RunDemo() { Console.WriteLine("=== GC暂停在火焰图中的表现演示 ==="); Console.WriteLine(); Console.WriteLine("请选择测试模式:"); Console.WriteLine("1. 前台GC演示 - 会明显显示在火焰图中"); Console.WriteLine("2. 后台GC演示 - 可能不明显"); Console.WriteLine("3. 业务逻辑演示 - 在GC暂停期间会受到影响"); Console.WriteLine("4. 密集GC演示 - 专门设计用于在火焰图中产生明显热点"); Console.WriteLine("5. 超激进GC演示 - 尝试在用户线程上产生明显的GC相关CPU使用"); Console.WriteLine("6. GC活动诊断 - 监控GC相关指标并解释火焰图表现"); Console.WriteLine("7. 混合模式 - 所有任务同时运行(用于对比)"); Console.WriteLine("8. 分阶段测试 - 依次测试每种模式"); Console.WriteLine(); Console.Write("请输入选择 (1-8): "); var choice = Console.ReadLine(); switch (choice) { case "1": RunForegroundGCDemo(); break; case "2": RunBackgroundGCDemo(); break; case "3": RunBusinessLogicDemo(); break; case "4": RunIntensiveGCDemo(); break; case "5": RunUltraIntensiveGCDemo(); break; case "6": RunDiagnoseGCActivity(); break; case "7": RunMixedDemo(); break; case "8": RunStagedDemo(); break; default: Console.WriteLine("无效选择,运行混合模式演示..."); RunMixedDemo(); break; } } /// /// 演示前台GC - 在火焰图中会显示为明显的热点 /// static void DemonstrateForegroundGC(CancellationToken token) { Console.WriteLine("前台GC演示开始 - 这些操作会在火焰图中显示为热点"); while (!token.IsCancellationRequested) { // 大量分配内存,确保触发GC var objects = new List(); for (int i = 0; i < 200000; i++) // 增加分配量 { objects.Add(new byte[2000]); // 分配400MB } // 强制触发前台GC - 这会在火焰图中显示为CPU热点 var sw = Stopwatch.StartNew(); // 多次调用GC,确保在火焰图中可见 for (int i = 0; i < 5; i++) // 连续调用5次 { GC.Collect(0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // 短暂休眠,让采样器有机会捕获 Thread.Sleep(10); } sw.Stop(); Console.WriteLine($"[前台GC] 强制GC耗时: {sw.ElapsedMilliseconds}ms"); // 减少休眠时间,增加GC调用频率 Thread.Sleep(200); } } /// /// 演示后台GC - 在火焰图中可能不明显 /// static void DemonstrateBackgroundGC(CancellationToken token) { Console.WriteLine("后台GC演示开始 - 这些操作在火焰图中可能不明显"); while (!token.IsCancellationRequested) { // 大量分配,让后台GC自然触发 var objects = new List(); for (int i = 0; i < 50000; i++) { objects.Add(new byte[2000]); // 分配100MB } // 不强制GC,让后台GC线程自然工作 // 后台GC线程的工作可能不会在火焰图中明显显示 Thread.Sleep(300); } } /// /// 演示业务逻辑在GC暂停期间的表现 /// static void DemonstrateBusinessLogic(CancellationToken token) { Console.WriteLine("业务逻辑演示开始 - 在GC暂停期间会受到影响"); while (!token.IsCancellationRequested) { // 模拟正常的业务逻辑 var sw = Stopwatch.StartNew(); // 密集计算 for (int i = 0; i < 10000; i++) { var result = Math.Sqrt(i) * Math.PI; } sw.Stop(); // 如果业务逻辑在GC暂停期间执行,可能会显示为: // - Thread.Sleep (等待GC完成) // - WaitHandle.WaitOne (等待GC完成) // - 执行时间异常长 Console.WriteLine($"[业务逻辑] 计算耗时: {sw.ElapsedMilliseconds}ms"); Thread.Sleep(100); } } /// /// 专门用于在火焰图中产生明显GC热点的方法 /// static void DemonstrateIntensiveGC(CancellationToken token) { Console.WriteLine("密集GC演示开始 - 专门设计用于在火焰图中产生明显热点"); Console.WriteLine("注意:GC工作主要在专用线程上,可能不会在用户线程的火焰图中显示"); while (!token.IsCancellationRequested) { // 创建大量对象,确保触发GC var objects = new List(); for (int i = 0; i < 500000; i++) // 大量分配 { objects.Add(new byte[1000]); // 分配500MB } // 密集调用GC,确保在火焰图中可见 var sw = Stopwatch.StartNew(); // 连续多次GC,产生明显的CPU热点 for (int j = 0; j < 10; j++) // 10次循环 { // 强制Gen0 GC GC.Collect(0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // 强制Gen1 GC GC.Collect(1, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // 强制Gen2 GC(最耗时) GC.Collect(2, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // 短暂休眠,让采样器捕获 Thread.Sleep(5); } sw.Stop(); Console.WriteLine($"[密集GC] 强制GC耗时: {sw.ElapsedMilliseconds}ms"); // 短暂休眠后继续 Thread.Sleep(100); } } /// /// 超激进GC测试 - 尝试在用户线程上产生明显的GC相关CPU使用 /// static void DemonstrateUltraIntensiveGC(CancellationToken token) { Console.WriteLine("超激进GC演示开始 - 尝试在用户线程上产生明显的GC相关CPU使用"); Console.WriteLine("这个测试会尝试在用户线程上执行GC相关工作"); while (!token.IsCancellationRequested) { // 创建大量对象 var objects = new List(); for (int i = 0; i < 1000000; i++) // 1M个对象 { objects.Add(new byte[1000]); // 分配1GB } var sw = Stopwatch.StartNew(); // 在用户线程上执行密集的GC相关操作 for (int j = 0; j < 20; j++) // 20次循环 { // 连续调用GC,尝试在用户线程上产生CPU热点 for (int k = 0; k < 5; k++) { GC.Collect(0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // 在用户线程上执行一些计算,确保CPU使用 for (int l = 0; l < 10000; l++) { var result = Math.Sqrt(l) * Math.PI; } } // 强制Gen1和Gen2 GC GC.Collect(1, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); GC.Collect(2, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); // 在用户线程上执行更多计算 for (int l = 0; l < 5000; l++) { var result = Math.Sin(l) * Math.Cos(l); } } sw.Stop(); Console.WriteLine($"[超激进GC] 强制GC耗时: {sw.ElapsedMilliseconds}ms"); Console.WriteLine($"当前内存使用: {GC.GetTotalMemory(false) / 1024 / 1024} MB"); Thread.Sleep(50); } } /// /// 诊断GC活动的方法 /// static void DiagnoseGCActivity(CancellationToken token) { Console.WriteLine("GC活动诊断开始 - 监控GC相关指标"); var lastGen0 = GC.CollectionCount(0); var lastGen1 = GC.CollectionCount(1); var lastGen2 = GC.CollectionCount(2); var lastMemory = GC.GetTotalMemory(false); while (!token.IsCancellationRequested) { var currentGen0 = GC.CollectionCount(0); var currentGen1 = GC.CollectionCount(1); var currentGen2 = GC.CollectionCount(2); var currentMemory = GC.GetTotalMemory(false); if (currentGen0 > lastGen0 || currentGen1 > lastGen1 || currentGen2 > lastGen2) { Console.WriteLine($"\n=== GC事件检测到 [{DateTime.Now:HH:mm:ss.fff}] ==="); Console.WriteLine($"Gen0: {currentGen0 - lastGen0} 次"); Console.WriteLine($"Gen1: {currentGen1 - lastGen1} 次"); Console.WriteLine($"Gen2: {currentGen2 - lastGen2} 次"); Console.WriteLine($"内存变化: {(currentMemory - lastMemory) / 1024 / 1024} MB"); Console.WriteLine($"总内存: {currentMemory / 1024 / 1024} MB"); // 解释为什么GC可能不在火焰图中显示 Console.WriteLine("
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |