web-dev-qa-db-fra.com

MemoryCache n'obéit pas aux limites de mémoire dans la configuration

Je travaille avec la classe .NET 4.0 MemoryCache dans une application et j'essaie de limiter la taille maximale du cache, mais dans mes tests, il ne semble pas que le cache respecte réellement les limites.

J'utilise les paramètres qui, selon MSDN , sont censés limiter la taille du cache:

  1. CacheMemoryLimitMegabytes : taille maximale de la mémoire, en mégaoctets, pouvant atteindre une instance d'un objet. "
  2. PhysicalMemoryLimitPercentage : "Le pourcentage de mémoire physique que le cache peut utiliser, exprimé sous la forme d'un entier compris entre 1 et 100. Le la valeur par défaut est zéro, ce qui indique que les instances MemoryCache gèrent leur propre mémoire1 en fonction de la quantité de mémoire installée sur l'ordinateur. " 1. Ce n'est pas tout à fait correct - toute valeur inférieure à 4 est ignorée et remplacée par 4.

Je comprends que ces valeurs sont approximatives et non des limites strictes car le thread qui purge le cache est déclenché toutes les x secondes et dépend également de l'intervalle d'interrogation et d'autres variables non documentées. Cependant, même en tenant compte de ces écarts, je vois des tailles de cache extrêmement incohérentes lorsque le premier élément est expulsé du cache après avoir défini CacheMemoryLimitMegabytes et PhysicalMemoryLimitPercentage ensemble ou individuellement dans une application de test. Pour être sûr, j'ai exécuté chaque test 10 fois et calculé le chiffre moyen.

Ce sont les résultats du test de l'exemple de code ci-dessous sur un PC Windows 7 32 bits avec 3 Go de RAM. La taille du cache est prise après le premier appel à CacheItemRemoved () à chaque test. (Je suis conscient que la taille réelle du cache sera supérieure à celle-ci)

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

Voici l'application de test:

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

Pourquoi MemoryCache n'obéit pas aux limites de mémoire configurées?

84
Canacourse

Wow, donc j'ai juste passé trop de temps à fouiller dans le CLR avec un réflecteur, mais je pense que j'ai enfin une bonne idée de ce qui se passe ici.

Les paramètres sont lus correctement, mais il semble y avoir un problème profond dans le CLR lui-même qui semble rendre le paramètre de limite de mémoire essentiellement inutile.

Le code suivant se reflète dans la DLL System.Runtime.Caching, pour la classe CacheMemoryMonitor (il existe une classe similaire qui surveille la mémoire physique et traite l'autre paramètre, mais c'est le plus important):

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 première chose que vous remarquerez peut-être, c'est qu'il n'essaie même pas de regarder la taille du cache avant une récupération de place Gen2, au lieu de simplement retomber sur la valeur de taille stockée existante dans cacheSizeSamples. Donc, vous ne pourrez jamais atteindre la cible directement, mais si le reste fonctionnait, nous obtiendrions au moins une mesure de taille avant d'avoir de vrais ennuis.

Donc, en supposant qu'un GC Gen2 s'est produit, nous rencontrons le problème 2, qui est que ref2.ApproximateSize fait un travail horrible pour approximer réellement la taille du cache. En parcourant les fichiers indésirables CLR, j'ai trouvé qu'il s'agit d'un System.SizedReference, et c'est ce qu'il fait pour obtenir la valeur (IntPtr est un descripteur de l'objet MemoryCache lui-même):

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

Je suppose que la déclaration externe signifie qu'elle va plonger dans des fenêtres non gérées à ce stade, et je n'ai aucune idée de comment commencer à découvrir ce qu'elle fait là-bas. D'après ce que j'ai observé, cela fait un travail horrible d'essayer d'approcher la taille de l'ensemble.

La troisième chose notable est l'appel à manager.UpdateCacheSize qui semble devoir faire quelque chose. Malheureusement, dans tout exemple normal de fonctionnement, s_memoryCacheManager sera toujours nul. Le champ est défini à partir du membre statique public ObjectCache.Host. Cela est exposé pour que l'utilisateur puisse jouer s'il le souhaite, et j'ai pu faire fonctionner ce genre de chose comme il est censé le faire en regroupant ma propre implémentation IMemoryCacheManager, en la définissant sur ObjectCache.Host, puis en exécutant l'exemple . À ce stade, cependant, il semble que vous pourriez aussi bien créer votre propre implémentation de cache et ne pas même vous embêter avec tout cela, d'autant plus que je n'ai aucune idée si vous définissez votre propre classe sur ObjectCache.Host (statique, donc cela affecte tout le monde) de ceux qui pourraient être en cours de traitement) pour mesurer le cache pourrait gâcher d'autres choses.

Je dois croire qu'au moins une partie de cela (sinon quelques parties) n'est qu'un bug direct. Ce serait bien d'entendre quelqu'un de MS quel était le problème avec cette chose.

Version TLDR de cette réponse géante: Supposons que CacheMemoryLimitMegabytes soit complètement détruit à ce stade. Vous pouvez le définir sur 10 Mo, puis remplir le cache à ~ 2 Go et souffler une exception de mémoire insuffisante sans déclencher la suppression de l'élément.

96
David Hay

Je sais que cette réponse est folle en retard, mais mieux vaut tard que jamais. Je voulais vous faire savoir que j'ai écrit une version de MemoryCache qui résout automatiquement les problèmes de collection Gen 2 pour vous. Il s'ajuste donc chaque fois que l'intervalle d'interrogation indique la pression mémoire. Si vous rencontrez ce problème, essayez-le!

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

Vous pouvez également le trouver sur GitHub si vous êtes curieux de savoir comment je l'ai résolu. Le code est quelque peu simple.

https://github.com/haneytron/sharpmemorycache

29
Haney

J'ai également rencontré ce problème. Je mets en cache des objets qui sont tirés dans mon processus des dizaines de fois par seconde.

J'ai trouvé que la configuration et l'utilisation suivantes libèrent les éléments toutes les 5 secondes la plupart du temps.

App.config:

Prenez note de cacheMemoryLimitMegabytes. Lorsque ce paramètre était réglé sur zéro, la routine de purge ne se déclencherait pas dans un délai raisonnable.

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

Ajout au cache:

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

Confirmer que la suppression du cache fonctionne:

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

Heureusement, je suis tombé sur ce message utile hier lors de la première tentative d'utilisation de MemoryCache. Je pensais que ce serait un simple cas de définition de valeurs et d'utilisation des classes, mais j'ai rencontré des problèmes similaires décrits ci-dessus. Pour essayer de voir ce qui se passait, j'ai extrait la source à l'aide d'ILSpy, puis mis en place un test et parcouru le code. Mon code de test était très similaire au code ci-dessus, donc je ne le publierai pas. De mes tests, j'ai remarqué que la mesure de la taille du cache n'était jamais particulièrement précise (comme mentionné ci-dessus) et étant donné que la mise en œuvre actuelle ne fonctionnerait jamais de manière fiable. Cependant, la mesure physique était bonne et si la mémoire physique était mesurée à chaque sondage, il me semblait que le code fonctionnerait de manière fiable. J'ai donc supprimé la vérification de la récupération de place de la génération 2 dans MemoryCacheStatistics; dans des conditions normales, aucune mesure de la mémoire ne sera prise à moins qu'il n'y ait eu une autre collecte de déchets de génération 2 depuis la dernière mesure.

Dans un scénario de test, cela fait évidemment une grande différence car le cache est constamment touché afin que les objets n'aient jamais la chance d'atteindre la génération 2. Je pense que nous allons utiliser la version modifiée de cette DLL sur notre projet et utiliser le MS officiel compiler lorsque .net 4.5 sort (qui selon l'article de connexion mentionné ci-dessus devrait avoir le correctif). Logiquement, je peux voir pourquoi la vérification de la génération 2 a été mise en place, mais dans la pratique, je ne sais pas si cela a beaucoup de sens. Si la mémoire atteint 90% (ou quelle que soit la limite à laquelle elle a été fixée), peu importe si une collecte de deuxième génération s'est produite ou non, les éléments doivent être expulsés malgré tout.

J'ai laissé mon code de test en cours d'exécution pendant environ 15 minutes avec un paramètre physicalMemoryLimitPercentage défini sur 65%. J'ai vu l'utilisation de la mémoire rester entre 65 et 68% pendant le test et j'ai vu que les choses étaient expulsées correctement. Dans mon test, j'ai défini le pollingInterval à 5 ​​secondes, physicalMemoryLimitPercentage à 65 et physicalMemoryLimitPercentage à 0 par défaut.

Suivre les conseils ci-dessus; une implémentation de IMemoryCacheManager pourrait être faite pour expulser les choses du cache. Il souffrirait cependant du problème de vérification de la génération 2 mentionné. Bien que, selon le scénario, cela ne soit pas un problème dans le code de production et puisse fonctionner suffisamment pour les gens.

3
Ian Gibson

J'ai fait quelques tests avec l'exemple de @Canacourse et la modification de @woany et je pense qu'il y a des appels critiques qui bloquent le nettoyage du cache mémoire.

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

Mais pourquoi la modification de @woany semble-t-elle maintenir la mémoire au même niveau? Premièrement, le RemovedCallback n'est pas défini et il n'y a pas de sortie de console ou d'attente d'entrée qui pourrait bloquer le thread du cache mémoire.

Deuxièmement...

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) tous les ~ 1000th AddItem () aurait le même effet.

Eh bien, ce n'est pas une enquête très approfondie du problème, mais il semble que le thread du MemoryCache n'ait pas assez de temps CPU pour le nettoyage, tandis que de nombreux nouveaux éléments sont ajoutés.

3
Jezze

Il s'est avéré que ce n'est pas un bogue, tout ce que vous devez faire est de définir l'intervalle de mise en commun pour appliquer les limites, il semble que si vous laissez le regroupement non défini, il ne se déclenchera jamais. Je viens de le tester et pas besoin de wrappers ou tout code supplémentaire:

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

        };

Définissez la valeur de "PollingInterval" en fonction de la vitesse de croissance du cache, s'il augmente trop rapidement, augmentez la fréquence des vérifications d'interrogation, sinon gardez les vérifications peu fréquentes pour ne pas provoquer de surcharge.

2
sino

Si vous utilisez la classe modifiée suivante et surveillez la mémoire via le Gestionnaire des tâches, vous obtenez en fait un ajustement:

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