10

记一次 .NET 某医保平台 CPU 爆高分析

 1 year ago
source link: https://www.cnblogs.com/huangxincheng/p/17139891.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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>)
...

214741-20230221094153904-211118557.png

从输出以及截图来看,有 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;
        }
    }

214741-20230221094153904-996099947.png

从输出结果看 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);

214741-20230221094153958-773621944.png

虽然 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 次,下午大妹子反馈问题已经解决。

214741-20230221094153894-2143114650.png

看到大妹子开心的笑了,我的饭有着落了😁😁😁

这个问题是一个不良习惯的写法造成的,说实话,我不是看到线程栈上的 WhereListIterator 方法我也不相信会执行多次,可能最近中了 SQLSERVER 的毒,不管怎么说,这些都是小事,有大餐吃才是最重要的!🤣


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK