一:背景

1. 讲故事

前些天有位朋友找到我,说他们的系统出现了CPU 100%的情况,让你帮忙看一下怎么回事?dump也拿到了,本想着这种情况让他多抓几个,既然有了就拿现有的分析吧。

二:WinDbg 分析

1. 为什么会爆高

既然说是 100%,作为调试者得拿数据说话,可以使用 !tp 来观测一下。


0:000:x86> !tp
CPU utilization: 100%
Worker Thread: Total: 382 Running: 382 Idle: 0 MaxLimit: 8191 MinLimit: 8
Work Request in Queue: 8694
Unknown Function: 6f62b650 Context: 4a36bbbc
Unknown Function: 6f62b650 Context: 4a36e1d4
Unknown Function: 6f62b650 Context: 4a372384
Unknown Function: 6f62b650 Context: 239adfec
Unknown Function: 6f62b650 Context: 4a374994
Unknown Function: 6f62b650 Context: 239b9e14
Unknown Function: 6f62b650 Context: 2399fd9c
...

从卦中看,不得了,CPU 100% 之外,所有的线程池线程全部被打满,人生自古最忌满,半贫半富半自安。同时线程池队列还累计了8694个任务待处理,说明这时候的线程池已经全面沦陷,要想找到这个答案,需要用 ~*e !clrstack 命令观察每一个线程此时正在做什么,输出如下:


0:000:x86> ~*e !clrstack
OS Thread Id: 0x22f4 (429)
Child SP IP Call Site
4bc1e060 0000002b [GCFrame: 4bc1e060]
4bc1e110 0000002b [HelperMethodFrame_1OBJ: 4bc1e110] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
4bc1e19c 24aad7da System.Threading.Monitor.Wait(System.Object, Int32, Boolean)
4bc1e1ac 2376f0d6 ServiceStack.Redis.PooledRedisClientManager.GetClient()
4bc1e1dc 2420bbc6 xxx.Service.CacheService.GetClient()
...
4bc1e234 24206fbe xxxBLL.GetxxxCount(System.Collections.Generic.Dictionary`2<System.String,System.Object>)
4bc1e3e0 216e25f9 DynamicClass.lambda_method(System.Runtime.CompilerServices.Closure, System.Web.Mvc.ControllerBase, System.Object[])
4bc1e3f0 238b86b7 System.Web.Mvc.ActionMethodDispatcher.Execute(System.Web.Mvc.ControllerBase, System.Object[])
...
4bc1eee0 2353d448 System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)
4bc1efb8 00a9e3c2 [ContextTransitionFrame: 4bc1efb8]

从卦中可以看到当前有 371个线程在 PooledRedisClientManager.GetClient 中的 Wait 上出不来,那为什么出不来呢?

2. 探究源码

要想找到这个答案,只能从源代码中观察,简化后的代码如下:


public IRedisClient GetClient()
{
lock (writeClients)
{
AssertValidReadWritePool();
RedisClient inActiveWriteClient;
while ((inActiveWriteClient = GetInActiveWriteClient()) == null)
{
if (!Monitor.Wait(writeClients, PoolTimeout.Value))
{
throw new TimeoutException("Redis Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use.");
}
}
}
} private RedisClient GetInActiveWriteClient()
{
int num = WritePoolIndex % writeClients.Length;
for (int i = 0; i < ReadWriteHosts.Count; i++)
{
int num2 = (num + i) % ReadWriteHosts.Count;
RedisEndPoint redisEndPoint = ReadWriteHosts[num2];
for (int j = num2; j < writeClients.Length; j += ReadWriteHosts.Count)
{
if (writeClients[j] != null && !writeClients[j].Active && !writeClients[j].HadExceptions)
{
return writeClients[j];
}
}
}
return null;
}

仔细阅读卦中代码,之所以进入Wait主要是因为 GetInActiveWriteClient() 方法返回 null 所致,从异常信息看也知道此时是因为 writeClients 池已满,那这个池是不是满了呢?可以把 writeClients 数组挖出来,使用 !dso 命令。


0:429:x86> !dso
OS Thread Id: 0x22f4 (429)
ESP/REG Object Name
...
4BC1E0D0 0ea38d18 ServiceStack.Redis.RedisClient[]
4BC1E100 0ea38bb0 ServiceStack.Redis.PooledRedisClientManager
... 0:429:x86> !da 0ea38d18
Name: ServiceStack.Redis.RedisClient[]
MethodTable: 237af1c0
EEClass: 0129a224
Size: 52(0x34) bytes
Array: Rank 1, Number of elements 10, Type CLASS
Element Methodtable: 237ae954
[0] 0ea38dd4
[1] 0a9f9f58
[2] 0296e468
[3] 0c9786a0
[4] 0a9fe768
[5] 04a21f24
[6] 0aa0d758
[7] 10946d90
[8] 04a8c8b0
[9] 02a2a2a0 0:429:x86> !DumpObj /d 0ea38dd4
Name: ServiceStack.Redis.RedisClient
MethodTable: 237ae954
EEClass: 2375d154
Size: 152(0x98) bytes
File: C:\Windows\xxx\ServiceStack.Redis.dll
Fields:
...
0129aa48 4000169 7d System.Boolean 1 instance 1 <Active>k__BackingField
...

从卦中看 writeClients 池只有10个大小,并且都是 Active=1,所以返回 null 就不足为奇了。

3. 为什么client都在使用中呢

要想找到这个答案,需要看下上层的 xxxBLL.GetxxxCount 方法是如何调用的,为了保护隐私,就多模糊一点。

从图中可以看到,问题出在用 foreach 去不断的迭代 ServiceStack.Redis 导致 writeClient 池耗尽,导致大量的请求在不断的阻塞,不要忘了这里有371个线程在争抢哦,真是大忌。

接下来顺带洞察下这个 foreach 要 foreach 多少次? 继续用 !dso 去挖。


0:429:x86> !DumpObj /d 077cec20
Name: System.Collections.Generic.List`1[[xxxInfo, xxx]]
MethodTable: 241ad794
EEClass: 0193166c
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
01860eec 4001891 4 System.__Canon[] 0 instance 077e0048 _items
0129c9b0 4001892 c System.Int32 1 instance 307 _size
0129c9b0 4001893 10 System.Int32 1 instance 307 _version
01296780 4001894 8 System.Object 0 instance 00000000 _syncRoot
01860eec 4001895 4 System.__Canon[] 0 static <no information>

从卦中看当前需要循环307次,也就再次验证了池耗尽的说法,我知道心细的朋友肯定会说,卡死这个我认,但能导致 CPU爆高 我就不能理解了,其实你仔细阅读源码就能理解了,这是经典的 锁护送(lock convoy) 现象,因为满足如下两个条件。

  1. 多线程的 foreach 高频调用。
  2. Wait 导致线程暂停进入等待队列。

4. 如何解决这个问题

知道了前因后果,解决起来就比较简单了,三种做法:

  1. 将 foreach 迭代 改成 批量化处理,减少对 writeclient 的租用。
  2. 增加 writeclient 的池大小,官网有所介绍。
  3. ServiceStack.Redis 的版本非常老,又是收费的,最好换掉已除后患。

三:总结

这次生产事故分析还是非常有意思的,一个看似阻塞的问题也会引发CPU爆高,超出了一些人的认知吧,对,其实它就是经典的 lock convoy 现象,大家有任何dump问题可以找我,一如既往的免费分析。

记一次 .NET某智慧出行系统 CPU爆高分析的更多相关文章

  1. 记一次 .NET 某医院HIS系统 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友加 wx 抱怨他的程序在高峰期总是莫名其妙的cpu爆高,求助如何分析? 和这位朋友沟通下来,据说这问题困扰了他们几年,还请了微软的工程师过来解决,无疾而终,应该还 ...

  2. 记一次 .NET 某医保平台 CPU 爆高分析

    一:背景 1. 讲故事 一直在追这个系列的朋友应该能感受到,我给这个行业中无数的陌生人分析过各种dump,终于在上周有位老同学找到我,还是个大妹子,必须有求必应 . 妹子公司的系统最近在某次升级之后, ...

  3. 记一次 .NET 某智慧物流 WCS系统 CPU 爆高分析

    一:背景 1. 讲故事 哈哈,再次见到物流类软件,上个月有位朋友找到我,说他的程序出现了 CPU 爆高,让我帮忙看下什么原因,由于那段时间在苦心研究 C++,分析和经验分享也就懈怠了,今天就给大家安排 ...

  4. 记一次 .NET游戏站程序的 CPU 爆高分析

    一:背景 1. 讲故事 上个月有个老朋友找到我,说他的站点晚高峰 CPU 会突然爆高,发了两份 dump 文件过来,如下图: 又是经典的 CPU 爆高问题,到目前为止,对这种我还是有一些经验可循的. ...

  5. 记一次 .NET 某旅行社Web站 CPU爆高分析

    一:背景 1. 讲故事 前几天有位朋友wx求助,它的程序内存经常飙升,cpu 偶尔飙升,没找到原因,希望帮忙看一下. 可惜发过来的 dump 只有区区2G,能在这里面找到内存泄漏那真有两把刷子..., ...

  6. 记一次 .NET 某电商交易平台Web站 CPU爆高分析

    一:背景 1. 讲故事 已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > ...

  7. 记一次 .NET 某机械臂智能机器人控制系统MRS CPU爆高分析

    一:背景 1. 讲故事 这是6月中旬一位朋友加wx求助dump的故事,他的程序 cpu爆高UI卡死,问如何解决,截图如下: 在拿到这个dump后,我发现这是一个关于机械臂的MRS程序,哈哈,在机械臂这 ...

  8. 记一次 .NET 差旅管理后台 CPU 爆高分析

    一:背景 1. 讲故事 前段时间有位朋友在微信上找到我,说他的 web 系统 cpu 运行一段时候后就爆高了,让我帮忙看一下是怎么回事,那就看吧,声明一下,我看 dump 是免费的,主要是锤炼自己技术 ...

  9. 记一次 .NET 某电子病历 CPU 爆高分析

    一:背景 1.讲故事 前段时间有位朋友微信找到我,说他的程序出现了 CPU 爆高,帮忙看下程序到底出了什么情况?图就不上了,我们直接进入主题. 二:WinDbg 分析 1. CPU 真的爆高吗? 要确 ...

  10. 记一次 .NET 某电厂Web系统 内存泄漏分析

    一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...

随机推荐

  1. 常用的jvm一些监控命令

    一.jmap 查看堆内对象示例的统计信息 jmap -heap pid 描述:查看堆信息 jmap -histo:live pid | head -30 描述:显示堆中对象的统计信息 命令:jmap ...

  2. Vulnhub Fall Walkthrough

    Recon 二层本地扫描,发现目标靶机. ┌──(kali㉿kali)-[~] └─$ sudo netdiscover -r 192.168.80.0/24 Currently scanning: ...

  3. 2.上传hdfs系统:将logs目录下的日志文件每隔十分钟上传一次 要求:上传后的文件名修为:2017111513xx.log_copy

    先在hdfs系统创建文件夹logshadoop fs -mkdir /logs 编辑shell脚本 filemv.sh #!/bin/bashPATH=/usr/local/bin:/bin:/usr ...

  4. 浏览器中JS的执行

    JS是在浏览器中运行的,浏览器为了运行JS, 必须要编译或解释JS,因为JS是高级语言,计算机不认识,必须把它编译或解释成机器语言,其次,在运行JS的过程,浏览器还要创建堆栈,因为程序是在栈中执行,执 ...

  5. 解决BitBucket仓库较大拉取失败,使用SSH拉取

    HTTPS 拉取 如果使用的是https拉取,可使用以下命令尝试,如果还是失败,可使用 ssh 拉取 git clone --depth=1 xxxx.git --depth=1:拉取最近1次提交记录 ...

  6. 单qubit量子门

    量子编程的基本单元就是量子门.量子编程有点像传统的电路设计,一个量子程序可以被写成量子门序列. 图中有一些符合,比如H门.X门.Z门.测量等,我们都会接触到. 传统计算机程序的输入和输出可以不一样,但 ...

  7. 韦东山freeRTOS系列教程之【第五章】队列(queue)

    目录 系列教程总目录 概述 5.1 队列的特性 5.1.1 常规操作 5.1.2 传输数据的两种方法 5.1.3 队列的阻塞访问 5.2 队列函数 5.2.1 创建 5.2.2 复位 5.2.3 删除 ...

  8. SpringBoot配置文件的优先级

    配置文件优先级 (1)命令行参数: (2)java:comp/env的JNDI属性(当前J2EE应用的环境): (3)JAVA系统的环境属性: (4)操作系统的环境变量: (5)JAR包外部的appl ...

  9. 实现ASP.Net Core3.1运行在DockeDesktop下并用Nginx实现负载均衡

    一.首先去https://docs.docker.com/get-docker/下载Windows版本的Docker Desktop并安装(需要win10专业版以上操作系统,并启用CPU虚拟化和安装H ...

  10. VUE商城项目 -商品分类功能 - 手稿