TL;DR:启用服务器 GC 的应用程序显示数十个特殊GC 线程并超时挂起。这有什么可以解释的?
这些天我被困在 .NET 服务上发生的一个奇怪的多线程/争用问题上。症状如下:
- 程序挂起很长一段时间(秒到分钟)
- 线程数异常高
- 就在程序停止响应时出现争用高峰(参见下图)
- 同一个程序部署在不同的服务器上,有些实例完全没有问题(同样的硬件/OS/CLR)
我立即怀疑我们代码中的一个问题会导致托管线程池随着时间的推移启动大量线程,所有线程都试图共享一个或多个公共资源。看起来我们对 ThreadPool 的使用非常小且非常可控。
我设法获得了一个尚未挂起的服务的转储文件,该服务已经有非常多的线程(超过 100,正常状态下应该是 20 左右)
使用 windbg + sos,我们确定 ThreadPool 大小是可以的:
0:000> !threadpool
CPU utilization: 0%
Worker Thread: Total: 8 Running: 1 Idle: 7 MaxLimit: 32767 MinLimit: 32
Work Request in Queue: 0
--------------------------------------
Number of Timers: 1
--------------------------------------
Completion Port Thread:Total: 1 Free: 1 MaxFree: 64 CurrentLimit: 1 MaxLimit: 1000 MinLimit: 32
只有8个工作线程...然后我列出了所有托管线程堆栈,发现其中很多我无法识别。请参阅下面的一个示例:
0:000> !eestack
(...)
Thread 94
Current frame: ntdll!NtWaitForSingleObject+0xa
Child-SP RetAddr Caller, Callee
0000008e25b2f770 000007f8f5a210ea KERNELBASE!WaitForSingleObjectEx+0x92, calling ntdll!NtWaitForSingleObject
0000008e25b2f810 000007f8ece549bf clr!CLREventBase::WaitEx+0x16c, calling kernel32!WaitForSingleObjectEx
0000008e25b2f820 000007f8f5a2152c KERNELBASE!SetEvent+0xc, calling ntdll!NtSetEvent
0000008e25b2f850 000007f8ece54977 clr!CLREventBase::WaitEx+0x103, calling clr!CLREventBase::WaitEx+0x134
0000008e25b2f8b0 000007f8ece548f8 clr!CLREventBase::WaitEx+0x70, calling clr!CLREventBase::WaitEx+0xe4
0000008e25b2f8e0 000007f8ed06526d clr!SVR::gc_heap::gc1+0x323, calling clr!SVR::GCStatistics::Enabled
0000008e25b2f940 000007f8ecfbe0b3 clr!SVR::gc_heap::bgc_thread_function+0x83, calling clr!CLREventBase::Wait
0000008e25b2f980 000007f8ecf3d5b6 clr!Thread::intermediateThreadProc+0x7d
0000008e25b2fd00 000007f8ecf3d59f clr!Thread::intermediateThreadProc+0x66, calling clr!_chkstk
0000008e25b2fd40 000007f8f8281832 kernel32!BaseThreadInitThunk+0x1a
0000008e25b2fd70 000007f8f8aad609 ntdll!RtlUserThreadStart+0x1d
(...)
使用!threads -special
命令,我终于发现这些线程是特殊的GC 线程:
0:000> !threads -special
ThreadCount: 81
UnstartedThread: 0
BackgroundThread: 49
PendingThread: 0
DeadThread: 21
Hosted Runtime: no
(...)
OSID Special thread type
1 804 DbgHelper
2 f48 GC
3 3f8 GC
4 1380 GC
5 af4 GC
6 1234 GC
7 fac GC
8 12e4 GC
9 17fc GC
10 644 GC
11 16e0 GC
12 6cc GC
13 9d4 GC
14 f7c GC
15 d5c GC
16 d74 GC
17 8d0 GC
18 1574 GC
19 8e0 GC
20 5bc GC
21 82c GC
22 e4c GC
23 129c GC
24 e28 GC
25 45c GC
26 340 GC
27 15c0 GC
28 16d4 GC
29 f4c GC
30 10e8 GC
31 1350 GC
32 164 GC
33 1620 GC
34 1444 Finalizer
35 c2c ProfilingAPIAttach
62 50 Timer
64 14a8 GC
65 145c GC
66 cdc GC
67 af8 GC
68 12e8 GC
69 1398 GC
70 e80 GC
71 a60 GC
72 834 GC
73 1b0 GC
74 2ac GC
75 eb8 GC
76 ec4 GC
77 ea8 GC
78 28 GC
79 11d0 GC
80 1700 GC
81 1434 GC
82 1510 GC
83 9c GC
84 c64 GC
85 11c0 GC
86 1714 GC
87 1360 GC
88 1610 GC
89 6c4 GC
90 cf0 GC
91 13d0 GC
92 1050 GC
93 1600 GC
94 16c4 GC
95 1558 GC
96 1b74 IOCompletion
97 ce4 ThreadpoolWorker
98 19a4 ThreadpoolWorker
99 1a00 ThreadpoolWorker
100 1b64 ThreadpoolWorker
101 1b38 ThreadpoolWorker
102 1844 ThreadpoolWorker
103 1b90 ThreadpoolWorker
104 1a10 ThreadpoolWorker
105 1894 Gate
超过 60 个“GC”线程...所以我检查了我的不同服务实例的设置,并发现有问题的配置了GC Server
,而其他的则没有。
更多信息:
- 我们使用 .NET 4.5
- 我们在所有机器上使用 Windows 2012 Server
- 我们在双八核服务器(2 个 CPU、16 个物理内核、32 个逻辑内核)上运行
我现在正在尝试做的事情:
- 我正在尝试让其他人转储(当程序有更多线程时,程序何时挂起等)
- 我会尝试禁用
GC Server
有问题的实例上的设置,但问题可能需要一些时间才会出现。
所以这是我的问题:
- 一个 GC 服务器配置的 .NET 程序有这么多 GC 线程是正常的吗?我认为 Server GC 每个处理器只有一个 GC 线程。
- 这是否与我在这些服务上看到的问题有关,即随着时间的推移数百个线程,由于争用而导致大量进程冻结?