記一次 .NET某MES自動化桌面程序 卡死分析

一:背景

1. 講故事

前些天有位朋友在微信上找到我,說他們的客戶端程序卡死了,讓我幫忙看下是什麼原因導致的?dump也拿到了手,既然有了dump就開始正式分析吧。

二:WinDbg 分析

1. 什麼導致的卡死

客戶端的程序卡死比較好找原因,入手點就是主線程,看下它此時正在做什麼,可以用 k 命令。


0:000> k
 # ChildEBP RetAddr      
00 003cdf7c 74c115ce     ntdll!NtWaitForSingleObject+0x15
01 003cdf7c 756e1194     KERNELBASE!WaitForSingleObjectEx+0x98
02 003cdf94 6f573bea     kernel32!WaitForSingleObjectExImplementation+0x75
03 003cdfc4 6f573c31     clr!CLREventWaitHelper2+0x33
04 003ce014 6f573bb6     clr!CLREventWaitHelper+0x2a
05 003ce04c 6f57c8be     clr!CLREventBase::WaitEx+0x152
06 003ce060 6f5764a9     clr!WKS::GCHeap::WaitUntilGCComplete+0x34
07 003ce0b0 6f583cf4     clr!Thread::RareDisablePreemptiveGC+0x231
08 003ce134 6a87a767     clr!JIT_RareDisableHelper+0x24
09 003ce16c 6a87472b     System_Drawing_ni+0x4a767
0a 003ce17c 0846b372     System_Drawing_ni!System.Drawing.Graphics.Clear+0x1b
...

從卦中信息看,代碼正在託管層做Graphics,突然程序觸發了GC,因爲STW的原因,clr需要使用SuspendRuntime把主線程導入到 WaitUntilGCComplete 進行等待,有了這些信息之後,接下來就是尋找爲什麼會觸發GC。

2. 爲什麼會觸發 GC

要找到GC觸發原因,首先要找哪一個線程觸發了GC,這個可以用 !t 看下託管線程列表中的 GC 字樣,輸出如下:


0:000> !t
ThreadCount:      48
UnstartedThread:  0
BackgroundThread: 35
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
  48   42  ee8 0ee39f60   1029220 Cooperative 00000000:00000000 0076c700 3     MTA (GC) (Threadpool Worker) 
  ...

0:048> k 10
 # ChildEBP RetAddr      
00 4775c9e8 6f57d24e     clr!WKS::gc_heap::mark_object_simple1+0x8a
01 4775ca14 6f57bf72     clr!WKS::gc_heap::mark_object_simple+0x22b
02 4775ca34 6f5774b2     clr!WKS::GCHeap::Promote+0xaa
03 4775ca4c 6f57809c     clr!GcEnumObject+0x37
04 4775cdbc 6f5777cb     clr!EECodeManager::EnumGcRefs+0x854
05 4775ce10 6f5723b9     clr!GcStackCrawlCallBack+0x167
06 4775d0dc 6f5724bf     clr!Thread::StackWalkFramesEx+0x92
07 4775d410 6f57743b     clr!Thread::StackWalkFrames+0x9d
08 4775d448 6f57ba0e     clr!GCToEEInterface::GcScanRoots+0x108
09 4775d4a8 6f5792db     clr!WKS::gc_heap::mark_phase+0x18a
0a 4775d4d0 6f57966f     clr!WKS::gc_heap::gc1+0xda
0b 4775d508 6f57978c     clr!WKS::gc_heap::garbage_collect+0x447
0c 4775d530 6f70b767     clr!WKS::GCHeap::GarbageCollectGeneration+0x1f6
0d 4775d590 6f70b7a3     clr!WKS::gc_heap::trigger_ephemeral_gc+0x1e
0e 4775d590 6f575f6f     clr!WKS::gc_heap::allocate_small+0x270
0f 4775d5bc 6f575fca     clr!WKS::gc_heap::try_allocate_more_space+0x17c
...

從卦中可以看到當前 48 號線程觸發了GC,並且是處於三階段中的標記階段,接下來需要觀察下到底觸發的是哪一代GC,可以用 dt 觀察下 setting 全局變量即可。


0:048> x clr!*settings*
...
6fbd4bd8          clr!WKS::gc_heap::settings = <no type information>
6fbd7538          clr!SVR::gc_heap::settings = <no type information>
...

0:048> dp clr!WKS::gc_heap::settings
6fbd4bd8  00002ce4 00000002 00000001 00000001
6fbd4be8  00000000 00000000 00000000 00000000
6fbd4bf8  00000001 00000000 00000000 00000000
6fbd4c08  00000000 00000000 00000005 00000001
6fbd4c18  00000000 00000000 00000000 00000001
...

從卦中的 +0x4 偏移可以看到當前觸發的是 FullGC,從 +0x38 可以看到GC的觸發原因是 reason_oos_soh = 5,有經驗的朋友看到這裏應該就知道是什麼原因了。

3. 爲什麼會觸發FullGC

相信大家都知道FullGC 有一個 STW 的概念,既然有STW自然就會讓程序卡死,回過頭來說一下經驗在哪裏,對,就是這個指針的長度,很顯然這個程序是 32bit 的,所以很大概率程序是 32bit 部署,會受到 2G 虛擬地址的限制,因爲可用內存不足導致高頻的觸發 FullGC,可以用 !address -summary 去驗證一下。


0:048> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown>                               955          6029f000 (   1.503 GB)  81.21%   75.13%
Image                                  1251          10105000 ( 257.020 MB)  13.57%   12.55%
Free                                    326           995d000 ( 153.363 MB)            7.49%
Stack                                   165           34c0000 (  52.750 MB)   2.78%    2.58%
Heap                                    137           2db0000 (  45.688 MB)   2.41%    2.23%
Other                                    12             47000 ( 284.000 kB)   0.01%    0.01%
TEB                                      55             37000 ( 220.000 kB)   0.01%    0.01%
PEB                                       1              1000 (   4.000 kB)   0.00%    0.00%
...
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             2152          66fc7000 (   1.609 GB)  86.97%   80.46%
MEM_RESERVE                             424           f6cc000 ( 246.797 MB)  13.03%   12.05%
MEM_FREE                                326           995d000 ( 153.363 MB)            7.49%
...

從卦中可以看到,當前程序吃了1.6G的虛擬地址,佔全量的 80% ,這樣情況按理說程序會拋 OutOfMemoryException 異常,在 !t 中也得到了驗證。


0:048> !t
ThreadCount:      48
UnstartedThread:  0
BackgroundThread: 35
PendingThread:    0
DeadThread:       0
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
...
  33   39 10c8 0ee3dec0   1029220 Preemptive  00000000:00000000 0076c700 0     MTA (Threadpool Worker) System.OutOfMemoryException 32614444 (nested exceptions)
  ...
  46   44  89c 0ee3c458   1029220 Preemptive  00000000:00000000 0076c700 1     MTA (Threadpool Worker) System.OutOfMemoryException 32605d34 (nested exceptions)
  ...

接下來的問題是誰喫掉了 1.6G 的內存,總有地方會喫,可以使用 !eeheap -gc 觀察下託管堆。


0:048> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x32632d50
generation 1 starts at 0x3262534c
generation 2 starts at 0x03291000
ephemeral segment allocation context: (0x3265ffb0, 0x3265ffbc)
 segment     begin  allocated      size
03290000  03291000  0428fee4  0xffeee4(16772836)
06c60000  06c61000  07c5ffc4  0xffefc4(16773060)
...
7d210000  7d211000  7e20ffac  0xffefac(16773036)
31660000  31661000  3265ffb0  0xffefb0(16773040)
Large object heap starts at 0x04291000
 segment     begin  allocated      size
04290000  04291000  0450fa78  0x27ea78(2615928)
53390000  53391000  54391020  0x1000020(16777248)
Total Size:              Size: 0x4622fd30 (1176698160) bytes.
------------------------------
GC Heap Size:    Size: 0x4622fd30 (1176698160) bytes.

從卦中看應該就是託管堆喫掉了,接下來就是看下託管堆中哪一類對象最多,最終找到了一個大集合,命令輸出如下:


0:048> !gcroot 4c0507c0
Thread 89c:
    471bd450 07f76405 IBatisNet.DataMapper.MappedStatements.MappedStatement.RunQueryForList[[System.__Canon, mscorlib]](IBatisNet.DataMapper.Scope.RequestScope, IBatisNet.DataMapper.ISqlMapSession, System.Object, System.Collections.Generic.IList`1<System.__Canon>, IBatisNet.DataMapper.RowDelegate`1<System.__Canon>)
        ebp+90: 471be6c4
            ->  32c2ea50 System.Collections.Generic.List`1[[xxx.Model]]
            ->  53391010 xxxRMT[]
            ->  4c0507c0 xxxMT

0:048> !do 32c2ea50
Name:        System.Collections.Generic.List`1[[xxx, xxx]]
MethodTable: 095f1b58
EEClass:     6e246b4c
Size:        24(0x18) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
6e67ca34  4001886        4     System.__Canon[]  0 instance 53391010 _items
6e66f2d8  4001887        c         System.Int32  1 instance  3011824 _size
6e66f2d8  4001888       10         System.Int32  1 instance  3011824 _version
6e66d824  4001889        8        System.Object  0 instance 00000000 _syncRoot
6e67ca34  400188a        4     System.__Canon[]  0   static  <no information>

從卦中看當前的List有length=3011824,並且還被 89c 線程持有,最終通過代碼找到了是某種查詢下導致的大SQL引發。

三:總結

這次程序卡死還是挺有意思的,表象是主線程被GC卡住,實則是大SQL導致虛擬地址不足,分享出來讓大家少踩坑吧!

图片名称
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章