一:背景

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. Java字符串==和equals的区别

    首先我们来了解一下String类,Java的字符串是一旦被赋值之后无法更改的(这里的无法更改是指不能将字符串中单个或一段字符重新赋值),这也是Java虚拟机为了减少内存开销,避免字符串的重复创建设立的 ...

  2. Android Studio 如何在TextView中设置图标并按需调整图标大小

    •任务 相信大家对这张图片都不陌生,没错,就是 QQ动态 向我们展示的界面. 如何实现呢? •添加文字并放入图标 新建一个 Activity,取名为 QQ,Android Studio 自动为我们生成 ...

  3. 敏捷史话(十一):敏捷宣言“间谍”——Steve Mellor

    Steve Mellor 是敏捷宣言的签署人之一,他自称是作为" 间谍"去参加雪鸟会议的. 起初收到会议邀请时,Steve 非常惊讶,因为他所做的工作一直都是关于建模方面的,很少将 ...

  4. AutoAssign源码分析

    目录 AutoAssign源码分析 一. 简介 二. 论文理论 2.1 联合表示 2.2 正样本权重 2.3 负样本权重 2.4 总的loss 2.5 补充loss 三. 论文代码 四. 总结 五. ...

  5. Typescript进阶之路

    TypeScript 何为TypeScript 一.编程语言类型 动态类型语言(Dynamically Typed Language) 类型的检查是在运行时才做 例子---JavaScript.Rub ...

  6. B1029/A1048 旧键盘损坏了,在输入一段文字时坏了的键不可以正常使用,现给出应输入的一段文字,和实际输出的文字,找出坏掉的键。

    #include<cstdio> #include<cstring> const int maxn = 1000; bool HashTable[maxn] = { false ...

  7. [Skill]VBA零基础入门及实践:根据链接展示图片

    简介 VBA(Visual Basic for Applications)是依附在应用程序(例如Excel)中的VB语言.只要你安装了Office Excel就自动默认安装了VBA,同样Word和Po ...

  8. 设计原则:接口隔离原则(ISP)

    接口隔离原则的英文是Interface Segregation Principle,缩写就是ISP.与里氏替换原则一样其定义同样有两种 定义1: Clients should not be force ...

  9. Ugly Numbers UVA - 136

    Ugly numbers are numbers whose only prime factors are 2, 3 or 5. The sequence 1, 2, 3, 4, 5, 6, 8, 9 ...

  10. mysql架构与存储引擎 (Myisam与Innodb)

    mysql抽象架构:可以分为SQL Layer和Storage Engine Layer mysql的engine层是基于表的,不是基于库的,创建表的语句可以指定engine Mysql的架构 Mys ...