记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析

2023-11-05 22:32

本文主要是介绍记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一:背景

1. 讲故事

这是6月中旬一位朋友加wx求助dump的故事,他的程序 cpu爆高➕UI卡死,问如何解决,截图如下:

在拿到这个dump后,我发现这是一个关于机械臂的MRS程序,哈哈,在机械臂这种智能机器人领域居然还有 .NET 的用武之地,有点超出我的认知哈,不知道把员工当兄弟的大强子仓库里可有 .NET 控制的几台机械臂 ????????????。

关于界面卡死的问题我这里就不讨论了,只讨论这个cpu爆高的问题如何解决,毕竟追这个系列的朋友都被前面那些各种 内存泄漏,内存爆涨 弄倦了,换个口味也挺好。

二:Windbg 分析

1. 现象验证

别人说cpu高,我得先用数据证明一下是否真的如此,方法很简单,用 !tp 命令即可。


0:000> !tp
CPU utilization: 100%
Worker Thread: Total: 151 Running: 151 Idle: 0 MaxLimit: 32767 MinLimit: 4
Work Request in Queue: 1AsyncTimerCallbackCompletion TimerInfo@000000001dc25bb0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 2 Free: 1 MaxFree: 8 CurrentLimit: 2 MaxLimit: 1000 MinLimit: 4

从卦象上看,有两个值得关注的指标:

  1. CPU utilization: 100%

这表明当时抓dump的那个时刻,机器的cpu确实为100%,确实比较糟糕,说点题外话,有几位朋友说他想抓这种100%的dump,发现阿里云的远程桌面连不上,太尴尬了。。。

  1. Total: 151 Running: 151

当前线程池的work线程有151个,正在运行的也是151个,这说明什么呢?说明每一个线程都在忙碌着,同时也预示着当前的线程不够用,急需招人,当前系统绝对有一股力在推着它。

2. 查看线程列表

接下来再看一下当前的线程列表,使用 !t 命令。


0:000> !t
ThreadCount:      171
UnstartedThread:  1
BackgroundThread: 167
PendingThread:    1
DeadThread:       2
Hosted Runtime:   noLock  ID OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception0    1  7e0 00000000028901c0    26020 Preemptive  00000000049C9360:00000000049C98A8 0000000000602420 1     STA (GC) 9    2  df8 00000000028bc850    2b220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA (Finalizer) 11    3  144 000000001fdef570  102a220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA (Threadpool Worker) 14    9  dbc 0000000020703650  202b220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA 15   10  5a4 00000000206d5860    2b220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA 16   11  17c 00000000206df220    2b220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA 17   12  dd4 00000000205e49a0    2b220 Preemptive  00000000049A7A20:00000000049A98A8 0000000000602420 0     MTA 18   14  8fc 0000000020495000    2b220 Preemptive  00000000049A5A40:00000000049A78A8 0000000000602420 0     MTA 19   17  a84 0000000020817490  202b220 Preemptive  00000000049ADBB0:00000000049AF8A8 0000000000602420 0     MTA ...180  167 12b8 0000000026436d70  1021220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA (Threadpool Worker) 181  168 11a4 0000000026437540  1021220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA (Threadpool Worker) 182  169  880 0000000026437d10  1021220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA (Threadpool Worker) 183  170 1334 00000000264384e0  1021220 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     MTA (Threadpool Worker) 184  171  278 0000000026438cb0     1400 Preemptive  0000000000000000:0000000000000000 0000000000602420 0     Ukn 

从卦象看:ID=1 的线程有一个 GC 标记,我去,看样子是触发GC了,这里要提醒一下,工作线程在 GC=Workstation 模式下,是可以充当GC回收线程的,这和 GC=Server 模式下是不同的。

3. 查看线程栈

既然是 GC 触发了,那就 死马当活马医,按照GC触发的套路查,基本流程如下:

  1. 调出所有线程栈,使用 !EEStack 命令。


0:000> !EEStack 
---------------------------------------------
Thread   0
Current frame: ntdll!NtGetContextThread+0xa
Child-SP         RetAddr          Caller, Callee
...
  1. 查找 WaitUntilGCComplete 关键词看有多少线程在等待 GC 回收

使用 Ctrl+F 检索即可,截图如下:

从图中看:有96个线程在等待GC完成,到这里,我的嘴角已经上扬了,????????????。。。

  1. 查找 try_allocate_more_space 关键词判断是什么业务逻辑触发了GC

我去,咋回事?这有头没尾的,既然没有 try_allocate_more_space 关键词也就说明当前的GC大概率不是自动触发的, 那又是谁触发的呢?有点奇葩哈?

4. GC到底是怎么触发的

要想找出答案,最简单粗暴的做法就是看下那个标记为 GC 的线程到底做了什么? 这里使用 !clrstack 即可。


0:000> !clrstack 
OS Thread Id: 0x7e0 (0)Child SP               IP Call Site
000000000043e470 00000000778c1fea [InlinedCallFrame: 000000000043e470] System.GC._Collect(Int32, Int32)
000000000043e470 000007feea38ce2a [InlinedCallFrame: 000000000043e470] System.GC._Collect(Int32, Int32)
000000000043e440 000007feea38ce2a System.GC.Collect()
000000000043e4f0 000007fe8bcd29ca xxx.xxx.T_Tick(System.Object, System.EventArgs)
000000000043e520 000007fee3d0ef6f System.Windows.Forms.Timer.OnTick(System.EventArgs)
000000000043e550 000007fee3d076fe System.Windows.Forms.Timer+TimerNativeWindow.WndProc(System.Windows.Forms.Message ByRef)
000000000043e580 000007fee3cea3c3 System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)
000000000043e620 000007fee43611f1 DomainBoundILStubClass.IL_STUB_ReversePInvoke(Int64, Int32, Int64, Int64)
000000000043e890 000007feeac1221e [InlinedCallFrame: 000000000043e890] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)
000000000043e890 000007fee3d6a378 [InlinedCallFrame: 000000000043e890] System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG ByRef)
000000000043e860 000007fee3d6a378 DomainBoundILStubClass.IL_STUB_PInvoke(MSG ByRef)
000000000043e920 000007fee3cff23e System.Windows.Forms.Application+ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr, Int32, Int32)
000000000043ea10 000007fee3cfebd2 System.Windows.Forms.Application+ThreadContext.RunMessageLoopInner(Int32, System.Windows.Forms.ApplicationContext)
000000000043eab0 000007fee3cfe9df System.Windows.Forms.Application+ThreadContext.RunMessageLoop(Int32, System.Windows.Forms.ApplicationContext)
000000000043eb10 000007fe8b6208a6 xxx.Program.Main()
000000000043ede0 000007feeac16bb3 [GCFrame: 000000000043ede0] 

我去,从线程栈上看,居然是一个 Timer 在手工调用 GC.Collect(),这是什么????????业务,接下来用 ip2md + savemodulexxx.xxx.T_Tick 源码导出来看一看。


0:000> !ip2md 000007fe8bcd29ca
MethodDesc:   000007fe8b50ae90
Method Name:  xxx.xxx.T_Tick(System.Object, System.EventArgs)
Class:        000007fe8b6ac628
MethodTable:  000007fe8b50b080
mdToken:      00000000060002b5
Module:       000007fe8b504118
IsJitted:     yes
CodeAddr:     000007fe8bcd29a0
Transparency: Critical
0:000> !savemodule 000007fe8b504118 D:\dumps\MRS-CPU\T_Tick.dll
3 ps in file
p 0 - VA=2000, VASize=1a85fc, FileAddr=200, FileSize=1a8600
p 1 - VA=1ac000, VASize=5088, FileAddr=1a8800, FileSize=5200
p 2 - VA=1b2000, VASize=c, FileAddr=1ada00, FileSize=200

用 ILSpy 打开 T_Tick.dll,截图如下:

从代码逻辑看,朋友做了 3min 触发一个 GC 的业务逻辑,我不知道这么做是想干嘛,所以就和朋友在wx上交流了下。

5. 真的全是GC背锅吗

其实在我分享过的很多cpu爆高的dump中,有相当一部分是由于频繁触发GC所致,比如大字符串拼接,误用正则表达式 等等,但3min一次的gc就能把cpu搞挂,这要是小白还能忽悠过去,在懂一点的朋友眼里是经不住推敲的,言外之意就是真正的祸首还没找到。。。要寻找可疑祸首,最好的方法就是对所有线程栈进行地毯式搜索,截图如下:

从上图中可以看到,当前有112个线程卡在 System.Collections.Generic.Dictionary2[[System.Int32, mscorlib],[System.__Canon, mscorlib]].FindEntry(Int32) 处,你肯定会说了,卡住是因为GC触发冻住了所有线程所致,当然这个理论无需反驳,确实是这样。

我相信有经验的朋友肯定会发现一个问题,那就是多线程环境下出现了一个线程不安全的 Dictionary,我在之前的一篇车联网CPU爆高分析中也提到了这个问题,它会导致 在 FindEntry 操作时出现死循环的怪异现象。

到这里为止,CPU爆高的问题基本也就全找到了。

三:总结

本次cpu爆高事故主要是由于:

  1. 多线程环境下使用了非线程安全的 Dictionary 导致了死循环。

  2. 周期性的调用 GC.Collection() 让其雪上加霜。

找出问题后,解决办法也就简单了,建议将 Dictioanry 改成 ConcurrentDictionary,同时去掉手工对 GC.Collection() 的调用。

END

工作中的你,是否已遇到 ... 

1. CPU爆高

2. 内存暴涨

3. 资源泄漏

4. 崩溃死锁

5. 程序呆滞

等紧急事件,全公司都指望着你能解决...  危难时刻才能展现你的技术价值,作为专注于.NET高级调试的技术博主,欢迎微信搜索: 一线码农聊技术,免费协助你分析Dump文件,希望我能将你的踩坑经验分享给更多的人。

这篇关于记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/352904

相关文章

基于Go语言实现Base62编码的三种方式以及对比分析

《基于Go语言实现Base62编码的三种方式以及对比分析》Base62编码是一种在字符编码中使用62个字符的编码方式,在计算机科学中,,Go语言是一种静态类型、编译型语言,它由Google开发并开源,... 目录一、标准库现状与解决方案1. 标准库对比表2. 解决方案完整实现代码(含边界处理)二、关键实现细

PostgreSQL 序列(Sequence) 与 Oracle 序列对比差异分析

《PostgreSQL序列(Sequence)与Oracle序列对比差异分析》PostgreSQL和Oracle都提供了序列(Sequence)功能,但在实现细节和使用方式上存在一些重要差异,... 目录PostgreSQL 序列(Sequence) 与 oracle 序列对比一 基本语法对比1.1 创建序

Linux区分SSD和机械硬盘的方法总结

《Linux区分SSD和机械硬盘的方法总结》在Linux系统管理中,了解存储设备的类型和特性是至关重要的,不同的存储介质(如固态硬盘SSD和机械硬盘HDD)在性能、可靠性和适用场景上有着显著差异,本文... 目录一、lsblk 命令简介基本用法二、识别磁盘类型的关键参数:ROTA查询 ROTA 参数ROTA

使用easy connect之后,maven无法使用,原来需要配置-Djava.net.preferIPv4Stack=true问题

《使用easyconnect之后,maven无法使用,原来需要配置-Djava.net.preferIPv4Stack=true问题》:本文主要介绍使用easyconnect之后,maven无法... 目录使用easGWowCy connect之后,maven无法使用,原来需要配置-DJava.net.pr

在.NET平台使用C#为PDF添加各种类型的表单域的方法

《在.NET平台使用C#为PDF添加各种类型的表单域的方法》在日常办公系统开发中,涉及PDF处理相关的开发时,生成可填写的PDF表单是一种常见需求,与静态PDF不同,带有**表单域的文档支持用户直接在... 目录引言使用 PdfTextBoxField 添加文本输入域使用 PdfComboBoxField

慢sql提前分析预警和动态sql替换-Mybatis-SQL

《慢sql提前分析预警和动态sql替换-Mybatis-SQL》为防止慢SQL问题而开发的MyBatis组件,该组件能够在开发、测试阶段自动分析SQL语句,并在出现慢SQL问题时通过Ducc配置实现动... 目录背景解决思路开源方案调研设计方案详细设计使用方法1、引入依赖jar包2、配置组件XML3、核心配

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

基于Python打造一个智能单词管理神器

《基于Python打造一个智能单词管理神器》这篇文章主要为大家详细介绍了如何使用Python打造一个智能单词管理神器,从查询到导出的一站式解决,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 项目概述:为什么需要这个工具2. 环境搭建与快速入门2.1 环境要求2.2 首次运行配置3. 核心功能使用指

Python中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

判断PyTorch是GPU版还是CPU版的方法小结

《判断PyTorch是GPU版还是CPU版的方法小结》PyTorch作为当前最流行的深度学习框架之一,支持在CPU和GPU(NVIDIACUDA)上运行,所以对于深度学习开发者来说,正确识别PyTor... 目录前言为什么需要区分GPU和CPU版本?性能差异硬件要求如何检查PyTorch版本?方法1:使用命