记一次 .NET 某安全生产信息系统 CPU爆高分析
一:背景
1.讲故事
今天是的第四天,头终于不巨疼了,写文章已经没什么问题,赶紧爬起来写。
这个月初有位朋友找到我,说他的程序出现了CPU爆高,让我帮忙看下怎么回事,简单分析了下有两点比较有意思。
这是一个安全生产的信息管理平台,第一次听说,我的格局小了。
这是一个经典的 CPU 爆高问题,过往虽有分析,但没有刨根问底,刚好这一篇就来问一下底吧。
话不多说,我们上 WinDbg 说话。
二:WinDbg 分析
1. 真的 CPU 爆高吗?
别人说爆高不算,我们得拿数据说话不是,验证命令就是 !tp
。
0:085> !tp
CPU utilization: 100%
Worker Thread: Total: 40 Running: 26 Idle: 6 MaxLimit: 32767 MinLimit: 8
Work Request in Queue: 0
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 1 Free: 1 MaxFree: 16 CurrentLimit: 1 MaxLimit: 1000 MinLimit: 8
从卦中看果然是被打满了,接下来可以用 ~*e !clrstack
观察各个线程都在做什么,稍微一观察就会发现有很多的线程卡在 FindEntry()
方法上,截图如下:
从图中可以看到,有 25 个线程都停在 FindEntry()
之上,如果你的经验比较丰富的话,我相信你马上就知道这是多线程环境下使用了非线程安全集合 Dictionary
造成的死循环,把 CPU 直接打爆。
按以往套路到这里就结束了,今天我们一定要刨到底。
2. 为什么会出现死循环
要知道死循环的成因,那就一定要从 FindEntry
上入手。
private int FindEntry(TKey key)
{
if (key == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}
if (buckets != null)
{
int num = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int num2 = buckets[num % buckets.Length]; num2 >= 0; num2 = entries[num2].next)
{
if (entries[num2].hashCode == num && comparer.Equals(entries[num2].key, key))
{
return num2;
}
}
}
return -1;
}
仔细观察上面的代码,如果真有死循环肯定是在 for
中出不来,如果是真的出在 for
上,那问题自然在 next
指针上。
关于 Dictionary 的内部布局和解析 可以参见我的 高级调试训练营,这里我们就不细说了。
那是不是出在 next 指针上呢? 我们来剖析下方法上下文。
3. 观察 next 指针布局
为了方便观察,先切到 85
号线程。
0:085> ~85s
mscorlib_ni!System.Collections.Generic.Dictionary<string,F2.xxx.ORM.SqlEntity>.FindEntry+0x8f:
00007ff8`5f128ccf 488b4e10 mov rcx,qword ptr [rsi+10h] ds:0000017f`39c07d00=0000017eb9ee00c0
0:085> !clrstack
OS Thread Id: 0x4124 (85)
Child SP IP Call Site
0000007354ebcc70 00007ff85f128ccf System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon) [f:\dd\ndp\clr\src\BCL\system\collections\generic\dictionary.cs @ 305]
接下来把 Dictionary
中的 Entry[]
中的 next 给展示出来,可以用 !mdso
命令。
0:085> !mdso
Thread 85:
Location Object Type
------------------------------------------------------------
RCX: 0000017eb9ee00c0 System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[xx]][]
RSI: 0000017f39c07cf0 System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[xxx.xxx]]
0:085> !mdt -e:2 0000017eb9ee00c0
0000017eb9ee00c0 (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[xxx.xxx]][], Elements: 3, ElementMT=00007ff816cedc18)
[0] (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[F2.xxx]]) VALTYPE (MT=00007ff816cedc18, ADDR=0000017eb9ee00d0)
hashCode:0x0 (System.Int32)
next:0x0 (System.Int32)
key:NULL (System.__Canon)
value:NULL (System.__Canon)
[1] (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[F2.xxx]]) VALTYPE (MT=00007ff816cedc18, ADDR=0000017eb9ee00e8)
hashCode:0x5aba4760 (System.Int32)
next:0xffffffff (System.Int32)
key:0000017f39c0ab50 (System.String) Length=20, String="xxxMessage_Select"
value:0000017f39c0b5d0 (xxx.xxx.ORM.SqlEntity)
[2] (System.Collections.Generic.Dictionary`2+Entry[[System.String, mscorlib],[F2.xxx]]) VALTYPE (MT=00007ff816cedc18, ADDR=0000017eb9ee0100)
hashCode:0x65b6e27b (System.Int32)
next:0x1 (System.Int32)
key:0000017f39c09d58 (System.String) Length=20, String="xxxMessage_Insert"
value:0000017f39c0ba50 (xxx.xxx.ORM.SqlEntity)
从卦中看也蛮奇葩的,只有三个元素的 Dictionary
还能死循环。。。如果你仔细观察会发现 [0]
项是一种有损状态,value 没值不说, next:0x0
可是有大问题的,它会永远指向自己,因为 next 是指向 hash 挂链中的下一个节点的数组下标,画个图大概是这样。
接下来我们验证下是不是入口参数不幸进入了 [0]
号坑,然后在这个坑中永远指向自己呢?要想寻找答案,只需要在 FindEntry
的汇编代码中找到 int num = comparer.GetHashCode(key) & 0x7FFFFFFF;
中的 num 值,看它是不是 0 即可。
0:085> !U /d 00007ff85f128ccf
preJIT generated code
System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].FindEntry(System.__Canon)
Begin 00007ff85f128c40, size 130. Cold region begin 00007ff85ff07ff0, size 11
...
f:\dd\ndp\clr\src\BCL\system\collections\generic\dictionary.cs @ 303:
00007ff8`5f128c6f 488b5e18 mov rbx,qword ptr [rsi+18h]
00007ff8`5f128c73 488b0e mov rcx,qword ptr [rsi]
00007ff8`5f128c76 488b5130 mov rdx,qword ptr [rcx+30h]
00007ff8`5f128c7a 488b2a mov rbp,qword ptr [rdx]
00007ff8`5f128c7d 4c8b5d18 mov r11,qword ptr [rbp+18h]
00007ff8`5f128c81 4d85db test r11,r11
00007ff8`5f128c84 750f jne mscorlib_ni!System.Collections.Generic.Dictionary<string,xxx.SqlEntity>.FindEntry+0x55 (00007ff8`5f128c95)
00007ff8`5f128c86 488d154d2f1800 lea rdx,[mscorlib_ni+0x68bbda (00007ff8`5f2abbda)]
00007ff8`5f128c8d e8ce44f3ff call mscorlib_ni+0x43d160 (00007ff8`5f05d160) (mscorlib_ni)
00007ff8`5f128c92 4c8bd8 mov r11,rax
00007ff8`5f128c95 488bcb mov rcx,rbx
00007ff8`5f128c98 488bd7 mov rdx,rdi
00007ff8`5f128c9b 3909 cmp dword ptr [rcx],ecx
00007ff8`5f128c9d 41ff13 call qword ptr [r11]
00007ff8`5f128ca0 8bd8 mov ebx,eax
00007ff8`5f128ca2 81e3ffffff7f and ebx,7FFFFFFFh
...
0:085> ? ebx
Evaluate expression: 957083499 = 00000000`390bef6b
0:085> ? 0n957083499 % 0n3
Evaluate expression: 0 = 00000000`00000000
从汇编代码中分析得出,num 是放在 ebx
寄存器上,此时 num=957083499
,再 %3
之后就是 0 号坑,大家再结合源代码,你会发现这里永远都不会退出,永远都是指向自己,自然就是死循环了。
3. .NET6 下的补充
前段时间在整理课件时发现在 .NET6 中不再傻傻的死循环,而是在尝试 entries.Length
次之后还得不到结束的话,强制抛出异常,代码如下:
internal ref TValue FindValue(TKey key)
{
uint hashCode2 = (uint)comparer.GetHashCode(key);
int bucket2 = GetBucket(hashCode2);
Entry[] entries2 = _entries;
uint num2 = 0u;
bucket2--;
while ((uint)bucket2 < (uint)entries2.Length)
{
reference = ref entries2[bucket2];
if (reference.hashCode != hashCode2 || !comparer.Equals(reference.key, key))
{
bucket2 = reference.next;
num2++;
if (num2 <= (uint)entries2.Length)
{
continue;
}
goto IL_0171;
}
goto IL_0176;
}
return ref Unsafe.NullRef<TValue>();
IL_0176:
return ref reference.value;
IL_0171:
ThrowHelper.ThrowInvalidOperationException_ConcurrentOperationsNotSupported();
goto IL_0176;
}
可能是 .NET团队 被这样的问题咨询烦了,干脆抛一个异常得了。。。
三: 总结
多线程环境下使用线程不安全集合,问题虽然很小白,但还是有很多朋友栽在这上面,值得反思哈,借这一次机会进一步解释下死循环形成的内部机理。
记一次 .NET 某安全生产信息系统 CPU爆高分析的更多相关文章
- 记一次 .NET 车联网云端服务 CPU爆高分析
一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序CPU经常飙满,没找到原因,希望帮忙看一下. 这些天连续接到几个cpu爆高的dump,都看烦了,希望后面再来几个其他方面的dump,从沟通上看, ...
- 记一次 .NET 差旅管理后台 CPU 爆高分析
一:背景 1. 讲故事 前段时间有位朋友在微信上找到我,说他的 web 系统 cpu 运行一段时候后就爆高了,让我帮忙看一下是怎么回事,那就看吧,声明一下,我看 dump 是免费的,主要是锤炼自己技术 ...
- 记一次 .NET 某电子病历 CPU 爆高分析
一:背景 1.讲故事 前段时间有位朋友微信找到我,说他的程序出现了 CPU 爆高,帮忙看下程序到底出了什么情况?图就不上了,我们直接进入主题. 二:WinDbg 分析 1. CPU 真的爆高吗? 要确 ...
- 记一次 .NET 某资讯论坛 CPU爆高分析
大概有11天没发文了,真的不是因为懒,本想前几天抽空写,不知道为啥最近求助的朋友比较多,一天都能拿到2-3个求助dump,晚上回来就是一顿分析,有点意思的是大多朋友自己都分析了几遍或者公司多年的牛皮藓 ...
- 记一次 .NET 某电商交易平台Web站 CPU爆高分析
一:背景 1. 讲故事 已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > ...
- 记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析
一:背景 1. 讲故事 这是6月中旬一位朋友加wx求助dump的故事,他的程序 cpu爆高UI卡死,问如何解决,截图如下: 在拿到这个dump后,我发现这是一个关于机械臂的MRS程序,哈哈,在机械臂这 ...
- 记一次 .NET游戏站程序的 CPU 爆高分析
一:背景 1. 讲故事 上个月有个老朋友找到我,说他的站点晚高峰 CPU 会突然爆高,发了两份 dump 文件过来,如下图: 又是经典的 CPU 爆高问题,到目前为止,对这种我还是有一些经验可循的. ...
- 记一次 .NET 某医院HIS系统 CPU爆高分析
一:背景 1. 讲故事 前几天有位朋友加 wx 抱怨他的程序在高峰期总是莫名其妙的cpu爆高,求助如何分析? 和这位朋友沟通下来,据说这问题困扰了他们几年,还请了微软的工程师过来解决,无疾而终,应该还 ...
- 记一次 .NET 某旅行社Web站 CPU爆高分析
一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序内存经常飙升,cpu 偶尔飙升,没找到原因,希望帮忙看一下. 可惜发过来的 dump 只有区区2G,能在这里面找到内存泄漏那真有两把刷子..., ...
- 记一次 .NET 某市附属医院 Web程序 偶发性CPU爆高分析
一:背景 1. 讲故事 这个月初,一位朋友加微信求助他的程序出现了 CPU 偶发性爆高,希望能有偿解决一下. 从描述看,这个问题应该困扰了很久,还是医院的朋友给力,开门就是 100块 红包 ,那既然是 ...
随机推荐
- 2>&1到底是什么意思?
java -jar snapshot.jar > snapshot.log 2>&1 & 写Java的朋友一定对上面的命令很熟悉,相信大部分人都知道>表示的是重定向, ...
- PAT (Basic Level) Practice 1019 数字黑洞 分数 20
给定任一个各位数字不完全相同的 4 位正整数,如果我们先把 4 个数字按非递增排序,再按非递减排序,然后用第 1 个数字减第 2 个数字,将得到一个新的数字.一直重复这样做,我们很快会停在有" ...
- Kafka 之 Streams
Kafka 之 Streams 一.概述 1.1 Kafka Streams Kafka Streams.Apache Kafka开源项目的一个组成部分.是一个功能强大,易于使用的库.用于在Kafka ...
- 2.MongoDB系列之创建更新删除文档
1. 插入文档 // 单条插入 db.getCollection('blog').insertOne({'type': 'mongodb'}) // 批量 插入 db.getCollection('b ...
- 齐博x1细节优化,自定义二、三、四维字段支持自定描述
如下图所示,之前自定义字估中的二.三.四维字段,不支持自定义描述,导致用户输入的时候,不知道该输入什么信息内容.只有站长自己才知道. 现在支持自定义描述,及设置文本或数字.方便引导用户输入相应的信息内 ...
- json文本数据
本文主要针对三个问题:json格式数据,text数据与json数据之间的关系,json和python字典的区别 1.什么是json数据? json是文本数据,可以在网络中传输的通用数据,它是具有特定格 ...
- 基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理
在一个应用系统的开发框架中,往往很多地方需要用到缓存的处理,有些地方是为了便于记录用户的数据,有些地方是为了提高系统的响应速度,如有时候我们在发送一个短信验证码的时候,可以在缓存中设置几分钟的过期时间 ...
- 题解 AT2361 [AGC012A] AtCoder Group Contest
\(\sf{Solution}\) 显然要用到贪心的思想. 既然最终的结果只与每组强度第二大选手有关,那就考虑如何让他的值尽可能大. 其实,从小到大排个序就能解决,越靠后的值越大,使得每组强度第二大选 ...
- 云原生之旅 - 5)Kubernetes时代的包管理工具 Helm
前言 上一篇文章 [基础设施即代码 使用 Terraform 创建 Kubernetes] 教会了你如何在Cloud上面建Kubernetes资源,那么本篇来讲一下如何在Kubernetes上面部署应 ...
- 第一百零六篇:变量的不同声明(var,let和const的不同)
好家伙,JS基础接着学, 本篇内容为<JS高级程序设计>第三章学习笔记 1.变量 ECMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据. (确实松散,不像C或C++ ...