一:背景
1. 讲故事
早就听说过有什么 网络边缘计算
,这次还真给遇到了,有点意思,问了下 chatgpt 这是干嘛的 ?
网络边缘计算是一种计算模型,它将计算能力和数据存储位置从传统的集中式数据中心向网络边缘的用户设备、传感器和其他物联网设备移动。这种模型的目的是在接近数据生成源头的地方提供更快速的计算和数据处理能力,从而减少数据传输延迟并提高服务质量。网络边缘计算使得在设备本地进行数据处理和决策成为可能,同时也有助于减轻对中心数据中心的网络流量和负载。
看到.NET还有这样的应用场景还是挺欣慰的,接下来就来分析下这个dump到底是怎么回事?
二:WinDbg 分析
1. 为什么会卡死
不同程序的卡死有不同的分析方式,所以要先鉴别下程序的类型以及主线程的调用栈即可,参考如下:
从卦中的指标来看,这是一个 Linux 上部署的 Web网站,既然是网站的卡死,那就要关注各个线程都在做什么。
2. 线程都在干嘛
以我多年的分析经验,绝大多数都是由于 线程饥饿
或者说 线程池耗尽
导致的,首先我们看下线程池的情况。
从卦中看当前有 365 个托管线程,这个算多吗?对于64core 来说,这个线程其实算是正常,训练营里的朋友都知道,server版的gc仅gc线程就有 64*2=128
个,接下来再看一个指标就是当前是否存在任务积压? 可以使用 !ext tpq
命令,参考输出如下:
从卦中看当前没有任务积压,这就有点反经验了。
3. 真的不是线程饥饿吗
最后一招比较彻底,就是看各个线程栈都在做什么,可以使用 ~*e !clrstack
命令。
这不看不知道,一看吓一跳,有 193 个线程在 Task.Result
上等待,这玩意太经典了,然后从上面的调用栈 UIUpdateTimer_Elapsed
来看,貌似是一个定时器导致的,接下来我就好奇这代码是怎么写的?
分析上面的代码之后,我发现它是和 Linux Shell
窗口进行命令交互,不知道为何 Shell 没有响应导致代码在这里卡死。
4. 为什么线程池没有积压
相信有很多朋友对这个反经验的东西很好奇为什么请求没有积压在线程池,其实这个考验的是你对 PortableThreadPool 的底层了解,这里我就简单说一下吧。
- 在 ThreadPool 中有一个 GateThread 线程是专门给线程池动态注入线程的,参考代码如下:
- 一旦有人调用了 Task.Result 代码,内部会主动唤醒 DelayEvent 事件,告诉 GateThread 赶紧通过 MaybeAddWorkingWorker 方法给我注入新的线程,参考代码如下:
上面这种主动唤醒的机制是 C# 版 PortableThreadPool 做的优化来缓解线程饥饿的,这里有一个重点就是它只能缓解
,换句话说如果上游太猛了还是会有请求积压的,但为什么这里没有积压呢? 很显然上游不猛呗,那如何眼见为实呢? 这就需要看 timer 的周期数即可,到当前的线程栈上给扒出来。
从卦中看当前是 3s 为一个周期,这就能解释为什么线程池没有积压的底层原因了。
三:总结
这个卡死事故还是蛮好解决的,如果有一些经验直接用dotnet-counter
也是能搞定的,重点在于这是一个 Linux的dump,同时又是 .NET上的一个很好玩的场景,故此分享出来。