天天看點

記一次 .NET 某公交卡扣費系統 程式卡死分析

上個月底,有位朋友微信找到我,說他的程式 多線程處理 RabbitMQ 時過慢,幫忙分析下什麼原因,截圖如下:

記一次 .NET 某公交卡扣費系統 程式卡死分析

這問題抛出來,有點懵逼,沒說CPU爆高,也沒說記憶體洩漏,也沒說程式卡死。。。鬼知道為啥 Rabbitmq 處理過慢哈🤣🤣🤣 。

既然沒有一個緣由,那就往 freeze 這個方向去找吧,上 windbg 說話。

處理過慢原因有很多,可能是幹活的人少了,也可能這些人摸魚去了,總之工作不飽和,接下來看看這幫人有沒有摸魚,從 線程池 看起。

從輸出中看,目前的 <code>CPU=81%</code>, 大機率是這個程式引起的,既然 CPU 過高,可能是某些計算密集型操作,可能是大量鎖導緻的上下文切換,也可能是 GC 頻繁觸發所緻。

本系列到這裡,我相信有很多朋友應該知道如何從線程棧上看是否為 GC 觸發所緻,用指令 <code>~*e !dumpstack</code> 經過一通尋找,發現是 214 号線程觸發了 GC,如下圖所示:

記一次 .NET 某公交卡扣費系統 程式卡死分析

用 <code>!dumpstack</code> 輸出的資訊太多,為了讓資訊更加簡化,改用 <code>!clrstack</code>。

從卦中資訊看,程式在 <code>RedisHelper.HashGet</code> 操作的過程中觸發了 GC,我去,難道是從 redis 中讀了什麼大資料?接下來用 <code>!clrstack -a</code> 看看到底從redis中讀了個啥?

可以看到,redis 讀了大概 <code>6.6 k</code> 的資料,才這麼點資料,我還以為 <code>幾十M</code> 呢 🤣🤣🤣, 要說這點東西就能觸發GC,我是不相信的 。

那這個 <code>罪魁禍首</code> 到底是誰呢? 其實仔細想一想,GC觸發無非就是将對象從 <code>gen0</code> 推到 <code>gen1</code>,然後由 <code>gen1</code> 推到 <code>gen2</code>,那朋友的這個 RabbitMQ 處理慢,肯定是觸發了多次GC,才有了這個肉眼可見的處理慢。

這裡就有一個突破點,既然觸發多次GC,那gen2上肯定有很多帶根的對象,是以從這裡入手比較好,用 <code>!dumpgen</code> 指令檢視。

我去,gen2 上居然有 <code>1.4 kw</code> 的對象,這太讓人好奇了,接下來随便抽幾個 address ,看看它的引用根是個啥?

從引用鍊看,它是被 <code>Dictionary</code> 所持有,那我們就 dump 一下這個 dictionary,看看是個啥。

可以看到,目前dict 的 <code>size= 8.3w</code>,總大小為 <code>1.1G</code>, 然後用 <code>!mdt</code> 抽查一下字典内容。

記一次 .NET 某公交卡扣費系統 程式卡死分析

可以看出,字典中的item大概都是 <code>key= select * from xxx where AccountNo= xxxx</code>, <code>value = Func</code> ,

從 <code>!gcroot</code> 中知道目前根在 <code>187c0</code> 号線程,然後我們通過線程棧去尋找下源碼。

從線程棧看,程式做了一個 db 查詢的操作,源碼大概如下:

問題就出現在這個 sql 拼接上,底層的架構為了提高 mapping 速度,将 <code>sql</code> 和與之對應的 <code>Func&lt;DataReader,Data&gt;</code> 做了緩存處理,由于每次sql都不一樣,這就導緻底層的 dict越來越大,直到目前的 1.1 G。

總的來說,本次事故是由于不斷增長的 dict 在不斷的擠壓小對象堆,加之程式多線程火力全開配置設定對象導緻gc在小對象堆上的頻繁回收造成的 rabbitmq 處理過慢。

修改辦法就是将 sql 參數化。

改成

最後提一下,這個dump有一定的迷惑性,因為觸發GC的線程并不是罪魁禍首,它隻不過是壓死駱駝的那最後一根稻草罷了。

記一次 .NET 某公交卡扣費系統 程式卡死分析