MemoryCache non obbedisce ai limiti di memoria nella configurazione


87

Sto lavorando con la classe .NET 4.0 MemoryCache in un'applicazione e sto cercando di limitare la dimensione massima della cache, ma nei miei test non sembra che la cache stia effettivamente rispettando i limiti.

Sto usando le impostazioni che, secondo MSDN , dovrebbero limitare la dimensione della cache:

  1. CacheMemoryLimitMegabytes : la dimensione massima della memoria, in megabyte, che può raggiungere un'istanza di un oggetto. "
  2. PhysicalMemoryLimitPercentage : "La percentuale di memoria fisica che la cache può utilizzare, espressa come valore intero compreso tra 1 e 100. Il valore predefinito è zero, che indica che leistanze di MemoryCache gestiscono la propria memoria 1 in base alla quantità di memoria installata sul computer." 1. Questo non è del tutto corretto: qualsiasi valore inferiore a 4 viene ignorato e sostituito con 4.

Capisco che questi valori sono approssimativi e non limiti rigidi poiché il thread che elimina la cache viene attivato ogni x secondi e dipende anche dall'intervallo di polling e da altre variabili non documentate. Tuttavia, anche tenendo conto di queste variazioni, vedo dimensioni della cache estremamente incoerenti quando il primo elemento viene rimosso dalla cache dopo aver impostato CacheMemoryLimitMegabytes e PhysicalMemoryLimitPercentage insieme o singolarmente in un'app di test. Per essere sicuro ho eseguito ogni test 10 volte e ho calcolato la cifra media.

Questi sono i risultati del test del codice di esempio riportato di seguito su un PC Windows 7 a 32 bit con 3 GB di RAM. La dimensione della cache viene presa dopo la prima chiamata a CacheItemRemoved () in ogni test. (Sono consapevole che la dimensione effettiva della cache sarà maggiore di questa)

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

Ecco l'applicazione di prova:

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

Perché MemoryCache non obbedisce ai limiti di memoria configurati?


2
for loop è sbagliato, senza i ++
xiaoyifang

4
Ho aggiunto un report di MS Connect per questo bug (forse qualcun altro lo ha già fatto, ma comunque ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

3
Vale la pena notare che Microsoft ha ora (a partire da 9/2014) ha aggiunto una risposta abbastanza completa sul ticket di connessione collegato sopra. Il TLDR è che MemoryCache non controlla intrinsecamente questi limiti su ogni operazione, ma piuttosto che i limiti vengono rispettati solo al momento del trimming della cache interna, che è periodico basato su timer interni dinamici.
Dusty

5
Sembra che abbiano aggiornato i documenti per MemoryCache.CacheMemoryLimit: "MemoryCache non applica immediatamente CacheMemoryLimit ogni volta che un nuovo elemento viene aggiunto a un'istanza di MemoryCache. L'euristica interna che rimuove gli elementi extra da MemoryCache lo fa gradualmente ..." msdn.microsoft .com / en-us / library /…
Sully

1
@ Zeus, penso che MSFT abbia rimosso il problema. In ogni caso, MSFT ha chiuso la questione dopo alcune discussioni con me, in cui mi hanno detto che il limite viene applicato solo dopo la scadenza di PoolingTime.
Bruno Brant

Risposte:


100

Wow, quindi ho passato troppo tempo a scavare nel CLR con il riflettore, ma penso di avere finalmente una buona padronanza di quello che sta succedendo qui.

Le impostazioni vengono lette correttamente, ma sembra esserci un problema radicato nel CLR stesso che sembra rendere sostanzialmente inutile l'impostazione del limite di memoria.

Il codice seguente viene riflesso dalla DLL System.Runtime.Caching, per la classe CacheMemoryMonitor (esiste una classe simile che monitora la memoria fisica e si occupa dell'altra impostazione, ma questa è la più importante):

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

La prima cosa che potresti notare è che non prova nemmeno a guardare la dimensione della cache fino a dopo una garbage collection di Gen2, invece ripiega semplicemente sul valore della dimensione memorizzata esistente in cacheSizeSamples. Quindi non sarai mai in grado di centrare l'obiettivo, ma se il resto funzionasse, avremmo almeno una misurazione delle dimensioni prima di metterci nei guai.

Quindi, supponendo che si sia verificato un GC Gen2, ci imbattiamo nel problema 2, che è che ref2.ApproximateSize fa un lavoro orribile di approssimare effettivamente la dimensione della cache. Slogging attraverso la spazzatura CLR ho scoperto che questo è un System.SizedReference, e questo è ciò che sta facendo per ottenere il valore (IntPtr è un handle per l'oggetto MemoryCache stesso):

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

Presumo che la dichiarazione esterna significhi che a questo punto va a tuffarsi in Windows Land non gestito e non ho idea di come iniziare a scoprire cosa fa lì. Da quello che ho osservato, però, fa un lavoro orribile nel cercare di approssimare le dimensioni della cosa complessiva.

La terza cosa evidente è la chiamata a manager.UpdateCacheSize che suona come dovrebbe fare qualcosa. Sfortunatamente in qualsiasi normale esempio di come dovrebbe funzionare s_memoryCacheManager sarà sempre nullo. Il campo viene impostato dal membro statico pubblico ObjectCache.Host. Questo è esposto per l'utente con cui scherzare se lo desidera, e sono stato effettivamente in grado di far funzionare questa cosa come dovrebbe combinando la mia implementazione IMemoryCacheManager, impostandola su ObjectCache.Host e quindi eseguendo l'esempio . A quel punto, però, sembra che potresti anche creare la tua implementazione della cache e non preoccuparti nemmeno di tutte queste cose, soprattutto perché non ho idea se impostare la tua classe su ObjectCache.Host (static,

Devo credere che almeno una parte di questo (se non un paio di parti) sia solo un bug diretto. Sarebbe bello sentire da qualcuno alla SM quale fosse il problema con questa cosa.

Versione TLDR di questa risposta gigante: supponiamo che CacheMemoryLimitMegabytes sia completamente rotto in questo momento. Puoi impostarlo su 10 MB, quindi procedere a riempire la cache a ~ 2 GB e far saltare un'eccezione di memoria insufficiente senza che la rimozione dell'elemento scatti.


4
Una bella risposta grazie. Ho rinunciato a cercare di capire cosa stesse succedendo e invece ora gestisco la dimensione della cache contando gli elementi in entrata / uscita e chiamando .Trim () manualmente secondo necessità. Ho pensato che System.Runtime.Caching fosse una scelta facile per la mia app in quanto sembra essere ampiamente utilizzata e ho pensato che quindi non avrebbe avuto grossi bug.
Canacourse

3
Wow. Ecco perché amo SO. Mi sono imbattuto nello stesso identico comportamento, ho scritto un'app di test e sono riuscito a bloccare il mio PC molte volte anche se il tempo di polling era di soli 10 secondi e il limite di memoria cache era di 1 MB. Grazie per tutti gli approfondimenti.
Bruno Brant

7
So di averlo appena menzionato nella domanda ma, per completezza, lo menzionerò di nuovo qui. Ho aperto un problema su Connect per questo. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

1
Sto utilizzando il MemoryCache per i dati esterni di servizi, e quando prova iniettando spazzatura nel MemoryCache, che lo fa di contenuti auto-trim, ma solo quando si utilizza il valore limite percentuale. La dimensione assoluta non fa nulla per limitare la dimensione, almeno quando inpsecting con un profiler della memoria. Non testato in un ciclo while, ma da usi più "realistici" (è un sistema di backend, quindi ho aggiunto un servizio WCF che mi consente di iniettare dati nelle cache su richiesta).
Svend

È ancora un problema in .NET Core?
Павле

29

So che questa risposta è pazzesca in ritardo, ma meglio tardi che mai. Volevo farti sapere che ho scritto una versione MemoryCacheche risolve automaticamente i problemi della Gen 2 Collection per te. Pertanto viene tagliato ogni volta che l'intervallo di polling indica la pressione della memoria. Se stai riscontrando questo problema, provalo!

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

Puoi anche trovarlo su GitHub se sei curioso di sapere come l'ho risolto. Il codice è piuttosto semplice.

https://github.com/haneytron/sharpmemorycache


2
Funziona come previsto, testato con un generatore che riempie la cache con carichi di stringhe di 1000 caratteri. Tuttavia, sommando quello che dovrebbe essere 100 MB alla cache, in realtà si aggiungono 200-300 MB alla cache, il che ho trovato piuttosto strano. Forse alcune volte non le conto.
Karl Cassar

5
Le stringhe @KarlCassar in .NET hanno all'incirca le 2n + 20dimensioni rispetto ai byte, dove nè la lunghezza della stringa. Ciò è dovuto principalmente al supporto Unicode.
Haney

4

Ho fatto alcuni test con l'esempio di @Canacourse e la modifica di @woany e penso che ci siano alcune chiamate critiche che bloccano la pulizia della cache di memoria.

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

Ma perché la modifica di @woany sembra mantenere la memoria allo stesso livello? In primo luogo, RemovedCallback non è impostato e non è presente alcun output della console o in attesa di input che potrebbe bloccare il thread della cache di memoria.

Secondo ...

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

Un Thread.Sleep (1) ogni ~ 1000th AddItem () avrebbe lo stesso effetto.

Bene, non è un'indagine molto approfondita del problema, ma sembra che il thread di MemoryCache non riceva abbastanza tempo CPU per la pulizia, mentre vengono aggiunti molti nuovi elementi.


4

Ho riscontrato anche questo problema. Sto memorizzando nella cache oggetti che vengono attivati ​​nel mio processo dozzine di volte al secondo.

Ho riscontrato che la seguente configurazione e l'utilizzo libera gli elementi ogni 5 secondi la maggior parte delle volte .

App.config:

Prendi nota di cacheMemoryLimitMegabytes . Quando questo era impostato su zero, la routine di spurgo non si attivava in un tempo ragionevole.

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

Aggiunta alla cache:

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

Conferma che la rimozione della cache funziona:

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

3

Ieri (per fortuna) mi sono imbattuto in questo utile post quando ho tentato per la prima volta di utilizzare MemoryCache. Ho pensato che sarebbe stato un semplice caso di impostazione dei valori e utilizzo delle classi, ma ho riscontrato problemi simili descritti sopra. Per provare a vedere cosa stava succedendo, ho estratto la fonte usando ILSpy, quindi ho impostato un test e ho eseguito il codice. Il mio codice di prova era molto simile al codice sopra, quindi non lo pubblicherò. Dai miei test ho notato che la misurazione della dimensione della cache non è mai stata particolarmente accurata (come detto sopra) e vista l'attuale implementazione non avrebbe mai funzionato in modo affidabile. Tuttavia, la misurazione fisica andava bene e se la memoria fisica veniva misurata ad ogni sondaggio, mi sembrava che il codice avrebbe funzionato in modo affidabile. Quindi, ho rimosso il controllo della garbage collection di seconda generazione all'interno di MemoryCacheStatistics;

In uno scenario di test questo ovviamente fa una grande differenza poiché la cache viene colpita costantemente, quindi gli oggetti non hanno mai la possibilità di arrivare alla gen 2. Penso che useremo la build modificata di questa dll sul nostro progetto e useremo il MS ufficiale build quando esce .net 4.5 (che secondo l'articolo di connect menzionato sopra dovrebbe contenere la correzione). Logicamente posso capire perché è stato messo in atto il controllo gen 2, ma in pratica non sono sicuro che abbia molto senso. Se la memoria raggiunge il 90% (o qualsiasi limite a cui è stata impostata), non dovrebbe importare se una raccolta di seconda generazione si è verificata o meno, gli elementi dovrebbero essere rimossi a prescindere.

Ho lasciato il mio codice di prova in esecuzione per circa 15 minuti con un PhysicalMemoryLimitPercentage impostato al 65%. Ho visto l'utilizzo della memoria rimanere tra il 65-68% durante il test e ho visto che le cose venivano rimosse correttamente. Nel mio test ho impostato pollingInterval su 5 secondi, physicalMemoryLimitPercentage su 65 e physicalMemoryLimitPercentage su 0 per impostazione predefinita.

Seguendo il consiglio di cui sopra; potrebbe essere realizzata un'implementazione di IMemoryCacheManager per eliminare le cose dalla cache. Tuttavia, soffrirebbe del problema di controllo gen 2 menzionato. Sebbene, a seconda dello scenario, questo potrebbe non essere un problema nel codice di produzione e potrebbe funzionare sufficientemente per le persone.


4
Un aggiornamento: sto usando .NET framework 4.5 e in nessun modo il problema viene risolto. La cache può crescere abbastanza da causare il crash della macchina.
Bruno Brant

Una domanda: hai il link all'articolo Connect che hai citato?
Bruno Brant


3

Si è scoperto che non è un bug, tutto ciò che devi fare è impostare l'intervallo di tempo del pool per far rispettare i limiti, sembra che se lasci il pool non impostato, non si innescherà mai. L'ho appena testato e non c'è bisogno di wrapper o qualsiasi codice aggiuntivo:

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

        };

Imposta il valore di " PollingInterval" in base alla velocità con cui cresce la cache, se cresce troppo velocemente aumenta la frequenza dei controlli di polling altrimenti mantieni i controlli non molto frequenti per non causare overhead.


1

Se si utilizza la seguente classe modificata e si monitora la memoria tramite Task Manager viene effettivamente ridotta:

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

Stai dicendo che viene o non viene tagliato?
Canacourse

Sì, viene tagliato. Strano, considerando tutti i problemi con cui le persone sembrano avere MemoryCache. Mi chiedo perché questo campione funzioni.
Daniel Lidström

1
Io non lo seguo. Ho provato a ripetere l'esempio, ma la cache continua a crescere indefinitamente.
Bruno Brant

Un esempio di classe confuso: "Statlock", "ItemCount", "size" sono inutili ... Il NameValueCollection (3) contiene solo 2 elementi? ... Infatti hai creato una cache con proprietà sizelimit e pollInterval, niente di più! Il problema del "non sfrattare" gli oggetti non è toccato ...
Bernhard
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.