前天有位朋友加微信求助他的程序出现了CPU爆高的问题,开局就是一个红包,把我吓懵了!🤣🤣🤣

由于是南方小年,我在老家张罗处理起来不方便,没有第一时间帮他处理,朋友在第二天上午已经找出问题了,反馈说是一个 while(true) 导致的,这就有点意思了,在我分析的众多 CPU 爆高案例中,还真没遇到 while(true) 的情况,一直都抱有遗憾,真是运气好,年前赶上了,哈哈 😄😄😄。
接下来我们就用 windbg 一起来分析下吧。
一直关注我的朋友都知道,用 !tp 命令就可以了。
- 0:022> !tp
- CPU utilization: 95 Unknown format characterUnknown format control characterWorker Thread: Total: 11 Running: 11 Idle: 0 MaxLimit: 32767 MinLimit: 4
- Work Request in Queue: 0
- --------------------------------------
- Number of Timers: 3
- --------------------------------------
- Completion Port Thread:Total: 4 Free: 4 MaxFree: 8 CurrentLimit: 4 MaxLimit: 1000 MinLimit: 4
-
上面有一句 Unknown format characterUnknown format control characterWorker 显得不太和谐,其实就是 % 的意思,不知道为啥在 .NETCore 会出现这种编码问题 😂😂😂,接下来我们用 !eeversion 看一下。
- 0:022> !eeversion
- 4.700.21.56803 (3.x runtime) free
- 4,700,21,56803 @Commit: 28bb6f994c28bc91f09bc0ddb5dcb51d0f066806
- Workstation mode
- In plan phase of garbage collection
- SOS Version: 5.0.4.36902 retail build
-
从基本信息看,当前是 .netcore 3.x 版本,而且很明显看到当前 GC 处于计划阶段。那何为计划阶段呢?
简而言之,计划阶段的GC需要决定当前的的托管堆是做简单的标记free操作,还是要做重量级的压缩操作,如果要压缩处理,还需要涉及到托管堆对象的重定位,这往往会耗费相当多的 CPU 时间片,接下来要探究的是什么导致了 GC 触发?
由于 GC 的触发往往是用户线程分配数据导致的,在GC触发的整个执行流中,其中有一环就是冻结 CLR执行引擎,也就是 SuspendEE,可以在 gc.app 中一探究竟。

为什么一定要提 SuspendEE 呢?是因为我可以通过 !t -special 找出那个 SuspendEE 的线程,这样准确度更高一点。
- 0:072> !t -special
- ThreadCount: 54
- UnstartedThread: 0
- BackgroundThread: 40
- PendingThread: 0
- DeadThread: 1
- Hosted Runtime: no
-
- OSID Special thread type
- 1 6328 DbgHelper
- 2 35c0 Finalizer
- 4 5aac Timer
- 5 38b0 ThreadpoolWorker
- 17 3530 ThreadpoolWorker
- 18 4484 ThreadpoolWorker
- 19 1e4c ThreadpoolWorker
- 21 6380 ThreadpoolWorker
- 44 5bc4 SuspendEE
- 52 8ac ThreadpoolWorker
- 54 4164 ThreadpoolWorker
- 56 61c8 ThreadpoolWorker
- 58 1fa4 ThreadpoolWorker
- 60 2788 ThreadpoolWorker
- 69 48f4 IOCompletion
- 70 5708 IOCompletion
- 71 3b58 ThreadpoolWorker
- 72 17a0 GC
- 73 2f00 Gate
- 74 35e8 IOCompletion
- 75 5730 IOCompletion
-
可以看到当前的 44 号线程就是触发 GC 的线程,接下来就明朗了,看看 44 号线程在做啥?切到 44 号线程,然后 !clrstack 即可。
- 0:044> !clrstack
- OS Thread Id: 0x5bc4 (44)
- Child SP IP Call Site
- 000000A2B0C3E4C8 00007ffd471ead3a [HelperMethodFrame: 000000a2b0c3e4c8]
- 000000A2B0C3E5E0 00007ffce8b4e506 System.Collections.Generic.List`1[[System.__Canon, System.Private.CoreLib]].System.Collections.Generic.IEnumerable.GetEnumerator() [/_/src/System.Private.CoreLib/shared/System/Collections/Generic/List.cs @ 585]
- 000000A2B0C3E630 00007ffce85e7a10 xxx.Program.DeletexxxExipredDate()
- 000000A2B0C3E780 00007ffd46bc1f0b System.Threading.ThreadHelper.ThreadStart_Context(System.Object) [/_/src/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs @ 44]
- 000000A2B0C3E7B0 00007ffd46bb90b6 System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/System.Private.CoreLib/shared/System/Threading/ExecutionContext.cs @ 172]
- 000000A2B0C3E830 00007ffd46ba535b System.Threading.ThreadHelper.ThreadStart() [/_/src/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs @ 93]
- 000000A2B0C3EA48 00007ffd47236c93 [GCFrame: 000000a2b0c3ea48]
- 000000A2B0C3ECB0 00007ffd47236c93 [DebuggerU2MCatchHandlerFrame: 000000a2b0c3ecb0]
-
从输出信息看,问题出在了 DeletexxxExipredDate() 方法,接下来探究下这个方法的源码。
- private static void DeletexxxExipredDate()
- {
- while (true)
- {
- foreach (string key in xxx.xxxSpeedLimit.Keys)
- {
- try
- {
- string[] array = xxx.xxxSpeedLimit[key].Split('$');
- if (array.Length > 1)
- {
- DateTime dateTime = Convert.ToDateTime(array[1]);
- if ((DateTime.Now - dateTime).TotalSeconds > 21600.0 && xxx.xxxSpeedLimit.ContainsKey(key))
- {
- xxx.xxxSpeedLimit.TryRemove(key, out var _);
- }
- }
- }
- catch (Exception ex)
- {
- LogHelper.WriteAppExceptionLog("删除数据出现异常:" + ex.Message, ex);
- }
- Thread.Sleep(20000);
- }
- }
- }
-
如果有丰富踩坑经验的朋友,我相信一眼就能看出这代码中存在的问题,对,就是当 xxxSpeedLimit 字典为空的时候,就相当于一个 while(true) 死循环啦,对不对?
为了验证我的说法,可以用 !dso 找到 dict 的内存地址,然后用 !wconcurrentdict 即可。
- 0:044> !dso
- OS Thread Id: 0x5bc4 (44)
- RSP/REG Object Name
- ...
- 000000A2B0C3E708 000001ba8007f618 System.Collections.Concurrent.ConcurrentDictionary`2[[System.String, System.Private.CoreLib],[System.String, System.Private.CoreLib]]
- 000000A2B0C3E760 000001ba88501cd0 System.Collections.Generic.List`1+Enumerator[[System.String, System.Private.CoreLib]]
- 000000A2B0C3E768 000001ba80050ec0 System.Threading.ContextCallback
- 000000A2B0C3E7F8 000001ba80a1a818 System.Threading.Thread
- 000000A2B0C3EA28 000001ba80a1a898 System.Threading.ThreadStart
- 0:044> !wconcurrentdict 000001ba8007f618
- Empty ConcurrentDictionary
-
可以看到,当前就是一个 空字典 😂😂😂
这次事故的主要原因:编码人员缺少了一定的编程经验,在写业务逻辑的时候缺少了对 空字典 的流程处理,导致了 while(true) 的尴尬,也有可能是将那个 Thread.Sleep(20000) 放错了位置🤣🤣🤣
总的来说很感谢这位朋友提供的dump,让我真的眼见为实啦!