易大爷 发表于 2024-7-1 17:51:52

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

一:背景

1. 讲故事

早就听说过有什么 网络边缘计算,这次还真给遇到了,有点意思,问了下 chatgpt 这是干嘛的 ?
网络边缘计算是一种计算模型,它将计算能力和数据存储位置从传统的集中式数据中心向网络边缘的用户设备、传感器和其他物联网设备移动。这种模型的目的是在接近数据生成源头的地方提供更快速的计算和数据处理能力,从而减少数据传输延迟并提高服务质量。网络边缘计算使得在设备本地进行数据处理和决策成为可能,同时也有助于减轻对中心数据中心的网络流量和负载。
看到.NET还有这样的应用场景还是挺欣慰的,接下来就来分析下这个dump到底是怎么回事?
二:WinDbg 分析

1. 为什么会卡死

不同程序的卡死有不同的分析方式,所以要先鉴别下程序的类型以及主线程的调用栈即可,参考如下:
0:000> !eeversion
5.0.721.25508
5.0.721.25508 @Commit: 556582d964cc21b82a88d7154e915076f6f9008e
Server mode with 64 gc heaps
SOS Version: 8.0.10.10501 retail build

0:000> k
# Child-SP          RetAddr               Call Site
00 0000ffff`e0dddac0 0000fffd`c194c30c   libpthread_2_28!pthread_cond_wait+0x238
...
18 (Inline Function) --------`--------   libcoreclr!RunMain::$_0::operator()::{lambda(Param *)#1}::operator()+0x14c
19 (Inline Function) --------`--------   libcoreclr!RunMain::$_0::operator()+0x188
1a 0000ffff`e0dde600 0000fffd`c153e860   libcoreclr!RunMain+0x298
...
20 0000ffff`e0dded10 0000fffd`c1bf7800   libhostpolicy!corehost_main+0xc0
21 (Inline Function) --------`--------   libhostfxr!execute_app+0x2c0
22 (Inline Function) --------`--------   libhostfxr!<unnamed-namespace>::read_config_and_execute+0x3b4
23 0000ffff`e0ddeeb0 0000fffd`c1bf6840   libhostfxr!fx_muxer_t::handle_exec_host_command+0x57c
24 0000ffff`e0ddf000 0000fffd`c1bf4090   libhostfxr!fx_muxer_t::execute+0x2ec
25 0000ffff`e0ddf130 0000aaad`c9e1d22c   libhostfxr!hostfxr_main_startupinfo+0xa0
26 0000ffff`e0ddf200 0000aaad`c9e1d468   dotnet!exe_start+0x36c
27 0000ffff`e0ddf370 0000fffd`c1c63fe0   dotnet!main+0x90
28 0000ffff`e0ddf3b0 0000aaad`c9e13adc   libc_2_28!_libc_start_main+0xe0
29 0000ffff`e0ddf4e0 00000000`00000000   dotnet!start+0x34从卦中的指标来看,这是一个 Linux 上部署的 Web网站,既然是网站的卡死,那就要关注各个线程都在做什么。
2. 线程都在干嘛

以我多年的分析经验,绝大多数都是由于 线程饥饿 或者说 线程池耗尽 导致的,首先我们看下线程池的情况。
0:000> !t
ThreadCount:      365
UnstartedThread:0
BackgroundThread: 354
PendingThread:    0
DeadThread:       10
Hosted Runtime:   no
                                                                                                            Lock
DBG   ID   OSID ThreadOBJ         State GC Mode   GC Alloc Context                  Domain         Count Apt Exception
   0    1    31eaf 0000AAADF267C6002020020 Preemptive0000000000000000:0000000000000000 0000aaadf26634b0 -00001 Ukn
...
423363    36d30 0000FFDDB4000B201020220 Preemptive0000000000000000:0000000000000000 0000aaadf26634b0 -00001 Ukn (Threadpool Worker)
424364    36d31 0000FFDDA8000B201020220 Preemptive0000000000000000:0000000000000000 0000aaadf26634b0 -00001 Ukn (Threadpool Worker)
425365    36d32 0000FFDDAC000B201020220 Preemptive0000000000000000:0000000000000000 0000aaadf26634b0 -00001 Ukn (Threadpool Worker)

0:000> !tp
Using the Portable thread pool.

CPU utilization:9%
Workers Total:    252
Workers Running:236
Workers Idle:   13
Worker Min Limit: 64
Worker Max Limit: 32767

Completion Total:   0
Completion Free:    0
Completion MaxFree: 128
Completion Current Limit: 0
Completion Min Limit:   64
Completion Max Limit:   1000从卦中看当前有 365 个托管线程,这个算多吗?对于64core 来说,这个线程其实算是正常,训练营里的朋友都知道,server版的gc仅gc线程就有 64*2=128 个,接下来再看一个指标就是当前是否存在任务积压? 可以使用 !ext tpq 命令,参考输出如下:
0:000> !ext tpq
global work item queue________________________________

local per thread work items_____________________________________从卦中看当前没有任务积压,这就有点反经验了。
3. 真的不是线程饥饿吗

最后一招比较彻底,就是看各个线程栈都在做什么,可以使用 ~*e !clrstack 命令。

这不看不知道,一看吓一跳,有 193 个线程在 Task.Result 上等待,这玩意太经典了,然后从上面的调用栈 UIUpdateTimer_Elapsed 来看,貌似是一个定时器导致的,接下来我就好奇这代码是怎么写的?

分析上面的代码之后,我发现它是和 Linux Shell 窗口进行命令交互,不知道为何 Shell 没有响应导致代码在这里卡死。
4. 为什么线程池没有积压

相信有很多朋友对这个反经验的东西很好奇为什么请求没有积压在线程池,其实这个考验的是你对 PortableThreadPool 的底层了解,这里我就简单说一下吧。

[*]在 ThreadPool 中有一个 GateThread 线程是专门给线程池动态注入线程的,参考代码如下:
private static class GateThread
{
    private static void GateThreadStart()
    {
      while (true)
      {
            bool wasSignaledToWake = DelayEvent.WaitOne((int)delayHelper.GetNextDelay(tickCount));

            WorkerThread.MaybeAddWorkingWorker(threadPoolInstance);
      }
    }
}
[*]一旦有人调用了 Task.Result 代码,内部会主动唤醒 DelayEvent 事件,告诉 GateThread 赶紧通过 MaybeAddWorkingWorker 方法给我注入新的线程,参考代码如下:
private bool SpinThenBlockingWait(int millisecondsTimeout, CancellationToken cancellationToken)
{
    bool flag3 = ThreadPool.NotifyThreadBlocked();

}
internal static bool NotifyThreadBlocked()
{
    if (UsePortableThreadPool)
    {
      return PortableThreadPool.ThreadPoolInstance.NotifyThreadBlocked();
    }
    return false;
}
public bool NotifyThreadBlocked()
{
    GateThread.Wake(this);
}上面这种主动唤醒的机制是 C# 版 PortableThreadPool 做的优化来缓解线程饥饿的,这里有一个重点就是它只能缓解,换句话说如果上游太猛了还是会有请求积压的,但为什么这里没有积压呢? 很显然上游不猛呗,那如何眼见为实呢? 这就需要看 timer 的周期数即可,到当前的线程栈上给扒出来。
0:417> !DumpObj /d 0000ffee380757f8
Name:      System.Timers.Timer
MethodTable: 0000fffd4ab24030
EEClass:   0000fffd4ad6e140
Size:      88(0x58) bytes
File:      /home/user/env/dotnet/shared/Microsoft.NETCore.App/5.0.7/System.ComponentModel.TypeConverter.dll
Fields:
            MT    Field   Offset               Type VT   Attr            Value Name
0000fffd4c947498400001c      8 ...ponentModel.ISite0 instance 0000000000000000 _site
0000000000000000400001d       10 ....EventHandlerList0 instance 0000000000000000 _events
0000fffd479195d8400001b       98      System.Object0   static 0000000000000000 s_eventDisposed
0000fffd47926f60400000e       40      System.Double1 instance 3000.000000 _interval
0000fffd4791fb10400000f       48       System.Boolean1 instance                1 _enabled
0000fffd4791fb104000010       49       System.Boolean1 instance                0 _initializing
0000fffd4791fb104000011       4a       System.Boolean1 instance                0 _delayedEnable
0000fffd4ab241d84000012       18 ...apsedEventHandler0 instance 0000ffee3807aae8 _onIntervalElapsed
0000fffd4791fb104000013       4b       System.Boolean1 instance                1 _autoReset
0000fffd4c944ea04000014       20 ...SynchronizeInvoke0 instance 0000000000000000 _synchronizingObject
0000fffd4791fb104000015       4c       System.Boolean1 instance                0 _disposed
0000fffd49963e284000016       28 ...m.Threading.Timer0 instance 0000ffee38098dc8 _timer
0000fffd48b90a304000017       30 ...ing.TimerCallback0 instance 0000ffee3807aaa8 _callback
0000fffd479195d84000018       38      System.Object0 instance 0000ffee38098db0 _cookie从卦中看当前是 3s 为一个周期,这就能解释为什么线程池没有积压的底层原因了。
三:总结

这个卡死事故还是蛮好解决的,如果有一些经验直接用dotnet-counter也是能搞定的,重点在于这是一个 Linux的dump,同时又是 .NET上的一个很好玩的场景,故此分享出来。

来源:https://www.cnblogs.com/huangxincheng/p/18277831
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 记一次 .NET某网络边缘计算系统 卡死分析