MemoryCache nie przestrzega limitów pamięci w konfiguracji

87

Pracuję z klasą .NET 4.0 MemoryCache w aplikacji i próbuję ograniczyć maksymalny rozmiar pamięci podręcznej, ale z moich testów nie wynika, że ​​pamięć podręczna faktycznie przestrzega limitów.

Używam ustawień, które według MSDN mają ograniczać rozmiar pamięci podręcznej:

  1. CacheMemoryLimitMegabytes : maksymalna wielkość pamięci w megabajtach, do jakiej może wzrosnąć wystąpienie obiektu. "
  2. PhysicalMemoryLimitPercentage : "Procent pamięci fizycznej, której może używać pamięć podręczna, wyrażony jako liczba całkowita od 1 do 100. Wartość domyślna to zero, co oznacza, żeinstancje MemoryCache zarządzają własną pamięcią 1 na podstawie ilości pamięci zainstalowanej w komputer." 1. Nie jest to całkowicie poprawne - każda wartość poniżej 4 jest ignorowana i zastępowana 4.

Rozumiem, że te wartości są przybliżone i nie są twardymi limitami, ponieważ wątek, który czyści pamięć podręczną, jest uruchamiany co x sekund i jest również zależny od interwału sondowania i innych nieudokumentowanych zmiennych. Jednak nawet biorąc pod uwagę te wariancje, widzę szalenie niespójne rozmiary pamięci podręcznej, gdy pierwszy element jest eksmitowany z pamięci podręcznej po ustawieniu CacheMemoryLimitMegabytes i PhysicalMemoryLimitPercentage razem lub pojedynczo w aplikacji testowej. Dla pewności przeprowadziłem każdy test 10 razy i obliczyłem średnią wartość.

Oto wyniki testowania poniższego przykładowego kodu na 32-bitowym komputerze z systemem Windows 7 i 3 GB pamięci RAM. Rozmiar pamięci podręcznej jest pobierany po pierwszym wywołaniu CacheItemRemoved () w każdym teście. (Zdaję sobie sprawę, że rzeczywisty rozmiar pamięci podręcznej będzie większy niż ten)

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

Oto aplikacja testowa:

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

Dlaczego MemoryCache nie przestrzega skonfigurowanych limitów pamięci?

Canacourse
źródło
2
pętla for jest zła, bez i ++
xiaoyifang
4
Dodałem raport MS Connect dotyczący tego błędu (może ktoś już to zrobił, ale w każdym razie ...) connect.microsoft.com/VisualStudio/feedback/details/806334/ ...
Bruno Brant
3
Warto zauważyć, że Microsoft dodał teraz (stan na 9/2014) dość dokładną odpowiedź na bilet połączenia, do którego link znajduje się powyżej. TLDR polega na tym, że MemoryCache z natury nie sprawdza tych limitów przy każdej operacji, ale raczej, że limity są przestrzegane tylko przy przycinaniu wewnętrznej pamięci podręcznej, które jest okresowe w oparciu o dynamiczne wewnętrzne zegary.
Zakurzone
5
Wygląda na to, że zaktualizowali dokumentację dla MemoryCache.CacheMemoryLimit: „MemoryCache nie wymusza natychmiastowo CacheMemoryLimit za każdym razem, gdy nowy element jest dodawany do instancji MemoryCache. Wewnętrzna heurystyka, która usuwa dodatkowe elementy z MemoryCache, robi to stopniowo…” msdn.microsoft .com / en-us / library /…
Sully
1
@Zeus, myślę, że MSFT usunęło problem. W każdym razie firma MSFT zamknęła ten problem po krótkiej dyskusji ze mną, w której poinformowano mnie, że limit obowiązuje dopiero po wygaśnięciu PoolingTime.
Bruno Brant

Odpowiedzi:

100

Wow, więc spędziłem zbyt dużo czasu na kopaniu w CLR z reflektorem, ale myślę, że w końcu mam dobry uchwyt na to, co się tutaj dzieje.

Ustawienia są odczytywane poprawnie, ale wydaje się, że istnieje głęboko zakorzeniony problem w samym CLR, który wygląda tak, że spowoduje, że ustawienie limitu pamięci będzie zasadniczo bezużyteczne.

Poniższy kod jest odzwierciedlany z biblioteki DLL System.Runtime.Caching dla klasy CacheMemoryMonitor (istnieje podobna klasa, która monitoruje pamięć fizyczną i zajmuje się innym ustawieniem, ale ta jest ważniejsza):

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

Pierwszą rzeczą, którą możesz zauważyć, jest to, że nawet nie próbuje spojrzeć na rozmiar pamięci podręcznej, dopóki nie nastąpi wyrzucenie elementów bezużytecznych Gen2, zamiast tego po prostu cofa się do istniejącej wartości rozmiaru przechowywanego w cacheSizeSamples. Więc nigdy nie będziesz w stanie trafić w cel od razu, ale jeśli reszta zadziała, uzyskalibyśmy przynajmniej pomiar rozmiaru, zanim wpadniemy w prawdziwe kłopoty.

Zakładając więc, że wystąpił GC Gen2, napotkamy problem 2, który polega na tym, że ref2.ApproximateSize wykonuje okropną robotę, faktycznie przybliżając rozmiar pamięci podręcznej. Przebijając się przez śmieci CLR, odkryłem, że to jest System.SizedReference i oto co robi, aby uzyskać wartość (IntPtr jest uchwytem do samego obiektu MemoryCache):

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

Zakładam, że zewnętrzna deklaracja oznacza, że ​​w tym momencie nurkuje w niezarządzanych oknach i nie mam pojęcia, jak zacząć się dowiedzieć, co tam robi. Z tego, co zaobserwowałem, okropna robota polega na tym, że próba przybliżenia rozmiaru całej rzeczy jest straszna.

Trzecią zauważalną rzeczą jest wywołanie managera.UpdateCacheSize, które wygląda na to, że powinno coś zrobić. Niestety w każdej normalnej próbce tego, jak to powinno działać, s_memoryCacheManager zawsze będzie miało wartość null. Pole jest ustawiane z publicznego statycznego elementu członkowskiego ObjectCache.Host. Jest to eksponowane dla użytkownika, który może z nim zadzierać, jeśli tak zdecyduje, i faktycznie byłem w stanie zrobić to tak, jak powinno, przez zbicie mojej własnej implementacji IMemoryCacheManager, ustawienie jej na ObjectCache.Host, a następnie uruchomienie próbki . W tym momencie wydaje się jednak, że równie dobrze możesz po prostu stworzyć własną implementację pamięci podręcznej i nawet nie zawracać sobie głowy tym wszystkim, zwłaszcza że nie mam pojęcia, czy ustawić własną klasę na ObjectCache.Host (statyczny,

Muszę wierzyć, że przynajmniej część tego (jeśli nie kilka części) to zwykły błąd. Byłoby miło usłyszeć od kogoś ze stwardnienia rozsianego, o co chodzi z tym problemem.

Wersja TLDR tej gigantycznej odpowiedzi: Załóżmy, że CacheMemoryLimitMegabytes jest w tym momencie całkowicie wyeliminowana. Możesz ustawić go na 10 MB, a następnie przystąpić do zapełnienia pamięci podręcznej do ~ 2 GB i wydmuchania wyjątku braku pamięci bez wyzwalania usuwania elementu.

David Hay
źródło
4
Dobra odpowiedź, dziękuję. Zrezygnowałem z próby zrozumienia, co się z tym dzieje i zamiast tego zarządzam teraz rozmiarem pamięci podręcznej, licząc elementy wchodzące / wychodzące i wywołując ręcznie .Trim () w razie potrzeby. Pomyślałem, że System.Runtime.Caching był łatwym wyborem dla mojej aplikacji, ponieważ wydaje się być szeroko stosowany i dlatego pomyślałem, że nie będzie miał żadnych poważnych błędów.
Canacourse,
3
Łał. Dlatego tak kocham. Natrafiłem na dokładnie to samo zachowanie, napisałem aplikację testową i udało mi się wiele razy zawiesić komputer, mimo że czas odpytywania wynosił zaledwie 10 sekund, a limit pamięci podręcznej wynosił 1 MB. Dzięki za wszystkie spostrzeżenia.
Bruno Brant
7
Wiem, że właśnie wspomniałem o tym w pytaniu, ale ze względu na kompletność wspomnę o tym ponownie. W tym celu otworzyłem problem w Connect. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
1
Używam MemoryCache dla danych usługi zewnętrznej, a kiedy testuję, wstrzykując śmieci do MemoryCache, zawartość jest automatycznie przycinana, ale tylko wtedy, gdy używam wartości limitu procentowego. Absolutny rozmiar nie ogranicza rozmiaru, przynajmniej w przypadku przeprowadzania testów za pomocą profilera pamięci. Nie testowane w pętli while, ale przy bardziej „realistycznych” zastosowaniach (jest to system zaplecza, więc dodałem usługę WCF, która pozwala mi wstrzykiwać dane do pamięci podręcznych na żądanie).
Svend
Czy to nadal problem w .NET Core?
Павле
29

Wiem, że ta odpowiedź jest szalona późno, ale lepiej późno niż wcale. Chciałem poinformować, że napisałem wersję, MemoryCachektóra automatycznie rozwiązuje problemy z kolekcją Gen 2. Dlatego tnie, gdy interwał odpytywania wskazuje ciśnienie pamięci. Jeśli napotykasz ten problem, spróbuj!

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

Możesz go również znaleźć na GitHub, jeśli jesteś ciekawy, jak go rozwiązałem. Kod jest dość prosty.

https://github.com/haneytron/sharpmemorycache

Haney
źródło
2
Działa to zgodnie z oczekiwaniami, przetestowane z generatorem, który zapełnia pamięć podręczną ciągami 1000 znaków. Chociaż dodanie tego, co powinno być jak 100 MB do pamięci podręcznej, daje w rzeczywistości 200-300 MB do pamięci podręcznej, co wydało mi się dość dziwne. Może kilka podsłuchów, których nie liczę.
Karl Cassar
5
Ciągi @KarlCassar w .NET mają mniej więcej 2n + 20rozmiar w odniesieniu do bajtów, gdzie njest długością ciągu. Wynika to głównie z obsługi Unicode.
Haney
4

Przeprowadziłem kilka testów na przykładzie @Canacourse i modyfikacji @woany i myślę, że jest kilka krytycznych wywołań, które blokują czyszczenie pamięci podręcznej.

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

Ale dlaczego modyfikacja @woany wydaje się utrzymywać pamięć na tym samym poziomie? Po pierwsze, RemovedCallback nie jest ustawiony i nie ma wyjścia konsoli ani oczekiwania na dane wejściowe, które mogłyby zablokować wątek pamięci podręcznej.

Po drugie...

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) co ~ 1000th AddItem () miałby ten sam efekt.

Cóż, nie jest to bardzo szczegółowe badanie problemu, ale wygląda na to, że wątek pamięci MemoryCache nie ma wystarczającej ilości czasu procesora na wyczyszczenie, podczas gdy wiele nowych elementów jest dodawanych.

Jezze
źródło
4

Napotkałem również ten problem. Buforuję obiekty, które są uruchamiane w moim procesie dziesiątki razy na sekundę.

Zauważyłem, że następująca konfiguracja i użytkowanie zwalnia elementy co 5 sekund przez większość czasu .

App.config:

Zwróć uwagę na cacheMemoryLimitMegabytes . Gdy to było ustawione na zero, procedura oczyszczania nie uruchomiłaby się w rozsądnym czasie.

   <system.runtime.caching>
    <memoryCache>
      <namedCaches>
        <add name="Default" cacheMemoryLimitMegabytes="20" physicalMemoryLimitPercentage="0" pollingInterval="00:00:05" />
      </namedCaches>
    </memoryCache>
  </system.runtime.caching>  

Dodawanie do pamięci podręcznej:

MemoryCache.Default.Add(someKeyValue, objectToCache, new CacheItemPolicy { AbsoluteExpiration = DateTime.Now.AddSeconds(5), RemovedCallback = cacheItemRemoved });

Potwierdzenie, że usunięcie pamięci podręcznej działa:

void cacheItemRemoved(CacheEntryRemovedArguments arguments)
{
    System.Diagnostics.Debug.WriteLine("Item removed from cache: {0} at {1}", arguments.CacheItem.Key, DateTime.Now.ToString());
}
Aaron Hudon
źródło
3

(Na szczęście) natknąłem się na ten przydatny post wczoraj, kiedy po raz pierwszy próbowałem użyć MemoryCache. Myślałem, że będzie to prosty przypadek ustawiania wartości i używania klas, ale napotkałem podobne problemy opisane powyżej. Aby spróbować zobaczyć, co się dzieje, wyodrębniłem źródło za pomocą ILSpy, a następnie skonfigurowałem test i przeszedłem przez kod. Mój kod testowy był bardzo podobny do kodu powyżej, więc go nie opublikuję. Z moich testów zauważyłem, że pomiar rozmiaru pamięci podręcznej nigdy nie był szczególnie dokładny (jak wspomniano powyżej) i biorąc pod uwagę obecną implementację, nigdy nie działałby niezawodnie. Jednak fizyczny pomiar był w porządku i jeśli fizyczna pamięć była mierzona przy każdym sondażu, wydawało mi się, że kod będzie działał niezawodnie. Więc usunąłem sprawdzanie wyrzucania elementów bezużytecznych gen 2 w MemoryCacheStatistics;

W scenariuszu testowym robi to oczywiście dużą różnicę, ponieważ pamięć podręczna jest nieustannie uderzana, więc obiekty nigdy nie mają szansy na dotarcie do generacji 2. Myślę, że zamierzamy użyć zmodyfikowanej kompilacji tej biblioteki dll w naszym projekcie i użyć oficjalnego MS build, gdy wyjdzie .net 4.5 (który zgodnie z powyższym artykułem connect powinien mieć poprawkę). Logicznie rozumiem, dlaczego wprowadzono kontrolę gen 2, ale w praktyce nie jestem pewien, czy ma to sens. Jeśli pamięć osiągnie 90% (lub jakikolwiek limit, na jaki został ustawiony), nie powinno mieć znaczenia, czy kolekcja gen 2 wystąpiła, czy nie, elementy powinny zostać eksmitowane niezależnie.

Zostawiłem mój kod testowy działający przez około 15 minut z fizycznymMemoryLimitPercentage ustawionym na 65%. Widziałem, że podczas testu zużycie pamięci utrzymuje się na poziomie 65-68% i widziałem, jak rzeczy są prawidłowo eksmitowane. W moim teście ustawiłem pollingInterval na 5 sekund, physicsMemoryLimitPercentage na 65 i physicsMemoryLimitPercentage na 0, aby domyślnie to.

Postępując zgodnie z powyższą radą; można by wykonać implementację IMemoryCacheManager w celu usunięcia rzeczy z pamięci podręcznej. Miałoby to jednak negatywny wpływ na wspomniany problem z czekiem gen 2. Chociaż w zależności od scenariusza może to nie stanowić problemu w kodzie produkcyjnym i może działać wystarczająco dla ludzi.

Ian Gibson
źródło
4
Aktualizacja: używam .NET framework 4.5 i w żaden sposób problem nie został rozwiązany. Pamięć podręczna może urosnąć na tyle, aby spowodować awarię komputera.
Bruno Brant
Pytanie: czy masz link do wspomnianego artykułu dotyczącego połączenia?
Bruno Brant
3

Okazało się, że to nie jest błąd, wszystko, co musisz zrobić, to ustawić czas pulowania, aby wymusić limity, wydaje się, że jeśli zostawisz pulę nie ustawioną, nigdy się nie uruchomi. lub dowolny dodatkowy kod:

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

        };

Ustaw wartość „ PollingInterval” w zależności od tego, jak szybko rośnie pamięć podręczna, jeśli rośnie zbyt szybko, zwiększ częstotliwość sprawdzania odpytywania, w przeciwnym razie sprawdzaj je niezbyt często, aby nie powodować narzutów.

sino
źródło
1

Jeśli używasz następującej zmodyfikowanej klasy i monitorujesz pamięć za pomocą Menedżera zadań, w rzeczywistości jest ona przycinana:

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());
    }
}
woany
źródło
Czy mówisz, że jest przycinany czy nie?
Canacourse,
Tak, jest przycinany. Dziwne, biorąc pod uwagę wszystkie problemy, z którymi ludzie się zdają MemoryCache. Zastanawiam się, dlaczego ta próbka działa.
Daniel Lidström
1
Nie podążam za tym. Próbowałem powtórzyć przykład, ale pamięć podręczna wciąż rośnie w nieskończoność.
Bruno Brant
Mylące klasy przykładowe: „Statlock”, „ItemCount”, „size” są bezużyteczne ... NameValueCollection (3) zawiera tylko 2 elementy? ... W rzeczywistości utworzyłeś pamięć podręczną z właściwościami sizelimit i pollInterval, nic więcej! Problem „niewyrzucania” przedmiotów nie został poruszony ...
Bernhard,