一:背景

1. 讲故事

前几天有位朋友wx求助,它的程序内存经常飙升,cpu 偶尔飙升,没找到原因,希望帮忙看一下。

可惜发过来的 dump 只有区区2G,能在这里面找到内存泄漏那真有两把刷子。。。,所以我还是希望他的程序内存涨到 5G+ 的时候再给我看看,既然内存看不了,那就看看这个偶尔飙升的CPU是个啥情况?老办法,上windbg说话。

二: windbg 分析

1. CPU 到底是多少

要想查看这个快照生成时机器的cpu使用率,可以使用 !tp 命令。


0:033> !tp
CPU utilization: 93%
Worker Thread: Total: 800 Running: 800 Idle: 0 MaxLimit: 800 MinLimit: 320
Work Request in Queue: 3203
Unknown Function: 000007fefb551500 Context: 000000002a198480
Unknown Function: 000007fefb551500 Context: 0000000028a70780
Unknown Function: 000007fefb551500 Context: 000000002a182610
Unknown Function: 000007fefb551500 Context: 00000000262a2700
...

本以为一个简单的命令,结果屏幕上呼啦啦的一堆。。。 有点意外,从上面的卦象看:当前CPU利用率是 93%,没毛病,确实是CPU飙升,比较惊讶的是,线程池上限800个线程全部被打满,太悲壮了。。。可更悲壮的是线程池队列中还有 3203 个待处理的任务,可以猜测程序不仅高CPU,还有挂死现象。。。

接下来的问题是:这800个壮士到底怎么啦,程序现在正是用人之际,要想找出答案,还是按照我的惯性思维,查看同步块表。

2. 线程同步块表

要想查看同步块表,可以使用 !synblk 命令。


0:033> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
188 0000000010defc28 1 1 000000001e8fb400 9f4 715 00000003ff1e3d80 System.Web.HttpApplicationStateLock
126159 000000001e424e28 1 1 0000000023425e00 1f14 695 0000000301210038 ASP.global_asax
126173 00000000281acaf8 1 1 0000000024b8ea70 24ec 785 00000000ff8c5e10 ASP.global_asax
126289 00000000247a4068 1 1 0000000027ee93c0 808 413 0000000306aca288 ASP.global_asax
126368 0000000027180dd8 1 1 0000000028005cb0 1e7c 650 00000002008d6280 ASP.global_asax
126489 0000000027211dd8 1 1 0000000026862420 ec4 220 000000030611a290 ASP.global_asax
126788 00000000247924b8 1 1 0000000021871ff0 2784 529 00000004039901a8 ASP.global_asax
126843 00000000285b8d28 1 1 000000001cbd6710 2170 456 00000004007ec748 ASP.global_asax
126934 0000000021b212b8 1 1 0000000026ca7590 16cc 472 000000030090e810 ASP.global_asax
127251 0000000024769188 1 1 000000002831eaf0 2b68 648 0000000207051038 ASP.global_asax
... -----------------------------
Total 141781
CCW 2
RCW 4
ComClassFactory 0
Free 140270

我去,又是呼啦啦的一堆,从上面的卦象可以看出两点信息:

  • MonitorHeld: 1

表示当前有一个线程正在持有锁。

  • ASP.global_asax , System.Web.HttpApplicationStateLock

表示当前线程持有的对象。

不过综合来看有点奇怪,除了第一个线程持有 HttpApplicationStateLock,后面所有的线程持有的 ASP.global_asax 对象都有不同的内存地址:0000000301210038,00000000ff8c5e10,感觉lock的对象不是线程共享式的 static,更像是一个 instance,蛮有意思的,接下来抽两个线程看看它的线程栈,比如这里的:715,695

3. 查看线程栈

要想查看线程栈,可以用 !clrstack 命令。



从这两个线程栈上看,分别是卡在 xxx.MvcApplication.Session_Start 方法中的 System.Threading.Monitor.Enter(System.Object)System.Threading.Monitor.ObjWait ,总的来说这里的 Session_Start 方法肯定是有问题的,所以得想办法把源码导出来看一看。

4. 查看问题代码

要想导出 Session_Start 方法,使用组合命令 !ip2md + !savemodule 即可。


||2:2:1781> !ip2md 000007fe99c6f0c5
MethodDesc: 000007fe990fe080
Method Name: xxx.xxx.xxx.MvcApplication.Session_Start(System.Object, System.EventArgs)
Class: 000007fe991ae0c0
MethodTable: 000007fe990fe238
mdToken: 0000000006000119
Module: 000007fe990fd750
IsJitted: yes
CodeAddr: 000007fe99c6e1f0
Transparency: Critical
||2:2:1781> !savemodule 000007fe990fd750 E:\dumps\Session_Start.dll
3 sections in file
section 0 - VA=2000, VASize=17538, FileAddr=200, FileSize=17600
section 1 - VA=1a000, VASize=3ac, FileAddr=17800, FileSize=400
section 2 - VA=1c000, VASize=c, FileAddr=17c00, FileSize=200

然后借助 ILSpy 反编译工具查看,由于比较敏感,我就多模糊一点,请大家见谅!

看完上面的代码,我其实有一点不解,既然是往 Application 中赋值,为啥不提取到 Application_Start 中呢? 我猜测开发人员也是无所谓,怎么方便怎么来,接下来看一下 Application 的源码。


public sealed class HttpApplicationState : NameObjectCollectionBase
{
private HttpApplicationStateLock _lock = new HttpApplicationStateLock(); public void Set(string name, object value)
{
_lock.AcquireWrite();
try
{
BaseSet(name, value);
}
finally
{
_lock.ReleaseWrite();
}
}
} internal class HttpApplicationStateLock : ReadWriteObjectLock
{
internal override void AcquireWrite()
{
int currentThreadId = SafeNativeMethods.GetCurrentThreadId();
if (_threadId == currentThreadId)
{
_recursionCount++;
return;
}
base.AcquireWrite();
_threadId = currentThreadId;
_recursionCount = 1;
} internal override void ReleaseWrite()
{
int currentThreadId = SafeNativeMethods.GetCurrentThreadId();
if (_threadId == currentThreadId && --_recursionCount == 0)
{
_threadId = 0;
base.ReleaseWrite();
}
}
} internal class ReadWriteObjectLock
{
internal virtual void AcquireWrite()
{
lock (this)
{
while (_lock != 0)
{
try
{
Monitor.Wait(this);
}
catch (ThreadInterruptedException)
{
}
}
_lock = -1;
}
}
internal virtual void ReleaseWrite()
{
lock (this)
{
_lock = 0;
Monitor.PulseAll(this);
}
}
}

代码有点长,但总的来说这里的代码不简单,Application 通过 lock 自己封装了一个 读写锁,不简单归不简单,但这里有什么问题呢 ? 就算写错了地方貌似也不会造成 cpu 爆高吧?

其实这里涉及到了一个概念:那就是 lock convoys (锁护送)

5. lock convoys (锁护送)

关于什么是 lock convoys ,我找了一篇解释很好的文章: 锁护送 ,这里我截一张图,大家仔细品品。

这也是 无锁编程 一直在抨击的现象。

三:总结

我看了下这个 Session_Start 方法中,大概有 105 个 Application[xxx],也就意味着有 105 个 lock 等着当前线程去闯关。。。 而此时有近800个线程已进入到此方法中,合计一下不少于 8W个锁等着这些线程去闯,在配上被迫的海量cpu时间片切换,唤醒再休眠,休眠再唤醒,大家相互交错一起把 cpu 给抬起来了。

解决方法很简单,尽最大努力降低这些 串行lock 的个数,能降到一个甚至没有就更好了 。

  • 对 Application 的赋值全部提取到 Application_Start 中,毕竟程序启用时无人竞争。

  • 尽量将 单行赋值 改成 批量赋值

更多高质量干货:参见我的 GitHub: dotnetfly

记一次 .NET 某旅行社Web站 CPU爆高分析的更多相关文章

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

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

  2. 记一次 .NET 某外贸Web站 内存泄漏分析

    一:背景 1. 讲故事 上周四有位朋友加wx咨询他的程序内存存在一定程度的泄漏,并且无法被GC回收,最终机器内存耗尽,很尴尬. 沟通下来,这位朋友能力还是很不错的,也已经做了初步的dump分析,发现了 ...

  3. 记一次 .NET 某供应链WEB网站 CPU 爆高事故分析

    一:背景 1. 讲故事 年前有位朋友加微信求助,说他的程序出现了偶发性CPU爆高,寻求如何解决,截图如下: 我建议朋友用 procdump 在 cpu 高的时候连抓两个dump,这样分析起来比较稳健, ...

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

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

  5. 记一次 .NET 某市附属医院 Web程序 偶发性CPU爆高分析

    一:背景 1. 讲故事 这个月初,一位朋友加微信求助他的程序出现了 CPU 偶发性爆高,希望能有偿解决一下. 从描述看,这个问题应该困扰了很久,还是医院的朋友给力,开门就是 100块 红包 ,那既然是 ...

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

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

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

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

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

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

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

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

随机推荐

  1. Centos7 Firewall 使用笔记

    在 Centos 7 中防火墙由 firewalld 来管理,而不是以前的 iptables. 记录一下常用操作备查 firewall-cmd 操作 firewall-cmd --state 查看防火 ...

  2. 翻译:《实用的Python编程》06_02_Customizing_iteration

    目录 | 上一节 (6.1 迭代协议) | 下一节 (6.3 生产者/消费者) 6.2 自定义迭代 本节探究如何使用生成器函数自定义迭代. 问题 假设你想要自定义迭代模式. 例如:倒数: >&g ...

  3. A Color Game

    题目大意:  给定一个只包含七种字母的字符串,如果满足一段连续相同的字符长度大于等于K那么即可消除,问最后能不能变为空字符. 题解:很明显是用区间dp来解决,我们设dp[l][r][k]代表的是在[l ...

  4. P1308_统计单词数(JAVA语言)

    题目描述 一般的文本编辑器都有查找单词的功能,该功能可以快速定位特定单词在文章中的位置,有的还能统计出特定单词在文章中出现的次数. 现在,请你编程实现这一功能,具体要求是:给定一个单词,请你输出它在给 ...

  5. vue自定义插件封装,实现简易的elementUi的Message和MessageBox

    vue自定义插件封装示例 1.实现message插件封装(类似简易版的elementUi的message) message组件 <template>     <transition  ...

  6. pwn题命令行解题脚本

    目录 脚本说明 脚本内容 使用 使用示例 参考与引用 脚本说明 这是专门为本地调试与远程答题准备的脚本,依靠命令行参数进行控制. 本脚本支持的功能有: 本地调试 开启tmux调试 设置gdb断点,支持 ...

  7. Redis持久化——内存快照(RDB)

    最新:Redis持久化--如何选择合适的持久化方式 最新:Redis持久化--AOF日志 最新:Redis持久化--内存快照(RDB) 一文回顾Redis五大对象(数据类型) Redis对象--有序集 ...

  8. 201871030140-朱婷婷 实验三 结对项目—《D{0-1}KP 实例数据集算法实验平台》项目报告

    项目 内容 课程班级博客链接 2018级卓越班 这个作业要求链接 实验三 结对项目 我的课程学习目标 1.体验软件项目开发中的两人合作,练习结对编程:2.掌握GitHub协作开发程序的操作方法. 这个 ...

  9. Java中获取类的运行时结构

    获取运行时类的完整结构 通过反射获取运行时类的完整结构 Field(属性).Method(方法).Constructor(构造器).Superclass(父类).Interface(接口).Annot ...

  10. 【笔记】《Redis设计与实现》chapter11 AOF持久化

    11.1 AOF持久化的实现 命令追加 当AOF持久化处于开启状态时,服务器执行完一个写命令之后,会以协议格式将被执行的写明了追加到服务器状态的aof_buf缓冲区 struct redisServe ...