记一次 .NET游戏站程序的 CPU 爆高分析
一:背景
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 爆高分析的更多相关文章
- 记一次 .NET 某市附属医院 Web程序 偶发性CPU爆高分析
一:背景 1. 讲故事 这个月初,一位朋友加微信求助他的程序出现了 CPU 偶发性爆高,希望能有偿解决一下. 从描述看,这个问题应该困扰了很久,还是医院的朋友给力,开门就是 100块 红包 ,那既然是 ...
- 记一次 .NET 某医院HIS系统 CPU爆高分析
一:背景 1. 讲故事 前几天有位朋友加 wx 抱怨他的程序在高峰期总是莫名其妙的cpu爆高,求助如何分析? 和这位朋友沟通下来,据说这问题困扰了他们几年,还请了微软的工程师过来解决,无疾而终,应该还 ...
- 记一次 .NET 某电商交易平台Web站 CPU爆高分析
一:背景 1. 讲故事 已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > ...
- 记一次 .NET 某智慧物流 WCS系统 CPU 爆高分析
一:背景 1. 讲故事 哈哈,再次见到物流类软件,上个月有位朋友找到我,说他的程序出现了 CPU 爆高,让我帮忙看下什么原因,由于那段时间在苦心研究 C++,分析和经验分享也就懈怠了,今天就给大家安排 ...
- 记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析
一:背景 1. 讲故事 这是6月中旬一位朋友加wx求助dump的故事,他的程序 cpu爆高UI卡死,问如何解决,截图如下: 在拿到这个dump后,我发现这是一个关于机械臂的MRS程序,哈哈,在机械臂这 ...
- 记一次 .NET 某旅行社Web站 CPU爆高分析
一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序内存经常飙升,cpu 偶尔飙升,没找到原因,希望帮忙看一下. 可惜发过来的 dump 只有区区2G,能在这里面找到内存泄漏那真有两把刷子..., ...
- 记一次 .NET 车联网云端服务 CPU爆高分析
一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序CPU经常飙满,没找到原因,希望帮忙看一下. 这些天连续接到几个cpu爆高的dump,都看烦了,希望后面再来几个其他方面的dump,从沟通上看, ...
- 记一次 .NET 某智能交通后台服务 CPU爆高分析
一:背景 1. 讲故事 前天有位朋友加微信求助他的程序出现了CPU爆高的问题,开局就是一个红包,把我吓懵了! 由于是南方小年,我在老家张罗处理起来不方便,没有第一时间帮他处理,朋友在第二天上午已经找出 ...
- 记一次 .NET 某供应链WEB网站 CPU 爆高事故分析
一:背景 1. 讲故事 年前有位朋友加微信求助,说他的程序出现了偶发性CPU爆高,寻求如何解决,截图如下: 我建议朋友用 procdump 在 cpu 高的时候连抓两个dump,这样分析起来比较稳健, ...
随机推荐
- 记录PyQt5 学习中遇到的一些问题
1 信号与槽的设置中,槽函数不用写括号: btn.clicked.connect(cao()) def cao(): ******** 会报错:argument 1 has unexpected ...
- hive分区分桶
目录 1.分区 1.1.静态分区 1.1.1.一个分区 1.1.2.多个分区 1.2.动态分区 2.分桶 1.分区 如果一个表中数据很多,我们查询时就很慢,耗费大量时间,如果要查询其中部分数据该怎么办 ...
- vue 递归调用组件出错
报错信息: Avoid mutating an injected value directly since the changes will be overwritten whenever the p ...
- jquery通过live绑定toggle事件
$("a[name=reply]").live("click",function(){ $(this).toggle( function () { var $c ...
- 前端学习 node 快速入门 系列 —— 初步认识 node
其他章节请看: 前端学习 node 快速入门 系列 初步认识 node node 是什么 node(或者称node.js)是 javaScript(以下简称js) 运行时的一个环境.不是一门语言. 以 ...
- 订单退款&重复支付需求疑问点归纳整理
更新历史记录: 更新内容 更新人 更新时间 新建 Young 2020.12.10 16:45 更新产品疑问解答 Young 2020.12.11 10:14 更新退款权益终止时间 Young 2 ...
- 【odoo14】第二章、管理odoo实例
本章主要介绍肖odoo实例添加用户自定义的模块.你可以从多个路径载入模块.但是建议你将自己的模块儿放在特定的目录当中,避免与odoo的核心模块混淆. 在这一章节,中我们主要涉及以下内容: 配置插件路径 ...
- string与bson.ObjectId之间格式转换
string转bson.ObjectId bson.ObjectIdHex(string) bson.ObjectId转string日后再补
- flutter简易教程
跟Java等很多语言不同的是,Dart没有public protected private等关键字,如果某个变量以下划线 _ 开头,代表这个变量在库中是私有的.Dart中变量可以以字母或下划线开头,后 ...
- Hznu_oj 2340 你敢一个人过桥吗?
Description 在漆黑的夜里,N位旅行者来到了一座狭窄而且没有护栏的桥边.如果不借助手电筒的话,大家是无论如何也不敢过桥去的.不幸的是,N个人一共只带了一只手电筒,而桥窄得只够让两个人同时过. ...