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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次 .NET 某电商交易平台Web站 CPU爆高分析相关的知识,希望对你有一定的参考价值。

一:背景

1. 讲故事

已经连续写了几篇关于内存暴涨的真实案例,有点麻木了,这篇换个口味,分享一个 CPU爆高 的案例,前段时间有位朋友在 wx 上找到我,说他的一个老项目经常收到 CPU > 90% 的告警信息,挺尴尬的。

既然找到我,那就用 windbg 分析呗,还能怎么办。

二: windbg 分析

1. 勘探现场

既然说 CPU > 90%,那我就来验证一下是否真的如此?


0:359> !tp
CPU utilization: 100%
Worker Thread: Total: 514 Running: 514 Idle: 0 MaxLimit: 2400 MinLimit: 32
Work Request in Queue: 1
    Unknown Function: 00007ff874d623fc  Context: 0000003261e06e40
--------------------------------------
Number of Timers: 2
--------------------------------------
Completion Port Thread:Total: 2 Free: 2 MaxFree: 48 CurrentLimit: 2 MaxLimit: 2400 MinLimit: 32

从卦象看,真壮观,CPU直接被打满,线程池里 514 个线程也正在满负荷奔跑,那到底都奔跑个啥呢? 首先我得怀疑一下这些线程是不是被什么锁给定住了。

2. 查看同步块表

观察锁情况,优先查看同步块表,毕竟大家都喜欢用 lock 玩多线程同步,可以用 !syncblk 命令查看。


0:359> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
   53 000000324cafdf68          498         0 0000000000000000     none    0000002e1a2949b0 System.Object
-----------------------------
Total           1025
CCW             3
RCW             4
ComClassFactory 0
Free            620

我去,这卦看起来很奇怪, MonitorHeld=498 是什么鬼??? 教科书上都说: owner + 1 , waiter + 2,所以你肉眼看到的总会是一个奇数,那偶数又是个啥意思? 查了下神奇的 StackOverflow,大概总结成如下两种情况:

  • 内存损坏

这种情况比中彩还难,我也坚信不会走这种天罗运。。。

  • lock convoy (锁护送)

前段时间我分享了一篇真实案例: 记一次 .NET 某旅行社Web站 CPU爆高分析 ,它就是因为 lock convoy 造成的 CPU 爆高,果然世界真小,又遇到了。。。为了方便大家理解,我还是把那张图贴上吧。

看完这张图你应该就明白了,一个线程在时间片内频繁的争抢锁,所以就很容易的出现一个持有锁的线程刚退出,那些等待锁的线程此时还没有一个真正的持有锁,刚好抓到的dump就是这么一个时间差,换句话说,当前的 498 全部是 waiter 线程的计数,也就是 249 个 waiter 线程,接下来就可以去验证了,把所有线程的线程栈调出来,再检索下 Monitor.Enter 关键词。

从图中可以看出当前有 220 个线程正卡在 Monitor.Enter 处,貌似丢了29个,不管了,反正大量线程卡住就对了,从堆栈上看貌似是在 xxx.Global.PreProcess方法中设置上下文后卡住的,为了满足好奇心,我就把问题代码给导出来。

3. 查看问题代码

还是用老命令 !ip2md + !savemodule


0:359> !ip2md 00007ff81ae98854
MethodDesc:   00007ff819649fa0
Method Name:  xxx.Global.PreProcess(xxx.JsonRequest, System.Object)
Class:        00007ff81966bdf8
MethodTable:  00007ff81964a078
mdToken:      0000000006000051
Module:       00007ff819649768
IsJitted:     yes
CodeAddr:     00007ff81ae98430
Transparency: Critical
0:359> !savemodule 00007ff819649768 E:\\dumps\\PreProcess.dll
3 sections in file
section 0 - VA=2000, VASize=b6dc, FileAddr=200, FileSize=b800
section 1 - VA=e000, VASize=3d0, FileAddr=ba00, FileSize=400
section 2 - VA=10000, VASize=c, FileAddr=be00, FileSize=200

然后用 ILSpy 打开问题代码,截图如下:

尼玛,果然每个 DataContext.SetContextItem() 方法中都有一个 lock 锁,完美命中 lock convoy

4. 真的就这样结束了吗?

本来准备汇报了,但想着500多个线程栈都调出来了,闲着也是闲着,干脆扫扫看吧,结果我去,意外发现有 134 个线程卡在 ReaderWriterLockSlim.TryEnterReadLockCore 处,如下图所示:

从名字上可以看出,这是一个优化版的读写锁: ReaderWriterLockSlim,为啥有 138 个线程都卡在这里呢? 真的很好奇,再次导出问题。



internal class LocalMemoryCache : ICache
{
    private string CACHE_LOCKER_PREFIX = "xx_xx_";

    private static readonly NamedReaderWriterLocker _namedRwlocker = new NamedReaderWriterLocker();

    public T GetWithCache<T>(string cacheKey, Func<T> getter, int cacheTimeSecond, bool absoluteExpiration = true) where T : class
    {
        T val = null;
        ReaderWriterLockSlim @lock = _namedRwlocker.GetLock(cacheKey);
        try
        {
            @lock.EnterReadLock();
            val = (MemoryCache.Default.Get(cacheKey) as T);
            if (val != null)
            {
                return val;
            }
        }
        finally
        {
            @lock.ExitReadLock();
        }
        try
        {
            @lock.EnterWriteLock();
            val = (MemoryCache.Default.Get(cacheKey) as T);
            if (val != null)
            {
                return val;
            }
            val = getter();
            CacheItemPolicy cacheItemPolicy = new CacheItemPolicy();
            if (absoluteExpiration)
            {
                cacheItemPolicy.AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddSeconds(cacheTimeSecond));
            }
            else
            {
                cacheItemPolicy.SlidingExpiration = TimeSpan.FromSeconds(cacheTimeSecond);
            }
            if (val != null)
            {
                MemoryCache.Default.Set(cacheKey, val, cacheItemPolicy);
            }
            return val;
        }
        finally
        {
            @lock.ExitWriteLock();
        }
    }

看了下上面的代码大概想实现一个对 MemoryCache 的 GetOrAdd 操作,而且貌似为了安全起见,每一个 cachekey 都配了一个 ReaderWriterLockSlim,这逻辑就有点奇葩了,毕竟 MemoryCache 本身就带了实现此逻辑的线程安全方法,比如:


public class MemoryCache : ObjectCache, IEnumerable, IDisposable
{
    public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
    {
        if (regionName != null)
        {
            throw new NotSupportedException(R.RegionName_not_supported);
        }
        CacheItemPolicy cacheItemPolicy = new CacheItemPolicy();
        cacheItemPolicy.AbsoluteExpiration = absoluteExpiration;
        return AddOrGetExistingInternal(key, value, cacheItemPolicy);
    }
}

5. 用 ReaderWriterLockSlim 有什么问题吗?

哈哈,肯定有很多朋友这么问?

以上是关于记一次 .NET 某电商交易平台Web站 CPU爆高分析的主要内容,如果未能解决你的问题,请参考以下文章

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

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

记一次 .NET 某娱乐聊天流平台 CPU 爆高分析

记一次 .NET 某医保平台 CPU 爆高分析

记一次.NET 某安全生产系统 CPU爆高分析

记一次 .NET 某智能交通后台服务 CPU爆高分析