MemoryCache在配置中不遵守内存限制。

94
我正在应用程序中使用.NET 4.0的MemoryCache类,并尝试限制最大缓存大小,但在我的测试中,缓存似乎没有遵守限制。根据MSDN的说明,我正在使用以下设置来限制缓存大小:
  1. CacheMemoryLimitMegabytes:对象实例可以增长到的最大内存大小(以兆字节为单位)。
  2. 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不遵守配置的内存限制?

2
for循环有误,没有i++。 - xiaoyifang
4
我已经添加了一个 MS Connect 的报告来描述这个 bug(也许别人已经添加过了,但无论如何……)https://connect.microsoft.com/VisualStudio/feedback/details/806334/system-runtime-caching-memorycache-do-not-respect-memory-limits - Bruno Brant
4
值得注意的是,微软现在(截至2014年9月)已对上述连接的相关问题作出了相当详尽的回应。回应的要点是,MemoryCache并不会在每次操作时本质上都检查这些限制,而是只有在内部缓存修剪期间才会尊重这些限制,这是基于动态内部计时器定期进行的。 - Dusty
6
看起来他们更新了MemoryCache.CacheMemoryLimit文档:“每次向MemoryCache实例添加新项时,MemoryCache不会立即强制执行CacheMemoryLimit。内部启发式算法逐渐清除MemoryCache中的额外项…” https://msdn.microsoft.com/zh-cn/library/system.runtime.caching.memorycache.cachememorylimit(v=vs.110).aspx - Sully
1
@Zeus,我认为微软已经解决了这个问题。无论如何,在与我进行一些讨论后,微软关闭了这个问题,告诉我限制只有在PoolingTime过期后才会生效。 - Bruno Brant
显示剩余4条评论
7个回答

105

哇,我刚刚在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,我们遇到了第二个问题,即ref2.ApproximateSize实际上并没有很好地近似缓存的大小。通过CLR垃圾中的System.SizedReference,我发现这是它正在执行的操作(IntPtr是对MemoryCache对象本身的句柄):

[SecurityCritical]
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern long GetApproximateSizeOfSizedRef(IntPtr h);
我假设extern声明意味着它在这一点上进入了未管理的Windows领域,我不知道如何开始查找它在那里做什么。从我观察到的情况来看,它非常糟糕地尝试近似整个物体的大小。
第三个显著的事情是对manager.UpdateCacheSize的调用,听起来应该会做一些事情。不幸的是,在任何正常的工作示例中,s_memoryCacheManager都将为null。该字段是从public static成员ObjectCache.Host设置的。如果用户愿意,可以修改此设置,我实际上能够通过拼凑自己的IMemoryCacheManager实现并将其设置为ObjectCache.Host,然后运行示例使其正常工作。但是,此时似乎您可能只需创建自己的高速缓存实现,并且甚至不需要理会所有这些东西,特别是因为我不知道将自己的类设置为ObjectCache.Host(静态,因此会影响进程中可能存在的每个类)来测量缓存是否会损坏其他内容。
我必须相信,这其中至少一部分(如果不是几部分)只是一个直截了当的错误。听起来很好听的是,有人可以告诉我们MS的问题是什么。
简而言之,假设CacheMemoryLimitMegabytes在此时完全破损。您可以将其设置为10 MB,然后继续填充缓存以达到约2GB,并引发内存不足异常,而无需删除项。

4
非常感谢您的精彩回答。我放弃了试图弄清楚这是怎么回事,现在通过计算进出项并根据需要手动调用.Trim()来管理缓存大小。我曾认为System.Runtime.Caching是我的应用程序的一个不错选择,因为它似乎被广泛使用,我认为因此不会有任何重大错误。 - Canacourse
3
哇,这就是我喜欢 Stack Overflow 的原因。我遇到了完全相同的行为,编写了一个测试应用程序,尽管轮询时间低至10秒且缓存内存限制为1MB,但仍然多次导致我的电脑崩溃。感谢您提供的所有见解。 - Bruno Brant
7
我知道在问题中已经提到过了,但为了完整起见,我会在这里再次提及它。我已经在Connect上提交了一个问题。http://connect.microsoft.com/VisualStudio/feedback/details/806334/system-runtime-caching-memorycache-do-not-respect-memory-limits - Bruno Brant
1
我正在使用MemoryCache来存储外部服务数据,当我注入垃圾数据到MemoryCache中进行测试时,它确实会自动清理内容,但仅在使用百分比限制值时才会这样。绝对大小对于限制大小没有任何作用,至少在使用内存分析器检查时是如此。虽然没有在while循环中进行测试,但通过更“现实”的用法进行了测试(它是一个后端系统,因此我添加了一个WCF服务,可以让我按需向缓存中注入数据)。 - Svend
1
在.NET Core中,这个问题还存在吗? - Павле
@Павле https://dev59.com/xm035IYBdhLWcg3wPNVk 和 https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory?view=aspnetcore-5.0 - Igor Beaufils

31

我知道这个回答有点晚,但迟到总比没有好。我想告诉你,我编写了一个版本的MemoryCache,它可以自动解决Gen 2集合的问题。因此,每当轮询间隔指示内存压力时,它就会进行修剪。如果你遇到这个问题,请试试它!

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

如果你想知道我是如何解决这个问题的,你也可以在GitHub上找到它。代码相对简单。

https://github.com/haneytron/sharpmemorycache


2
这个程序按照预期工作,在使用一个生成器测试时,它会用大量1000个字符的字符串填充缓存。然而,将应该像100MB一样的内容添加到缓存中实际上会增加200-300MB的缓存空间,我觉得这很奇怪。可能是我没有计算到的一些开销。 - Karl Cassar
5
在.NET中,字符串的大小大约为2n + 20个字节,其中n是字符串的长度。这主要是由于Unicode支持所导致的。 - Haney

5

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

我发现以下配置和用法可以在大多数情况下每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时间进行清理。


3
我很庆幸昨天在第一次尝试使用MemoryCache时发现了这篇有用的文章。我以为只需要设置值并使用类就可以了,但是遇到了上面提到的类似问题。为了尝试看看发生了什么,我使用ILSpy提取了源代码,然后设置了一个测试并逐步执行了代码。我的测试代码与上面的代码非常相似,所以我不会贴出来。从我的测试中,我注意到缓存大小的测量从未特别准确(如上所述),并且在当前实现下永远不会可靠地工作。然而,物理测量很好,如果每次轮询时测量物理内存,那么对我来说代码似乎会可靠地工作。因此,我删除了MemoryCacheStatistics中的gen 2垃圾收集检查;在正常情况下,除非自上次测量以来发生了另一个gen 2垃圾收集,否则不会进行任何内存测量。

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

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

按照上面的建议,可以实现IMemoryCacheManager来从缓存中删除内容。然而,它会遇到上述gen 2检查问题。尽管如此,根据情况不同,在生产代码中可能不是问题,并且对于人们来说工作足够。


4
更新:我正在使用.NET Framework 4.5,但问题并没有得到解决。缓存可能会变得足够大,以至于导致机器崩溃。 - Bruno Brant
一个问题:你有提到的连接文章的链接吗? - Bruno Brant
@BrunoBrant 或许可以看一下这个链接:https://connect.microsoft.com/VisualStudio/feedback/details/661340/memorycache-evictions-do-not-fire-when-memory-limits-are-reached。当内存限制达到时,MemoryCache驱逐不会触发。 - Ohad Schneider

3

事实证明这不是一个错误,你需要做的就是设置池化时间间隔来强制限制。如果你不设置池化时间间隔,似乎它永远不会触发。我刚刚测试过了,不需要包装器或任何额外的代码:

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

        };

根据缓存增长的速度设置“PollingInterval”的值,如果增长过快,则增加轮询检查的频率,否则保持检查不太频繁以避免造成额外负担。


1
如果您使用以下修改后的类,并通过任务管理器监视内存,确实会得到修剪:
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());
    }
}

你是在说它被修剪了还是没有被修剪? - Canacourse
是的,它确实被修剪了。奇怪的是,考虑到人们似乎在使用“MemoryCache”时遇到了很多问题,我想知道为什么这个示例可以正常工作。 - Daniel Lidström
1
我不明白。我尝试重复示例,但缓存仍然无限增长。 - Bruno Brant
一个令人困惑的示例类:“Statlock”,“ItemCount”,“size”都是无用的... NameValueCollection(3)只包含2个项目?...实际上,您创建了一个具有sizelimit和pollInterval属性的缓存,仅此而已! “不清除”项目的问题没有得到解决... - Bernhard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接