记一次 .NET某酒店后台服务 卡死分析

net · 浏览次数 : 0

小编点评

**1. 线程池底层了解** 线程池底层是一个异步执行框架,用于执行并行任务。它使用 GateThread 和 Task 来管理线程的创建和调度。在 .NET Framework 时代,线程池内部会根据配置的线程数量动态调整线程数量,以满足应用程序的性能需求。 **2. GateThread 注入速度** GateThread 注入速度取决于线程池的配置和应用程序的负载。通常,在 .NET Framework 中,GateThread 注入速度约为 1-2 个线程每秒。 **3. 代码分析** 您提供的代码显示,每个 `Created` 事件之间延迟约 1 秒,这意味着每个 `GateThread` 注入一个新线程花费 1 秒的时间。这可能是由于线程池底层动态调整线程数量导致的,或可能是其他因素造成的。 **4. 解决方案** * 升级 .NET 版本到 .NET 8 或更高版本,该版本优化了线程池底层,可以提升 GateThread 注入的速度。 * 优化应用程序的性能,以减少线程创建所需的资源。 * 考虑使用异步编程框架,例如 Async.NET 或 Task.Run,这些框架允许您更灵活地控制线程创建和执行。

正文

一:背景

1. 讲故事

停了一个月没有更新文章了,主要是忙于写 C#内功修炼系列的PPT,现在基本上接近尾声,可以回头继续更新这段时间分析dump的一些事故报告,有朋友微信上找到我,说他们的系统出现了大量的http超时,程序不响应处理了,让我帮忙看下怎么回事,dump也抓到了。

二:WinDbg分析

1. 为什么会出现请求超时

既然超时说明server端不响应这个请求,继而达到了超时时间的一种异常情况,所以首先要想到的就是 线程池的健康度,可以用 !tp 命令观察,输出如下:


0:000> !tp
CPU utilization: 0%
Worker Thread: Total: 537 Running: 537 Idle: 0 MaxLimit: 32767 MinLimit: 12
Work Request in Queue: 82
    Unknown Function: 00007fff566a17d0  Context: 0000020f08cbd658
    Unknown Function: 00007fff566a17d0  Context: 0000020f09acfa80
    Unknown Function: 00007fff566a17d0  Context: 0000020f08702198
    Unknown Function: 00007fff566a17d0  Context: 0000020f09ad9068
    Unknown Function: 00007fff566a17d0  Context: 0000020f09abffe8
    Unknown Function: 00007fff566a17d0  Context: 0000020f093c9948
    Unknown Function: 00007fff566a17d0  Context: 0000020f093cfd28
    Unknown Function: 00007fff566a17d0  Context: 0000020f093d9358
    Unknown Function: 00007fff566a17d0  Context: 0000020f093c34e8
    Unknown Function: 00007fff566a17d0  Context: 0000020f093dc568
    ...
--------------------------------------
Number of Timers: 2
--------------------------------------
Completion Port Thread:Total: 2 Free: 2 MaxFree: 24 CurrentLimit: 2 MaxLimit: 1000 MinLimit: 12

从上面的卦象看异常非常明显,线程池总共有 537个工作线程都是处于运行状态,相信有经验的朋友应该一眼就知道是怎么回事,专业术语叫:线程饥饿,并且线程池队列也积压了 82个 待处理的任务。

2. 线程为什么会饥饿

线程饥饿的原因有更多,我特意问了下 chatgpt,列举如下:

  1. 优先级倾斜:如果某些线程的优先级设置过高,而其他线程的优先级设置过低,高优先级的线程可能会长时间占用CPU资源,导致低优先级线程无法获得执行机会。
  2. 死锁:当多个线程相互等待对方释放资源时,可能会导致死锁。在死锁情况下,所有线程都无法继续执行,从而导致线程饥饿。
  3. 资源竞争:多个线程竞争有限的资源(如共享内存、文件、网络连接等)时,可能会导致某些线程长时间无法获取到所需的资源而处于饥饿状态。
  4. 不公平的调度策略:调度器可能存在不公平的调度策略,导致某些线程无法获得公平的CPU时间片,从而长时间无法执行。
  5. 线程阻塞:某些线程可能由于等待I/O操作、锁或其他原因而被阻塞,如果阻塞时间过长,可能导致其他线程饥饿。
  6. 线程池配置不当:如果线程池中的线程数量设置不当,可能会导致某些任务长时间等待执行,从而引发线程饥饿。

那到底是哪一种情况呢?可以用 ~*e !clrstack 看一下各个线程此时正在做什么,输出如下:


0:000> ~*e !clrstack
...
OS Thread Id: 0x2924 (74)
        Child SP               IP Call Site
000000e0ef47dc30 00007fff60fd6974 [GCFrame: 000000e0ef47dc30] 
000000e0ef47dd58 00007fff60fd6974 [HelperMethodFrame_1OBJ: 000000e0ef47dd58] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
000000e0ef47de70 00007ffef33e7269 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)
000000e0ef47df00 00007ffef33e6b58 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken)
000000e0ef47df70 00007ffef33e69e1 System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken)
000000e0ef47e040 00007ffef60cce33 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task)
000000e0ef47e070 00007ffef9df2c73 Exceptionless.Submission.DefaultSubmissionClient.SendHeartbeat(System.String, Boolean, Exceptionless.ExceptionlessConfiguration)
000000e0ef47e110 00007ffef109f03f System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000e0ef47e1e0 00007ffef109e784 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
000000e0ef47e210 00007ffef15b670b System.Threading.TimerQueueTimer.CallCallback()
000000e0ef47e270 00007ffef15b644d System.Threading.TimerQueueTimer.Fire()
000000e0ef47e2e0 00007ffef15b5613 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
000000e0ef47e320 00007ffef10b8319 System.Threading.ThreadPoolWorkQueue.Dispatch()
000000e0ef47e7a0 00007fff4fa06993 [DebuggerU2MCatchHandlerFrame: 000000e0ef47e7a0] 
000000e0ef47e908 00007fff4fa06993 [ContextTransitionFrame: 000000e0ef47e908] 
000000e0ef47eb40 00007fff4fa06993 [DebuggerU2MCatchHandlerFrame: 000000e0ef47eb40] 
...

发现有 473 个线程都在 Exceptionless.Submission.DefaultSubmissionClient.SendHeartbeat 方法上进行等待,这就有意思了,原来是开源的日志收集组件发送的心跳检测方法,接下来赶紧看一下这个方法的源码。


public void SendHeartbeat(string sessionIdOrUserId, bool closeSession, ExceptionlessConfiguration config)
{
	if (!config.IsValid)
	{
		return;
	}
	string requestUri = $"{GetHeartbeatServiceEndPoint(config)}/events/session/heartbeat?id={sessionIdOrUserId}&close={closeSession}";
	try
	{
		_client.Value.AddAuthorizationHeader(config.ApiKey);
		_client.Value.GetAsync(requestUri).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter()
			.GetResult();
	}
	catch (Exception exception)
	{
		config.Resolver.GetLog().Error("Error submitting heartbeat: " + exception.GetMessage());
	}
}

从源码看,居然用同步的方式发送 http请求,在这异步方法满天飞的世界里,上面的写法实属异类。

3. 该如何解决呢?

既然是 Exceptionless 内部写的 SendHeartbeat 方法,我们程序员基本上无法干预,能做到的无非如下两点:

  • 升级框架

看下了用的还是超老的 4.3 版本,可以升级到目前最新的 6.0.4 观察试试。


[assembly: AssemblyTitle("Exceptionless")]
[assembly: AssemblyProduct("Exceptionless")]
[assembly: AssemblyCompany("Exceptionless")]
[assembly: AssemblyTrademark("Exceptionless")]
[assembly: AssemblyCopyright("Copyright (c) 2017 Exceptionless.  All rights reserved.")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("4.3.2027.0")]
[assembly: AssemblyInformationalVersion("4.3.2027$(VERSION_SUFFIX) f8d73f2fd7")]
[assembly: TargetFramework(".NETFramework,Version=v4.5", FrameworkDisplayName = ".NET Framework 4.5")]
[assembly: AssemblyVersion("4.3.2027.0")]

  • 使用替代品,或者不用

哈哈,不用它,这是万能的治根之法。

三:对线程注入速度的解答

1. 朋友提了一个疑问

我现在知道这个 url 某个时段可能响应出了问题,但我线程池里的线程增速应该很快呀,多余的线程不是可以响应客户端请求吗?为什么我发现的情况是全部卡死呢?

2. 疑问的简单解答

这个问题其实是考察对线程池底层的了解,尤其是多久会向线程池注入一个活线程,在 .NET Framework 时代,在线程饥饿的情况下线程池内部的 GateThread线程 会 1s 注入一个活线程,那如何验证呢? 我们观察后续的线程创建时间即可,使用 ~*e .ttime


0:000> ~*e .ttime
...
Created: Thu Nov 16 11:10:21.582 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.000
Created: Thu Nov 16 11:10:22.593 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.000
Created: Thu Nov 16 11:10:23.562 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.000
Created: Thu Nov 16 11:10:24.062 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.000
Created: Thu Nov 16 11:10:24.577 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.000
Created: Thu Nov 16 11:10:25.562 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.000
Created: Thu Nov 16 11:10:26.562 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.015
Created: Thu Nov 16 11:10:27.562 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.015
Created: Thu Nov 16 11:10:28.562 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.015
Created: Thu Nov 16 11:10:29.577 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.015
Created: Thu Nov 16 11:10:30.562 2023 (UTC + 8:00)
Kernel:  0 days 0:00:00.000
User:    0 days 0:00:00.000

从卦中的输出来看,每一个 Created 大概差 1s 钟,这也是 GateThread 的功劳,这种注入速度在 .NET8 中已经做了优化,比如上面这种情况,Task 内部会主动唤醒 GateThread 线程让其立即注入新线程,从而提升程序的响应速度。

四:总结

很多时候分析下来发现是 第三方组件 拖垮了程序,自己又没有太多的介入能力,真的很无奈,框架都用了那么久,现在看到了一只苍蝇,已是食之无味,弃之可惜。
图片名称

与记一次 .NET某酒店后台服务 卡死分析相似的内容:

记一次 .NET某酒店后台服务 卡死分析

一:背景 1. 讲故事 停了一个月没有更新文章了,主要是忙于写 C#内功修炼系列的PPT,现在基本上接近尾声,可以回头继续更新这段时间分析dump的一些事故报告,有朋友微信上找到我,说他们的系统出现了大量的http超时,程序不响应处理了,让我帮忙看下怎么回事,dump也抓到了。 二:WinDbg分析

记一次 .NET某酒业业务系统 崩溃分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他的程序每次关闭时就会自动崩溃,一直找不到原因让我帮忙看一下怎么回事,这位朋友应该是第二次找我了,分析了下 dump 还是挺经典的,拿出来给大家分享一下吧。 二:WinDbg 分析 1. 为什么会崩溃 找崩溃原因比较简单,用 !analyze -v 命

记一次 .NET某上位视觉程序 离奇崩溃分析

一:背景 1. 讲故事 前段时间有位朋友找到我,说他们有一个崩溃的dump让我帮忙看下怎么回事,确实有太多的人在网上找各种故障分析最后联系到了我,还好我一直都是免费分析,不收取任何费用,造福社区。 话不多说,既然有 dump 来了,那就上 windbg 说话吧。 二:WinDbg 分析 1. 为什么

记一次 .NET某网络边缘计算系统 卡死分析

一:背景 1. 讲故事 早就听说过有什么 网络边缘计算,这次还真给遇到了,有点意思,问了下 chatgpt 这是干嘛的 ? 网络边缘计算是一种计算模型,它将计算能力和数据存储位置从传统的集中式数据中心向网络边缘的用户设备、传感器和其他物联网设备移动。这种模型的目的是在接近数据生成源头的地方提供更快速

记一次 .NET某机械臂上位系统 卡死分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他们的程序会偶发性的卡死一段时间,然后又好了,让我帮忙看下怎么回事?窗体类的程序解决起来相对来说比较简单,让朋友用procdump自动抓一个卡死时的dump,拿到dump之后,上 windbg 说话。 二:WinDbg 分析 1. 主线程在做什么 要想

记一次 .NET某工厂报警监控设置 崩溃分析

一:背景 1. 讲故事 前些天有位朋友在微信上丢了一个崩溃的dump给我,让我帮忙看下为什么出现了崩溃,在 Windows 的事件查看器上显示的是经典的 访问违例 ,即 c0000005 错误码,不管怎么说有dump就可以上windbg开干了。 二:WinDbg 分析 1. 程序为谁崩溃了 在 Wi

记一次 .NET某游戏币自助机后端 内存暴涨分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他们的程序内存会偶发性暴涨,自己分析了下是非托管内存问题,让我帮忙看下怎么回事?哈哈,看到这个dump我还是非常有兴趣的,居然还有这种游戏币自助机类型的程序,下次去大玩家看看他们出币的机器后端是不是C#写的?由于dump是linux上的程序,刚好win

记一次 .NET某工控视觉自动化系统 卡死分析

一:背景 1. 讲故事 今天分享的dump是训练营里一位学员的,从一个啥也不会到现在分析的有模有样,真的是看他成长起来的,调试技术学会了就是真真实实自己的,话不多说,上windbg说话。 二:WinDbg 分析 1. 为什么会卡死 这位学员是从事工控大类下的视觉自动化,也是目前.NET的主战场,这个

记一次 .NET某质量检测中心系统 崩溃分析

一:背景 1. 讲故事 这些天有点意思,遇到的几个程序故障都是和Windows操作系统或者第三方组件有关系,真的有点无语,今天就带给大家一例 IIS 相关的与大家分享,这是一家国企的.NET程序,出现了崩溃急需分析。 二:WinDbg 分析 1. 为什么会崩溃 崩溃原因相对还是好找的,双击dump文

记一次 .NET某工业设计软件 崩溃分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他的软件在客户那边不知道什么原因崩掉了,从windows事件日志看崩溃在 clr 里,让我能否帮忙定位下,dump 也抓到了,既然dump有了,接下来就上 windbg 分析吧。 二:WinDbg 分析 1. 为什么崩溃在 clr 一般来说崩溃在clr