记一次 .NET 某安全生产信息系统 CPU爆高分析

news2025/1/12 20:49:09

一:背景

1.讲故事

今天是🐏的第四天,头终于不巨疼了,写文章已经没什么问题,赶紧爬起来写。

这个月初有位朋友找到我,说他的程序出现了CPU爆高,让我帮忙看下怎么回事,简单分析了下有两点比较有意思。

  1. 这是一个安全生产的信息管理平台,第一次听说,我的格局小了。

  2. 这是一个经典的 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团队 被这样的问题咨询烦了,干脆抛一个异常得了。。。

三: 总结

多线程环境下使用线程不安全集合,问题虽然很小白,但还是有很多朋友栽在这上面,值得反思哈,借这一次机会进一步解释下死循环形成的内部机理。

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

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

相关文章

JaveWeb框架(三):JavaWeb项目实战 基于Servlet 实现系统登录注册功能

MVC实战项目 仓储管理系统需求&#xff1a;实现基本的登录和注册功能MVC实战项目&#xff1a;登录和注册登录功能实现注册功能实现总结Redis章节复习已经过去&#xff0c;新的章节JavaWeb开始了&#xff0c;这个章节中将会回顾JavaWeb实战项目 公司管理系统部分功能 代码会同步…

软件测试线上故障规范及模板,希望能帮到大家

目录 前言 线上故障规范及模板 [NOF-32] 全平台所有业务下单后支付异常&#xff0c;无法调起支付 创建: XX年/XX月/XX日 更新: XX年/XX月/XX日 解决: XX年/XX月/XX日 总结 前言 对于每一个测试人员来说&#xff0c;软件测试过程中有一个四字成语&#xff0c;真的是如噩梦一…

通用springboot框架

前言 到现在已经学习并工作了许久&#xff0c;于是打算弄一个通用的springboot框架。 这个框架&#xff0c;哪怕是你到正式工作的时候&#xff0c;也是能用上的&#xff0c;也不会给人感觉特别的low 那么&#xff0c;本项目的git我会放在结尾 接下来我来具体说一下该通用的spr…

谷粒商城技术栈总结

文章目录谷粒商城ElasticSearch一、基本概念1、Index&#xff08;索引&#xff09;2、Type&#xff08;类型&#xff09;3、Document&#xff08;文档&#xff09;4、倒排索引机制二、Docker 安装 Es1、下载镜像文件2、创建实例三、初步检索1、_cat2、索引一个文档&#xff08;…

vue - vue中的process.env.NODE_ENV讲解

vue中process.env讲解&#xff1a; 1&#xff0c;什么是process.env process.env 是 Node.js 中的一个环境对象。其中保存着系统的环境的变量信息。可使用 Node.js 命令行工具直接进行查看。如下&#xff1a; 而 NODE_ENV 就是其中的一个环境变量。这个变量主要用于标识当前的环…

【微服务】Nacos 如何做到⼀致性协议下沉的与自研 Distro 协议

目录 一、⼀致性协议下沉 1、⼀致性协议抽象 2、数据存储抽象 二、Nacos 自研 Distro 协议 1、背景 2、设计思想 2.1、数据初始化 2.2、数据校验 2.3、写操作 2.4、读操作 3、小结 一、⼀致性协议下沉 既然 Nacos 已经做到了将 AP、CP 协议下沉到了内核模块&#xff…

Hasoop实训2:实现课件分发

目录 1、准备工作 2、创建工作目录 3、上传课件压缩包 4、创建IP地址列表文件 5、创建脚本完成课件分发任务 6、总结 1、准备工作 在实训1&#xff1a;Hadoop实训1&#xff1a;Linux基本搭建和操作_open_test01的博客-CSDN博客​​​​​​ 中已经配置好了三台虚拟主机…

架构师必读 —— 逻辑模型(6)

横向思考与纵向思考 为了不陷入歪理之中&#xff0c;养成从宏观到微观的思考习惯极其重要。换句话说&#xff0c;就是“先横向思考&#xff0c;再纵向思考”。 横向思考是指&#xff0c;“广而浅地把握整体”。纵向思考是指&#xff0c; “针对某部分深入分析“。有了广泛而基本…

微服务框架 SpringCloud微服务架构 服务异步通讯 51 死信交换机 51.2 TTL

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 服务异步通讯 文章目录微服务框架服务异步通讯51 死信交换机51.2 TTL51.2.1 TTL51.2.2 总结51 死信交换机 51.2 TTL 51.2.1 TTL TTL&…

如何使用vs code远程连接服务器?如何免密登录?VSCode SSH

依旧是写在前面的废话环节 背景&#xff1a; 计算机专业。实验室电脑情况&#xff1a;两台服务器&#xff0c;一台配置3029ti&#xff0c;一台配置2080ti。深度学习训练跑代码用自己的电脑远程连接服务器&#xff0c;进行代码运行。 用到的软件&#xff1a; visual studio c…

vue3+vite+ts项目集成科大讯飞语音识别(项目搭建过程以及踩坑记录)

&#x1f431;个人主页&#xff1a;不叫猫先生 &#x1f64b;‍♂️作者简介&#xff1a;前端领域新星创作者、华为云享专家、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &#x1f4ab;系列专栏&#xff…

Web前端105天-day48-jQuery

jQuery01 目录 前言 一、复习 二、jQuery 三、css操作 四、点击事件 五、class 六、show_hide 七、标签栏切换 八、自定有动画 九、属性操作 十、大小图切换 总结 前言 jQuery01学习开始 一、复习 DOM: 文档 对象 模型 HTML代码 转换成 document 对象, 然后再渲染…

ospf,三层交换机,热备,以太网通道练习实验(含命令)

♥️作者&#xff1a;小刘在这里 ♥️每天分享云计算网络运维课堂笔记&#xff0c;疫情之下&#xff0c;你我素未谋面&#xff0c;但你一定要平平安安&#xff0c;一 起努力&#xff0c;共赴美好人生&#xff01; ♥️夕阳下&#xff0c;是最美的&#xff0c;绽放&#xff0c;…

Sikuli循环执行点击图标的脚本

首先需要sikulix jar包 新建java项目 导入sikulix jar包 编写代码如下 这样电脑每隔一段时间就会去点击一下c.png这个图标 package one; import org.sikuli.script.Screen; public class clickMouse { public static void main(String[] args) throws Exception { …

人力资本管理(HCM)软件的主要好处是什么?

人力资本管理&#xff08;HCM&#xff09;包括企业在招聘、雇用和培训期间为优化生产力采取的所有流程。为了最大限度地发挥团队的作用&#xff0c;留住顶尖人才&#xff0c;管理者和领导者需要投资于适当的实践和资源。实现这一目标的方法之一是通过人力资本管理。 作为一套…

js中数组是如何在内存中存储的?

数组不是以一组连续的区域存储在内存中&#xff0c;而是一种哈希映射的形式。它可以通过多种数据结构来实现&#xff0c;其中一种是链表。 js分为基本类型和引用类型&#xff1a; 基本类型是保存在栈内存中的简单数据段&#xff0c;它们的值都有固定的大小&#xff0c;保存在…

【FFmpeg+Qt】视频进度条控制——点击跳转和拖动跳转

首先进度条采用Qslider&#xff0c;设置进度条主要有两点&#xff0c;一是当前视频总时长&#xff0c;二是当前播放时长&#xff0c;需要通过FFmpeg转码成mp4文件才能获取相应的时长数据&#xff1b; 往期回顾&#xff1a; 【QtFFmpeg】视频转码详细流程_logani的博客-CSDN博…

从用户测试中学到的知识

从客户那里获得良好的反馈是个挑战。用户测试有的时候看起来是一个艰巨而且昂贵的任务。但是用户测试可以带来良好的经验&#xff0c;从而帮助设计更好的产品。 那么&#xff0c;从哪里开始呢?我测试了几种方法&#xff0c;有些失败&#xff0c;有些成功。下面我将讲述我所学到…

基于JAVA的教学进度在线管理系统/教学大纲在线管理系统源代码+数据库,含详细项目需求分析、概要设计、详细设计及项目总结文档

项目启动步骤 使用 SQL_Scripts/tms.sql 中的 sql 语句创建数据库与数据库表(数据库建立中&#xff0c;暂无) 修改 src/a_little_config.txt 文件&#xff0c;填入正确的数据库连接用户名、密码 将项目导入 IntelliJ IDEA 或 eclipse。 打开 cn.findix.tms.bin 包下的 WWW 文…

C#使用随机数模拟器来模拟世界杯排名(三)

接上篇 C#使用随机数模拟器来模拟世界杯排名(二)_斯内科的博客-CSDN博客 上一篇我们使用随机数匹配比赛的世界杯国家&#xff0c; 这一篇我们使用随机数以及胜率模拟器 决赛出 世界杯冠军、亚军。 我们在主界面 新增按钮【开始比赛 直到 决出冠军】和【刷新重新随机分配】 …