MemoryCache não obedece aos limites de memória na configuração


87

Estou trabalhando com a classe .NET 4.0 MemoryCache em um aplicativo e tentando limitar o tamanho máximo do cache, mas em meus testes não parece que o cache está realmente obedecendo aos limites.

Estou usando as configurações que, de acordo com o MSDN , devem limitar o tamanho do cache:

  1. CacheMemoryLimitMegabytes : O tamanho máximo da memória, em megabytes, que uma instância de um objeto pode atingir. "
  2. PhysicalMemoryLimitPercentage : "A porcentagem de memória física que o cache pode usar, expressa como um valor inteiro de 1 a 100. O padrão é zero, o que indica que as instâncias de MemoryCache gerenciam sua própria memória 1 com base na quantidade de memória instalada no computador." 1. Isso não está totalmente correto - qualquer valor abaixo de 4 é ignorado e substituído por 4.

Eu entendo que esses valores são aproximados e não limites rígidos, pois o thread que limpa o cache é disparado a cada x segundos e também depende do intervalo de pesquisa e de outras variáveis ​​não documentadas. No entanto, mesmo levando em consideração essas variações, estou vendo tamanhos de cache totalmente inconsistentes quando o primeiro item está sendo removido do cache após definir CacheMemoryLimitMegabytes e PhysicalMemoryLimitPercentage juntos ou individualmente em um aplicativo de teste. Para ter certeza, executei cada teste 10 vezes e calculei o valor médio.

Estes são os resultados do teste do código de exemplo abaixo em um PC com Windows 7 de 32 bits com 3 GB de RAM. O tamanho do cache é obtido após a primeira chamada para CacheItemRemoved () em cada teste. (Estou ciente de que o tamanho real do cache será maior do que isso)

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

Aqui está o aplicativo de teste:

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 que o MemoryCache não está obedecendo aos limites de memória configurados?


2
o loop for está errado, sem i ++
xiaoyifang

4
Eu adicionei um relatório do MS Connect para esse bug (talvez outra pessoa já tenha adicionado, mas de qualquer maneira ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

3
É importante notar que a Microsoft agora (em 9/2014) adicionou uma resposta bastante completa sobre o tíquete de conexão com link acima. O TLDR disso é que o MemoryCache não verifica inerentemente esses limites em todas as operações, mas sim que os limites são respeitados apenas no corte de cache interno, que é periódico com base em temporizadores internos dinâmicos.
Dusty

5
Parece que eles atualizaram os documentos para MemoryCache.CacheMemoryLimit: "MemoryCache não impõe instantaneamente CacheMemoryLimit cada vez que um novo item é adicionado a uma instância de MemoryCache. A heurística interna que expulsa itens extras do MemoryCache faz isso gradualmente ..." msdn.microsoft .com / en-us / library /…
Sully

1
@ Zeus, acho que a MSFT removeu o problema. Em qualquer caso, a MSFT fechou o problema após alguma discussão comigo, onde me disseram que o limite só é aplicado depois que o PoolingTime expirar.
Bruno Brant

Respostas:


100

Uau, passei muito tempo vasculhando o CLR com refletor, mas acho que finalmente entendi bem o que está acontecendo aqui.

As configurações estão sendo lidas corretamente, mas parece haver um problema profundo no próprio CLR que parece que tornará a configuração do limite de memória essencialmente inútil.

O código a seguir é refletido na DLL System.Runtime.Caching, para a classe CacheMemoryMonitor (há uma classe semelhante que monitora a memória física e lida com a outra configuração, mas esta é a mais 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);
}

A primeira coisa que você pode notar é que ele nem tenta olhar para o tamanho do cache até depois de uma coleta de lixo Gen2, em vez disso, apenas recorre ao valor de tamanho armazenado existente em cacheSizeSamples. Portanto, você nunca será capaz de acertar o alvo na hora, mas se o resto funcionasse, pelo menos obteríamos uma medição do tamanho antes de termos problemas reais.

Portanto, supondo que ocorreu um GC Gen2, encontramos o problema 2, que é que ref2.ApproximateSize faz um trabalho horrível de realmente aproximar o tamanho do cache. Lutando contra o lixo do CLR, descobri que este é um System.SizedReference e é isso que ele está fazendo para obter o valor (IntPtr é um identificador para o próprio objeto MemoryCache):

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

Estou assumindo que a declaração externa significa que ele vai mergulhar em áreas não gerenciadas do Windows neste ponto, e não tenho ideia de como começar a descobrir o que ele faz lá. Pelo que observei, porém, é um péssimo trabalho tentar aproximar o tamanho do conjunto.

A terceira coisa notável é a chamada para manager.UpdateCacheSize, que parece que deveria fazer algo. Infelizmente, em qualquer amostra normal de como isso deve funcionar, s_memoryCacheManager sempre será nulo. O campo é definido a partir do membro público estático ObjectCache.Host. Isso é exposto para o usuário mexer se ele quiser, e eu pude realmente fazer essa coisa funcionar como deveria, juntando minha própria implementação IMemoryCacheManager, definindo-a como ObjectCache.Host e, em seguida, executando a amostra . Nesse ponto, porém, parece que você pode muito bem fazer sua própria implementação de cache e nem mesmo se preocupar com todas essas coisas, especialmente porque não tenho ideia se definir sua própria classe para ObjectCache.Host (estático,

Eu tenho que acreditar que pelo menos parte disso (se não algumas partes) é apenas um bug direto. Seria bom ouvir de alguém da MS qual é o problema com isso.

Versão TLDR desta resposta gigante: suponha que CacheMemoryLimitMegabytes esteja completamente quebrado neste momento. Você pode configurá-lo para 10 MB e, em seguida, continuar a preencher o cache para ~ 2 GB e explodir uma exceção de falta de memória sem interromper a remoção do item.


4
Uma boa resposta, obrigado. Desisti de tentar descobrir o que estava acontecendo com isso e, em vez disso, agora gerencio o tamanho do cache contando os itens de entrada / saída e chamando .Trim () manualmente conforme necessário. Achei que System.Runtime.Caching fosse uma escolha fácil para meu aplicativo, pois parece ser amplamente usado e, portanto, achei que não teria grandes bugs.
Canacourse de

3
Uau. É por isso que amo ASSIM. Corri exatamente o mesmo comportamento, escrevi um aplicativo de teste e consegui travar meu PC muitas vezes, embora o tempo de pesquisa fosse tão baixo quanto 10 segundos e o limite de memória cache fosse de 1 MB. Obrigado por todos os insights.
Bruno Brant,

7
Eu sei que acabei de mencionar isso lá na pergunta, mas, para completar, vou mencioná-lo aqui novamente. Abri um problema no Connect para isso. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

1
Estou usando o MemoryCache para dados de serviços externos, e quando eu testar pela injeção de lixo no MemoryCache, ele faz o conteúdo auto-trim, mas somente quando estiver usando o valor percentual limite. O tamanho absoluto não faz nada para limitar o tamanho, pelo menos quando detectado com um profiler de memória. Não testado em um loop while, mas por usos mais "realistas" (é um sistema de back-end, então adicionei um serviço WCF que me permite injetar dados nos caches sob demanda).
Svend

Isso ainda é um problema no .NET Core?
Павле

29

Sei que essa resposta é uma loucura tarde, mas antes tarde do que nunca. Gostaria de informar que escrevi uma versão do MemoryCacheque resolve os problemas da coleção Gen 2 automaticamente para você. Portanto, ele corta sempre que o intervalo de pesquisa indica pressão de memória. Se você estiver enfrentando esse problema, experimente!

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

Você também pode encontrá-lo no GitHub se estiver curioso para saber como resolvi. O código é um tanto simples.

https://github.com/haneytron/sharpmemorycache


2
Isso funciona como esperado, testado com um gerador que preenche o cache com cargas de strings de 1000 caracteres. Embora, somar o que deveria ser cerca de 100 MB para o cache adiciona na verdade 200 - 300 MB para o cache, o que eu achei muito estranho. Talvez alguns ouviram que não estou contando.
Karl Cassar

5
As strings @KarlCassar no .NET têm aproximadamente o 2n + 20tamanho em relação aos bytes, onde né o comprimento da string. Isso se deve principalmente ao suporte Unicode.
Haney

4

Fiz alguns testes com o exemplo de @Canacourse e a modificação de @woany e acho que existem algumas chamadas críticas que bloqueiam a limpeza do cache de memória.

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

Mas por que a modificação de @woany parece manter a memória no mesmo nível? Em primeiro lugar, o RemovedCallback não está definido e não há saída do console ou espera por uma entrada que possa bloquear o thread do cache de memória.

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

Um Thread.Sleep (1) a cada ~ 1000º AddItem () teria o mesmo efeito.

Bem, não é uma investigação muito profunda do problema, mas parece que o thread do MemoryCache não obtém tempo de CPU suficiente para a limpeza, enquanto muitos novos elementos são adicionados.


4

Eu também encontrei esse problema. Estou armazenando objetos em cache que estão sendo disparados em meu processo dezenas de vezes por segundo.

Eu descobri que a seguinte configuração e uso libera os itens a cada 5 segundos na maioria das vezes .

App.config:

Anote cacheMemoryLimitMegabytes . Quando isso era definido como zero, a rotina de purga não disparava em um tempo razoável.

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

Adicionando ao cache:

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

Confirmando se a remoção do cache está funcionando:

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

3

Eu (felizmente) deparei com esta postagem útil ontem quando tentei usar o MemoryCache pela primeira vez. Achei que seria um caso simples de definir valores e usar as classes, mas encontrei problemas semelhantes descritos acima. Para tentar ver o que estava acontecendo, extraí o código-fonte usando ILSpy e, em seguida, configurei um teste e examinei o código. Meu código de teste era muito semelhante ao código acima, então não vou postá-lo. Em meus testes, percebi que a medição do tamanho do cache nunca foi particularmente precisa (como mencionado acima) e, dada a implementação atual, nunca funcionaria de forma confiável. No entanto, a medição física foi boa e se a memória física foi medida em todas as pesquisas, então me pareceu que o código funcionaria de forma confiável. Portanto, removi a verificação de coleta de lixo da geração 2 em MemoryCacheStatistics;

Em um cenário de teste, isso obviamente faz uma grande diferença, já que o cache está sendo atingido constantemente, então os objetos nunca têm a chance de chegar à geração 2. Acho que vamos usar a compilação modificada desta dll em nosso projeto e usar o MS oficial construir quando .net 4.5 for lançado (que de acordo com o artigo de conexão mencionado acima deve ter a correção nele). Logicamente, posso ver por que a verificação gen 2 foi implementada, mas na prática não tenho certeza se faz muito sentido. Se a memória atingir 90% (ou qualquer limite que tenha sido definido), então não deve importar se uma coleção gen 2 ocorreu ou não, os itens devem ser despejados de qualquer maneira.

Deixei meu código de teste em execução por cerca de 15 minutos com o PhysicalMemoryLimitPercentage definido para 65%. Eu vi que o uso de memória permaneceu entre 65-68% durante o teste e vi coisas sendo despejadas corretamente. Em meu teste, configurei pollingInterval para 5 segundos, physicalMemoryLimitPercentage para 65 e physicalMemoryLimitPercentage para 0 para definir esse padrão.

Seguindo o conselho acima; uma implementação de IMemoryCacheManager pode ser feita para remover coisas do cache. No entanto, ele sofreria do problema de verificação gen 2 mencionado. Embora, dependendo do cenário, isso possa não ser um problema no código de produção e pode funcionar o suficiente para as pessoas.


4
Uma atualização: estou usando o .NET framework 4.5 e de forma alguma o problema foi corrigido. O cache pode crescer o suficiente para travar a máquina.
Bruno Brant

Uma pergunta: você tem o link para o artigo de conexão que mencionou?
Bruno Brant,


3

Acontece que não é um bug, tudo o que você precisa fazer é definir o intervalo de tempo de pooling para aplicar os limites, parece que se você deixar o pooling não definido, ele nunca será acionado. Acabei de testar e não há necessidade de invólucros ou qualquer código extra:

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

        };

Defina o valor de " PollingInterval" com base em quão rápido o cache está crescendo; se crescer muito rápido, aumente a frequência das verificações de pesquisa, caso contrário, mantenha as verificações não muito frequentes para não causar sobrecarga


1

Se você usar a seguinte classe modificada e monitorar a memória por meio do Gerenciador de Tarefas, de fato será cortada:

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

Você está dizendo que ele é cortado ou não?
Canacourse

Sim, ele é cortado. Estranho, considerando todos os problemas que as pessoas parecem ter MemoryCache. Eu me pergunto por que esse exemplo funciona.
Daniel Lidström

1
Eu não sigo isso. Tentei repetir o exemplo, mas o cache ainda cresce indefinidamente.
Bruno Brant

Um exemplo de classe confuso: "Statlock", "ItemCount", "size" são inúteis ... O NameValueCollection (3) contém apenas 2 itens? ... Na verdade, você criou um cache com as propriedades sizelimit e pollInterval, nada mais! O problema de "não despejar" itens não é tocado ...
Bernhard
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.