BitArray虽好,但请不要滥用,又一次线上内存暴增排查
source link: https://segmentfault.com/a/1190000022712086
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.
一:背景
1. 讲故事
前天写了一篇大内存排查在园子里挺火,这是做自媒体最开心的事拉,干脆再来一篇满足大家胃口,上个月我写了一篇博客提到过使用 bitmap
对原来的 List<CustomerID>
进行高强度压缩,将原来的List内存压缩了将近106倍,但是bitmap不是一味的好,你必须在正确的场景中使用,而不是闭着眼睛滥用,bitmap在C#中对应的集合是BitArray。
好像剧透了:smile::smile::smile:,结果就是BitArray的滥用导致内存小10G的涨跌,不过这种东西重点还是看解决思路,写给以后的自己,可不能让这难得的实践经验蒸发啦~~~
二:解决思路
1. 一看托管堆
看托管堆虽然是一个好主意,但也不是每次都凑效,毕竟造成内存暴涨暴跌的原因各种各样,就像人感冒有风寒,风热和病毒性,对吧:grin:,还是使用老命令: !dumpheap -stat -min 102400
,在托管堆上找大于 100M
的对象。
0:030> !dumpheap -stat -min 102400 Statistics: MT Count TotalSize Class Name 00007ffe094ec988 1 1438413 System.Byte[] 00007ffdab934c48 1 1810368 System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.Collections.Generic.HashSet`1[[System.Int64, mscorlib]], System.Core]][] 00007ffe094e6948 1 2527996 System.String 00007ffdab9ace78 4 29499552 System.Collections.Generic.Dictionary`2+Entry[[System.Int64, mscorlib],[System.DateTime, mscorlib]][] 00007ffe094e4078 4 267342240 System.String[] 00007ffe094e9220 135 452683336 System.Int32[] 00007ffdab8cd620 123 1207931808 System.Collections.Generic.HashSet`1+Slot[[System.Int64, mscorlib]][] 00007ffe094c8510 185 1579292760 System.Int64[] 00007ffdab9516b0 154 1934622720 System.Linq.Set`1+Slot[[System.Int64, mscorlib]][] 000001cc882de970 347 3660623866 Free Total 1371 objects
去掉一些敏感类后,再观察好像没有特别显眼的集合,像 System.Int64[] ,System.Linq.Set1+Slot[[System.Int64, mscorlib]][]
一般都是用作其他集合的内存存储,很多时候用 !gcroort
抓不出来,最大的反而是Free列,有347个碎片,高达 3.5G
,说明此时的大对象堆是一塌糊涂啊,要是GC能帮忙压缩一下该多好:smile:。
2. 查看每一个线程的调用栈
先惯性的偷窥一下程序中有多少个线程。
0:000> !threads ThreadCount: 74 UnstartedThread: 0 BackgroundThread: 72 PendingThread: 0 DeadThread: 0 Hosted Runtime: no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 2958 000001cc882e5a40 2a020 Preemptive 0000000000000000:0000000000000000 000001cc882d8db0 1 MTA 2 2 2358 000001cc883122c0 2b220 Preemptive 000001D41B132930:000001D41B1348A0 000001cc882d8db0 0 MTA (Finalizer) 3 4 2204 000001cc883ae5d0 102a220 Preemptive 0000000000000000:0000000000000000 000001cc882d8db0 0 MTA (Threadpool Worker) 5 7 278c 000001cca29d8ef0 202b220 Preemptive 000001D41AB53A98:000001D41AB55A58 000001cc882d8db0 1 MTA 6 40 2a64 000001cca3048f10 1020220 Preemptive 0000000000000000:0000000000000000 000001cc882d8db0 0 Ukn (Threadpool Worker) 7 46 e34 000001cca311c390 202b220 Preemptive 0000000000000000:0000000000000000 000001cc882d8db0 0 MTA 8 47 27d8 000001cca3115e00 2b220 Preemptive 0000000000000000:0000000000000000 000001cc882d8db0 0 MTA ...
可以看到当前有74个线程,后台线程有72个,接下来用 ~*e !clrstack
查看每个托管线程都在做什么,由于内容太多,我就节选一下了哈。
0:000> ~*e !clrstack OS Thread Id: 0x2d64 (29) Child SP IP Call Site 000000d908cfe698 00007ffe28646bf4 [GCFrame: 000000d908cfe698] 000000d908cfe768 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d908cfe768] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object) OS Thread Id: 0x214c (30) Child SP IP Call Site 000000d90957e6e8 00007ffe28646bf4 [GCFrame: 000000d90957e6e8] 000000d90957e7b8 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d90957e7b8] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object) OS Thread Id: 0x1dc0 (40) Child SP IP Call Site 000000d950ebe878 00007ffe28646bf4 [GCFrame: 000000d950ebe878] 000000d950ebe948 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d950ebe948] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object) OS Thread Id: 0x274c (53) Child SP IP Call Site 000000d9693fe518 00007ffe28646bf4 [GCFrame: 000000d9693fe518] 000000d9693fe5e8 00007ffe28646bf4 [HelperMethodFrame_1OBJ: 000000d9693fe5e8] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object) 000000d9693fe700 00007ffe09314d05 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) 000000d9693fe790 00007ffe0930d996 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) 000000d9693fe800 00007ffe09c9b7a1 System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken)
发现一个奇怪的现象,有4个线程 29,30,40,53
在 Monitor.ObjWait
处卡住了,从调用栈来看这四个家伙正在准备向Mongodb批量插入数据[InsertBatch],此时应该有其他的一个线程先行获取到了lock正在做InsertBatch,这四个线程在等待,如何觉得抽象了,我画一张图:
3. 寻找insertbatch处的集合
这里我就拿 30
号线程说事,从上图调用栈中你应该看到一个 System.Collections.Generic.IEnumerable1<System.__Canon>
,从IEnumerable中可以猜测实现类应该是List或者HashSet这样的集合,接下来用 !dso
把30号线程栈上的对象全部dump出来。
从图中看应该就是这个 List<xxx.Common.GroupConditionCustomerIDCacheModel>
,然后用 !objsize
:heavy_plus_sign: !do
给List量个尺寸并且dump一下。
0:030> !objsize 000001d3fa581518 sizeof(000001d3fa581518) = 1487587080 (0x58aac708) bytes (System.Collections.Generic.List`1[[DataMipCRM.Common.GroupConditionCustomerIDCacheModel, DataMipCRM.Common]]) 0:030> !do 000001d3fa581518 Name: System.Collections.Generic.List`1[[DataMipCRM.Common.GroupConditionCustomerIDCacheModel, DataMipCRM.Common]] MethodTable: 00007ffdab9557d0 EEClass: 00007ffe08eb22a0 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 00007ffe09478740 4001871 8 System.__Canon[] 0 instance 000001d3fa5b9bf8 _items 00007ffe094e9288 4001872 18 System.Int32 1 instance 1520 _size 00007ffe094e9288 4001873 1c System.Int32 1 instance 1520 _version 00007ffe094e6f28 4001874 10 System.Object 0 instance 0000000000000000 _syncRoot 00007ffe09478740 4001875 8 System.__Canon[] 0 static <no information>
可以看出list占用 1487587080/1024/1024=1.4G
,尼玛这么大,吓人哈,从 _size
看也就1520个,说明重点都在 _items
数组里啦,接下里用 da
把第一个item拿出来解剖下。
0:030> !da -length 1 -details 000001d3fa5b9bf8 Name: DataMipCRM.Common.GroupConditionCustomerIDCacheModel[] MethodTable: 00007ffdab955e10 EEClass: 00007ffe08eaaa00 Size: 16408(0x4018) bytes Array: Rank 1, Number of elements 2048, Type CLASS Element Methodtable: 00007ffdab955740 [0] 000001d3fa581540 Name: DataMipCRM.Common.GroupConditionCustomerIDCacheModel MethodTable: 00007ffdab955740 EEClass: 00007ffdab94b9e8 Size: 64(0x40) bytes File: D:\LuneceService\DataMipCRM.Common.dll Fields: MT Field Offset Type VT Attr Value Name 00007ffdaac69258 4000589 28 ...oDB.Bson.ObjectId 1 instance 000001d3fa581568 <_id>k__BackingField 00007ffe094e9288 400058a 20 System.Int32 1 instance 1901 <ShopId>k__BackingField 00007ffe094e6948 400058b 8 System.String 0 instance 000001d3f7154070 <GroupConditionHasCode>k__BackingField 00007ffe094e6948 400058c 10 System.String 0 instance 000001cca7b46ac0 <unit>k__BackingField 00007ffe094f1cb0 400058d 18 ...lections.BitArray 0 instance 000001d3fa581580 <customeridArray>k__BackingField
从最后一行的Type列可以看到有一个 BitArray
类,还是一样,先量个尺寸再打出来。
0:030> !objsize 000001d3fa581580 sizeof(000001d3fa581580) = 956008 (0xe9668) bytes (System.Collections.BitArray) 0:030> !do 000001d3fa581580 Name: System.Collections.BitArray MethodTable: 00007ffe094f1cb0 EEClass: 00007ffe08ead968 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 00007ffe094e9220 40017e2 8 System.Int32[] 0 instance 000001d5320c6d18 m_array 00007ffe094e9288 40017e3 18 System.Int32 1 instance 7647524 m_length 00007ffe094e9288 40017e4 1c System.Int32 1 instance 2 _version 00007ffe094e6f28 40017e5 10 System.Object 0 instance 0000000000000000 _syncRoot
从output中看,这个bitarray占用 956008/1024/1024 = 0.91M
,真tmd的大,看bit位有 764w
个,说明有一个CustomerID=7647524-1放在这里面,问题基本上就算找到了,现在终于知道内存为什么这么大,算一下就出来了。
四:总结
最后去问了下搬砖的为什么这么写,是因为给客户展示的一张报表,为了加速。。。将每一个点上的人群保存起来放在BitArray上然后进入Monogdb缓存,这样客户后续选择某一个点进行 下钻 操作的话,可以直接从mongodb中将BitArray的人群复原出来,免去程序重复计算之苦,因为每个点的人群具有排他性,落在每个点上的人可能只有几十,几百,几千,而偏偏这家客户有800w之多,自然这个CustomerID就特别大了,更不巧的就用了bitArray存少量的大数字,这些赶在一起之后,就是一个典型的滥用bitarray,您说的?
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK