IT评测·应用市场-qidao123.com技术社区

标题: 记一次 .NET某旅店背景服务 卡死分析 [打印本页]

作者: 泉缘泉    时间: 2024-5-20 22:53
标题: 记一次 .NET某旅店背景服务 卡死分析
一:背景

1. 讲故事

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

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

既然超时说明server端不相应这个请求,继而到达了超时时间的一种非常情况,以是首先要想到的就是 线程池的健康度,可以用 !tp 下令观察,输出如下:
  1. 0:000> !tp
  2. CPU utilization: 0%
  3. Worker Thread: Total: 537 Running: 537 Idle: 0 MaxLimit: 32767 MinLimit: 12
  4. Work Request in Queue: 82
  5.     Unknown Function: 00007fff566a17d0  Context: 0000020f08cbd658
  6.     Unknown Function: 00007fff566a17d0  Context: 0000020f09acfa80
  7.     Unknown Function: 00007fff566a17d0  Context: 0000020f08702198
  8.     Unknown Function: 00007fff566a17d0  Context: 0000020f09ad9068
  9.     Unknown Function: 00007fff566a17d0  Context: 0000020f09abffe8
  10.     Unknown Function: 00007fff566a17d0  Context: 0000020f093c9948
  11.     Unknown Function: 00007fff566a17d0  Context: 0000020f093cfd28
  12.     Unknown Function: 00007fff566a17d0  Context: 0000020f093d9358
  13.     Unknown Function: 00007fff566a17d0  Context: 0000020f093c34e8
  14.     Unknown Function: 00007fff566a17d0  Context: 0000020f093dc568
  15.     ...
  16. --------------------------------------
  17. Number of Timers: 2
  18. --------------------------------------
  19. Completion Port Thread:Total: 2 Free: 2 MaxFree: 24 CurrentLimit: 2 MaxLimit: 1000 MinLimit: 12
复制代码
从上面的卦象看非常非常明显,线程池统共有 537个工作线程都是处于运行状态,相信有履历的朋友应该一眼就知道是怎么回事,专业术语叫:线程饥饿,并且线程池队列也积压了 82个 待处理的使命。
2. 线程为什么会饥饿

线程饥饿的缘故原由有更多,我特意问了下 chatgpt,列举如下:
那到底是哪一种情况呢?可以用 ~*e !clrstack 看一下各个线程此时正在做什么,输出如下:
  1. 0:000> ~*e !clrstack
  2. ...
  3. OS Thread Id: 0x2924 (74)
  4.         Child SP               IP Call Site
  5. 000000e0ef47dc30 00007fff60fd6974 [GCFrame: 000000e0ef47dc30]
  6. 000000e0ef47dd58 00007fff60fd6974 [HelperMethodFrame_1OBJ: 000000e0ef47dd58] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
  7. 000000e0ef47de70 00007ffef33e7269 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)
  8. 000000e0ef47df00 00007ffef33e6b58 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken)
  9. 000000e0ef47df70 00007ffef33e69e1 System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken)
  10. 000000e0ef47e040 00007ffef60cce33 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task)
  11. 000000e0ef47e070 00007ffef9df2c73 Exceptionless.Submission.DefaultSubmissionClient.SendHeartbeat(System.String, Boolean, Exceptionless.ExceptionlessConfiguration)
  12. 000000e0ef47e110 00007ffef109f03f System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
  13. 000000e0ef47e1e0 00007ffef109e784 System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
  14. 000000e0ef47e210 00007ffef15b670b System.Threading.TimerQueueTimer.CallCallback()
  15. 000000e0ef47e270 00007ffef15b644d System.Threading.TimerQueueTimer.Fire()
  16. 000000e0ef47e2e0 00007ffef15b5613 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
  17. 000000e0ef47e320 00007ffef10b8319 System.Threading.ThreadPoolWorkQueue.Dispatch()
  18. 000000e0ef47e7a0 00007fff4fa06993 [DebuggerU2MCatchHandlerFrame: 000000e0ef47e7a0]
  19. 000000e0ef47e908 00007fff4fa06993 [ContextTransitionFrame: 000000e0ef47e908]
  20. 000000e0ef47eb40 00007fff4fa06993 [DebuggerU2MCatchHandlerFrame: 000000e0ef47eb40]
  21. ...
复制代码
发现有 473 个线程都在 Exceptionless.Submission.DefaultSubmissionClient.SendHeartbeat 方法上进行等待,这就故意思了,原来是开源的日志网络组件发送的心跳检测方法,接下来赶紧看一下这个方法的源码。
  1. public void SendHeartbeat(string sessionIdOrUserId, bool closeSession, ExceptionlessConfiguration config)
  2. {
  3.         if (!config.IsValid)
  4.         {
  5.                 return;
  6.         }
  7.         string requestUri = $"{GetHeartbeatServiceEndPoint(config)}/events/session/heartbeat?id={sessionIdOrUserId}&close={closeSession}";
  8.         try
  9.         {
  10.                 _client.Value.AddAuthorizationHeader(config.ApiKey);
  11.                 _client.Value.GetAsync(requestUri).ConfigureAwait(continueOnCapturedContext: false).GetAwaiter()
  12.                         .GetResult();
  13.         }
  14.         catch (Exception exception)
  15.         {
  16.                 config.Resolver.GetLog().Error("Error submitting heartbeat: " + exception.GetMessage());
  17.         }
  18. }
复制代码
从源码看,居然用同步的方式发送 http请求,在这异步方法满天飞的天下里,上面的写法实属异类。
3. 该怎样解决呢?

既然是 Exceptionless 内部写的 SendHeartbeat 方法,我们程序员基本上无法干预,能做到的无非如下两点:
看下了用的还是超老的 4.3 版本,可以升级到如今最新的 6.0.4 观察试试。
  1. [assembly: AssemblyTitle("Exceptionless")]
  2. [assembly: AssemblyProduct("Exceptionless")]
  3. [assembly: AssemblyCompany("Exceptionless")]
  4. [assembly: AssemblyTrademark("Exceptionless")]
  5. [assembly: AssemblyCopyright("Copyright (c) 2017 Exceptionless.  All rights reserved.")]
  6. [assembly: AssemblyConfiguration("Release")]
  7. [assembly: AssemblyFileVersion("4.3.2027.0")]
  8. [assembly: AssemblyInformationalVersion("4.3.2027$(VERSION_SUFFIX) f8d73f2fd7")]
  9. [assembly: TargetFramework(".NETFramework,Version=v4.5", FrameworkDisplayName = ".NET Framework 4.5")]
  10. [assembly: AssemblyVersion("4.3.2027.0")]
复制代码

哈哈,不用它,这是万能的治根之法。
三:对线程注入速度的解答

1. 朋友提了一个疑问

我如今知道这个 url 某个时段大概相应出了问题,但我线程池里的线程增速应该很快呀,多余的线程不是可以相应客户端请求吗?为什么我发现的情况是全部卡死呢?
2. 疑问的简单解答

这个问题其实是观察对线程池底层的了解,尤其是多久会向线程池注入一个活线程,在 .NET Framework 时代,在线程饥饿的情况下线程池内部的 GateThread线程 会 1s 注入一个活线程,那怎样验证呢? 我们观察后续的线程创建时间即可,使用 ~*e .ttime 。
  1. 0:000> ~*e .ttime
  2. ...
  3. Created: Thu Nov 16 11:10:21.582 2023 (UTC + 8:00)
  4. Kernel:  0 days 0:00:00.000
  5. User:    0 days 0:00:00.000
  6. Created: Thu Nov 16 11:10:22.593 2023 (UTC + 8:00)
  7. Kernel:  0 days 0:00:00.000
  8. User:    0 days 0:00:00.000
  9. Created: Thu Nov 16 11:10:23.562 2023 (UTC + 8:00)
  10. Kernel:  0 days 0:00:00.000
  11. User:    0 days 0:00:00.000
  12. Created: Thu Nov 16 11:10:24.062 2023 (UTC + 8:00)
  13. Kernel:  0 days 0:00:00.000
  14. User:    0 days 0:00:00.000
  15. Created: Thu Nov 16 11:10:24.577 2023 (UTC + 8:00)
  16. Kernel:  0 days 0:00:00.000
  17. User:    0 days 0:00:00.000
  18. Created: Thu Nov 16 11:10:25.562 2023 (UTC + 8:00)
  19. Kernel:  0 days 0:00:00.000
  20. User:    0 days 0:00:00.000
  21. Created: Thu Nov 16 11:10:26.562 2023 (UTC + 8:00)
  22. Kernel:  0 days 0:00:00.000
  23. User:    0 days 0:00:00.015
  24. Created: Thu Nov 16 11:10:27.562 2023 (UTC + 8:00)
  25. Kernel:  0 days 0:00:00.000
  26. User:    0 days 0:00:00.015
  27. Created: Thu Nov 16 11:10:28.562 2023 (UTC + 8:00)
  28. Kernel:  0 days 0:00:00.000
  29. User:    0 days 0:00:00.015
  30. Created: Thu Nov 16 11:10:29.577 2023 (UTC + 8:00)
  31. Kernel:  0 days 0:00:00.000
  32. User:    0 days 0:00:00.015
  33. Created: Thu Nov 16 11:10:30.562 2023 (UTC + 8:00)
  34. Kernel:  0 days 0:00:00.000
  35. User:    0 days 0:00:00.000
复制代码
从卦中的输出来看,每一个 Created 大概差 1s 钟,这也是 GateThread 的功劳,这种注入速度在 .NET8 中已经做了优化,好比上面这种情况,Task 内部会主动唤醒 GateThread 线程让其立刻注入新线程,从而提升程序的相应速度。
四:总结

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


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4