记一次 .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.pdbLoaded symbol image file: w3wp.exeImage path: C:\Windows\SysWOW64\inetsrv\w3wp.exeImage name: w3wp.exe
71510000 71cc0000   clr        (pdb symbols)          c:\mysymbols\clr.pdb\9B2B2A02EC2D43899F87AC20F11B82DF2\clr.pdbLoaded symbol image file: clr.dllImage path: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dllImage name: clr.dllBrowse all global symbols  functions  dataTimestamp:        Thu Sep  3 03:30:58 2020 (5F4FF2F2)CheckSum:         007AC92BImageSize:        007B0000File version:     4.8.4261.0Product 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 ps in file
p 0 - VA=2000, VASize=112b8, FileAddr=200, FileSize=11400
p 1 - VA=14000, VASize=3c8, FileAddr=11600, FileSize=400
p 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.xxxselect new GetUserLoginGameMapIdsDomainOutput.GetUserLoginGameMapIdsDataDomainOutput{xxx = mc.xxx,xxx = ((u != null) ? new DateTime?(u.xxx) : null).GetValueOrDefault(DateTime.Now)} into dgroup 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>) = 0x08053f6cvalue = <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>) = 0x08053f6citem (<CLR reg>) = 0x2d7b07bcLOCALS:<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     MHz0  6,63,2  GenuineIntel    22951  6,63,2  GenuineIntel    22952  6,63,2  GenuineIntel    22953  6,63,2  GenuineIntel    22950: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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/303053.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

干货 | 数据挖掘过关40题

今天为大家准备了40道数据挖掘的题&#xff0c;试试你的能力吧&#xff01;答案请见文末1.某超市研究销售纪录数据后发现&#xff0c;买啤酒的人很大概率也会购买尿布&#xff0c;这种属于数据挖掘的哪类问题&#xff1f;A.关联规则发现B.聚类C.分类D.自然语言处理2.以下两种描…

.Net程序内存泄漏解析

↑↑↑ 点击左上角蓝字关注我&#xff0c;为您提供技术新动态。本期内容分享实战中内存泄漏解决思路。Part1 初步分析原因Part2 查找内存泄漏的根本原因Part2.1 解决方案Part3 总结Part4 彩蛋一、概要大概在今年三月份的时候突然被紧急调到另外一个项目组解决线上内存泄漏问题。…

js 上下箭头滚动_JS中的this完全讲解,再也不会被this搞晕了

关于This对象js 中的this 是一个比较难理解的对象&#xff1b;所以也经常作为面试的考点&#xff0c;考察应聘者的js 基础能力&#xff1b;其实this的指向也就那么几种情况&#xff0c;接下来我们一一看一下&#xff1a;函数中的this取何值是在函数真正被调用时确定的(也就是运…

9张图,Kafka为什么要放弃Zookeeper

最近&#xff0c;confluent社区发表了一篇文章&#xff0c;主要讲述了Kafka未来的2.8版本将要放弃Zookeeper&#xff0c;这对于Kafka用户来说&#xff0c;是一个重要的改进。之前部署Kafka就必须得部署Zookeeper&#xff0c;而之后就只要单独部署Kafka就行了。[1]1.Kafka简介Ap…

Python能用来做什么?以下是Python的三大主要用途

如果你想学Python&#xff0c;或者你刚开始学习Python&#xff0c;那么你可能会问&#xff1a;“我能用Python做什么&#xff1f;”这个问题不好回答&#xff0c;因为Python有很多用途。但是随着时间&#xff0c;我发现有Python主要有以下三大主要应用&#xff1a; Web开发 数据…

qbuttongroup如何都不选中_全程马拉松,半程马拉松该如何跑?很多人都不知道这些细节...

现在已经到12月份了&#xff0c;全国各地的马拉松也接近尾声&#xff0c;回想自己这一年参加了多少次全程马拉松或半程马拉松&#xff1f;很多人跑步最初的宗旨是为了减肥、为了健康&#xff0c;为了让自己拥有一个好身体&#xff0c;可是不知不觉中在朋友又或自己的兴趣中“投…

通过Dapr实现一个简单的基于.net的微服务电商系统(五)——一步一步教你如何撸Dapr之状态管理...

状态管理和上一章的订阅发布都算是Dapr相较于其他服务网格框架来讲提供的比较特异性的内容&#xff0c;今天我们来讲讲状态管理。目录&#xff1a;一、通过Dapr实现一个简单的基于.net的微服务电商系统二、通过Dapr实现一个简单的基于.net的微服务电商系统(二)——通讯框架讲解…

过Div将页面分三块(上,中,下),然后通过Ext来改变Content的内容(三)--终结版

2019独角兽企业重金招聘Python工程师标准>>> 这几天的页面重构终于快要结束了。 最终页面架构选择了Sitemesh&#xff0c;经过昨天下午及今天上午的调试终于把Sitemesh下css、js问题解决了。 使用Sitemesh总结以下几点&#xff1a; 1、<decorator:head />放在…

Python+人工智能的超强组合,再不学就跟不上时代啦!

《Python人工智能》原价 899.00 现超 410 人参团仅售 199.00 点击文末阅读原文立即参团参团&#xff0c;咨询&#xff0c;查看课程&#xff0c;请点击【阅读原文】↓↓↓

如何证明服从卡方分布_谈谈抽样分布定理

各位阿娜答&#xff0c;这个月就更新了一篇文章&#xff0c;这都月底了&#xff0c;还有两次自荐机会没用&#xff0c;所以最后这几天要更两篇文章&#xff0c;大家敬请期待&#xff01;明明是夏天&#xff0c;但却是个多事之秋啊~(ง •_•)ง2020年注定是不平凡的一年&#x…

C#中ManualResetEvent用法简介

简单介绍多个线程可以通过调用ManualResetEvent对象的WaitOne方法进入等待或阻塞状态。当控制线程调用Set()方法&#xff0c;所有等待线程将恢复并继续执行。ManualResetEvent是如何工作的在内存中保持着一个bool值&#xff0c;如果bool值为False&#xff0c;则使所有线程阻塞&…

anaconda和python有什么不一样_黄山毛峰的味道为什么会不一样?

黄山毛峰&#xff0c;最早为人所熟知的黄山名茶&#xff0c;最经典的绿茶&#xff0c;特别是那种清香清爽的味道&#xff0c;很受茶友的喜爱。这两年经常接到茶友的电话问&#xff0c;为什么之次每次买的黄山毛峰都不一样&#xff0c;在不同的茶叶店&#xff0c;买的味道都不一…

Newbe.Claptrap 0.10.2 发布,Blazor 演示

Newbe.Claptrap 0.10.2 发布&#xff0c;我们为项目模板引入了 Minion 以及 Blazor 制作的交互界面。更新内容 类库常规升级升级了相关的所有类库至最新版本。包括 Dapr SDK 1.1 等等。项目模板增强现在&#xff0c;我们为最新的项目模板引入了 Minion 以演示如何使用 Minion 处…

Linux资料分享,强势来袭!

小编从大学开始&#xff0c;便开启资料收集功能。随着大数据时代的来临&#xff0c;计算机发展进入新的阶段&#xff0c;再加上日常的深入研究&#xff0c;小编收集整理了丰富的Linux资料&#xff0c;内容涵盖“集群类”&#xff0c;“监控类”、“编程类”“系统类”等。这次小…

es6 ie不兼容 函数_ES6:什么是块级作用域?

在 ES5 只有全局作用域和函数作用域&#xff0c;没有块级作用域&#xff0c;这带来很多不合理的场景。我们先来看一下下面这种情况&#xff1a;内层变量可能会覆盖外层变量。var txt 外层变量-->你好呀;function fn() {console.log(txt);if (false) {var txt 内层变量--&g…

一个简单的规则引擎例子

本例使用的规则引擎包是RulesEngine&#xff0c;关于RulesEngine的介绍&#xff0c;请自行补充&#xff0c;这里只是一个极简单使用场景。例子大体就是用户有一些优惠券&#xff0c;系统会根据用户订单情况&#xff0c;筛选可以使用的优惠券供用户选择&#xff0c;用户选择后会…

程序员面试必备的20条Python经典面试题

1、Python如何实现单例模式&#xff1f;Python有两种方式可以实现单例模式&#xff0c;下面两个例子使用了不同的方式实现单例模式&#xff1a;1.class Singleton(type):def __init__(cls, name, bases, dict):super(Singleton, cls).__init__(name, bases, dict)cls.instance …

Active Directory系列之十七:实战详解域信任关系

实战详解域信任关系上篇博文中我们对域信任关系作了一下概述&#xff0c;本文中我们将通过一个实例为大家介绍如何创建域信任关系。拓扑如下图所示&#xff0c;当前网络中有两个域&#xff0c;一个域是ITET.COM&#xff0c;另一个域是HOMEWAY.COM。两个域内各有一个域控制器&am…

vim 寄存器 操作_vim指令

vim 是一个基于【动词】 【名词】 建立的语法表。Vim 中常用的名词方位名词表基于字符的移动&#xff1a;h&#xff1a;左j&#xff1a;下k&#xff1a;上l&#xff1a;右 ^ | k 提示&#xff1a; h 的键位于左边&#xff0c;每次按下就会向…

35岁老程序员因身体原因没加班,老板:不想干就滚蛋

近日&#xff0c;某论坛上一名 35 岁老程序员说出了他最近的遭遇&#xff0c;高强度的工作本来就让他感觉越来越劳累&#xff0c;加上又在医院做了一个小手术&#xff0c;于是就按照公司的正常上下班时间下了班&#xff0c;但是领导却说了一句让他难以接受的话&#xff0c;都三…