主頁 > .NET開發 > 記一次 .NET游戲站程式的 CPU 爆高分析

記一次 .NET游戲站程式的 CPU 爆高分析

2021-04-22 06:00:31 .NET開發

一:背景

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 = https://www.cnblogs.com/huangxincheng/p/source.ToList()
	};
}

看的出來,這是一段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 = https://www.cnblogs.com/huangxincheng/p/
    LOCALS:
        

2501d3ec 704fba62 System.Collections.Generic.List`1[[System.__Canon, mscorlib]].EnsureCapacity(Int32)
    PARAMETERS:
        this = 
        min = 
    LOCALS:
        

2501d3f8 70516799 System.Collections.Generic.List`1[[System.__Canon, mscorlib]].Add(System.__Canon)
    PARAMETERS:
        this () = 0x08053f6c
        item () = 0x2d7b07bc
    LOCALS:
        

從呼叫堆疊上看,由于 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 =https://www.cnblogs.com/huangxincheng/p/= _items.Length)
		{
			return;
		}
		if (value > 0)
		{
			T[] array = new T[value];   //這里申請了一個 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

圖片名稱

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/278727.html

標籤:C#

上一篇:我想用行內代碼寫進ASPX里,查資料說<%%>里不能寫方法,有現成代碼該如何拆分

下一篇:使用 EPPlus 封裝的 excel 表格匯入功能 (二) delegate 委托 --永遠滴神

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more