转载本文请联系一线码农聊聊技术公众号。一:背景1.讲故事的前几天,wx的一个朋友求助。它的程序内存经常飙升,CPU偶尔飙升。我找不到原因。希望帮我看看。可惜这里发送的dump只有区区2G。如果你能在里面找到内存溢出,你就真的有两把刷子了。..??????,所以我还是希望他的程序内存增加到5G+再给我看。既然看不到内存,那我们就看看这个偶尔飙升的CPU是怎么回事?老办法,谈论windbg。二:windbg分析1.什么是CPU?如果想在生成快照时查看机器的CPU使用情况,可以使用!tp命令。0:033>!tpCPUutilization:93%WorkerThread:Total:800Running:800Idle:0MaxLimit:800MinLimit:320WorkRequestinQueue:3203UnknownFunction:000007fefb551500Context:000000002a198480UnknownFunction:000007fefb551500Context:0000000028a70780UnknownFunction:000007fefb551500Context:000000002a182610UnknownFunction:000007fefb551500Context:00000000262a2700本以为一个简单的命令,结果屏幕上一堆草裙舞。..这有点令人惊讶。从上卦来看:当前CPU使用率为93%,没有什么问题。CPU确实在飙升。比较意外的是,线程池800个线程的上限已经用完了,太惨烈了。..但更悲惨的是,线程池队列中还有3203个待处理任务。可以猜到程序不仅CPU高,而且挂了。..下一个问题是:这800名壮汉怎么了?该计划现在正在雇用人员。要想找到答案,还是要按照我的惯性思维去查一下同步块表。2.线程同步块表查看同步块表可以使用!synblk命令。0:033>!syncblkIndexSyncBlockMonitorHeldRecursionOwningThreadInfoSyncBlockOwner1880000000010defc2811000000001e8fb4009f471500000003ff1e3d80System.Web.HttpApplicationStateLock126159000000001e424e28110000000023425e001f146950000000301210038ASP.global_asax12617300000000281acaf8110000000024b8ea7024ec78500000000ff8c5e10ASP.global_asax12628900000000247a4068110000000027ee93c08084130000000306aca288ASP.global_asax1263680000000027180dd8110000000028005cb01e7c65000000002008d6280ASP.global_asax1264890000000027211dd8110000000026862420ec4220000000030611a290ASP.global_asax12678800000000247924b8110000000021871ff0278452900000004039901a8ASP.global_asax12684300000000285b8d2811000000001cbd6710217045600000004007ec748ASP.global_asax1269340000000021b212b8110000000026ca759016cc472000000030090e810ASP.global_asax127251000000002476918811000000002831eaf02b686480000000207051038ASP.global_asax...-----------------------------Total141781CCW2RCW4ComClassFactory0Free140270我去,又是呼啦啦从上面的卦象我们可以看出两点信息:MonitorHeld:1表示当前有一个线程持有锁ASP.global_asax,System.Web.HttpApplicationStateLock表示当前线程持有的对象。但是,总体来说有点奇怪。除了第一个线程持有HttpApplicationStateLock外,后续所有线程持有的ASP.global_asax对象内存地址不同:0000000301210038、00000000ff8c5e10。感觉加锁的对象不是线程共享的static,更像是一个实例,挺有意思的。接下来拿两个线程来看它的线程栈,例如:这里的715695。3、查看线程栈要查看线程栈,可以使用!clrstack命令。从这两个线程栈来看,分别是System.Threading.Monitor.Enter(System.Object)和System.Threading.Monitor.ObjWait卡在了xxx.MvcApplication.Session_Start方法中。一般来说,这里的Session_Start方法肯定有问题,只好想办法导出源码看看。4.查看问题代码要导出Session_Start方法,使用组合命令!ip2md+!savemodule。||2:2:1781>!ip2md000007fe99c6f0c5MethodDesc:000007fe990fe080MethodName:xxx.xxx.xxx.MvcApplication.Session_Start(System.Object,System.EventArgs)Class:000007fe991ae0c0MethodTable:000007fe990fe238mdToken:0000000006000119Module:000007fe990fd750IsJitted:yesCodeAddr:000007fe99c6e1f0Transparency:Critical||2:2:1781>!savemodule000007fe990fd750E:\dumps\Session_Start.dll3sectionsinfilesection0-VA=2000,VASize=17538,FileAddr=200,FileSize=17600section1-VA=1a000,VASize=3ac,FileAddr=17800,FileSize=400,2VASize=c部分=17c00,FileSize=200然后用ILSpy反编译工具查看,因为比较敏感,我说的比较模糊,还请见谅!看了上面的代码,其实有点疑惑,既然是要ApplicationAssignment,为什么不把它提取到Application_Start中呢?估计开发者不在乎,到底有多方便,还是来看看Application的源码吧。publicsealedclassHttpApplicationState:NameObjectCollectionBase{privateHttpApplicationStateLock_lock=newHttpApplicationStateLock();publicvoidSet(stringname,objectvalue){_lock.AcquireWrite();try{BaseSet(name,value);}finally{_lock.ReleaseWrite();}}}internalclassHttpApplicationStateLock:ReadWriteObjectLock{internaloverridevoidAcquireWrite(){intcurrentThreadId=SafeNativeMethods.GetCurrentThreadId();if(_threadId==currentThreadId){_recursionCount++;return;}base.AcquireWrite();_threadId=currentThreadId;_recursionCount=1;}internaloverridevoidReleaseWrite(){intcurrentThreadId=SafeNativeMethods.GetCurrentThreadId();if(_threadId==currentThreadId&&--_recursionCount==0){_threadId=0;base.ReleaseWrite();}}}internalclassReadWriteObjectLock{internalvirtualvoidAcquireWrite(){lock(this){while(_lock!=0){try{Monitor.Wait(this);}catch(ThreadInterruptedException){}}_lock=-1;}}internalvirtualvoidReleaseWrite(){lock(this){_lock=0;Monitor.PulseAll(this);}}}代码有点长,但总说起来,这里的代码也不简单。Application自己封装了一个读写锁,一点也不简单,但是这里有什么问题呢?就算是写错了地方,好像也不会导致CPU爆掉吧?其实这里涉及到一个概念:那就是lockconvoys(锁护送)5.lockconvoys(锁护送)关于什么是lockconvoys,这里我截图一下,大家仔细看看。这也是无锁编程一直在攻击的现象。三:综上,我看了一下Session_Start方法,大概有105个Application[xxx],也就是说有105个锁在等待当前线程突破。..此时已经有近800个线程进入了这个方法,总共有不少于8W的锁在等待这些线程的突破。再加上强制海量的cpu时间片切换,唤醒再睡眠,睡眠再唤醒,大家错开,一起升cpu。解决办法很简单,尽量减少这些串口锁的个数,如果能减少到一个甚至没有就更好了。??????。所有对Application的赋值都被提取到Application_Start中,毕竟程序启动时没有人竞争。尝试将单行分配更改为批量分配。
