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

87

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

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

  1. CacheMemoryLimitMegabytes : el tamaño máximo de memoria, en megabytes, al que puede crecer una instancia de un objeto ".
  2. PhysicalMemoryLimitPercentage : "El porcentaje de memoria física que puede usar la caché, expresado 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 según la cantidad de memoria que está instalada en el computadora." 1. Esto no es del todo correcto: cualquier valor por debajo de 4 se ignora y se reemplaza por 4.

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

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

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 obedece los límites de memoria configurados?

Canacourse
fuente
2
for loop es incorrecto, sin i ++
xiaoyifang
4
Agregué un informe de MS Connect para este error (tal vez alguien más ya lo hizo, pero de todos modos ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
3
Vale la pena señalar que Microsoft ahora (a partir del 9/2014) agregó una respuesta bastante completa en el ticket de conexión vinculado anteriormente. El TLDR de esto es que MemoryCache no verifica de manera inherente estos límites en cada operación, sino que los límites solo se respetan en el recorte de caché interno, que es periódico basado en temporizadores internos dinámicos.
Dusty
5
Parece que actualizaron los documentos de MemoryCache.CacheMemoryLimit: "MemoryCache no aplica instantáneamente CacheMemoryLimit cada vez que se agrega un nuevo elemento a una instancia de MemoryCache. La heurística interna que expulsa elementos adicionales de MemoryCache lo hace gradualmente ..." msdn.microsoft .com / en-us / library /…
Sully
1
@ Zeus, creo que MSFT eliminó el problema. En cualquier caso, MSFT cerró el problema después de una discusión conmigo, donde me dijeron que el límite solo se aplica después de que PoolingTime haya expirado.
Bruno Brant

Respuestas:

100

Vaya, pasé demasiado tiempo buscando en el CLR con reflector, pero creo que finalmente tengo una buena idea de lo que está pasando aquí.

La configuración se está leyendo correctamente, pero parece haber un problema profundamente 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 fuera de la DLL System.Runtime.Caching, para la clase CacheMemoryMonitor (hay una clase similar que monitorea la memoria física y se ocupa de 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 puede notar es que ni siquiera intenta mirar el tamaño de la caché hasta después de una recolección de basura Gen2, sino que simplemente recurre al valor de tamaño almacenado existente en cacheSizeSamples. Por lo tanto, nunca podrá dar en el blanco directamente, pero si el resto funciona, al menos obtendríamos una medición del tamaño antes de tener problemas reales.

Entonces, asumiendo que se ha producido un GC Gen2, nos encontramos con el problema 2, que es que ref2.ApproximateSize hace un trabajo horrible al aproximarse realmente al tamaño de la caché. Trabajando a través de la basura CLR, encontré que esto es un System.SizedReference, y esto es lo que está haciendo para obtener el valor (IntPtr es un identificador del objeto MemoryCache en sí):

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

Supongo que la declaración externa significa que se sumerge en la tierra de ventanas no administradas en este punto, y no tengo idea de cómo comenzar a descubrir qué hace allí. Sin embargo, por lo que he observado, hace un trabajo horrible al tratar de aproximar el tamaño del conjunto.

La tercera cosa notable es la llamada a manager.UpdateCacheSize, que parece que debería hacer algo. Desafortunadamente, en cualquier muestra normal de cómo debería funcionar, 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 de hecho pude hacer que esto funcione como se supone que debe hacer al juntar mi propia implementación de IMemoryCacheManager, configurándola en ObjectCache.Host y luego ejecutando la muestra . Sin embargo, en ese punto, parece que también podría hacer su propia implementación de caché y ni siquiera molestarse con todas estas cosas, especialmente porque no tengo idea si configurar su propia clase en ObjectCache.Host (estático,

Tengo que creer que al menos parte de esto (si no un par de partes) es simplemente un error. Sería bueno saber de alguien de MS cuál fue el trato con esto.

Versión TLDR de esta respuesta gigante: suponga que CacheMemoryLimitMegabytes está completamente destruido en este momento. Puede configurarlo en 10 MB y luego proceder a llenar el caché a ~ 2 GB y hacer una excepción de falta de memoria sin que se interrumpa la eliminación del elemento.

David Hay
fuente
4
Una buena respuesta gracias. Dejé de intentar averiguar qué estaba pasando con esto y, en su lugar, ahora administro el tamaño de la caché contando los elementos que entran / salen y llamando a .Trim () manualmente según sea necesario. Pensé que System.Runtime.Caching era una opción fácil para mi aplicación, ya que parece ser ampliamente utilizada y pensé que, por lo tanto, no tendría ningún error importante.
Canacourse
3
Guau. Por eso me encanta SO. Me encontré exactamente con el mismo comportamiento, escribí una aplicación de prueba y logré bloquear mi PC muchas veces a pesar de que el tiempo de sondeo era tan bajo como 10 segundos y el límite de memoria caché era de 1 MB. Gracias por todas las ideas.
Bruno Brant
7
Sé que lo acabo de mencionar en la pregunta, pero, para completarlo, lo mencionaré aquí nuevamente. Abrí un problema en Connect para esto. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
1
Estoy utilizando el MemoryCache para los datos de servicios externos, y cuando pruebo mediante la inyección de basura en el MemoryCache, que lo hace el contenido de auto-ajuste, pero sólo cuando se utiliza el valor límite porcentual. El tamaño absoluto no hace nada para limitar el tamaño, al menos cuando se interactúa con un perfilador de memoria. No probado en un ciclo while, pero con usos más "realistas" (es un sistema de backend, así que agregué un servicio WCF que me permite inyectar datos en las cachés bajo demanda).
Svend
¿Sigue siendo un problema en .NET Core?
Павле
29

Sé que esta respuesta es muy tarde, pero más vale tarde que nunca. Quería informarle que escribí una versión MemoryCacheque resuelve los problemas de la Colección Gen 2 automáticamente para usted. Por lo tanto, recorta cada vez que el intervalo de sondeo indica presión de memoria. Si está experimentando este problema, ¡pruébelo!

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

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

https://github.com/haneytron/sharpmemorycache

Haney
fuente
2
Esto funciona según lo previsto, probado con un generador que llena el caché con un montón de cadenas de 1000 caracteres. Aunque, agregar lo que debería ser como 100 MB al caché suma en realidad 200 - 300 MB al caché, lo que me pareció bastante extraño. Quizás algunos escuchan, no estoy contando.
Karl Cassar
5
Las cadenas de @KarlCassar en .NET tienen un 2n + 20tamaño aproximado con respecto a los bytes, donde nes la longitud de la cadena. Esto se debe principalmente a la compatibilidad con Unicode.
Haney
4

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 al 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 es una investigación muy profunda del problema, pero parece que el hilo de MemoryCache no obtiene suficiente tiempo de CPU para limpiar, mientras que se agregan muchos elementos nuevos.

Jezze
fuente
4

También encontré este problema. Estoy almacenando en caché objetos que se disparan en mi proceso docenas de veces por segundo.

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

App.config:

Tome nota de cacheMemoryLimitMegabytes . Cuando se establece en cero, la rutina de purga no se activa en un tiempo razonable.

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

Agregar al caché:

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

Confirmando 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());
}
Aaron Hudon
fuente
3

(Afortunadamente) me encontré con esta útil publicación ayer cuando intenté por primera vez usar MemoryCache. Pensé que sería un caso simple de establecer valores y usar las clases, pero encontré problemas similares descritos anteriormente. Para intentar ver qué estaba pasando, extraje la fuente usando ILSpy y luego configuré una prueba y recorrí el código. Mi código de prueba era 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 dada la implementación actual nunca funcionaría de manera confiable. Sin embargo, la medición física estuvo bien y si la memoria física se midió 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 un escenario de prueba, esto obviamente hace una gran diferencia, ya que el caché se golpea constantemente, por lo que los objetos nunca tienen la oportunidad de llegar a la generación 2. Creo que usaremos la compilación modificada de este dll en nuestro proyecto y usaremos el MS oficial. compilar cuando salga .net 4.5 (que de acuerdo con el artículo de conexión mencionado anteriormente debería tener la solución). Lógicamente, puedo ver por qué se ha implementado la verificación de gen 2, pero en la práctica no estoy seguro de si tiene mucho sentido. Si la memoria alcanza el 90% (o cualquier límite en el que se haya establecido), entonces no debería importar si se ha producido una recopilación de generación 2 o no, los elementos deben ser desalojados independientemente.

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

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

Ian Gibson
fuente
4
Una actualización: estoy usando .NET framework 4.5 y de ninguna manera se corrige el problema. La caché puede crecer lo suficiente como para bloquear la máquina.
Bruno Brant
Una pregunta: ¿tiene el enlace al artículo de conexión que mencionó?
Bruno Brant
3

Resultó que no es un error, todo lo que necesita hacer es establecer el período de tiempo de agrupación para hacer cumplir los límites, parece que si deja la agrupación no configurada, nunca se activará. Lo acabo de probar y no es necesario utilizar envoltorios o cualquier código adicional:

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

        };

Establezca el valor de " PollingInterval" en función de la rapidez con la que crece la caché; si crece demasiado rápido, aumente la frecuencia de las comprobaciones de sondeo; de lo contrario, mantenga las comprobaciones no muy frecuentes para no causar gastos generales.

sino
fuente
1

Si usa la siguiente clase modificada y monitorea 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());
    }
}
woany
fuente
¿Estás diciendo que se recorta o no?
Canacourse
Sí, se recorta. Es extraño, considerando todos los problemas con los que la gente parece tener MemoryCache. Me pregunto por qué funciona esta muestra.
Daniel Lidström
1
No lo sigo. Intenté repetir el ejemplo, pero el caché aún crece indefinidamente.
Bruno Brant
Una clase de ejemplo confusa: "Statlock", "ItemCount", "size" son inútiles ... ¿El NameValueCollection (3) solo contiene 2 elementos? ... De hecho, creó un caché con las propiedades sizelimit y pollInterval, ¡nada más! El problema de "no desalojar" artículos no se toca ...
Bernhard