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


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

6 79

6 ответов:

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

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

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

Итак, предполагая, что GC Gen2 произошел, мы сталкиваемся с проблемой 2, которая заключается в том, что ref2.ApproximateSize делает ужасную работу по фактическому приближению размера кэша. Пробираясь через мусор CLR, я обнаружил, что это система.SizedReference, и это то, что он делает, чтобы получить значение (IntPtr-это дескриптор самого объекта MemoryCache):

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

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

третья заметная вещь-это звонок менеджеру.UpdateCacheSize, который звучит так, как будто он должен что-то делать. К сожалению, в любом обычном примере того, как это должно работать s_memoryCacheManager всегда будет null. Поле задается из открытого статического элемента ObjectCache.Хозяин. Это выставляется для пользователь, с которым можно возиться, если он так хочет, и я действительно смог сделать эту вещь такой, как она должна работать, объединив мою собственную реализацию IMemoryCacheManager, установив ее в ObjectCache.Хост, а затем запуск образца. В этот момент, однако, кажется, что вы можете просто сделать свою собственную реализацию кэша и даже не беспокоиться обо всем этом, тем более, что я понятия не имею, если установить свой собственный класс В ObjectCache.Хост (статический, поэтому он влияет на каждый из них что может быть там, в процессе) для измерения кэша может испортить другие вещи.

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

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

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

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

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

https://github.com/haneytron/sharpmemorycache

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

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

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

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

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

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

приложение.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());
}

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

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 и модификации @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 Нитка.Сон (1) каждый ~1000-й AddItem () будет иметь тот же эффект.

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