記一次 .NET 某醫保平臺 CPU 爆高分析

一線碼農發表於2023-02-21

一:背景

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();
        }

但看了執行緒棧上的 WhereListIteratorFirstOrDefault 方法,貌似沒有做最佳化,為了驗證我的想法,我還特意寫了段程式碼。


    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 的毒,不管怎麼說,這些都是小事,有大餐吃才是最重要的!?

相關文章