一:背景
1. 講故事
一直在追這個系列的朋友應該能感受到,我給這個行業中無數的陌生人分析過各種dump,終於在上週有位老同學找到我,還是個大妹子,必須有求必應 ???。
妹子公司的系統最近在某次升級之後,在高峰期會遇到 CPU 爆高的現象,有些單位你懂的,很強勢,所以就苦逼了程式媛,不管怎麼說,既然找上我,得想各種辦法給解決掉,用遠端的方式告訴了老同學怎麼用 procdump 去抓 dump,在一個小時之後 dump 成功拿到,接下來就來分析了。
二:WinDbg 分析
1. CPU 真的爆高嗎
是不是爆高一定要拿資料說話,可以用 !tp
命令驗證。
0:234> !tp
CPU utilization: 44%
Worker Thread: Total: 68 Running: 38 Idle: 10 MaxLimit: 4800 MinLimit: 48
Work Request in Queue: 0
--------------------------------------
Number of Timers: 2
--------------------------------------
Completion Port Thread:Total: 1 Free: 1 MaxFree: 96 CurrentLimit: 1 MaxLimit: 4800 MinLimit: 48
從輸出看 CPU=44%
就拿到了 dump ,真的有點奇怪,我設定的閾值是 70%
,可能是老同學後來改了閾值,雖然不是我的預期但還是可以探究一下的。
接下來怎麼排查呢?我們都知道 CPU 高本質都是由 執行緒 造成的,所以到每個執行緒棧上去兜兜,看看有沒有什麼收穫,使用命令 ~*e !clrstack
即可。
0:234> ~*e !clrstack
OS Thread Id: 0x6de8 (234)
Child SP IP Call Site
000000959273dfd8 00007ff89d287174 [HelperMethodFrame_2OBJ: 000000959273dfd8] System.Security.Cryptography.Utils._AcquireCSP(System.Security.Cryptography.CspParameters, System.Security.Cryptography.SafeProvHandle ByRef)
000000959273e0d0 00007ff88af3074e System.Security.Cryptography.Utils.AcquireProvHandle(System.Security.Cryptography.CspParameters)
000000959273e110 00007ff88b85e585 System.Security.Cryptography.CryptoAPITransform..ctor(Int32, Int32, Int32[], System.Object[], Byte[], System.Security.Cryptography.PaddingMode, System.Security.Cryptography.CipherMode, Int32, Int32, Boolean, System.Security.Cryptography.CryptoAPITransformMode)
000000959273e230 00007ff88b86057d System.Security.Cryptography.DESCryptoServiceProvider._NewEncryptor(Byte[], System.Security.Cryptography.CipherMode, Byte[], Int32, System.Security.Cryptography.CryptoAPITransformMode)
000000959273e2e0 00007ff88b8602f3 System.Security.Cryptography.DESCryptoServiceProvider.CreateDecryptor(Byte[], Byte[])
0000009588e7e300 00007ff82f7bd49a xxx.DesDecrypt(System.String)
0000009588e7e370 00007ff82f9848e2 xxx.b__17(xxx_UserInfo)
0000009588e7e3a0 00007ff887d7778e System.Linq.Enumerable+WhereListIterator`1[[System.__Canon, mscorlib]].MoveNext()
0000009588e7e3e0 00007ff887d7767e System.Linq.Enumerable.FirstOrDefault[[System.__Canon, mscorlib]](System.Collections.Generic.IEnumerable`1<System.__Canon>)
...
從輸出以及截圖來看,有 38 個執行緒正在執行 xxx.DesDecrypt
方法,看起來是一個 DES 解密操作,一般來說 加解密
比較費CPU資源,看樣子找到問題根了,根據執行緒棧找一下程式碼到底是怎麼寫的,為了保密起見,這裡就多用幾個 XXX 來替代吧,參考程式碼如下:
protected void xxx(string xxx)
{
xxx userInfo = xxxUserInfoList.Where((xxx p) => p.xxx.Name == xxx.DesDecrypt(base.User.Name)).FirstOrDefault();
}
說實話第一眼看到在 Where 中使用 DesDecrypt(base.User.Name)
方法,雖然有點彆扭,內心還是感覺擴充套件方法應該會幫我最佳化把它單獨給提出來的,比如下面這樣。
protected void xxx(string xxx)
{
var nameWhere= xxx.DesDecrypt(base.User.Name);
xxx userInfo = xxxUserInfoList.Where((xxx p) => p.xxx.Name == nameWhere).FirstOrDefault();
}
但看了執行緒棧上的 WhereListIterator
和 FirstOrDefault
方法,貌似沒有做最佳化,為了驗證我的想法,我還特意寫了段程式碼。
internal class Program
{
static void Main(string[] args)
{
var query = new List<string>() { "1", "2","3","4" };
var text = "hello world";
query.Where(i => i == Run(text)).FirstOrDefault();
}
public static string Run(string str)
{
Console.WriteLine(str);
return str;
}
}
從輸出結果看 hello world
輸出了 4 次,也就表明當前並沒有做任何最佳化。
再說點題外話,最近在研究 SQLSERVER,我覺得它的 SQL最佳化器應該能夠處理這種情況,截圖如下:
SELECT OrderID FROM dbo.Orders
WHERE OrderDate > SUBSTRING('1996-07-10 00:00:10.000', 0, 11) AND
OrderDate < SUBSTRING('1996-07-20 00:00:20.000', 0, 11);
雖然 SQLSERVER 非常智慧的做了最佳化,但後來想一想C#不最佳化是對的,因為框架程式碼沒法保證向 xxx.DesDecrypt
方法中傳入相同引數,返回的結果一定是相同的,所以採用保守的方法能夠理解。
2. 解密操作真的會爆高嗎
就算WHERE中逐項處理解密
操作就一定會爆高嗎?這是一個需要求證的問題,可以切到某個執行緒上用 !dso
找出那個 list,然後 !do
即可,參考程式碼如下:
0:234> !DumpObj /d 000001f631799240
Name: System.Collections.Generic.List`1[[xxxx_UserInfo,xxxx]]
MethodTable: 00007ff82f5f3b20
EEClass: 00007ff88ab59f90
Size: 40(0x28) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ff88b12d6c8 4001843 8 System.__Canon[] 0 instance 000001f6b14ae380 _items
00007ff88b123e98 4001844 18 System.Int32 1 instance 3506 _size
00007ff88b123e98 4001845 1c System.Int32 1 instance 3506 _version
00007ff88b121c98 4001846 10 System.Object 0 instance 0000000000000000 _syncRoot
00007ff88b12d6c8 4001847 8 System.__Canon[] 0 static <no information>
從輸出可以清晰的看到,當前的 list.count=3506
個,在加上有 38
個執行緒在併發處理,所以最壞情況下是 3506 * 38 = 13w
的解密操作,難怪說高峰期的時候 CPU 爆高。
解決辦法也很簡單,把 xxx.DesDecrypt
給提出來,將解密操作由原來的 13w
最佳化到 38
次,下午大妹子反饋問題已經解決。
看到大妹子開心的笑了,我的飯有著落了???
三:總結
這個問題是一個不良習慣的寫法造成的,說實話,我不是看到執行緒棧上的 WhereListIterator
方法我也不相信會執行多次,可能最近中了 SQLSERVER 的毒,不管怎麼說,這些都是小事,有大餐吃才是最重要的!?