c# - MemoryCache在配置中不遵守内存限制

我正在使用应用程序中的.NET 4.0 MemoryCache类并尝试限制最大缓存大小,但在我的测试中,似乎缓存实际上并未遵守限制。

我使用的设置,根据MSDN,应该限制缓存大小:

  1. CacheMemoryLimitMegabytes:对象实例可以增长到的最大内存大小(以兆字节为单位)。“
  2. PhysicalMemoryLimitPercentage:“缓存可以使用的物理内存百分比,表示为1到100之间的整数值。默认值为零,表示MemoryCache实例根据计算机上安装的内存量管理自己的内存1“。 1.这不完全正确 - 忽略任何低于4的值并替换为4。

我理解这些值是近似值而不是硬限制,因为清除缓存的线程每隔x秒触发一次,并且还取决于轮询间隔和其他未记录的变量。 然而,即使考虑到这些差异,在将测试应用程序中的CacheMemoryLimitMegabytes和PhysicalMemoryLimitPercentage一起或单独设置后,当第一项从缓存中逐出时,我看到了非常不一致的缓存大小。 为了确保我每次测试10次并计算出平均值。

这些是在具有3GB RAM的32位Windows 7 PC上测试以下示例代码的结果。 在每次测试第一次调用CacheItemRemoved()之后获取缓存的大小。 (我知道缓存的实际大小将大于此)

MemLimitMB    MemLimitPct     AVG Cache MB on first expiry    
   1            NA              84
   2            NA              84
   3            NA              84
   6            NA              84
  NA             1              84
  NA             4              84
  NA            10              84
  10            20              81
  10            30              81
  10            39              82
  10            40              79
  10            49              146
  10            50              152
  10            60              212
  10            70              332
  10            80              429
  10           100              535
 100            39              81
 500            39              79
 900            39              83
1900            39              84
 900            41              81
 900            46              84

 900            49              1.8 GB approx. in task manager no mem errros
 200            49              156
 100            49              153
2000            60              214
   5            60              78
   6            60              76
   7           100              82
  10           100              541

这是测试应用程序:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.Caching;
using System.Text;
namespace FinalCacheTest
{       
    internal class Cache
    {
        private Object Statlock = new object();
        private int ItemCount;
        private long size;
        private MemoryCache MemCache;
        private CacheItemPolicy CIPOL = new CacheItemPolicy();

        public Cache(long CacheSize)
        {
            CIPOL.RemovedCallback = new CacheEntryRemovedCallback(CacheItemRemoved);
            NameValueCollection CacheSettings = new NameValueCollection(3);
            CacheSettings.Add("CacheMemoryLimitMegabytes", Convert.ToString(CacheSize)); 
            CacheSettings.Add("physicalMemoryLimitPercentage", Convert.ToString(49));  //set % here
            CacheSettings.Add("pollingInterval", Convert.ToString("00:00:10"));
            MemCache = new MemoryCache("TestCache", CacheSettings);
        }

        public void AddItem(string Name, string Value)
        {
            CacheItem CI = new CacheItem(Name, Value);
            MemCache.Add(CI, CIPOL);

            lock (Statlock)
            {
                ItemCount++;
                size = size + (Name.Length + Value.Length * 2);
            }

        }

        public void CacheItemRemoved(CacheEntryRemovedArguments Args)
        {
            Console.WriteLine("Cache contains {0} items. Size is {1} bytes", ItemCount, size);

            lock (Statlock)
            {
                ItemCount--;
                size = size - 108;
            }

            Console.ReadKey();
        }
    }
}

namespace FinalCacheTest
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            int MaxAdds = 5000000;
            Cache MyCache = new Cache(1); // set CacheMemoryLimitMegabytes

            for (int i = 0; i < MaxAdds; i++)
            {
                MyCache.AddItem(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            }

            Console.WriteLine("Finished Adding Items to Cache");
        }
    }
}

为什么MemoryCache不遵守配置的内存限制?

Canacourse asked 2019-09-16T22:13:59Z
7个解决方案
96 votes

哇,所以我花了很多时间在CLR上用反射器挖掘,但我想我终于可以很好地处理这里发生的事情了。

正在正确读取设置,但在CLR本身中似乎存在一个根深蒂固的问题,看起来它会使内存限制设置基本上无用。

以下代码反映在System.Runtime.Caching DLL中,用于CacheMemoryMonitor类(有一个类似的类监视物理内存并处理其他设置,但这是更重要的一个):

protected override int GetCurrentPressure()
{
  int num = GC.CollectionCount(2);
  SRef ref2 = this._sizedRef;
  if ((num != this._gen2Count) && (ref2 != null))
  {
    this._gen2Count = num;
    this._idx ^= 1;
    this._cacheSizeSampleTimes[this._idx] = DateTime.UtcNow;
    this._cacheSizeSamples[this._idx] = ref2.ApproximateSize;
    IMemoryCacheManager manager = s_memoryCacheManager;
    if (manager != null)
    {
      manager.UpdateCacheSize(this._cacheSizeSamples[this._idx], this._memoryCache);
    }
  }
  if (this._memoryLimit <= 0L)
  {
    return 0;
  }
  long num2 = this._cacheSizeSamples[this._idx];
  if (num2 > this._memoryLimit)
  {
    num2 = this._memoryLimit;
  }
  return (int) ((num2 * 100L) / this._memoryLimit);
}

您可能会注意到的第一件事是,它甚至不会在Gen2垃圾收集之后查看缓存的大小,而只是回退到cacheSizeSamples中现有的存储大小值。 因此,你永远无法直接击中目标,但如果其余的工作,我们至少会在遇到真正麻烦之前进行尺寸测量。

因此,假设发生了Gen2 GC,我们遇到问题2,这就是ref2.ApproximateSize实际上接近缓存大小的可怕工作。 通过CLR垃圾进行Slogging我发现这是一个System.SizedReference,这就是它为获取值而做的事情(IntPtr是MemoryCache对象本身的句柄):

[SecurityCritical]
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern long GetApproximateSizeOfSizedRef(IntPtr h);

我假设extern声明意味着它在这一点上潜入非托管窗口,我不知道如何开始发现它在那里做了什么。 从我观察到的情况来看,尽管它试图估算整体事物的大小是一件糟糕的工作。

第三个值得注意的事情是对manager.UpdateCacheSize的调用,听起来应该做点什么。 不幸的是,在这应该如何工作的任何正常样本中,s_memoryCacheManager将始终为null。 该字段是从公共静态成员ObjectCache.Host设置的。 如果用户选择的话,这可能会让用户感到困惑,而且我实际上可以通过将我自己的IMemoryCacheManager实现,将其设置为ObjectCache.Host,然后运行示例来实现这样的工作。。 但是,在这一点上,似乎你可能只是制作自己的缓存实现,甚至没有烦恼所有这些东西,特别是因为我不知道是否将自己的类设置为ObjectCache.Host(静态,所以它影响每一个 这些可能在那里的过程中测量缓存可能会弄乱其他东西。

我必须相信至少部分(如果不是几个部分)只是一个直接的错误。 很高兴听到MS的某个人与这件事有什么关系。

这个巨大答案的TLDR版本:假设此时CacheMemoryLimitMegabytes完全被破坏。 您可以将其设置为10 MB,然后继续将缓存填充到~2GB并吹掉内存不足异常,而不删除项目。

David Hay answered 2019-09-16T22:15:08Z
29 votes

我知道这个答案很晚才很疯狂,但迟到总比没有好。 我想告诉您我写了一个版本MemoryCache,它会自动为您解决Gen 2 Collection问题。 因此,只要轮询间隔指示内存压力,它就会进行修剪。 如果您遇到此问题,请试试吧!

[http://www.nuget.org/packages/SharpMemoryCache]

如果你对我如何解决它很好奇,你也可以在GitHub上找到它。 代码有点简单。

[https://github.com/haneytron/sharpmemorycache]

Haney answered 2019-09-16T22:15:51Z
4 votes

我也遇到过这个问题。 我正在缓存每秒被触发几十次的对象。

我发现以下配置和用法大多数时间每5秒释放一次项目。

App.config中:

记下cacheMemoryLimitMegabytes。 当此设置为零时,清除程序将无法在合理的时间内启动。

   <system.runtime.caching>
    <memoryCache>
      <namedCaches>
        <add name="Default" cacheMemoryLimitMegabytes="20" physicalMemoryLimitPercentage="0" pollingInterval="00:00:05" />
      </namedCaches>
    </memoryCache>
  </system.runtime.caching>  

添加到缓存:

MemoryCache.Default.Add(someKeyValue, objectToCache, new CacheItemPolicy { AbsoluteExpiration = DateTime.Now.AddSeconds(5), RemovedCallback = cacheItemRemoved });

确认缓存删除工作正常:

void cacheItemRemoved(CacheEntryRemovedArguments arguments)
{
    System.Diagnostics.Debug.WriteLine("Item removed from cache: {0} at {1}", arguments.CacheItem.Key, DateTime.Now.ToString());
}
Aaron Hudon answered 2019-09-16T22:16:42Z
3 votes

我(谢天谢地)昨天在第一次尝试使用MemoryCache时偶然发现了这篇有用的帖子。 我认为设置值和使用类是一个简单的例子,但我遇到了上面列出的类似问题。 为了尝试查看发生了什么,我使用ILSpy提取源,然后设置测试并逐步完成代码。 我的测试代码与上面的代码非常相似所以我不会发布它。 根据我的测试,我注意到缓存大小的测量从来没有特别准确(如上所述),并且鉴于当前的实现永远不会可靠地工作。 然而,物理测量很好,如果在每次轮询时测量物理内存,那么在我看来代码可以正常工作。 所以,我删除了MemoryCacheStatistics中的gen 2垃圾收集检查; 在正常情况下,除非自上次测量后再进行了第二代垃圾回收,否则不会进行内存测量。

在测试场景中,这显然会产生很大的不同,因为缓存不断被击中,因此对象永远不会有机会进入第2代。我认为我们将在我们的项目中使用此dll的修改版本并使用官方MS 在.net 4.5出来时构建(根据上面提到的连接文章应该有修复)。 从逻辑上讲,我可以看到为什么第二代检查已经到位但实际上我不确定它是否有意义。 如果内存达到90%(或者它设置的任何限制)那么无论是否发生了第2代收集都应该无关紧要,无论如何都要逐出项目。

我将测试代码运行了大约15分钟,并将physicalMemoryLimitPercentage设置为65%。 我看到测试期间内存使用量保持在65-68%之间,看到事情被正确驱逐。 在我的测试中,我将pollingInterval设置为5秒,将physicalMemoryLimitPercentage设置为65,将physicalMemoryLimitPercentage设置为0以默认为此。

遵循上述建议; 可以使用IMemoryCacheManager的实现来从缓存中清除事物。 然而,它会受到提到的第2代检查问题的影响。 虽然,根据情况,这可能不是生产代码中的问题,并且可能对人们充分有效。

Ian Gibson answered 2019-09-16T22:17:29Z
3 votes

我已经用@Canacourse和@woany的修改做了一些测试,我认为有一些关键的调用阻止了内存缓存的清理。

public void CacheItemRemoved(CacheEntryRemovedArguments Args)
{
    // this WriteLine() will block the thread of
    // the MemoryCache long enough to slow it down,
    // and it will never catch up the amount of memory
    // beyond the limit
    Console.WriteLine("...");

    // ...

    // this ReadKey() will block the thread of 
    // the MemoryCache completely, till you press any key
    Console.ReadKey();
}

但为什么@woany的修改似乎将内存保持在同一水平? 首先,未设置RemovedCallback,并且没有控制台输出或等待可能阻止内存缓存线程的输入。

其次...

public void AddItem(string Name, string Value)
{
    // ...

    // this WriteLine will block the main thread long enough,
    // so that the thread of the MemoryCache can do its work more frequently
    Console.WriteLine("...");
}

一个Thread.Sleep(1)每个~1000个AddItem()会产生相同的效果。

好吧,这不是对问题的深入调查,但看起来MemoryCache的线程没有足够的CPU时间进行清理,同时添加了许多新元素。

Jezze answered 2019-09-16T22:18:19Z
2 votes

如果您使用以下修改过的类并通过任务管理器监视内存,实际上会进行修剪:

internal class Cache
{
    private Object Statlock = new object();
    private int ItemCount;
    private long size;
    private MemoryCache MemCache;
    private CacheItemPolicy CIPOL = new CacheItemPolicy();

    public Cache(double CacheSize)
    {
        NameValueCollection CacheSettings = new NameValueCollection(3);
        CacheSettings.Add("cacheMemoryLimitMegabytes", Convert.ToString(CacheSize));
        CacheSettings.Add("pollingInterval", Convert.ToString("00:00:01"));
        MemCache = new MemoryCache("TestCache", CacheSettings);
    }

    public void AddItem(string Name, string Value)
    {
        CacheItem CI = new CacheItem(Name, Value);
        MemCache.Add(CI, CIPOL);

        Console.WriteLine(MemCache.GetCount());
    }
}
woany answered 2019-09-16T22:18:42Z
1 votes

事实证明这不是一个错误,你需要做的就是设置池化时间跨度以强制执行限制,看起来如果你没有设置池,它就永远不会触发。我只是测试它而不需要包装器 或任何额外的代码:

 private static readonly NameValueCollection Collection = new NameValueCollection
        {
            {"CacheMemoryLimitMegabytes", "20"},
           {"PollingInterval", TimeSpan.FromMilliseconds(60000).ToString()}, // this will check the limits each 60 seconds

        };

根据缓存增长的速度设置“PollingInterval”的值,如果增长太快,则增加轮询检查的频率,否则保持检查不频繁,不会导致开销。

sino answered 2019-09-16T22:19:13Z
translate from https://stackoverflow.com:/questions/6895956/memorycache-does-not-obey-memory-limits-in-configuration