MemoryCache не соответствует ограничениям памяти в конфигурации

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

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

  • CacheMemoryLimitMegabytes: максимальный размер памяти в мегабайтах, который может увеличить экземпляр объекта. "
  • 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 не подчиняется установленным ограничениям памяти?

Ответ 1

Ничего себе, поэтому я просто потратил слишком много времени на поиск в 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. Таким образом, вы никогда не сможете достичь цели прямо, но если все остальное будет работать, мы, по крайней мере, получим измерение размера, прежде чем у нас появятся реальные проблемы.

Итак, предполагая, что Gen2 GC произошел, мы столкнулись с проблемой 2, которая заключается в том, что ref2.ApproximateSize делает ужасную работу, фактически приближающую размер кеша. Slogging через CLR junk Я обнаружил, что это 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, что сделка была с этой штукой.

Версия TL;DR этого гигантского ответа: предположим, что CacheMemoryLimitMegabytes полностью перегружен в этот момент времени. Вы можете установить его на 10 МБ, а затем продолжить заполнять кеш до ~ 2 ГБ и выпустить исключение из памяти без отключения удаления элемента.

Ответ 2

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

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

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

https://github.com/haneytron/sharpmemorycache

Ответ 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

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

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

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

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

Ответ 5

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

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

Ответ 6

Я провел некоторое тестирование с примером @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) каждый ~ 1000th AddItem() будет иметь тот же эффект.

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

Ответ 7

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

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

        };

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