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:
- CacheMemoryLimitMegabytes : maksymalna wielkość pamięci w megabajtach, do jakiej może wzrosnąć wystąpienie obiektu. "
- 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?
źródło
Odpowiedzi:
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.
źródło
Wiem, że ta odpowiedź jest szalona późno, ale lepiej późno niż wcale. Chciałem poinformować, że napisałem wersję,
MemoryCache
któ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
źródło
2n + 20
rozmiar w odniesieniu do bajtów, gdzien
jest długością ciągu. Wynika to głównie z obsługi Unicode.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.
źródło
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()); }
źródło
(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.
źródło
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.źródło
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()); } }
źródło
MemoryCache
. Zastanawiam się, dlaczego ta próbka działa.