MemoryCache no obedece los límites de memoria en la configuración

Estoy trabajando con la clase .NET 4.0 MemoryCache en una aplicación e bash limitar el tamaño máximo de caché, pero en mis pruebas no parece que la caché realmente esté cumpliendo los límites.

Estoy usando la configuración que, según MSDN , se supone que limitan el tamaño de la caché:

  1. CacheMemoryLimitMegabytes : el tamaño de memoria máximo, en megabytes, que una instancia de un objeto puede crecer “.
  2. PhysicalMemoryLimitPercentage : “El porcentaje de memoria física que puede usar la caché, expresada como un valor entero de 1 a 100. El valor predeterminado es cero, lo que indica que las instancias de MemoryCache administran su propia memoria 1 en función de la cantidad de memoria instalada en la computadora.” 1. Esto no es del todo correcto: se ignora cualquier valor por debajo de 4 y se reemplaza por 4.

Entiendo que estos valores son aproximados y no tienen límites estrictos, ya que el hilo que purga la caché se dispara cada x segundos y también depende del intervalo de sondeo y de otras variables no documentadas. Sin embargo, incluso teniendo en cuenta estas variaciones, estoy viendo tamaños de caché muy inconsistentes cuando el primer elemento se expulsa de la memoria caché después de establecer CacheMemoryLimitMegabytes y PhysicalMemoryLimitPercentage juntos o singularmente en una aplicación de prueba. Para estar seguro, realicé cada prueba 10 veces y calculé la cifra promedio.

Estos son los resultados de probar el siguiente código de ejemplo en una PC con Windows 7 de 32 bits con 3 GB de RAM. El tamaño de la memoria caché se toma después de la primera llamada a CacheItemRemoved () en cada prueba. (Soy consciente de que el tamaño real de la memoria caché será mayor que este)

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 

Aquí está la aplicación de prueba:

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

¿Por qué MemoryCache no cumple con los límites de memoria configurados?

Wow, así que dediqué demasiado tiempo a hurgar en el CLR con reflector, pero creo que finalmente tengo una buena idea de lo que está sucediendo aquí.

La configuración se lee correctamente, pero parece que hay un problema muy arraigado en el CLR que parece que hará que la configuración del límite de memoria sea esencialmente inútil.

El siguiente código se refleja en la DLL System.Runtime.Caching, para la clase CacheMemoryMonitor (hay una clase similar que supervisa la memoria física y trata con la otra configuración, pero esta es la más 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); } 

Lo primero que notará es que ni siquiera intenta ver el tamaño de la memoria caché hasta después de una recolección de basura Gen2, en lugar de limitarse al valor de tamaño almacenado existente en cacheSizeSamples. Entonces nunca podrás alcanzar el objective, pero si el rest funciona, al menos obtendremos una medida de tamaño antes de tener problemas.

Entonces, suponiendo que se haya producido un Gen2 GC, nos topamos con el problema 2, que es que ref2.ApproximateSize hace un trabajo horrible al aproximar realmente el tamaño del caché. Slogging a través de CLR basura encontré que esta es una System.SizedReference, y esto es lo que está haciendo para obtener el valor (IntPtr es un controlador para el objeto MemoryCache en sí):

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

Estoy asumiendo que la statement externa significa que se lanza a la tierra de ventanas no administradas en este punto, y no tengo idea de cómo empezar a descubrir qué hace allí. Por lo que he observado, aunque hace un trabajo horrible de tratar de aproximar el tamaño de la cosa en general.

La tercera cosa notable es la llamada a manager.UpdateCacheSize que suena como debería hacer algo. Desafortunadamente en cualquier muestra normal de cómo debería funcionar esto, s_memoryCacheManager siempre será nulo. El campo se establece desde el miembro público estático ObjectCache.Host. Esto está expuesto para que el usuario se meta con él si así lo desea, y pude hacer que este tipo de trabajo funcionara como se suponía al juntar mi propia implementación de IMemoryCacheManager, establecerla en ObjectCache.Host y luego ejecutar la muestra . En ese momento, parece que también podrías hacer tu propia implementación de caché y ni siquiera molestarte con todo esto, especialmente porque no tengo idea si estableces tu propia clase en ObjectCache.Host (estática, por lo que afecta a todos de estos que podrían estar en proceso) para medir el caché podría estropear otras cosas.

Tengo que creer que al menos parte de esto (si no es un par de partes) es solo una falla directa. Sería bueno saber de alguien en MS cuál era el problema con esto.

Versión TLDR de esta respuesta gigante: suponga que CacheMemoryLimitMegabytes está completamente reventado en este momento. Puede configurarlo en 10 MB, y luego continuar para llenar la memoria caché a ~ 2 GB y volar una excepción de memoria sin disparar la eliminación de elementos.

Sé que esta respuesta es una locura tarde, pero es mejor tarde que nunca. Quería que supieras que escribí una versión de MemoryCache que resuelve automáticamente los problemas de la Colección Gen 2. Por lo tanto, recorta cada vez que el intervalo de sondeo indica la presión de la memoria. Si tienes este problema, pruébalo!

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

También puedes encontrarlo en GitHub si tienes curiosidad sobre cómo lo resolví. El código es algo simple.

https://github.com/haneytron/sharpmemorycache

Yo (afortunadamente) tropecé con esta publicación útil ayer cuando primero intentaba usar MemoryCache. Pensé que sería un simple caso de establecer valores y usar las clases, pero me encontré con problemas similares descritos anteriormente. Para probar y ver qué estaba sucediendo, extraje la fuente usando ILSpy y luego establecí una prueba y revisé el código. Mi código de prueba fue muy similar al código anterior, así que no lo publicaré. De mis pruebas noté que la medición del tamaño de la caché nunca fue particularmente precisa (como se mencionó anteriormente) y dado que la implementación actual nunca funcionaría de manera confiable. Sin embargo, la medición física fue buena y si se midió la memoria física en cada encuesta, me pareció que el código funcionaría de manera confiable. Entonces, eliminé la verificación de recolección de basura gen 2 dentro de MemoryCacheStatistics; en condiciones normales, no se tomarán medidas de memoria a menos que haya habido otra recolección de basura gen 2 desde la última medición.

En un escenario de prueba, esto obviamente hace una gran diferencia ya que el caché está siendo golpeado constantemente para que los objetos nunca tengan la oportunidad de llegar al gen 2. Creo que vamos a usar la comstackción modificada de este dll en nuestro proyecto y usar el MS oficial construir cuando .net 4.5 sale (que de acuerdo con el artículo de conexión mencionado anteriormente debe tener la solución). Lógicamente, puedo ver por qué se ha implementado el control gen 2, pero en la práctica no estoy seguro si tiene mucho sentido. Si la memoria alcanza el 90% (o el límite que se haya establecido), entonces no debería importar si se ha producido o no una colección de gen 2, los elementos deben desalojarse independientemente.

Dejé mi código de prueba en ejecución durante aproximadamente 15 minutos con un PhysicalMimoryLimitPercentage establecido en 65%. Vi que el uso de la memoria se mantuvo entre 65-68% durante la prueba y vi que las cosas se desalojaban correctamente. En mi prueba configuré el pollingInterval en 5 segundos, physicalMemoryLimitPercentage en 65 y physicalMemoryLimitPercentage en 0 para establecer esto por defecto.

Siguiendo el consejo anterior; una implementación de IMemoryCacheManager podría hacerse para expulsar cosas de la caché. Sin embargo, sufriría el problema de verificación gen 2 mencionado. Aunque, dependiendo del escenario, esto puede no ser un problema en el código de producción y puede funcionar lo suficiente para las personas.

Me he encontrado con este problema también. Estoy almacenando en caché objetos que se activan en mi proceso docenas de veces por segundo.

He encontrado la siguiente configuración y el uso libera los elementos cada 5 segundos la mayor parte del tiempo .

App.config:

Tome nota de cacheMemoryLimitMegabytes . Cuando esto se estableció en cero, la rutina de purga no se activará en un tiempo razonable.

         

Agregar a la caché:

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

Confirmar que la eliminación de caché está funcionando:

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

Si utiliza la siguiente clase modificada y supervisa la memoria a través del Administrador de tareas, de hecho se recorta:

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

He hecho algunas pruebas con el ejemplo de @Canacourse y la modificación de @woany y creo que hay algunas llamadas críticas que bloquean la limpieza de la memoria caché.

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

Pero ¿por qué la modificación de @woany parece mantener la memoria en el mismo nivel? En primer lugar, RemovedCallback no está configurado y no hay salida de consola o esperando entrada que pueda bloquear el hilo de la memoria caché.

En segundo lugar…

 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) cada ~ 1000th AddItem () tendría el mismo efecto.

Bueno, no se trata de una investigación muy profunda del problema, pero parece que el hilo del MemoryCache no tiene suficiente tiempo de CPU para limpiarlo, mientras que se agregan muchos elementos nuevos.