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

87

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 obéit réellement aux 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, à laquelle une instance d'un objet peut atteindre. "
  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. La valeur par défaut est zéro, ce qui indique que lesinstances MemoryCache gèrent leur propre mémoire 1 en fonction de la quantité de mémoire installée sur le 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 variations, je constate 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.

Voici 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 plus grande que cela)

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 ne respecte- t- il pas les limites de mémoire configurées?

Canacourse
la source
2
la boucle for est fausse, sans i ++
xiaoyifang
4
J'ai ajouté un rapport MS Connect pour ce bogue (peut-être que quelqu'un d'autre l'a déjà fait, mais quand même ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
3
Il convient de noter que Microsoft a maintenant (à partir de 9/2014) ajouté une réponse assez complète sur le ticket de connexion lié ci-dessus. Le TLDR de celui-ci est que MemoryCache ne vérifie pas intrinsèquement ces limites à chaque opération, mais plutôt que les limites ne sont respectées que lors du réglage du cache interne, qui est périodique basé sur des minuteries internes dynamiques.
Dusty
5
On dirait qu'ils ont mis à jour la documentation de MemoryCache.CacheMemoryLimit: "MemoryCache n'applique pas instantanément CacheMemoryLimit chaque fois qu'un nouvel élément est ajouté à une instance de MemoryCache. L'heuristique interne qui évite les éléments supplémentaires de MemoryCache le fait progressivement ..." msdn.microsoft .com / en-us / library /…
Sully
1
@Zeus, je pense que MSFT a supprimé le problème. Dans tous les cas, MSFT a clos le problème après quelques discussions avec moi, où ils m'ont dit que la limite n'est appliquée qu'après l'expiration de PoolingTime.
Bruno Brant

Réponses:

100

Wow, j'ai donc passé trop de temps à fouiller dans le CLR avec 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 est reflété 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 est qu'il n'essaie même pas de regarder la taille du cache avant un garbage collection Gen2, mais simplement de revenir sur la valeur de taille stockée existante dans cacheSizeSamples. Vous ne pourrez donc jamais atteindre la cible directement, mais si le reste fonctionnait, nous aurions au moins une mesure de la taille avant d'avoir de vrais problèmes.

Donc, en supposant qu'un GC Gen2 s'est produit, nous nous heurtons au problème 2, à savoir que ref2.ApproximateSize fait un travail horrible pour approximer la taille du cache. En parcourant les ordures CLR, j'ai trouvé qu'il s'agissait d'un System.SizedReference, et c'est ce qu'il fait pour obtenir la valeur (IntPtr est un handle 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 plonge 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 en essayant 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 la façon dont cela devrait fonctionner, s_memoryCacheManager sera toujours nul. Le champ est défini à partir du membre statique public ObjectCache.Host. Ceci est exposé pour que l'utilisateur puisse jouer avec s'il le souhaite, et j'ai été en mesure de faire fonctionner cette chose comme il est censé le faire en assemblant 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 même pas vous soucier de tout cela, d'autant plus que je n'ai aucune idée si vous définissez votre propre classe sur ObjectCache.

Je dois croire qu'au moins une partie de cela (sinon quelques parties) est juste un bug. Ce serait bien d'entendre quelqu'un de MS quel était l'accord avec cette chose.

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

David Hay
la source
4
Une belle réponse merci. J'ai renoncé à essayer de comprendre ce qui se passait avec cela et à la place maintenant gérer la taille du cache en comptant les éléments entrants / sortants et en appelant .Trim () manuellement si nécessaire. Je pensais que System.Runtime.Caching était un choix facile pour mon application car il semble être largement utilisé et je pensais donc qu'il n'y aurait pas de bogues majeurs.
Canacourse du
3
Sensationnel. C'est pourquoi j'aime SO. J'ai rencontré exactement le même comportement, j'ai écrit une application de test et j'ai réussi à planter mon PC plusieurs fois même si le temps d'interrogation était aussi bas que 10 secondes et la limite de la mémoire cache était de 1 Mo. Merci pour toutes les informations.
Bruno Brant
7
Je sais que je viens de le mentionner là-haut dans la question mais, par souci d'exhaustivité, je le mentionnerai à nouveau ici. J'ai ouvert un problème chez Connect pour cela. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
1
J'utilise le MemoryCache pour les données de services externes, et quand je teste en injectant des ordures dans le MemoryCache, il ne contenu auto-trim, mais seulement lorsque vous utilisez la valeur limite de pourcentage. La taille absolue ne fait rien pour limiter la taille, du moins lors de l'inspection avec un profileur de mémoire. Pas testé dans une boucle while, mais par des usages plus "réalistes" (c'est un système backend, j'ai donc ajouté un service WCF qui me permet d'injecter des données dans les caches à la demande).
Svend
Est-ce toujours un problème dans .NET Core?
Павле
29

Je sais que cette réponse est folle tardivement, mais mieux vaut tard que jamais. Je voulais vous faire savoir que j'ai écrit une version de MemoryCachequi résout automatiquement les problèmes de la collection Gen 2 pour vous. Il coupe donc chaque fois que l'intervalle d'interrogation indique une pression de 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 assez simple.

https://github.com/haneytron/sharpmemorycache

Haney
la source
2
Cela fonctionne comme prévu, testé avec un générateur qui remplit le cache avec des charges de chaînes de 1000 caractères. Bien que l'addition de ce qui devrait être de 100 Mo au cache ajoute en fait 200 à 300 Mo au cache, ce que j'ai trouvé assez étrange. Peut-être que je ne compte pas quelques surprises.
Karl Cassar
5
Les chaînes @KarlCassar dans .NET ont une 2n + 20taille approximative par rapport aux octets, où nest la longueur de la chaîne. Cela est principalement dû au support Unicode.
Haney
4

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

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

Jezze
la source
4

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

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

App.config:

Prenez note de cacheMemoryLimitMegabytes . Lorsqu'il était réglé sur zéro, le programme de purge ne se déclenche 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());
}
Aaron Hudon
la source
3

Je suis (heureusement) tombé sur ce post utile hier lors de la première tentative d'utilisation du 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 en utilisant ILSpy, puis mis en place un test et parcouru le code. Mon code de test était très similaire au code ci-dessus, je ne le publierai donc pas. D'après 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 que l'implémentation 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 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'accéder à 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 build lorsque .net 4.5 sort (qui, selon l'article de connexion mentionné ci-dessus, devrait contenir le correctif). Logiquement, je peux voir pourquoi la vérification de la génération 2 a été mise en place, mais en 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é définie), peu importe si une collection de génération 2 s'est produite ou non, les éléments doivent être expulsés malgré tout.

J'ai laissé mon code de test s'exécuter pendant environ 15 minutes avec un physicalMemoryLimitPercentage réglé à 65%. J'ai vu l'utilisation de la mémoire rester entre 65 et 68% pendant le test et j'ai vu les choses être expulsées correctement. Dans mon test, j'ai défini pollingInterval sur 5 secondes, physicalMemoryLimitPercentage sur 65 et physicalMemoryLimitPercentage sur 0 par défaut.

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

Ian Gibson
la source
4
Une mise à jour: j'utilise .NET Framework 4.5 et en aucun cas le problème n'est corrigé. Le cache peut devenir suffisamment grand pour planter la machine.
Bruno Brant
Une question: avez-vous le lien vers l'article de connexion que vous avez mentionné?
Bruno Brant
3

Il s'est avéré que ce n'est pas un bogue, tout ce que vous devez faire est de définir la durée 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. 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, si elle augmente trop rapidement, augmentez la fréquence des contrôles d'interrogation, sinon gardez les contrôles peu fréquents pour ne pas entraîner de surcharge.

sino
la source
1

Si vous utilisez la classe modifiée suivante et que vous surveillez la mémoire via le Gestionnaire des tâches, elle est en fait réduite:

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());
    }
}
malheur
la source
Êtes-vous en train de dire qu'il est taillé ou non?
Canacourse
Oui, il est coupé. Étrange, compte tenu de tous les problèmes avec lesquels les gens semblent avoir MemoryCache. Je me demande pourquoi cet exemple fonctionne.
Daniel Lidström
1
Je ne le suis pas. J'ai essayé de répéter l'exemple, mais le cache se développe toujours indéfiniment.
Bruno Brant
Un exemple de classe déroutant: "Statlock", "ItemCount", "size" sont inutiles ... Le NameValueCollection (3) ne contient que 2 éléments? ... En fait Vous avez créé un cache avec les propriétés sizelimit et pollInterval, rien de plus! Le problème de "ne pas expulser" des articles n'est pas touché ...
Bernhard