.Net Windbg 记录一次CPU100%内存暴涨情况 电脑版发表于:2026/5/18 1:19  ># .Net Windbg 记录一次CPU100%内存暴涨情况 [TOC] tn>首先非常感谢黄老师(一线码农)的大力支持,黄老师的高级调试训练营我非常推荐大家学习学习。 这是他的博客园:https://www.cnblogs.com/huangxincheng 想学习他的高级调试训练营可以关注他的微信公众号:一线码农聊技术 当然我还是有愧于他没有怎么好好学这一块,以后会补上的。 ## 背景 tn2>iis web网站突然在正式环境用户使用时导致cpu和内存爆满。 截图我没有截图很抱歉,这里我首先按照黄老师的方式进行抓包。 ```bash "C:\Program Files\Procdump\procdump.exe" -ma -s 5 -n 2 -c 70 w3wp ``` ## 开始分析 tn2>首先将dump包拖入windbg之后,通过`!tp`命令查看CPU的利用率。  tn2>我们发现CPU跑满了,但是线程数没有增加只有5个,并且只有4个在干活,那么问题就只在这4个当中。 然后我们可以通过`!cpuid`看一下有几个cpu,这里配置比较低只有2个cpu。  然后我们通过`!runaway`查看所有线程的占用CPU的时间。  tn>也有可能我分析得不太对啊。 tn2>我们看到前三个非常可疑,分别是`29`号线程占用18分钟、`30`号线程占用12分钟、`31`号线程占用11分钟。 由于`!runaway` 统计的是累计 CPU 时间,不是当前瞬时状态,不可以立即确认谁是元凶。 然后我们通过`~* k`查看每个线程的用户态/内核态时间  tn2>我们可以看到目前29号和31号线程`NtRemoveIoCompletion + GetQueuedCompletionStatus`在等 IO 完成端口信号,并且29号和31号线程出现`LowLevelLifoSemaphore.WaitForSignal`表示线程池工作线程空闲等待。 那么线程 29 当前 确实在等待,没有消耗 CPU。它的 18 分钟历史 CPU 是之前工作累计的。 那么还剩下30线程和31号线程,我们可以看到他们都在进行同样的操作都是在进行读取SQL数据库的某些操作,并且31号线程的栈情况是申请LOH对象,然后触发GC、执行GC回收、并且等待 GC 完成。 (栈都是从下往上看啊) ```bash 00 00000063`041bef18 00007ffe`03a5d75e ntdll!NtWaitForSingleObject+0x14 01 00000063`041bef20 00007ffd`b87330fd KERNELBASE!WaitForSingleObjectEx+0x8e 02 (Inline Function) --------`-------- coreclr!GCEvent::Impl::Wait+0x13 [D:\a\_work\1\s\src\coreclr\gc\windows\gcenv.windows.cpp @ 1372] 03 (Inline Function) --------`-------- coreclr!GCEvent::Wait+0x13 [D:\a\_work\1\s\src\coreclr\gc\windows\gcenv.windows.cpp @ 1422] ← 线程阻塞等待 04 00000063`041befc0 00007ffd`b8734bb2 coreclr!SVR::gc_heap::wait_for_gc_done+0x5d [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 14979] ← ?? **等待 GC 完成** 05 00000063`041beff0 00007ffd`b873311d coreclr!SVR::GCHeap::GarbageCollectGeneration+0xee [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 50760] ← ?? **执行 GC 回收!** 06 00000063`041bf040 00007ffd`b882d6d8 coreclr!SVR::gc_heap::trigger_gc_for_alloc+0x15 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 19134] ← ?? **触发 GC!** 07 00000063`041bf070 00007ffd`b87e0b70 coreclr!SVR::gc_heap::try_allocate_more_space+0x4ca20 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 19295] 08 00000063`041bf0e0 00007ffd`b87e0ae4 coreclr!SVR::gc_heap::allocate_more_space+0x68 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 19779] 09 00000063`041bf140 00007ffd`b8724e65 coreclr!SVR::gc_heap::allocate_uoh_object+0x58 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 45154] ← 线程阻塞等待 0a 00000063`041bf1b0 00007ffd`b86eb239 coreclr!SVR::GCHeap::Alloc+0x1c5 [D:\a\_work\1\s\src\coreclr\gc\gc.cpp @ 49673] ``` tn2>LOH的空间不够了吗?为什么GC会去触发和回收它,我们可以先看看GC情况,先通过`!eeversion`看看 GC 模式。  tn2>我们可以看到它是Server Mode模式。那么它有可能会不断请求CPU资源去完成回收工作。 下面有一张判断GC的异常表格 | 指标 | 危险阈值 | | ------------- | ------ | | LOH 碎片 (Free) | >100MB | | LOH 大对象数量 | >100 个 | | LOH 总大小 | >500MB | | GC 线程数 | >2 个 | | LOH 占总 GC 堆 | < 10% | tn2>接着我们通过命令`!eeheap -gc`查看托管堆的情况。  tn2>我们可以看到实际分配的对象是给GC的大概是`3,465,992,848 bytes ≈ 3.23 GB`,但是系统实际给的内存是`4,719,005,696 bytes ≈ 4.39 GB`。 那么我们再通过`LOH`查看占了多大的GC内存 ```bash !dumpheap -stat -min 85000 ```  ```bash Total 854 objects, 2,583,696,070 bytes ← 2.58 GB ``` tn2>我们可以发现GC Allocated 总共 3.23 GB,LOH 占了 2.58 GB,LOH 占比 = 2.58 / 3.23 ≈ 80%。 这是非常异常的情况。并且LOH 碎片有686MB大于了100MB。 可以大概判断是由于GC触发导致的问题。 接着我们通过`~30s`进入到30线程,执行`.loadby sos coreclr`加载SOS,然后执行`!t -special`查看特殊线程。  tn2>我们可以看到30线程的GC模式是`Cooperative`模式,配合GC协作回收这是一种异常情况,那我们来看看30线程具体做的什么操作。 通过`!clrstack -a`命令查看托管线程调用栈并显示所有参数和局部变量。  tn2>我们可以看到它的卡在 TryReadSqlStringValue,且 length = 0x0000000000399727(约 3,771,175 字节 ≈ 3.6 MB) TryReadSqlStringValue 正在读取一个 非常大的字符串列,3.6MB 的单行数据! 再者我们一步一步往前面找,发现有个Reader对象,我们想通过reader对象找到具体是其中哪一条脚本语句。 我们可以看到它的地址是`0x000001ef649773f8`  tn2>查看里面`!do 0x000001ef649773f8`,我们找到command对象,并查看里面的具体参数。  tn2>`!do 0x000001ef649770c8`是command对象。  tn2>然后我们找到`_commandText`属性,看看里面的具体语句,那么就是这个脚本导致的。 