MemoryCache hält die Speicherbeschränkungen in der Konfiguration nicht ein

86

Ich arbeite mit der .NET 4.0 MemoryCache- Klasse in einer Anwendung und versuche, die maximale Cache-Größe zu begrenzen. In meinen Tests scheint es jedoch nicht so zu sein, dass der Cache tatsächlich die Grenzwerte einhält.

Ich verwende die Einstellungen, die laut MSDN die Cache-Größe begrenzen sollen:

  1. CacheMemoryLimitMegabytes : Die maximale Speichergröße in Megabyte, auf die eine Instanz eines Objekts anwachsen kann. "
  2. PhysicalMemoryLimitPercentage : "Der Prozentsatz des physischen Speichers, den der Cache verwenden kann, ausgedrückt als ganzzahliger Wert von 1 bis 100. Der Standardwert ist Null. Dies gibt an, dass MemoryCache- Instanzen ihren eigenen Speicher 1 basierend auf derauf dem Computer installierten Speichermenge verwalten Computer." 1. Dies ist nicht ganz richtig - jeder Wert unter 4 wird ignoriert und durch 4 ersetzt.

Ich verstehe, dass diese Werte ungefähre und keine harten Grenzen sind, da der Thread, der den Cache löscht, alle x Sekunden ausgelöst wird und auch vom Abfrageintervall und anderen undokumentierten Variablen abhängt. Selbst unter Berücksichtigung dieser Abweichungen sehe ich wild inkonsistente Cache-Größen, wenn das erste Element aus dem Cache entfernt wird, nachdem CacheMemoryLimitMegabytes und PhysicalMemoryLimitPercentage zusammen oder einzeln in einer Test-App festgelegt wurden. Um sicherzugehen, habe ich jeden Test 10 Mal durchgeführt und die durchschnittliche Zahl berechnet.

Dies sind die Ergebnisse des Testens des folgenden Beispielcodes auf einem 32-Bit-Windows 7-PC mit 3 GB RAM. Die Größe des Caches wird nach dem ersten Aufruf von CacheItemRemoved () bei jedem Test übernommen. (Mir ist bekannt, dass die tatsächliche Größe des Caches größer sein wird)

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

Hier ist die Testanwendung:

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

Warum hält MemoryCache die konfigurierten Speichergrenzen nicht ein?

Canacourse
quelle
2
for loop ist falsch, ohne i ++
xiaoyifang
4
Ich habe einen MS Connect-Bericht für diesen Fehler hinzugefügt (vielleicht hat es bereits jemand anderes getan, aber trotzdem ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
3
Es ist erwähnenswert, dass Microsoft jetzt (Stand 9/2014) eine ziemlich gründliche Antwort auf das oben verlinkte Verbindungsticket hinzugefügt hat. Die TLDR davon ist , dass Memory nicht von Natur aus diesen Grenzen bei jeder Operation überprüfen, sondern vielmehr , dass die Grenzen sind nur auf interne Cache Trimmen, geachtet , die basierend auf dynamisches internes Timer periodisch ist.
Dusty
5
Sieht so aus, als hätten sie die Dokumente für MemoryCache.CacheMemoryLimit aktualisiert: "MemoryCache erzwingt CacheMemoryLimit nicht jedes Mal sofort, wenn ein neues Element zu einer MemoryCache-Instanz hinzugefügt wird. Die internen Heuristiken, die zusätzliche Elemente aus dem MemoryCache entfernen, tun dies schrittweise ..." msdn.microsoft .com / en-us / library /…
Sully
1
@ Zeus, ich denke, MSFT hat das Problem behoben. In jedem Fall schloss MSFT das Problem nach einigen Gesprächen mit mir, in denen mir mitgeteilt wurde, dass das Limit erst nach Ablauf von PoolingTime angewendet wird.
Bruno Brant

Antworten:

99

Wow, also habe ich einfach zu viel Zeit damit verbracht, mit Reflektor in der CLR herumzuwühlen, aber ich denke, ich habe endlich einen guten Überblick darüber, was hier vor sich geht.

Die Einstellungen werden korrekt eingelesen, aber es scheint ein tiefgreifendes Problem in der CLR selbst zu geben, das die Einstellung des Speicherlimits im Wesentlichen unbrauchbar macht.

Der folgende Code wird in der System.Runtime.Caching-DLL für die CacheMemoryMonitor-Klasse wiedergegeben (es gibt eine ähnliche Klasse, die den physischen Speicher überwacht und sich mit der anderen Einstellung befasst, dies ist jedoch die wichtigere):

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

Das erste, was Sie möglicherweise bemerken, ist, dass erst nach einer Gen2-Garbage Collection versucht wird, die Größe des Caches zu überprüfen, sondern nur auf den vorhandenen Wert für die gespeicherte Größe in cacheSizeSamples zurückzugreifen. Sie werden also nie in der Lage sein, das Ziel direkt zu treffen, aber wenn der Rest funktioniert, würden wir zumindest eine Größenmessung erhalten, bevor wir in echte Schwierigkeiten geraten.

Unter der Annahme, dass ein Gen2-GC aufgetreten ist, stoßen wir auf Problem 2, nämlich dass ref2.ApproximateSize einen schrecklichen Job darin macht, die Größe des Caches tatsächlich zu approximieren. Beim Durchsuchen von CLR-Junk stellte ich fest, dass es sich um eine System.SizedReference handelt, und dies geschieht, um den Wert abzurufen (IntPtr ist ein Handle für das MemoryCache-Objekt selbst):

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

Ich gehe davon aus, dass eine externe Deklaration bedeutet, dass sie zu diesem Zeitpunkt in nicht verwaltete Fenster landet, und ich habe keine Ahnung, wie ich herausfinden soll, was sie dort tut. Nach allem, was ich beobachtet habe, macht es einen schrecklichen Job, die Größe der gesamten Sache zu approximieren.

Das dritte bemerkenswerte Problem ist der Aufruf von manager.UpdateCacheSize, der so klingt, als ob er etwas tun sollte. Leider ist s_memoryCacheManager in jedem normalen Beispiel, wie dies funktionieren sollte, immer null. Das Feld wird vom öffentlichen statischen Member ObjectCache.Host festgelegt. Dies ist für den Benutzer offen, mit dem er sich herumschlagen kann, wenn er dies wünscht, und ich konnte diese Sache tatsächlich so machen, wie sie sollte, indem ich meine eigene IMemoryCacheManager-Implementierung zusammenfügte, sie auf ObjectCache.Host setzte und dann das Beispiel ausführte . An diesem Punkt scheint es jedoch so, als könnten Sie genauso gut Ihre eigene Cache-Implementierung erstellen und sich nicht einmal mit all diesen Dingen beschäftigen, zumal ich keine Ahnung habe, ob Sie Ihre eigene Klasse auf ObjectCache.Host setzen (statisch,

Ich muss glauben, dass zumindest ein Teil davon (wenn nicht ein paar Teile) nur ein direkter Fehler ist. Es wäre schön, von jemandem bei MS zu hören, was mit dieser Sache los war.

TLDR-Version dieser riesigen Antwort: Angenommen, CacheMemoryLimitMegabytes ist zu diesem Zeitpunkt vollständig kaputt. Sie können es auf 10 MB einstellen und dann den Cache auf ~ 2 GB füllen und eine Ausnahme wegen Speichermangels auslösen, ohne dass das Entfernen von Elementen ausgelöst wird.

David Hay
quelle
4
Eine schöne Antwort, danke. Ich habe es aufgegeben, herauszufinden, was damit los war, und stattdessen die Cache-Größe zu verwalten, indem ich Elemente ein- und ausgezählt und .Trim () nach Bedarf manuell aufgerufen habe. Ich dachte, System.Runtime.Caching sei eine einfache Wahl für meine App, da es weit verbreitet zu sein scheint und ich dachte, dass es keine größeren Fehler geben würde.
Canacourse
3
Beeindruckend. Deshalb liebe ich SO. Ich bin auf genau das gleiche Verhalten gestoßen, habe eine Test-App geschrieben und es geschafft, meinen PC viele Male zum Absturz zu bringen, obwohl die Abrufzeit nur 10 Sekunden und das Cache-Speicherlimit 1 MB betrug. Vielen Dank für alle Erkenntnisse.
Bruno Brant
7
Ich weiß, dass ich es dort oben in der Frage erwähnt habe, aber der Vollständigkeit halber werde ich es hier noch einmal erwähnen. Ich habe dafür ein Problem bei Connect geöffnet. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
1
Ich verwende den MemoryCache für externe Servicedaten. Wenn ich durch Einfügen von Müll in den MemoryCache teste, wird der Inhalt automatisch gekürzt , jedoch nur, wenn der prozentuale Grenzwert verwendet wird. Die absolute Größe schränkt die Größe nicht ein, zumindest wenn sie mit einem Speicherprofiler überprüft wird. Nicht in einer while-Schleife getestet, sondern durch "realistischere" Verwendungen (es ist ein Backend-System, daher habe ich einen WCF-Dienst hinzugefügt, mit dem ich bei Bedarf Daten in die Caches einfügen kann).
Svend
29

Ich weiß, dass diese Antwort spät verrückt ist, aber besser spät als nie. Ich wollte Sie wissen lassen, dass ich eine Version geschrieben habe MemoryCache, die die Probleme mit der Gen 2-Sammlung automatisch für Sie löst. Es wird daher immer dann gekürzt, wenn das Abfrageintervall den Speicherdruck anzeigt. Wenn dieses Problem auftritt, probieren Sie es aus!

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

Sie können es auch auf GitHub finden, wenn Sie neugierig sind, wie ich es gelöst habe. Der Code ist etwas einfach.

https://github.com/haneytron/sharpmemorycache

Haney
quelle
2
Dies funktioniert wie beabsichtigt und wurde mit einem Generator getestet, der den Cache mit einer Menge von Zeichenfolgen mit 1000 Zeichen füllt. Das Addieren von 100 MB zum Cache summiert sich jedoch auf 200 bis 300 MB zum Cache, was ich ziemlich seltsam fand. Vielleicht einige Abhörungen, die ich nicht zähle.
Karl Cassar
5
@ KarlCassar-Zeichenfolgen in .NET sind 2n + 20in Bezug auf Bytes ungefähr so groß, wobei ndie Länge der Zeichenfolge angegeben ist. Dies ist hauptsächlich auf die Unicode-Unterstützung zurückzuführen.
Haney
4

Ich bin auch auf dieses Problem gestoßen. Ich speichere Objekte zwischen, die Dutzende Male pro Sekunde in meinen Prozess abgefeuert werden.

Ich habe festgestellt, dass die folgende Konfiguration und Verwendung die Elemente die meiste Zeit alle 5 Sekunden freigibt .

App.config:

Beachten Sie cacheMemoryLimitMegabytes . Wenn dies auf Null gesetzt wurde, wurde die Spülroutine nicht in einer angemessenen Zeit ausgelöst.

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

Zum Cache hinzufügen:

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

Bestätigen, dass das Entfernen des Caches funktioniert:

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

Ich bin gestern (zum Glück) auf diesen nützlichen Beitrag gestoßen, als ich zum ersten Mal versuchte, den MemoryCache zu verwenden. Ich dachte, es wäre ein einfacher Fall, Werte festzulegen und die Klassen zu verwenden, aber ich stieß auf ähnliche Probleme, die oben beschrieben wurden. Um zu sehen, was los war, extrahierte ich die Quelle mit ILSpy, richtete einen Test ein und ging den Code durch. Mein Testcode war dem obigen Code sehr ähnlich, daher werde ich ihn nicht veröffentlichen. Bei meinen Tests habe ich festgestellt, dass die Messung der Cache-Größe nie besonders genau war (wie oben erwähnt) und angesichts der aktuellen Implementierung niemals zuverlässig funktionieren würde. Die physikalische Messung war jedoch in Ordnung, und wenn das physikalische Gedächtnis bei jeder Umfrage gemessen wurde, schien es mir, als würde der Code zuverlässig funktionieren. Also habe ich die Garbage Collection-Prüfung der 2. Generation in MemoryCacheStatistics entfernt.

In einem Testszenario macht dies offensichtlich einen großen Unterschied, da der Cache ständig getroffen wird, sodass Objekte nie die Chance haben, zu Gen 2 zu gelangen. Ich denke, wir werden den modifizierten Build dieser DLL für unser Projekt verwenden und die offizielle MS verwenden Build, wenn .net 4.5 herauskommt (das laut dem oben erwähnten Connect-Artikel das Update enthalten sollte). Logischerweise kann ich sehen, warum der Gen 2-Check durchgeführt wurde, aber in der Praxis bin ich mir nicht sicher, ob er sinnvoll ist. Wenn der Speicher 90% erreicht (oder welche Grenze auch immer festgelegt wurde), sollte es keine Rolle spielen, ob eine Gen 2-Sammlung stattgefunden hat oder nicht. Elemente sollten trotzdem entfernt werden.

Ich habe meinen Testcode etwa 15 Minuten lang laufen lassen, wobei der Wert für PhysicalMemoryLimitPercentage auf 65% festgelegt war. Ich habe gesehen, dass die Speichernutzung während des Tests zwischen 65 und 68% liegt, und habe gesehen, dass die Dinge ordnungsgemäß entfernt wurden. In meinem Test habe ich das pollingInterval auf 5 Sekunden festgelegt, physischMemoryLimitPercentage auf 65 und physischMemoryLimitPercentage auf 0, um dies als Standard festzulegen.

Befolgen Sie die obigen Ratschläge; Eine Implementierung von IMemoryCacheManager könnte vorgenommen werden, um Dinge aus dem Cache zu entfernen. Es würde jedoch unter dem erwähnten Gen 2 Check-Problem leiden. Abhängig vom Szenario ist dies möglicherweise kein Problem im Produktionscode und funktioniert möglicherweise ausreichend für Personen.

Ian Gibson
quelle
4
Ein Update: Ich verwende .NET Framework 4.5 und das Problem wird in keiner Weise behoben. Der Cache kann groß genug werden, um den Computer zum Absturz zu bringen.
Bruno Brant
Eine Frage: Haben Sie den Link zu dem von Ihnen erwähnten Connect-Artikel?
Bruno Brant
3

Ich habe einige Tests am Beispiel von @Canacourse und der Modifikation von @woany durchgeführt und denke, dass es einige kritische Aufrufe gibt, die die Bereinigung des Speichercaches blockieren.

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

Aber warum scheint die Modifikation von @woany den Speicher auf dem gleichen Niveau zu halten? Erstens ist der RemovedCallback nicht gesetzt und es gibt keine Konsolenausgabe oder Warten auf Eingaben, die den Thread des Speichercaches blockieren könnten.

Zweitens...

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

Ein Thread.Sleep (1) jedes ~ 1000. AddItem () hätte den gleichen Effekt.

Nun, es ist keine sehr gründliche Untersuchung des Problems, aber es sieht so aus, als ob der Thread des MemoryCache nicht genügend CPU-Zeit zum Bereinigen erhält, während viele neue Elemente hinzugefügt werden.

Jezze
quelle
3

Es stellte sich heraus, dass es kein Fehler ist. Alles, was Sie tun müssen, ist die Pooling-Zeitspanne festzulegen, um die Grenzwerte durchzusetzen. Wenn Sie das Pooling nicht festgelegt lassen, wird es anscheinend nie ausgelöst. Ich habe es nur getestet und brauche keine Wrapper oder ein zusätzlicher Code:

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

        };

Stellen Sie den Wert von "PollingInterval " basierend darauf ein, wie schnell der Cache wächst. Wenn er zu schnell wächst, erhöhen Sie die Häufigkeit der Abfrageprüfungen. Andernfalls bleiben die Überprüfungen nicht sehr häufig, um keinen Overhead zu verursachen.

Sino
quelle
1

Wenn Sie die folgende geänderte Klasse verwenden und den Speicher über den Task-Manager überwachen, wird er tatsächlich gekürzt:

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
quelle
Wollen Sie damit sagen, dass es beschnitten wird oder nicht?
Canacourse
Ja, es wird beschnitten. Seltsam angesichts all der Probleme, mit denen die Leute zu haben scheinen MemoryCache. Ich frage mich, warum dieses Beispiel funktioniert.
Daniel Lidström
1
Ich folge ihm nicht. Ich habe versucht, das Beispiel zu wiederholen, aber der Cache wächst immer noch auf unbestimmte Zeit.
Bruno Brant
Eine verwirrende Beispielklasse: "Statlock", "ItemCount", "size" sind nutzlos ... Die NameValueCollection (3) enthält nur 2 Elemente? ... Tatsächlich haben Sie einen Cache mit den Eigenschaften sizelimit und pollInterval erstellt, mehr nicht! Das Problem der "Nichträumung" von Gegenständen wird nicht angesprochen ...
Bernhard