一:背景1.讲故事前段时间有位朋友微信找到我,说他的程序出现了 CPU 爆高,帮忙看下程序到底出了什么情况?图就不上了,我们直接进入主题 。
二:WinDbg 分析1. CPU 真的爆高吗?要确认是否真的爆高,可以使用 !tp
观察 。
0:000> !tpCPU utilization: 96%Worker Thread: Total: 36 Running: 36 Idle: 0 MaxLimit: 32767 MinLimit: 16Work Request in Queue: 61Unknown Function: 00007ffc5c461750Context: 00000187da7a9788Unknown Function: 00007ffc5c461750Context: 0000017fcdd36e88...Unknown Function: 00007ffc5c461750Context: 00000187da5e87d8Unknown Function: 00007ffc5c461750Context: 00000187da872788--------------------------------------Number of Timers: 2--------------------------------------Completion Port Thread:Total: 1 Free: 1 MaxFree: 32 CurrentLimit: 1 MaxLimit: 1000 MinLimit: 16
从卦中可以看到 CPU=96%
, 果然是 CPU 爆高 , 而且 Work Request
也累积了 61
个任务未处理 , 看样子下游不给力哈? 不给力有可能是因为 GC 触发导致线程频繁停顿,也可能真的是处理太慢 。
2. 是 GC 触发了吗?要查看是否真的 GC 触发,可以用 !t -special
观察下是否有 SuspendEE
字样 。
0:000> !t -specialThreadCount:83UnstartedThread:0BackgroundThread: 74PendingThread:0DeadThread:9Hosted Runtime:noLockID OSID ThreadOBJState GC ModeGC Alloc ContextDomainCount Apt Exception191 1c84 0000017abe10cf6028220 Preemptive0000000000000000:0000000000000000 0000017abe103f70 0Ukn...OSID Special thread type26 1c78 DbgHelper27 1328 GC SuspendEE28 1e78 GC29 1ffc GC30 1de0 GC
果不其然 27
号线程带了 SuspendEE
,说明当前 GC 是触发状态,接下来看下 27 号线程的非托管栈, 到底发生了什么 。
0:027> k # Child-SPRetAddrCall Site00 00000074`11aff348 00007ffc`66624abfntdll!NtWaitForSingleObject+0x1401 00000074`11aff350 00007ffc`591aa747KERNELBASE!WaitForSingleObjectEx+0x8f02 00000074`11aff3f0 00007ffc`591aa6ffclr!CLREventWaitHelper2+0x3c03 00000074`11aff430 00007ffc`591aa67cclr!CLREventWaitHelper+0x1f04 00000074`11aff490 00007ffc`59048ef5clr!CLREventBase::WaitEx+0x7c05 00000074`11aff520 00007ffc`5905370eclr!SVR::t_join::join+0x10f06 00000074`11aff580 00007ffc`59049278clr!SVR::gc_heap::plan_phase+0x11f407 00000074`11aff900 00007ffc`590494d6clr!SVR::gc_heap::gc1+0xb808 00000074`11aff950 00007ffc`59048c64clr!SVR::gc_heap::garbage_collect+0x87009 00000074`11aff9f0 00007ffc`59192487clr!SVR::gc_heap::gc_thread_function+0x740a 00000074`11affa20 00007ffc`59194194clr!SVR::gc_heap::gc_thread_stub+0x7e0b 00000074`11affa60 00007ffc`694184d4clr!GCThreadStub+0x240c 00000074`11affa90 00007ffc`69dee8b1kernel32!BaseThreadInitThunk+0x140d 00000074`11affac0 00000000`00000000ntdll!RtlUserThreadStart+0x21
从栈方法 gc_thread_function()
来看,这是一个专有的 GC 线程,熟悉 server GC
的朋友应该知道 , 用户线程分配 引发GC后,会通过 event 唤醒GC线程,言外之意就是还没有找到这个用户线程触发的导火索,要想找到答案有很多方法 , 查看当前的 GCSettings 观察 GC 触发的诱因及代数,截图如下:
文章插图
我去,居然是一个诱导式FullGC,言外之意就是有代码会调用
GC.Collect()
,接下来我们用 ~*e !clrstack
导出所有的线程栈,观察 GC.Collect()
字样,还真给找到了 。。。0:117> !clrstackOS Thread Id: 0x170c (117)Child SPIP Call Site0000007419f1d580 00007ffc69e25ac4 [InlinedCallFrame: 0000007419f1d580] System.GC._Collect(Int32, Int32)0000007419f1d580 00007ffbfba0fbf2 [InlinedCallFrame: 0000007419f1d580] System.GC._Collect(Int32, Int32)0000007419f1d550 00007ffbfba0fbf2 Spire.Pdf.PdfDocument.Dispose()...0000007419f1e2f0 00007ffc504b1092 System.Web.Mvc.MvcHandler.EndProcessRequest(System.IAsyncResult)
从代码看居然是一个商业组件 Spire.Pdf
在 Dispose 时手工释放触发的 , 一般这么做的目的是想通过此方法间接释放非托管资源 。其实一个 FullGC 不代表什么,如果频繁的 FullGC 肯定是有问题的 , 那如何观察是否频繁呢?在 CLR 源码中有一个 full_gc_counts 的全局变量,记录着FullGC 的次数,代码如下:
size_t gc_heap::full_gc_counts[gc_type_max];enum gc_type{gc_type_compacting = 0,gc_type_blocking = 1,#ifdef BACKGROUND_GCgc_type_background = 2,#endif //BACKGROUND_GCgc_type_max = 3};
接下来可以用 x 命令去检索这个变量,观察各自的布局 。文章插图
因为
gc_type_compacting
和 gc_type_blocking
有重叠,而且观察进程运行了
推荐阅读
- .NET 7 RC 2 发布,倒计时一个月发布正式版
- KMP模式匹配 学习笔记
- CVPR2022 Oral OGM-GE阅读笔记
- P2216 [HAOI2007]理想的正方形方法记录
- 《基于Apache Flink的流处理》读书笔记
- 王者荣耀金秋印记一个如何获取
- .NET MAUI 社区工具包 1.3版本发布
- .NET平台下一个你不知道的框架,我只想说两个字:“牛逼”
- 记一次某制造业ERP系统 CPU打爆事故分析
- 支付宝的花呗怎么还款(支付宝怎么查花呗还款记录)