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 не дотримується налаштованих обмежень пам'яті?


2
for loop помилковий, без i ++
xiaoyifang

4
Я додав звіт MS Connect про цю помилку (можливо, хтось уже це робив, але в будь-якому випадку ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Бруно Брант

3
Варто зазначити, що зараз Microsoft (станом на 9/2014) додала досить ґрунтовну відповідь на зв’язаний вище квиток на підключення. Його TLDR полягає в тому, що 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. Тож ви ніколи не зможете потрапити прямо в ціль, але якби решта спрацювала, ми б принаймні отримали вимірювання розміру, перш ніж у нас виникли справжні проблеми.

Отже, припускаючи, що стався GC Gen2, ми стикаємося з проблемою 2, яка полягає у тому, що ref2.ApproximateSize робить жахливу роботу, фактично наближаючи розмір кешу. Проникаючи через сміття CLR, я виявив, що це System.SizedReference, і це те, що він робить, щоб отримати значення (IntPtr - це дескриптор самого об'єкта MemoryCache):

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

Я припускаю, що декларація extern означає, що вона зараз занурюється у некеровану землю вікон, і я не уявляю, як почати з'ясовувати, що вона там робить. З того, що я спостерігав, хоча це робить жахливу роботу, намагаючись наблизити розмір загальної речі.

Третя помітна річ - це виклик менеджера. UpdateCacheSize, який, здається, повинен щось робити. На жаль, у будь-якому звичайному зразку того, як це має працювати, s_memoryCacheManager завжди буде нульовим. Поле встановлюється із загальнодоступного статичного члена ObjectCache.Host. З цим користувач може возитися, якщо він так вирішить, і я насправді зміг змусити цю штуку працювати так, як це передбачається, склавши мою власну реалізацію IMemoryCacheManager, встановивши її на ObjectCache.Host, а потім запустивши зразок . На той момент, схоже, ви могли б просто зробити власну реалізацію кешу і навіть не заморочуватися з усіма цими речами, тим більше, що я не маю ідеї, якщо встановлювати свій власний клас на ObjectCache.Host (static,

Я повинен вірити, що принаймні частина цього (якщо не кілька частин) - це просто пряма помилка. Було б приємно почути від когось із MS, що було з цією справою.

TLDR-версія цієї гігантської відповіді: Припустимо, що CacheMemoryLimitMegabytes на даний момент повністю зруйнований. Ви можете встановити для нього 10 МБ, а потім перейти до заповнення кеш-пам'яті до ~ 2 ГБ і видалити виняток з пам'яті, не спрацювавши видалення елемента.


4
Чудова відповідь спасибі. Я відмовився від спроб з'ясувати, що з цим відбувається, і натомість тепер керую розміром кешу, підраховуючи елементи в / з і викликаючи .Trim () вручну за потреби. Я вважав, що System.Runtime.Caching є простим вибором для мого додатка, оскільки він, здається, широко використовується, і тому я думав, що у нього не буде серйозних помилок.
Канекюр

3
Ого. Ось чому я так люблю. Я зіткнувся з точно такою ж поведінкою, написав тестовий додаток і встиг багато разів зламати свій ПК, хоча час опитування був лише 10 секунд, а обмеження кеш-пам'яті - 1 МБ. Дякую за всі ідеї.
Бруно Брант

7
Я знаю, що я щойно згадав про це у питанні, але для повноти згадаю ще раз. Я відкрив для цього проблему в Connect. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Бруно Брант

1
Я використовую MemoryCache для зовнішніх службових даних, і коли я тестую, вводячи сміття в MemoryCache, він виконує автоматичне обрізання вмісту, але лише при використанні відсоткового граничного значення. Абсолютний розмір нічим не обмежує розмір, принаймні, коли виконується інсекція за допомогою профайлера пам'яті. Не тестується за деякий час, але за більш "реалістичним" звичаєм (це серверна система, тому я додав службу WCF, яка дозволяє вводити дані в кеші на вимогу).
Свенд,

Чи все ще це проблема у .NET Core?
Павле

29

Я знаю, що ця відповідь божевільна пізно, але краще пізно, ніж ніколи. Я хотів повідомити вам, що написав версію, MemoryCacheяка автоматично вирішує проблеми з колекцією 2. Тому він обрізається, коли інтервал опитування вказує на тиск пам'яті. Якщо у вас виникає ця проблема, спробуйте!

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

Ви також можете знайти його на GitHub, якщо вам цікаво, як я це вирішив. Код дещо простий.

https://github.com/haneytron/sharpmemorycache


2
Це працює за призначенням, перевірено генератором, який заповнює кеш навантаженням рядків 1000 символів. Хоча, додавання того, що повинно бути приблизно 100 МБ до кешу, додає насправді 200 - 300 МБ до кешу, що мені здалося досить дивним. Можливо, якісь підслухані я не рахую.
Карл Кассар,

5
Рядки @KarlCassar у .NET мають приблизно 2n + 20розмір щодо байтів, де nє довжина рядка. Це здебільшого завдяки підтримці Unicode.
Хейні

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 не отримує достатньо часу процесора для очищення, тоді як додано багато нових елементів.


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 build, коли виходить .net 4.5 (який відповідно до згаданої вище статті про підключення повинен мати виправлення). Логічно я бачу, чому перевірку Gen 2 було застосовано, але на практиці я не впевнений, чи має це сенс. Якщо пам’ять досягає 90% (або якого б обмеження для нього не було встановлено), тоді не має значення, відбулася колекція Gen 2 чи ні, предмети повинні бути виселені незалежно.

Я залишив свій тестовий код запущеним близько 15 хвилин із фізичним значеннямMemoryLimitPercentage, встановленим на 65%. Я бачив, як використання пам’яті залишалося в межах 65-68% під час тесту, і бачив, як речі виселяються належним чином. У моєму тесті я встановив pollingInterval на 5 секунд, physicalMemoryLimitPercentage - на 65, а physicalMemoryLimitPercentage - на 0 за замовчуванням.

Дотримуючись вищевказаних порад; реалізація IMemoryCacheManager може бути зроблена для виселення речей з кешу. Однак це постраждає від згаданої проблеми перевірки покоління 2. Хоча, залежно від сценарію, це не може бути проблемою у виробничому коді і може працювати достатньо для людей.


4
Оновлення: я використовую .NET framework 4.5, і жодним чином проблема не виправлена. Кеш-пам’ять може зрости досить великим, щоб зламати машину.
Бруно Брант

Питання: чи є у вас посилання на згадану статтю про підключення?
Бруно Брант


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, не більше того! Проблема "не виселення" предметів не зачіпається ...
Бернхард,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.