MemoryCache не подчиняется ограничениям памяти в конфигурации - PullRequest
83 голосов
/ 01 августа 2011

Я работаю с классом .NET 4.0 MemoryCache в приложении и пытаюсь ограничить максимальный размер кэша, но в моих тестах не показывалось, что кэшфактически соблюдая ограничения.

Я использую настройки, которые, согласно MSDN , должны ограничивать размер кэша:

  1. CacheMemoryLimitMegabytes : максимальный объем памяти в мегабайтах, до которого может увеличиться экземпляр объекта. "
  2. PhysicalMemoryLimitPercentage : " Процентное соотношениефизической памяти, которую может использовать кэш, выражается целочисленным значением от 1 до 100. По умолчанию используется ноль, что означает, что MemoryCache экземпляры управляют собственной памятью 1 на основе объема памяти, установленного на компьютере. " 1. Это не совсем правильно - любое значение ниже 4 игнорируется и заменяется на 4.

Я понимаю, что эти значения являются приблизительными, а не жесткими ограничениями, поскольку поток, который очищает кэш, запускается каждые x секунд, а также зависит от интервала опроса и других недокументированных переменных. Однако даже с учетомУчитывая эти различия, я вижу крайне несовместимые размеры кэша, когда первый элемент удаляется из кэша после установки CacheMemoryLimitMegabytes и PhysicalMemoryLimitPercentage вместе или по отдельности в тестовом приложении.Чтобы убедиться, что я выполнил каждый тест 10 раз и вычислил среднее значение.

Это результаты тестирования приведенного ниже примера кода на 32-разрядном ПК с Windows 7 с 3 ГБ ОЗУ.Размер кэша берется после первого вызова 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 не подчиняетсянастроенные пределы памяти?

Ответы [ 7 ]

96 голосов
/ 11 сентября 2011

Ух ты, поэтому я потратил слишком много времени на копание в CLR с отражателем, но я думаю, что я наконец-то хорошо разбираюсь в том, что здесь происходит.

Настройки читаются правильно,но, похоже, в самой CLR есть глубокая проблема, которая выглядит так, как будто это сделает настройку предела памяти практически бесполезной.

Следующий код отражен из DLL-файла System.Runtime.Caching дляКласс 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.Таким образом, вы никогда не сможете поразить цель с самого начала, но если все остальное сработает, мы, по крайней мере, получим измерение размера, прежде чем у нас возникнут реальные проблемы.

Итак, предположив, что произошел GC Gen2, мы столкнулись с проблемой 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 МБ, а затем приступить к заполнению кеша до ~ 2 ГБ и исключить исключение нехватки памяти без отключения удаления элемента.

29 голосов
/ 07 апреля 2014

Я знаю, что этот ответ сумасшедший поздно, но лучше поздно, чем никогда.Я хотел, чтобы вы знали, что я написал версию MemoryCache, которая автоматически разрешает проблемы с коллекцией Gen 2.Поэтому он обрезается всякий раз, когда интервал опроса указывает на давление памяти.Если у вас возникла эта проблема, попробуйте!

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

Вы также можете найти ее на GitHub, если вам интересно, как я ее решил.Код несколько прост.

https://github.com/haneytron/sharpmemorycache

4 голосов
/ 12 августа 2015

Я тоже сталкивался с этой проблемой.Я кеширую объекты, которые запускаются в мой процесс десятки раз в секунду.

Я обнаружил, что следующая конфигурация и использование освобождают элементы каждые 5 секунд большую часть времени .

App.config:

Обратите внимание на cacheMemoryLimitMegabytes .Если для этого параметра установлено значение 0, процедура очистки не будет запускаться в разумные сроки.

   <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());
}
3 голосов
/ 11 ноября 2013

Я провел некоторое тестирование на примере @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("...");
}

A Thread.Sleep(1) каждый ~ 1000-й AddItem () будет иметь тот же эффект.

Что ж, это не очень глубокое исследование проблемы, но выглядит так, как будто поток MemoryCache не получает достаточно времени ЦП дляочистка, в то время как добавлено много новых элементов.

3 голосов
/ 24 ноября 2011

Я (к счастью) вчера наткнулся на этот полезный пост, когда впервые пытался использовать MemoryCache.Я думал, что это будет простой случай установки значений и использования классов, но я столкнулся с похожими проблемами, описанными выше.Чтобы попытаться увидеть, что происходит, я извлек исходный код, используя ILSpy, а затем настроил тест и прошел по коду.Мой тестовый код был очень похож на код выше, поэтому я не буду его публиковать.Из моих тестов я заметил, что измерение размера кэша никогда не было особенно точным (как упомянуто выше) и учитывая, что текущая реализация никогда не будет работать надежно.Однако физические измерения были хорошими, и если физическая память измерялась при каждом опросе, тогда мне казалось, что код будет работать надежно.Итак, я удалил проверку сборки мусора 2-го поколения в MemoryCacheStatistics;при нормальных условиях измерения памяти не будут выполняться, если после последнего измерения не было другой сборки мусора второго поколения.

В тестовом сценарии это, очевидно, имеет большое значение, поскольку кэш постоянно поражается, поэтому у объектов никогда не будет шанса добраться до второго поколения. Я думаю, что мы собираемся использовать модифицированную сборку этой dll на нашемспроектируйте и используйте официальную сборку MS, когда выйдет .net 4.5 (который в соответствии с упомянутой выше статьей о подключении должен иметь исправление).Логически я понимаю, почему была введена проверка второго поколения, но на практике я не уверен, имеет ли она смысл.Если объем памяти достигает 90% (или какого-либо другого предела), то не имеет значения, произошла коллекция 2-го поколения или нет, предметы должны быть исключены независимо.

Я оставил свой тестовый код включенным примерно на 15 минут с физическим значениемMemoryLimitPercentage, установленным на 65%.Я видел, как во время теста использование памяти оставалось между 65-68%, и видел, как вещи выселяются правильно.В моем тесте я установил для pollingInterval значение 5 секунд, для физического элемента MEMoryLimitPercentage значение по умолчанию равно 65, а для параметра физического элемента MEMORYLimitPercentage значение равно 0.

Следуя приведенным выше советам;может быть реализована реализация IMemoryCacheManager для удаления объектов из кэша.Однако он будет страдать от упомянутой проблемы проверки второго поколения.Хотя, в зависимости от сценария, это может не быть проблемой в рабочем коде и может работать достаточно для людей.

2 голосов
/ 12 июля 2012

Если вы используете следующий измененный класс и следите за памятью через диспетчер задач, он действительно обрезается:

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());
    }
}
1 голос
/ 07 декабря 2018

Оказалось, что это не ошибка, все, что вам нужно сделать, это установить промежуток времени пула, чтобы применить ограничения, кажется, если вы оставите пул не установленным, он никогда не сработает. Я просто проверил это и нет Нужны обертки или любой дополнительный код:

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

        };

Установите значение "PollingInterval" в зависимости от того, насколько быстро растет кэш, если он растет слишком быстро, увеличьте частоту опросов, в противном случае проверки будут выполняться не очень часто, чтобы не вызывать накладных расходов.

...