一:背景
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 中,毕竟程序启用时无人竞争。
- 尽量将 单行赋值 改成 批量赋值。