一:背景

1. 讲故事

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

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

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

二: Windbg 分析

1. 现象验证

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


  1. 0:000> !tp
  2. CPU utilization: 100%
  3. Worker Thread: Total: 151 Running: 151 Idle: 0 MaxLimit: 32767 MinLimit: 4
  4. Work Request in Queue: 1
  5. AsyncTimerCallbackCompletion TimerInfo@000000001dc25bb0
  6. --------------------------------------
  7. Number of Timers: 0
  8. --------------------------------------
  9. 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 命令。


  1. 0:000> !t
  2. ThreadCount: 171
  3. UnstartedThread: 1
  4. BackgroundThread: 167
  5. PendingThread: 1
  6. DeadThread: 2
  7. Hosted Runtime: no
  8. Lock
  9. ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
  10. 0 1 7e0 00000000028901c0 26020 Preemptive 00000000049C9360:00000000049C98A8 0000000000602420 1 STA (GC)
  11. 9 2 df8 00000000028bc850 2b220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA (Finalizer)
  12. 11 3 144 000000001fdef570 102a220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA (Threadpool Worker)
  13. 14 9 dbc 0000000020703650 202b220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA
  14. 15 10 5a4 00000000206d5860 2b220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA
  15. 16 11 17c 00000000206df220 2b220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA
  16. 17 12 dd4 00000000205e49a0 2b220 Preemptive 00000000049A7A20:00000000049A98A8 0000000000602420 0 MTA
  17. 18 14 8fc 0000000020495000 2b220 Preemptive 00000000049A5A40:00000000049A78A8 0000000000602420 0 MTA
  18. 19 17 a84 0000000020817490 202b220 Preemptive 00000000049ADBB0:00000000049AF8A8 0000000000602420 0 MTA
  19. ...
  20. 180 167 12b8 0000000026436d70 1021220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA (Threadpool Worker)
  21. 181 168 11a4 0000000026437540 1021220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA (Threadpool Worker)
  22. 182 169 880 0000000026437d10 1021220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA (Threadpool Worker)
  23. 183 170 1334 00000000264384e0 1021220 Preemptive 0000000000000000:0000000000000000 0000000000602420 0 MTA (Threadpool Worker)
  24. 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 命令。

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

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

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

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

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

4. GC到底是怎么触发的

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


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

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


  1. 0:000> !ip2md 000007fe8bcd29ca
  2. MethodDesc: 000007fe8b50ae90
  3. Method Name: xxx.xxx.T_Tick(System.Object, System.EventArgs)
  4. Class: 000007fe8b6ac628
  5. MethodTable: 000007fe8b50b080
  6. mdToken: 00000000060002b5
  7. Module: 000007fe8b504118
  8. IsJitted: yes
  9. CodeAddr: 000007fe8bcd29a0
  10. Transparency: Critical
  11. 0:000> !savemodule 000007fe8b504118 D:\dumps\MRS-CPU\T_Tick.dll
  12. 3 sections in file
  13. section 0 - VA=2000, VASize=1a85fc, FileAddr=200, FileSize=1a8600
  14. section 1 - VA=1ac000, VASize=5088, FileAddr=1a8800, FileSize=5200
  15. section 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() 的调用。

记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析的更多相关文章

  1. 记一次 .NET 某电商交易平台Web站 CPU爆高分析

    一:背景 1. 讲故事 已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > ...

  2. 记一次 .NET 某市附属医院 Web程序 偶发性CPU爆高分析

    一:背景 1. 讲故事 这个月初,一位朋友加微信求助他的程序出现了 CPU 偶发性爆高,希望能有偿解决一下. 从描述看,这个问题应该困扰了很久,还是医院的朋友给力,开门就是 100块 红包 ,那既然是 ...

  3. 记一次 .NET游戏站程序的 CPU 爆高分析

    一:背景 1. 讲故事 上个月有个老朋友找到我,说他的站点晚高峰 CPU 会突然爆高,发了两份 dump 文件过来,如下图: 又是经典的 CPU 爆高问题,到目前为止,对这种我还是有一些经验可循的. ...

  4. 记一次 .NET 某医院HIS系统 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友加 wx 抱怨他的程序在高峰期总是莫名其妙的cpu爆高,求助如何分析? 和这位朋友沟通下来,据说这问题困扰了他们几年,还请了微软的工程师过来解决,无疾而终,应该还 ...

  5. 记一次 .NET 某旅行社Web站 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序内存经常飙升,cpu 偶尔飙升,没找到原因,希望帮忙看一下. 可惜发过来的 dump 只有区区2G,能在这里面找到内存泄漏那真有两把刷子..., ...

  6. 记一次 .NET 车联网云端服务 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序CPU经常飙满,没找到原因,希望帮忙看一下. 这些天连续接到几个cpu爆高的dump,都看烦了,希望后面再来几个其他方面的dump,从沟通上看, ...

  7. 记一次 .NET 某资讯论坛 CPU爆高分析

    大概有11天没发文了,真的不是因为懒,本想前几天抽空写,不知道为啥最近求助的朋友比较多,一天都能拿到2-3个求助dump,晚上回来就是一顿分析,有点意思的是大多朋友自己都分析了几遍或者公司多年的牛皮藓 ...

  8. 记一次 .NET 某智能交通后台服务 CPU爆高分析

    一:背景 1. 讲故事 前天有位朋友加微信求助他的程序出现了CPU爆高的问题,开局就是一个红包,把我吓懵了! 由于是南方小年,我在老家张罗处理起来不方便,没有第一时间帮他处理,朋友在第二天上午已经找出 ...

  9. 记一次 .NET 差旅管理后台 CPU 爆高分析

    一:背景 1. 讲故事 前段时间有位朋友在微信上找到我,说他的 web 系统 cpu 运行一段时候后就爆高了,让我帮忙看一下是怎么回事,那就看吧,声明一下,我看 dump 是免费的,主要是锤炼自己技术 ...

随机推荐

  1. 手撸一个SpringBoot-Starter

    1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...

  2. [源码解析] 机器学习参数服务器 Paracel (2)--------SSP控制协议实现

    [源码解析] 机器学习参数服务器 Paracel (2)-----SSP实现 目录 [源码解析] 机器学习参数服务器 Paracel (2)-----SSP实现 0x00 摘要 0x01 背景知识 1 ...

  3. MyBaits学习03(ResultMap和分页)

    5.ResultMap 5.1 查询为null问题 要解决的问题:属性名和字段名不一致 环境:新建一个项目,将之前的项目拷贝过来 1.数据库里的属性 2.Java中的实体类设计 public clas ...

  4. DVWA靶场之XSS(Reflected)通关

    反射型xss Low: <?php header ("X-XSS-Protection: 0"); // Is there any input? if( array_key_ ...

  5. DVWA靶场之File Inclusion(文件包含)通关

    文件包含,未经过严格过滤,将一些恶意构造带入了包含函数,可以使用一些包含函数来包含一些其他乱七八糟的东西,要么导致任意文件读取,要么命令执行 文件包含包括远程文件包含(RFI)和本地文件包含(LFI) ...

  6. MySQL数据类型 储存引擎

    存储引擎 日常生活中文件格式有很多种,并且针对不同的文件格式会有对应不同存储方式和处理机制(txt,pdf,word,mp4...) 针对不同的数据应该对应着不同的处理机制来存储 存储引擎就是不同的处 ...

  7. 基于Vue2和Node.js的反欺诈系统设计与实现

    最近包工头喊农民工小郑搬砖,小郑搬完砖后沉思片刻,决定写篇小作文分享下,作为一个初学者的全栈项目,去学习它的搭建,到落地,再到部署维护,是非常好的. ​ ------题记 写在前面 通过本文的学习,你 ...

  8. Java小题,通过JNI调用本地C++共享库中的对应方法实现杨辉三角的绘制

    1.在Eclipse中配置Javah,配置如下 位置是你javah.exe在你电脑磁盘上的路径 位置:C:\Program Files\Java\jdk1.8.0_112\bin\javah.exe ...

  9. 利用Java进行zip文件压缩与解压缩

    摘自: https://www.cnblogs.com/alphajuns/p/12442315.html 工具类: package com.alphajuns.util; import java.i ...

  10. C# 简单的对称加密

    const string KEY_64 = "HuidTeac";//注意了,是8个字符 const string IV_64 = "HuidTeac"; pu ...