MemoryCache 不遵守配置中的内存限制

Posted

技术标签:

【中文标题】MemoryCache 不遵守配置中的内存限制【英文标题】:MemoryCache does not obey memory limits in configuration 【发布时间】:2011-10-17 06:43:03 【问题描述】:

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

我正在使用according to MSDN 应该限制缓存大小的设置:

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

我了解这些值是近似值而非硬性限制,因为清除缓存的线程每 x 秒触发一次,并且还取决于轮询间隔和其他未记录的变量。然而,即使考虑到这些差异,当第一个项目在设置 CacheMemoryLimitMegabytesPhysicalMemoryLimitPercentage 一起或单独设置后从缓存中逐出时,我看到缓存大小非常不一致测试应用程序。为了确保我将每个测试运行了 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 不遵守配置的内存限制?

【问题讨论】:

for 循环错误,没有 i++ 我已经为这个错误添加了一个 MS Connect 报告(也许其他人已经这样做了,但无论如何......)connect.microsoft.com/VisualStudio/feedback/details/806334/… 值得注意的是,微软现在(截至 2014 年 9 月)对上面链接的连接票证添加了相当彻底的响应。它的 TLDR 是 MemoryCache 不会在每个操作中固有地检查这些限制,而是仅在内部缓存修剪时遵守这些限制,这是基于动态内部计时器的周期性。 看起来他们更新了 MemoryCache.CacheMemoryLimit 的文档:“每次将新项目添加到 MemoryCache 实例时,MemoryCache 不会立即强制执行 CacheMemoryLimit。从 MemoryCache 驱逐额外项目的内部启发式方法会做到这一点逐渐……”msdn.microsoft.com/en-us/library/… @Zeus,我认为 MSFT 解决了这个问题。无论如何,MSFT 在与我讨论后关闭了该问题,他们告诉我该限制仅在 PoolingTime 到期后应用。 【参考方案1】:

哇,所以我花了太多时间在带有反射器的 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 垃圾我发现这是一个 System.SizedReference,这就是它为获取值所做的事情(IntPtr 是 MemoryCache 对象本身的句柄):

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

我假设 extern 声明意味着它此时会潜入非托管窗口领域,我不知道如何开始找出它在那里做了什么。从我所观察到的情况来看,尽管它在尝试近似整体事物的大小方面做得很糟糕。

第三个值得注意的事情是对 manager.UpdateCacheSize 的调用,听起来它应该做点什么。不幸的是,在任何应该如何工作的正常示例中,s_memoryCacheManager 将始终为空。该字段是从公共静态成员 ObjectCache.Host 中设置的。如果用户愿意,这会暴露给用户来搞乱,实际上我可以通过将我自己的 IMemoryCacheManager 实现拼凑在一起,将其设置为 ObjectCache.Host,然后运行示例,从而使这件事像预期的那样工作.不过,在那一点上,您似乎还不如自己做缓存实现,甚至不用理会所有这些东西,特别是因为我不知道是否将您自己的类设置为 ObjectCache.Host (静态,所以它会影响每个人其中一些可能正在处理中)来测量缓存可能会搞砸其他事情。

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

这个巨大答案的 TLDR 版本:假设 CacheMemoryLimitMegabytes 在这个时间点完全被破坏。您可以将其设置为 10 MB,然后继续将缓存填充到 ~2GB,并在没有删除项目的情况下触发内存不足异常。

【讨论】:

一个很好的答案谢谢。我放弃了试图弄清楚这是怎么回事,而是现在通过计算输入/输出项目并根据需要手动调用 .Trim() 来管理缓存大小。我认为 System.Runtime.Caching 是我的应用程序的一个简单选择,因为它似乎被广泛使用,因此我认为不会有任何重大错误。 哇。这就是我喜欢SO的原因。我遇到了完全相同的行为,编写了一个测试应用程序并设法使我的 PC 多次崩溃,即使轮询时间低至 10 秒且缓存内存限制为 1MB。感谢您提供所有见解。 我知道我刚刚在问题中提到了它,但是为了完整起见,我会在这里再次提到它。为此,我在 Connect 上打开了一个问题。 connect.microsoft.com/VisualStudio/feedback/details/806334/… 我将 MemoryCache 用于外部服务数据,当我通过将垃圾注入 MemoryCache 进行测试时,它确实自动修剪内容,但仅在使用百分比时极限值。绝对大小不会限制大小,至少在使用内存分析器进行检查时是这样。没有在 while 循环中进行测试,而是通过更“现实”的用法进行测试(它是一个后端系统,所以我添加了一个 WCF 服务,它可以让我按需将数据注入缓存)。 在 .NET Core 中这仍然是一个问题吗?【参考方案2】:

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

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

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

https://github.com/haneytron/sharpmemorycache

【讨论】:

这按预期工作,使用生成器进行测试,该生成器用 1000 个字符的字符串加载缓存。虽然,将应该像 100MB 的缓存加起来实际上会增加 200 - 300MB 到缓存,我觉得这很奇怪。也许有些偷听我不算数。 .NET 中的@KarlCassar 字符串相对于字节的大小大致为2n + 20,其中n 是字符串的长度。这主要是由于 Unicode 支持。【参考方案3】:

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

我发现以下配置和用法大部分时间每 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());

【讨论】:

【参考方案4】:

我已经用@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("...");

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

嗯,对问题的调查不是很深,但是看起来MemoryCache的线程没有得到足够的CPU时间来清理,同时又添加了很多新元素。

【讨论】:

【参考方案5】:

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

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

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

遵循上述建议;可以使 IMemoryCacheManager 的实现从缓存中驱逐事物。然而,它会受到提到的第 2 代检查问题的影响。不过,根据场景的不同,这在生产代码中可能不是问题,并且对人们来说可能足够工作。

【讨论】:

更新:我使用的是 .NET 框架 4.5,但问题没有得到纠正。缓存可以增长到足以使机器崩溃。 一个问题:你有你提到的连接文章的链接吗? @BrunoBrant 也许connect.microsoft.com/VisualStudio/feedback/details/661340/…【参考方案6】:

事实证明这不是错误,您需要做的就是设置池时间跨度以强制执行限制,似乎如果您不设置池,它将永远不会触发。我只是测试了它并没有需要包装器或任何额外的代码:

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

        ;

根据缓存的增长速度设置“PollingInterval”的值,如果增长过快,则增加轮询检查的频率,否则保持检查不是很频繁,以免造成开销。

【讨论】:

【参考方案7】:

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

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());
    

【讨论】:

你是说它会修剪还是不会修剪? 是的,它确实被修剪了。奇怪,考虑到人们似乎对MemoryCache 的所有问题。我想知道为什么这个示例有效。 我不关注它。我试着重复这个例子,但缓存仍然无限增长。 一个令人困惑的示例类:“Statlock”、“ItemCount”、“size”没用... NameValueCollection(3) 只包含 2 个项目?...实际上您创建了一个带有 sizelimit 的缓存和 pollInterval 属性,仅此而已! “不驱逐”物品的问题没有触及...

以上是关于MemoryCache 不遵守配置中的内存限制的主要内容,如果未能解决你的问题,请参考以下文章

.NET Core 2.0迁移技巧之MemoryCache问题修复

使用内存缓存时 Asp.Net MVC 中的 InvalidOperationException

如何清除内存缓存?

分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S C/S项目均可以使用!

内存缓存中的 Dotnet Core - 默认到期时间是啥

增加wp中的服务器内存限制-配置.php