一:背景

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> lmv
start end module name
00b80000 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.exe
71510000 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.GetUserLoginGameMapIds
Module: 1c870580
Assembly: xxx.dll
Token: 0600000b
MethodDesc: 1c877504
Name: xxx.GetUserLoginGameMapIds(xxx.GetUserLoginGameMapIdsDomainInput)
JITTED Code Address: 1d5a2030
0:050> !savemodule 1c870580 E:\dumps\6.dll
3 sections in file
section 0 - VA=2000, VASize=112b8, FileAddr=200, FileSize=11400
section 1 - VA=14000, VASize=3c8, FileAddr=11600, FileSize=400
section 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 -a
OS Thread Id: 0x11a0 (50)
Child SP IP Call Site
2501d350 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 0x08053f6c
Name: System.Collections.Generic.List`1[[xxx.MapCategoryEntity, xxx.Entities]]
MethodTable: 1e81eed0
EEClass: 70219c7c
Size: 24(0x18) bytes
File: C:\Windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
701546bc 40018a0 4 System.__Canon[] 0 instance 168792c0 _items
701142a8 40018a1 c System.Int32 1 instance 32768 _size
701142a8 40018a2 10 System.Int32 1 instance 32768 _version
70112734 40018a3 8 System.Object 0 instance 00000000 _syncRoot
701546bc 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> !cpuid
CP 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 %ofTotal
PAGE_READWRITE 878 1eccd000 ( 492.801 MB) 29.61% 12.03%
  • 没有特殊原因的话,用 64bit 来跑程序,打破 32bit 的 4G 空间限制,这样也可以让gc拥有更大的堆分配空间。

参考网址:https://docs.microsoft.com/zh-cn/dotnet/standard/garbage-collection/fundamentals

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

记一次 .NET游戏站程序的 CPU 爆高分析的更多相关文章

  1. 记一次 .NET 某市附属医院 Web程序 偶发性CPU爆高分析

    一:背景 1. 讲故事 这个月初,一位朋友加微信求助他的程序出现了 CPU 偶发性爆高,希望能有偿解决一下. 从描述看,这个问题应该困扰了很久,还是医院的朋友给力,开门就是 100块 红包 ,那既然是 ...

  2. 记一次 .NET 某医院HIS系统 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友加 wx 抱怨他的程序在高峰期总是莫名其妙的cpu爆高,求助如何分析? 和这位朋友沟通下来,据说这问题困扰了他们几年,还请了微软的工程师过来解决,无疾而终,应该还 ...

  3. 记一次 .NET 某电商交易平台Web站 CPU爆高分析

    一:背景 1. 讲故事 已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > ...

  4. 记一次 .NET 某智慧物流 WCS系统 CPU 爆高分析

    一:背景 1. 讲故事 哈哈,再次见到物流类软件,上个月有位朋友找到我,说他的程序出现了 CPU 爆高,让我帮忙看下什么原因,由于那段时间在苦心研究 C++,分析和经验分享也就懈怠了,今天就给大家安排 ...

  5. 记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析

    一:背景 1. 讲故事 这是6月中旬一位朋友加wx求助dump的故事,他的程序 cpu爆高UI卡死,问如何解决,截图如下: 在拿到这个dump后,我发现这是一个关于机械臂的MRS程序,哈哈,在机械臂这 ...

  6. 记一次 .NET 某旅行社Web站 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序内存经常飙升,cpu 偶尔飙升,没找到原因,希望帮忙看一下. 可惜发过来的 dump 只有区区2G,能在这里面找到内存泄漏那真有两把刷子..., ...

  7. 记一次 .NET 车联网云端服务 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序CPU经常飙满,没找到原因,希望帮忙看一下. 这些天连续接到几个cpu爆高的dump,都看烦了,希望后面再来几个其他方面的dump,从沟通上看, ...

  8. 记一次 .NET 某智能交通后台服务 CPU爆高分析

    一:背景 1. 讲故事 前天有位朋友加微信求助他的程序出现了CPU爆高的问题,开局就是一个红包,把我吓懵了! 由于是南方小年,我在老家张罗处理起来不方便,没有第一时间帮他处理,朋友在第二天上午已经找出 ...

  9. 记一次 .NET 某供应链WEB网站 CPU 爆高事故分析

    一:背景 1. 讲故事 年前有位朋友加微信求助,说他的程序出现了偶发性CPU爆高,寻求如何解决,截图如下: 我建议朋友用 procdump 在 cpu 高的时候连抓两个dump,这样分析起来比较稳健, ...

随机推荐

  1. Javascript中的事件对象和事件类型

    接上次看JS的事件冒泡和捕获,所以顺带就把事件相关的知识都看完好了 而且想到一个好的学习方法,第一天自己看,第二天把前一天学习的东西写下来,一方面可以当复习,一方面当重新整理并且分享 事件对象 事件处 ...

  2. svn报错Previous operation has not finished; run 'cleanup' if it was interrupted

  3. Docker的架构

    一.Docker引擎 docker引擎是一个c/s结构的应用,主要组件见下图: Server是一个常驻进程 REST API 实现了client和server间的交互协议 CLI 实现容器和镜像的管理 ...

  4. 第47天学习打卡(HTML)

    什么是HTML HTML Hyper Text Markup Language(超文本标记语言) 超文本包括:文字,图片,音频,视频,动画等 HTML5,提供了一些新的元素和一些有趣的新特性,同时也建 ...

  5. How DRI and DRM Work

    How DRI and DRM Work Introduction This page is intended as an introduction to what DRI and DRM are, ...

  6. pytorch(05)计算图

    张量的一系列操作,增多,导致可能出现多个操作之间的串行并行,协同不同的底层之间的协作,避免操作的冗余.计算图就是为了解决这些问题产生的. 计算图与动态图机制 1. 计算图 计算图用来描述运算的有向无环 ...

  7. Java基础:常用基础dos命令

    打开cmd的方式1.开始+系统+命令提示符2.win键+R 输入cmd 打开控制台3.在任意的文件夹下,按住shift键+鼠标右键点击,在此处打开命令提示行4.在资源管理器的地址栏前面加上cmd路径 ...

  8. 任务3 PHP配置 1. PHP基础配置

    查看PHP配置文件得位置 #/ucsr/local/php/bin/php -i |grep -i "loaded configuration file" # cp /usr/lo ...

  9. css整理之-----------基本知识

    盒子模型 所有HTML元素可以看作盒子,它包括:边距,边框,填充,和实际内容. CSS中组成一个块级盒子需要: Content box: 这个区域是用来显示内容,大小可以通过设置 width 和 he ...

  10. 2019 南京网络赛 B super_log 【递归欧拉降幂】

    一.题目 super_log 二.分析 公式很好推出来,就是$$a^{a^{a^{a^{...}}}}$$一共是$b$个$a$. 对于上式,由于指数太大,需要降幂,这里需要用到扩展欧拉定理: 用这个定 ...