天天看點

記一次 .NET 某市附屬醫院 Web程式 偶發性CPU爆高分析

這個月初,一位朋友加微信求助他的程式出現了 CPU 偶發性爆高,希望能有償解決一下。

記一次 .NET 某市附屬醫院 Web程式 偶發性CPU爆高分析

從描述看,這個問題應該困擾了很久,還是醫院的朋友給力,開門就是 100塊 紅包 🤣🤣🤣,那既然是偶發性爆高,人工不行,還得用 <code>procdump</code> 自動抓,用 <code>procdump -ma -s 5 -n 2 -c 70 w3wp</code> 埋伏好,幾天後如願生成了兩個dump,太妙了,接下來就用 windbg 分析吧。

一切隻相信資料,這裡用 <code>!tp</code> 看一下此時 machine 的cpu值。

從資料看,此時 <code>CPU utilization: 83%</code>, 沒毛病。

既然是偶發性的bug,而且也說了可能是醫生操作了什麼觸發了什麼條件才導緻的,剛好這裡也有 2 個dump,那就比一下各個線程的耗時吧,這裡隻提取 top5 。

從資訊看,間隔15s的dump,相對來說 <code>62,44,39</code> 這三個線程耗時最多,是以這三個線程值得繼續挖一挖。

接下來用 <code>~62s; !clrstack;~44s; !clrstack;~39s; !clrstack</code> 切到這三個線程看下棧情況,如下圖所示:

記一次 .NET 某市附屬醫院 Web程式 偶發性CPU爆高分析

從棧中看,并沒有使用者代碼,這就很尴尬了,我一度懷疑是不是 webform 的同步上下文導緻的,但好歹我還是有一些經驗,既然 <code>!clrstack</code> 看不到,那就用 <code>!dumpstack</code> 。

真是太奇怪了,使用者代碼 <code>xxx.bl_baseInfo.getBljl</code> 怎麼跑到<code>非托管棧</code> ? 這真是第一次遇到,從棧上看,程式在 <code>xxx.bl_baseInfo.getBljl()</code> 方法中遇到了問題,接下來用 <code>!dso</code> 把堆對象都導出來。

我去,這個 dump 的棧被破壞了,可能是 cpu 爆高導緻的,也有可能是抓的不好,這下太折磨了,得,隻能用 kb 到非托管棧上找方法參數。

接下來我們 <code>!do</code> 一下 <code>132b35cc</code> 位址,看看是什麼 list。

用輸出中可以看到,這個 <code>list=23w</code> 條記錄,它正在 <code>list.Contains</code> 處,有了這些資訊,接下來就可以把源碼導出來,簡化後的代碼如下:

眼尖的朋友肯定能注意到,在資料量大的情況下,這裡的 <code>list.Insert(0, me_zyblbr);</code> 有大問題,畢竟 <code>list.Insert</code> 的複雜度是 <code>O(N)</code>,針對 <code>23w</code> 來說總的時間複雜度就是:

<code>n(n-1)/2 = 23w(23w-1)/2 = 26,450,000,000 = 264億</code> 。

然後就是 3個這樣的線程就一起把cpu給擡起來了。

雖然<code>問題根</code>已找到,但朋友最關心的是這位醫生到底輸入了什麼導緻 sql 查詢了如此大的資料, 不知道醫生要扣錢還是他們要向上面有個交代😂😂😂, 由于<code>堆,棧</code>都 被損壞了,找起來還是很麻煩的,我用了 sos 中的 <code>!lno, !dumpheap</code> 都是報錯,徹底趴窩了,最後想了下 <code>sosex</code> 中也有一個 <code>!mdso</code> 指令,終于一路坎坷的找到了重要的 <code>OracleParameter</code> 參數。

原來是醫生模糊查詢了一個 <code>高血壓病</code> 導緻的。。。

不過這裡主要是想告訴大家的是,當由于記憶體遭到一定程度的破壞導緻 sos 徹底趴窩也不要怕,可能還有其他的插件可以救我們于水火之中,多一個插件多一條路哈。

總的來說,這次偶發的CPU爆高事故,犯的相對比較低級,對 <code>List.Insert</code> 的複雜度可能也不是很了解,也有可能是為了趕業務所欠的債吧,改發也相對簡單,先用 <code>add</code> 送到 list,最後再統一按規則做一下重整排序。

記一次 .NET 某市附屬醫院 Web程式 偶發性CPU爆高分析