8

公司某资料子系统定期cpu过高的诊断 - dotnet程序故障诊断

 2 years ago
source link: https://www.cnblogs.com/dotnet-diagnostic/p/17181455.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

公司某资料子系统定期cpu过高的诊断

公司里的某负责保存用户文档的子系统有时会忽然cpu很高,过了大约5分钟后又恢复正常水平。领导协调让我帮看一下 (我心里是: 不熟悉这个子系统里面的代码,我尽力哈?)

其实确实是这样的,如果熟悉出问题的系统的代码,会对诊断问题起到很大的帮助,否则就需要更多的利用对底层的理解了。

打听后知道了这个子系统用.net core写的,可以运行在windows和linux docker上,且这次的cpu高的问题,他们在windows运行也可复现。于是,我让他们在windows上运行,发现cpu高的时候dump一下。(然后windbg就可以准备下地干活了?)

在用windbg看了大部分threadpool worker线程的情况后,我发现大部分线程都运行到这样类似的call stack上:

3115652-20230305194648347-1053389879.png

先大概看一下这个PeopleManagementService.Program+c__DisplayClass2_0.b__0(PeopleManagementService.User)在做什么,用IL反编译工具可以看到:

3115652-20230305194734312-223352651.png

再结合call stack,感觉上下文正在从某集合里查找东西呢?(一个题外话,上面那个c__DisplayClass2_0.b__0名字挺奇怪,其实这是C# compiler在build我们的C#代码到IL时帮着生成的一些code。对于目前这个,就对应我们C#程序员写出的lambda )

找东西会把cpu明显提高么?去看看PrintUserLastEnteredTimeDetail()在干什么吧。(其实子系统team的同事人很好,所以这次我直接找他要对应的代码得了?) 简化代码:

3115652-20230305194855866-757167176.png

 看起来是在一个叫users的List中按条件查找东西。看看这个list的size, 用!do试一下:

 1 0:027> !DumpObj /d 0000019a8000d938
 2 Name:        System.Collections.Generic.List`1[[PeopleManagementService.User, PeopleManagementService]]
 3 MethodTable: 00007ff96c313e88
 4 EEClass:     00007ff96c2df530
 5 Tracked Type: false
 6 Size:        32(0x20) bytes
 7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.14\System.Private.CoreLib.dll
 8 Fields:
 9               MT    Field   Offset                 Type VT     Attr            Value Name
10 00007ff96c2fd618  4002099        8     System.__Canon[]  0 instance 0000019a90bc12a0 _items
11 00007ff96c2a94b0  400209a       10         System.Int32  1 instance           524288 _size
12 00007ff96c2a94b0  400209b       14         System.Int32  1 instance           524288 _version
13 00007ff96c2fd618  400209c        8     System.__Canon[]  0   static dynamic statics NYI                 s_emptyArray
size大约524288,500K的items在list里面,然后多个线程在一起进行这样的查找,所以cpu提高了。

在告知同事这个原因前,我还有意无意地查了一下那个保存big list的Dictionary,用!gcroot:

3115652-20230305195128988-1490529197.png

 然后!do看一下:

1 !do 0000019A8000D818
2 Name:        System.Collections.Generic.Dictionary`2+Entry[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[PeopleManagementService.User, PeopleManagementService]], System.Private.CoreLib]][]
3 MethodTable: 00007ff96c322808
4 EEClass:     00007ff96c322770
5 Tracked Type: false
6 Size:        96(0x60) bytes
7 Array:       Rank 1, Number of elements 3, Type VALUETYPE
一个保存着那么大list的Dictionary, 其实只有<=3的key value pairs。再看看它的keys是啥。我们用windbg的memory view直接看内存中的"瓤"吧:
3115652-20230305195210423-1072216.png

64位的进程运行环境,clr array的第一个item在0000019A8000D818 + 8(mt ptr size) + 4(length field) + 4 (padding),所以看标红的那两个地址。

1 0:027> !DumpObj /d 0000019a8000cce8
2 Name:        System.String
3 MethodTable: 00007ff96c2bd708
4 EEClass:     00007ff96c299d60
5 Tracked Type: false
6 Size:        30(0x1e) bytes
7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.14\System.Private.CoreLib.dll
8 String:      Male
1 0:027> !DumpObj /d 0000019a8000cd08
2 Name:        System.String
3 MethodTable: 00007ff96c2bd708
4 EEClass:     00007ff96c299d60
5 Tracked Type: false
6 Size:        34(0x22) bytes
7 File:        C:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.14\System.Private.CoreLib.dll
8 String:      Female

原来Dictionary按性别分类保存了所有的用户信息。

我把这些信息告诉同事,他第一反应是没有想到这个运行的结果。按照他的代码,他预期是按性别和用户名一起来当key,然后放到Dictionary,这样查询起来至少时间复杂度不应这么高。于是我和那个同事打开相关的生成key的代码:

1 private static string GetUserKey(Gender gender, string userName) => string.Join(userName, gender);
大家发现没?想当然的用String.Join来连接2个object,却没注意其实String.Join的定义是这样,注意第一个参数可是separator啊:
3115652-20230305195337023-1031487843.png

这么错误的使用,就相当于把user name作为separator,而后面的集合参数其实就传了一个性别,所以生成的key就总是同一个,本想做成user cache的Dictionary的数据最终退化为两个很大的lists? 然后多个线程一起在这两个大大的list上以O(n)的时间复杂度查找东西,cpu就高起来了……

看来大家不能这么随随便便的写C#代码啊,虽然.net sdk会为C#程序员提供便利和安全性,但自己也不该想当然的无脑coding哈。最后同事的脸比较红哈?

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK