8

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 线程。
  • 这是否与我在这些服务上看到的问题有关,即随着时间的推移数百个线程,由于争用而导致大量进程冻结?
4

2 回答 2

1

使用 Server GC,每个逻辑核心将有一个线程(即关联设置为该核心)。因此,在您的情况下,应该至少有 32 个线程。如果您打开了后台 GC,那么可能会有更多的工作线程处理每个堆的图形(参考)。

还要记住,这些 GC 线程将运行在这些线程上THREAD_PRIORITY_HIGHEST,很容易饿死任何尚未被 GC 暂停的线程(参考)。

现在,就您的其他线程而言,无论垃圾收集器如何,一个进程中的 500 多个线程都会产生大量争用。所以弄清楚这些线程是什么对你的调查很重要。


要看的东西

  • 查看Background GC是否开启,如果开启,尝试不开启运行(4.5的Server GC支持此模式)。
  • 尝试减少线程池中的最大线程数(32767 是不健康的最大值)。

您还可以使用procdump.exe在性能下降时帮助捕获小型转储。

于 2013-09-17T15:55:02.037 回答
0

我在 NUMA 服务器上遇到了类似的问题。对我有帮助的事情:

  • 限制线程池
  • 限制挂起的托管进程的处理器关联掩码。这看起来很奇怪,但减少进程的处理器数量有时会使其在高并发负载下工作得更快。我怀疑自旋锁(忙于等待)。
于 2013-10-22T06:47:24.893 回答