一:背景

1. 讲故事

上个月有个老朋友找到我,说他的站点晚顶峰 CPU 会忽然爆高,发了两份 dump 文件过去,如下图:

又是经典的 CPU 爆高问题,到目前为止,对这种我还是有一些教训可循的。

  • 抓 2-3 个 dump

第一个:有利于算两份 dump 中的线程时间差,从而推算最耗时线程。

第二个:有时候你抓的dump刚好线程都解决完了,cpu 还未实在回落,所以剖析这种dump意义不大,我是吃了不少亏。

  • 优先揣测是否为 GC 捣鬼

当初的码农都精怪精怪的,根本不会傻傻的写出个死循环,绝大部分都是遇到某种 资源密集型计算密集型 场景下导致非托管的 GC 出了问题。

好了,有了这个先入为主的思路,接下来就能够用 windbg 去占卜了。

二: windbg 剖析

1. GC 捣鬼剖析

GC 捣鬼的实质是 GC 呈现了回收压力,尤其是对 大对象堆 的调配和开释,大家应该晓得 大对象堆 采纳的是链式管理法,不到万不得已 GC 都不敢回收它,所以在它下面的调配和开释都是一种 CPU密集型 操作,不信你能够去 StackOverflow 上搜搜 LOH 和 HighCPU 的关联关系。

2. 应用 x 命令搜寻

在 windbg 中有一个快捷命令 x ,可用于在非托管堆上检索指定关键词,检索之前先看看这个 dump 是什么 Framework 版本,决定用什么关键词。

0:050> lmvstart    end        module name00b80000 00b88000   w3wp       (pdb symbols)          c:\mysymbols\w3wp.pdb\0CED8B2D5CB84AEB91307A0CE6BF528A1\w3wp.pdb    Loaded symbol image file: w3wp.exe    Image path: C:\Windows\SysWOW64\inetsrv\w3wp.exe    Image name: w3wp.exe71510000 71cc0000   clr        (pdb symbols)          c:\mysymbols\clr.pdb\9B2B2A02EC2D43899F87AC20F11B82DF2\clr.pdb    Loaded symbol image file: clr.dll    Image path: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll    Image name: clr.dll    Browse all global symbols  functions  data    Timestamp:        Thu Sep  3 03:30:58 2020 (5F4FF2F2)    CheckSum:         007AC92B    ImageSize:        007B0000    File version:     4.8.4261.0    Product version:  4.0.30319.0

File version 上能够看出以后是基于 Net Framework 4.8 的,好了,用 x clr!SVR::gc_heap::trigger* 看看有没有触发 gc 的操作。

0:050> x clr!SVR::gc_heap::trigger*71930401          clr!SVR::gc_heap::trigger_ephemeral_gc (protected: int __thiscall SVR::gc_heap::trigger_ephemeral_gc(enum gc_reason))71665cf9          clr!SVR::gc_heap::trigger_gc_for_alloc (protected: void __thiscall SVR::gc_heap::trigger_gc_for_alloc(int,enum gc_reason,struct SVR::GCDebugSpinLock *,bool,enum SVR::msl_take_state))71930a08          clr!SVR::gc_heap::trigger_full_compact_gc (protected: int __thiscall SVR::gc_heap::trigger_full_compact_gc(enum gc_reason,enum oom_reason *,bool))

从输入信息看,gc 果然在高速运转,开心哈,接下来看一下是哪一个线程触发了gc,能够用 !eestack 把所有线程的托管和非托管堆栈打进去。

从图中能够看到以后 50 号线程的 GetUserLoginGameMapIds() 办法进行的大对象调配 try_allocate_more_space 触发了 clr!SVR::gc_heap::trigger_gc_for_alloc GC回收操作,最初 GC 通过 clr!SVR::GCHeap::GarbageCollectGeneration 进行回收,既然在回收,必然有很多线程正在卡死。

接下来再看看有几个线程正在共同努力调用 GetUserLoginGameMapIds() 办法。

到这里根本就能确定是 gc 捣的鬼。接下来的趣味点就是 GetUserLoginGameMapIds() 到底在干嘛?

3. 剖析 GetUserLoginGameMapIds() 办法

接下来把办法的源码导出来,应用 !name2ee 找到其所属 module,而后通过 !savemodule 导出该 module 的源码。

0:050> !name2ee *!xxx.GetUserLoginGameMapIdsModule:      1c870580Assembly:    xxx.dllToken:       0600000bMethodDesc:  1c877504Name:        xxx.GetUserLoginGameMapIds(xxx.GetUserLoginGameMapIdsDomainInput)JITTED Code Address: 1d5a20300:050> !savemodule  1c870580 E:\dumps\6.dll3 sections in filesection 0 - VA=2000, VASize=112b8, FileAddr=200, FileSize=11400section 1 - VA=14000, VASize=3c8, FileAddr=11600, FileSize=400section 2 - VA=16000, VASize=c, FileAddr=11a00, FileSize=200

关上导出的 6.dll,为了最大爱护隐衷,我就把字段名暗藏一下, GetUserLoginGameMapIds() 大体逻辑如下。

public GetUserLoginGameMapIdsDomainOutput GetUserLoginGameMapIds(GetUserLoginGameMapIdsDomainInput input){    List<int> xxxQueryable = this._xxxRepository.Getxxx();    List<UserLoginGameEntity> list = this._userLoginGameRepository.Where((UserLoginGameEntity u) => u.xxx == input.xxx, null, "").ToList<UserLoginGameEntity>();    List<int> userLoginGameMapIds = (from u in list select u.xxx).ToList<int>();    IEnumerable<GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput> source = (from mc in (from mc in this._mapCategoryRepository.AsQueryable().ToList<MapCategoryEntity>()    where userLoginGameMapIds.Any((int mid) => mid == mc.xxx) && mapIdsQueryable.Any((int xxx) => xxx == mc.xxx)    select mc).ToList<MapCategoryEntity>()    join u in list on mc.xxx equals u.xxx    select new GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput    {        xxx = mc.xxx,        xxx = ((u != null) ? new DateTime?(u.xxx) : null).GetValueOrDefault(DateTime.Now)    } into d    group d by d.MapId).Select(delegate(IGrouping<int, GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput> g)    {        GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput getUserLoginGameMapIdsDataDomainOutput = new GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput();        getUserLoginGameMapIdsDataDomainOutput.xxx = g.Key;        getUserLoginGameMapIdsDataDomainOutput.xxx = g.Max((GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput v) => v.xxxx);        return getUserLoginGameMapIdsDataDomainOutput;    });    return new GetUserLoginGameMapIdsDomainOutput    {        Data = source.ToList<GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput>()    };}

看的进去,这是一段EF读取DB的简单写法,敌人说这段代码波及到了多张表的关联操作,算是一个 资源密集型 的办法。

4. 到底持有什么大对象?

办法逻辑看完了,接下来看下 GetUserLoginGameMapIds() 办法到底调配了什么大对象触发了GC,能够探索下 50 线程的调用栈,应用 !clrstack -a 调出所有的 参数 + 部分 变量。

0:050> !clrstack -aOS Thread Id: 0x11a0 (50)Child SP       IP Call Site2501d350 7743c0bc [HelperMethodFrame: 2501d350] 2501d3dc 704fbab5 System.Collections.Generic.List`1[[System.__Canon, mscorlib]].set_Capacity(Int32)    PARAMETERS:        this (<CLR reg>) = 0x08053f6c        value = <no data>    LOCALS:        <no data>2501d3ec 704fba62 System.Collections.Generic.List`1[[System.__Canon, mscorlib]].EnsureCapacity(Int32)    PARAMETERS:        this = <no data>        min = <no data>    LOCALS:        <no data>2501d3f8 70516799 System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)    PARAMETERS:        this (<CLR reg>) = 0x08053f6c        item (<CLR reg>) = 0x2d7b07bc    LOCALS:        <no data>

从调用栈上看,因为 EF 的读取逻辑须要向 List 中增加一条记录刚好触发了List的扩容机制,就是因为这个扩容导致了GC大对象调配。

那怎么看呢? 很简略,先把 this (<CLR reg>) = 0x08053f6c 中地址拿进去do一下 !do 0x08053f6c 调出 List。

0:050> !do 0x08053f6cName:        System.Collections.Generic.List`1[[xxx.MapCategoryEntity, xxx.Entities]]MethodTable: 1e81eed0EEClass:     70219c7cSize:        24(0x18) bytesFile:        C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dllFields:      MT    Field   Offset                 Type VT     Attr    Value Name701546bc  40018a0        4     System.__Canon[]  0 instance 168792c0 _items701142a8  40018a1        c         System.Int32  1 instance    32768 _size701142a8  40018a2       10         System.Int32  1 instance    32768 _version70112734  40018a3        8        System.Object  0 instance 00000000 _syncRoot701546bc  40018a4        4     System.__Canon[]  0   static  <no information>

下面的 _size = 32768 看到了吗? 刚好是 2的15次方,因为再次新增必须要扩容,List 在底层需调配一个 System.__Canon[65536] 的数组来存储老内容,这个数组必定大于 85000byte 这个大对象的界定值啦。

如果有趣味,你能够看下 List 的扩容机制。

// System.Collections.Generic.List<T>private void EnsureCapacity(int min){    if (_items.Length < min)    {        int num = (_items.Length == 0) ? 4 : (_items.Length * 2);        if ((uint)num > 2146435071u)        {            num = 2146435071;        }        if (num < min)        {            num = min;        }        Capacity = num;    }}public int Capacity{    get    {        return _items.Length;    }    set    {        if (value < _size)        {            ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);        }        if (value == _items.Length)        {            return;        }        if (value > 0)        {            T[] array = new T[value];   //这里申请了一个 int[65536] 大小的数组            if (_size > 0)            {                Array.Copy(_items, 0, array, 0, _size);            }            _items = array;        }        else        {            _items = _emptyArray;        }    }}

三:总结

晓得了前因后果之后,大略提三点优化倡议。

  • 优化 GetUserLoginGameMapIds() 办法中的逻辑,这是最好的方法。
  • 从 dump 上看也就 4核4G 的小机器,晋升下机器配置,或者有点用。
0:017> !cpuidCP  F/M/S  Manufacturer     MHz 0  6,63,2  GenuineIntel    2295 1  6,63,2  GenuineIntel    2295 2  6,63,2  GenuineIntel    2295 3  6,63,2  GenuineIntel    2295 0:017> !address -summary--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotalPAGE_READWRITE                          878          1eccd000 ( 492.801 MB)  29.61%   12.03%
  • 没有非凡起因的话,用 64bit 来跑程序,突破 32bit 的 4G 空间限度,这样也能够让gc领有更大的堆调配空间。

参考网址:https://docs.microsoft.com/zh...

更多高质量干货:参见我的 GitHub: dotnetfly