一:背景 1. 講故事 早就聽說過有什麼 網路邊緣計算,這次還真給遇到了,有點意思,問了下 chatgpt 這是幹嘛的 ? 網路邊緣計算是一種計算模型,它將計算能力和數據存儲位置從傳統的集中式數據中心向網路邊緣的用戶設備、感測器和其他物聯網設備移動。這種模型的目的是在接近數據生成源頭的地方提供更快速 ...
一:背景
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 [/__w/1/s/src/coreclr/src/vm/assembly.cpp @ 1536]
19 (Inline Function) --------`-------- libcoreclr!RunMain::$_0::operator()+0x188 [/__w/1/s/src/coreclr/src/vm/assembly.cpp @ 1538]
1a 0000ffff`e0dde600 0000fffd`c153e860 libcoreclr!RunMain+0x298 [/__w/1/s/src/coreclr/src/vm/assembly.cpp @ 1538]
...
20 0000ffff`e0dded10 0000fffd`c1bf7800 libhostpolicy!corehost_main+0xc0 [/root/runtime/src/installer/corehost/cli/hostpolicy/hostpolicy.cpp @ 409]
21 (Inline Function) --------`-------- libhostfxr!execute_app+0x2c0 [/root/runtime/src/installer/corehost/cli/fxr/fx_muxer.cpp @ 146]
22 (Inline Function) --------`-------- libhostfxr!<unnamed-namespace>::read_config_and_execute+0x3b4 [/root/runtime/src/installer/corehost/cli/fxr/fx_muxer.cpp @ 520]
23 0000ffff`e0ddeeb0 0000fffd`c1bf6840 libhostfxr!fx_muxer_t::handle_exec_host_command+0x57c [/root/runtime/src/installer/corehost/cli/fxr/fx_muxer.cpp @ 1001]
24 0000ffff`e0ddf000 0000fffd`c1bf4090 libhostfxr!fx_muxer_t::execute+0x2ec
25 0000ffff`e0ddf130 0000aaad`c9e1d22c libhostfxr!hostfxr_main_startupinfo+0xa0 [/root/runtime/src/installer/corehost/cli/fxr/hostfxr.cpp @ 50]
26 0000ffff`e0ddf200 0000aaad`c9e1d468 dotnet!exe_start+0x36c [/root/runtime/src/installer/corehost/corehost.cpp @ 239]
27 0000ffff`e0ddf370 0000fffd`c1c63fe0 dotnet!main+0x90 [/root/runtime/src/installer/corehost/corehost.cpp @ 302]
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 0000AAADF267C600 2020020 Preemptive 0000000000000000:0000000000000000 0000aaadf26634b0 -00001 Ukn
...
423 363 36d30 0000FFDDB4000B20 1020220 Preemptive 0000000000000000:0000000000000000 0000aaadf26634b0 -00001 Ukn (Threadpool Worker)
424 364 36d31 0000FFDDA8000B20 1020220 Preemptive 0000000000000000:0000000000000000 0000aaadf26634b0 -00001 Ukn (Threadpool Worker)
425 365 36d32 0000FFDDAC000B20 1020220 Preemptive 0000000000000000: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
0000fffd4c947498 400001c 8 ...ponentModel.ISite 0 instance 0000000000000000 _site
0000000000000000 400001d 10 ....EventHandlerList 0 instance 0000000000000000 _events
0000fffd479195d8 400001b 98 System.Object 0 static 0000000000000000 s_eventDisposed
0000fffd47926f60 400000e 40 System.Double 1 instance 3000.000000 _interval
0000fffd4791fb10 400000f 48 System.Boolean 1 instance 1 _enabled
0000fffd4791fb10 4000010 49 System.Boolean 1 instance 0 _initializing
0000fffd4791fb10 4000011 4a System.Boolean 1 instance 0 _delayedEnable
0000fffd4ab241d8 4000012 18 ...apsedEventHandler 0 instance 0000ffee3807aae8 _onIntervalElapsed
0000fffd4791fb10 4000013 4b System.Boolean 1 instance 1 _autoReset
0000fffd4c944ea0 4000014 20 ...SynchronizeInvoke 0 instance 0000000000000000 _synchronizingObject
0000fffd4791fb10 4000015 4c System.Boolean 1 instance 0 _disposed
0000fffd49963e28 4000016 28 ...m.Threading.Timer 0 instance 0000ffee38098dc8 _timer
0000fffd48b90a30 4000017 30 ...ing.TimerCallback 0 instance 0000ffee3807aaa8 _callback
0000fffd479195d8 4000018 38 System.Object 0 instance 0000ffee38098db0 _cookie
從卦中看當前是 3s 為一個周期,這就能解釋為什麼線程池沒有積壓的底層原因了。
三:總結
這個卡死事故還是蠻好解決的,如果有一些經驗直接用dotnet-counter
也是能搞定的,重點在於這是一個 Linux的dump,同時又是 .NET上的一個很好玩的場景,故此分享出來。