记一次 .NET 车联网云端服务 CPU爆高分析
一:背景
1. 講故事
前幾天有位朋友wx求助,它的程序CPU經(jīng)常飆滿,沒(méi)找到原因,希望幫忙看一下。
這些天連續(xù)接到幾個(gè)cpu爆高的dump,都看煩了????????????,希望后面再來(lái)幾個(gè)其他方面的dump,從溝通上看,這位朋友表現(xiàn)的好慘,可能實(shí)際更慘,那既然找到我了,我就盡最大能力幫他找到幕后真兇,話不多說(shuō),上 windbg。
二:windbg 分析
1. 查看托管線程
因?yàn)榫€程都是靠cpu養(yǎng)著,所以從線程上入手也是一個(gè)很好的思路,要想查看程序的所有托管線程,可以使用 !t 命令。
0:000>?!t ThreadCount:??????38 UnstartedThread:??0 BackgroundThread:?34 PendingThread:????0 DeadThread:???????3 Hosted?Runtime:???noLock??ID?OSID?ThreadOBJ???????????State?GC?Mode?????GC?Alloc?Context??????????????????Domain???????????Count?Apt?Exception0????1?105c?000000000067f600????2a020?Preemptive??0000000000000000:0000000000000000?0000000000671ec0?0?????MTA?2????2?13d0?00000000192c4f40????2b220?Preemptive??0000000000000000:0000000000000000?0000000000671ec0?0?????MTA?(Finalizer)? ... XXXX???15????0?000000001bc64970??8039820?Preemptive??0000000000000000:0000000000000000?0000000000671ec0?0?????MTA?(Threadpool?Completion?Port)?24???23?1380?000000001bc660e0??8029220?Preemptive??0000000000000000:0000000000000000?0000000000671ec0?0?????MTA?(Threadpool?Completion?Port)? XXXX???53????0?000000001bc63200??8039820?Preemptive??0000000000000000:0000000000000000?0000000000671ec0?0?????MTA?(Threadpool?Completion?Port)? XXXX???27?10dc?000000001bd0dbf0??1029220?Preemptive??0000000002CB40F8:0000000002CB4108?0000000000671ec0?1?????MTA?(GC)?(Threadpool?Worker)?在卦象上看:程序有38個(gè)線程,死了3個(gè),我去,有一個(gè)亮點(diǎn),最后一行出現(xiàn)了一個(gè)熟悉的 MTA (GC) 字樣,這什么意思呢?這表示當(dāng)前線程觸發(fā)了GC,但奇怪的是,這個(gè)觸發(fā)GC的線程死了,你肯定要問(wèn)怎么看出來(lái)的,看行頭的 XXXX,先不管了,死馬當(dāng)活馬醫(yī),調(diào)出線程的所有托管和非托管棧,看看有沒(méi)有 WaitUntilGCComplete 和 try_allocate_more_space 字樣。
2. 查看線程棧
要想查看所有線程的線程棧,可以使用 ~*e !dumpstack 命令。
搜索 WaitUntilGCComplete 關(guān)鍵字。
從圖中看,嘿嘿,真的有18個(gè)線程在等待,而且還看到了 System.String.Concat ,是不是和我上上篇發(fā)的 his cpu爆高是一個(gè)套路?????????????
搜索 try_allocate_more_space 關(guān)鍵字。
我去,竟然沒(méi)有 try_allocate_more_space 關(guān)鍵詞,這就和 his 不是一個(gè)套路了,???????????? 有可能這個(gè)dump踩的不是特別好的時(shí)機(jī),有可能程序正處于某些怪異行為中。
看樣子這段路走到頭了,不過(guò)還是那句話,線程是靠cpu養(yǎng)著的,那就硬著頭皮看看各個(gè)線程都在做什么吧,為了讓結(jié)果更清晰一點(diǎn),換一個(gè)命令 ~*e !clrstack。
從圖中可以看出當(dāng)前有 25 個(gè)線程正卡在 FindEntry(System.__Canon) 處,而且從調(diào)用堆棧上看,貌似是 aliyun 封裝的dll,為什么有這么多的線程卡在這里呢?這就給人一個(gè)很大的問(wèn)號(hào)?接下來(lái)我就把阿里云的這段代碼給導(dǎo)出來(lái)看看到底發(fā)生了什么。
3. 查看問(wèn)題代碼
要想導(dǎo)出問(wèn)題代碼,還是用經(jīng)典的 !ip2md + !savemodule 組合命令。
0:000>?!ip2md?000007fe9a1a0641 MethodDesc:???000007fe9a5678e0 Method?Name:??Aliyun.Acs.Core.Utils.CacheTimeHelper.AddLastClearTimePerProduct(System.String,?System.String,?System.DateTime) Class:????????000007fe9a595a08 MethodTable:??000007fe9a567900 mdToken:??????00000000060000a6 Module:???????000007fe9a561f58 IsJitted:?????yes CodeAddr:?????000007fe9a1a0610 Transparency:?Critical 0:000>?!savemodule?000007fe9a561f58?E:\dumps\AddLastClearTimePerProduct.dll 3?ps?in?file p?0?-?VA=2000,?VASize=14148,?FileAddr=200,?FileSize=14200 p?1?-?VA=18000,?VASize=3fc,?FileAddr=14400,?FileSize=400 p?2?-?VA=1a000,?VASize=c,?FileAddr=14800,?FileSize=200然后用 ILSpy 反編譯一下這個(gè)dll,因?yàn)槭前⒗镌频拇a,我就可以放心大膽的放出來(lái)啦。
//?Aliyun.Acs.Core.Utils.CacheTimeHelper using?System; using?System.Collections.Generic;public?class?CacheTimeHelper {private?static?Dictionary<string,?DateTime>?lastClearTimePerProduct?=?new?Dictionary<string,?DateTime>();private?const?int?ENDPOINT_CACHE_TIME?=?3600;public?static?bool?CheckCacheIsExpire(string?product,?string?regionId){string?key?=?product?+?"_"?+?regionId;DateTime?dateTime;if?(lastClearTimePerProduct.ContainsKey(key)){dateTime?=?lastClearTimePerProduct[key];}else{dateTime?=?DateTime.Now;lastClearTimePerProduct.Add(key,?dateTime);}if?(3600.0?<?(DateTime.Now?-?dateTime).TotalSeconds){return?true;}return?false;}public?static?void?AddLastClearTimePerProduct(string?product,?string?regionId,?DateTime?lastClearTime){string?key?=?product?+?"_"?+?regionId;if?(lastClearTimePerProduct.ContainsKey(key)){lastClearTimePerProduct.Remove(key);}lastClearTimePerProduct.Add(key,?lastClearTime);} }可以看出,上面這段代碼在 if (lastClearTimePerProduct.ContainsKey(key)) 處走不下去了,如果往下追,可參考 Dictionary 的源碼。
public?class?Dictionary<TKey,?TValue>? {//?System.Collections.Generic.Dictionary<TKey,TValue>public?bool?ContainsKey(TKey?key){return?FindEntry(key)?>=?0;} }到這里,有沒(méi)有看出這個(gè) CacheTimeHelper 有什么問(wèn)題嗎?對(duì),竟然在多線程環(huán)境下用的是非線程安全的 Dictionary<string, DateTime>,這就很有問(wèn)題了。
4. 用 Dictionary 到底會(huì)有什么問(wèn)題
在多線程環(huán)境下用 Dictionary 肯定會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)亂,這個(gè)毫無(wú)疑問(wèn),而且還會(huì)遇到一些 迭代時(shí)異常,但如果說(shuō)這個(gè)誤用會(huì)導(dǎo)致 CPU 爆高,在我的視野范圍內(nèi)還沒(méi)看到過(guò)。。。為了確保起見(jiàn),到 bing 上搜搜這樣的 天涯淪落人。
嘿嘿,還真的有這樣的案例:High CPU in .NET app using a static Generic.Dictionary (
https://www.tessferrandez.com/blog/2009/12/21/high-cpu-in-net-app-using-a-static-generic-dictionary.html
),再截個(gè)圖。
從文章描述看,簡(jiǎn)直是一摸一樣????????????,這也就斷定在多線程環(huán)境下操作 Dictionary ,可能會(huì)導(dǎo)致 FindEntry(key) 時(shí)出現(xiàn)死循環(huán),然后 25 個(gè)死循環(huán)一起把cpu抬起來(lái)了,補(bǔ)充一下當(dāng)前爆滿的CPU利用率。。。
0:000>?!tp CPU?utilization:?100% Worker?Thread:?Total:?27?Running:?27?Idle:?0?MaxLimit:?32767?MinLimit:?4 Work?Request?in?Queue:?0 -------------------------------------- Number?of?Timers:?1 -------------------------------------- Completion?Port?Thread:Total:?4?Free:?3?MaxFree:?8?CurrentLimit:?3?MaxLimit:?1000?MinLimit:?4三:總結(jié)
既然是阿里云的sdk出的bug,這問(wèn)題就麻煩了。。。改也改不得,然后告訴朋友去提工單解決。
本以為事情就這樣結(jié)束了,但我想一想,幾年前用的阿里云其他 sdk 也遇到了類似CPU爆高的問(wèn)題,后來(lái)通過(guò)升級(jí)sdk就搞定了,這次也賭賭看,先看一下程序集信息。
[assembly:?CompilationRelaxations(8)] [assembly:?RuntimeCompatibility(WrapNonExceptionThrows?=?true)] [assembly:?Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly:?TargetFramework(".NETStandard,Version=v2.0",?FrameworkDisplayName?=?"")] [assembly:?AssemblyCompany("Alibaba?Cloud")] [assembly:?AssemblyConfiguration("Release")] [assembly:?AssemblyCopyright("?2009-2018?Alibaba?Cloud")] [assembly:?AssemblyDescription("Alibaba?Cloud?SDK?for?C#")] [assembly:?AssemblyFileVersion("1.1.12.0")] [assembly:?AssemblyInformationalVersion("1.1.12")] [assembly:?AssemblyProduct("aliyun-net-sdk-core")] [assembly:?AssemblyTitle("aliyun-net-sdk-core")] [assembly:?AssemblyVersion("1.1.12.0")]可以看到朋友當(dāng)前用的是 1.1.12.0 版本,那就把 aliyun-net-sdk-core 升級(jí)到最新再看看這個(gè) CacheTimeHelper 有沒(méi)有被修復(fù) ?
果然不出所料,在新版本中給修復(fù)好了,所以經(jīng)驗(yàn)告訴我,用阿里云的sdk,要記得經(jīng)常升級(jí),不然各種大坑等著你。。。????????????
END
工作中的你,是否已遇到 ...?
1. CPU爆高
2. 內(nèi)存暴漲
3. 資源泄漏
4. 崩潰死鎖
5. 程序呆滯
等緊急事件,全公司都指望著你能解決...? 危難時(shí)刻才能展現(xiàn)你的技術(shù)價(jià)值,作為專注于.NET高級(jí)調(diào)試的技術(shù)博主,歡迎微信搜索: 一線碼農(nóng)聊技術(shù),免費(fèi)協(xié)助你分析Dump文件,希望我能將你的踩坑經(jīng)驗(yàn)分享給更多的人。
總結(jié)
以上是生活随笔為你收集整理的记一次 .NET 车联网云端服务 CPU爆高分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: .NET Worker Service
- 下一篇: .NET上海社区线下Meetup - 5