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

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

 [[399697]]

本文转载自微信公众号「一线码农聊技术」,作者一线码农聊技术。转载本文请联系一线码农聊技术公众号。

一:背景

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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

本以为一个简单的命令,结果屏幕上呼啦啦的一堆。。。有点意外,从上面的卦象看:当前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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

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

  • 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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

然后借助 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); 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.

代码有点长,但总的来说这里的代码不简单,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 中,毕竟程序启用时无人竞争。
  • 尽量将 单行赋值 改成 批量赋值。 

 

责任编辑:武晓燕 来源: 一线码农聊技术
相关推荐

2021-10-27 07:30:32

.NETCPU论坛

2024-08-08 11:21:01

2021-04-21 07:38:41

CPU游戏站程序

2022-10-24 07:48:37

.NETCPUGC

2024-12-31 09:36:06

2023-05-12 17:42:22

CPUMES系统

2024-03-15 15:15:53

.NETCPU系统

2023-07-31 22:29:20

CPU.NETAPI

2022-02-23 10:12:58

CPUWeb.NET

2023-11-01 10:46:12

.NET线程同步

2023-06-26 00:12:46

2024-12-27 13:31:18

.NETdump调试

2024-03-28 12:56:36

2023-04-06 10:52:18

2011-09-19 18:32:50

GtrotFacebook

2022-10-13 18:40:05

.NETOA后端

2023-07-06 10:11:38

.NET模式dump

2023-09-27 07:23:10

.NET监控软件

2024-05-20 09:39:02

.NETurl线程池

2024-07-12 11:20:34

.NET崩溃视觉程序
点赞
收藏

51CTO技术栈公众号