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

87

Я работаю с классом .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 не подчиняется установленным ограничениям памяти?

Canacourse
источник
2
цикл for неверен, без i ++
xiaoyifang
4
Я добавил отчет MS Connect об этой ошибке (возможно, кто-то уже сделал это, но в любом случае ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Бруно Брант,
3
Стоит отметить, что Microsoft теперь (по состоянию на 9/2014) добавила довольно подробный ответ на запрос подключения, указанный выше. TL; DR заключается в том, что MemoryCache по своей сути не проверяет эти ограничения при каждой операции, а скорее, что ограничения соблюдаются только при обрезке внутреннего кеша, которая является периодической на основе динамических внутренних таймеров.
Дасти
5
Похоже, они обновили документацию для MemoryCache.CacheMemoryLimit: «MemoryCache не мгновенно применяет CacheMemoryLimit каждый раз, когда новый элемент добавляется в экземпляр MemoryCache. Внутренняя эвристика, которая вытесняет лишние элементы из MemoryCache, делает это постепенно ...» msdn.microsoft .com / en-us / library /…
Салли,
1
@Zeus, я думаю, что MSFT устранила проблему. В любом случае MSFT закрыла проблему после некоторого обсуждения со мной, в котором они сказали мне, что ограничение применяется только после истечения времени PoolingTime.
Бруно Брант,

Ответы:

100

Вау, я просто потратил слишком много времени, копаясь в 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 произошел, мы сталкиваемся с проблемой 2, которая заключается в том, что 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. Поле устанавливается из общедоступного статического члена ObjectCache.Host. Это открыто для пользователя, с которым он может возиться, если он того пожелает, и я действительно смог заставить эту работу работать так, как предполагалось, путем объединения моей собственной реализации IMemoryCacheManager, установки ее на ObjectCache.Host, а затем запуска образца . Однако в этот момент кажется, что вы могли бы просто создать свою собственную реализацию кеша и даже не беспокоиться обо всем этом, тем более что я понятия не имею, устанавливаете ли ваш собственный класс в ObjectCache.Host (static,

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

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

Дэвид Хэй
источник
4
Прекрасный ответ, спасибо. Я отказался от попыток выяснить, что с этим происходит, и теперь управляю размером кеша, подсчитывая количество входящих / исходящих элементов и вызывая .Trim () вручную по мере необходимости. Я думал, что System.Runtime.Caching был легким выбором для моего приложения, так как он, кажется, широко используется, и поэтому я думал, что в нем не будет серьезных ошибок.
Canacourse
3
Вау. Вот почему я ТАК люблю. Я столкнулся с точно таким же поведением, написал тестовое приложение и мне удалось много раз вывести компьютер из строя, хотя время опроса составляло всего 10 секунд, а ограничение кеш-памяти составляло 1 МБ. Спасибо за понимание.
Бруно Брант,
7
Я знаю, что только что упомянул об этом в вопросе, но для полноты картины я упомяну это здесь еще раз. Я открыл для этого проблему в Connect. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Бруно Брант,
1
Я с помощью MemoryCache данных внешних услуг, и когда я проверяю, вводя мусор в MemoryCache, он делает автоматическую обрезку содержания, но только при использовании процента предельного значения. Абсолютный размер никак не ограничивает размер, по крайней мере, при использовании профилировщика памяти. Не тестировалось в цикле while, но более «реалистично» использовалось (это серверная система, поэтому я добавил службу WCF, которая позволяет мне вводить данные в кеши по запросу).
Svend
Это все еще проблема в .NET Core?
Павле
29

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

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

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

https://github.com/haneytron/sharpmemorycache

Хейни
источник
2
Это работает по назначению, протестировано с помощью генератора, который заполняет кеш множеством строк по 1000 символов. Хотя добавление того, что должно быть примерно 100 МБ в кеш, на самом деле добавляет к кешу 200–300 МБ, что я нашел довольно странным. Может какие подслушанные я не в счет.
Karl Cassar
5
Строки @KarlCassar в .NET примерно соответствуют 2n + 20размеру байтов, где n- длина строки. В основном это связано с поддержкой Unicode.
Haney
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("...");
}

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

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

Jezze
источник
4

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

Я обнаружил, что следующая конфигурация и использование освобождают элементы каждые 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());
}
Аарон Худон
источник
3

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

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

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

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

Ян Гибсон
источник
4
Обновление: я использую .NET framework 4.5, и проблема никоим образом не решена. Кэш может стать достаточно большим, чтобы вывести компьютер из строя.
Бруно Брант,
Вопрос: есть ли у вас ссылка на упомянутую статью о подключении?
Бруно Брант,
@BrunoBrant, возможно, connect.microsoft.com/VisualStudio/feedback/details/661340/…
Охад Шнайдер,
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. Интересно, почему этот образец работает.
Даниэль Лидстрём
1
Я не слежу за этим. Я попытался повторить пример, но кеш все равно растет бесконечно.
Бруно Брант,
Запутанный пример класса: "Statlock", "ItemCount", "size" бесполезны ... NameValueCollection (3) содержит только 2 элемента? ... Фактически, вы создали кеш со свойствами sizelimit и pollInterval, не более того! Проблема «невывоза» вещей не коснулась ...
Бернхард