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:
- CacheMemoryLimitMegabytes : O tamanho máximo da memória, em megabytes, que uma instância de um objeto pode atingir. "
- 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?
fonte
Respostas:
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.
fonte
Sei que essa resposta é uma loucura tarde, mas antes tarde do que nunca. Gostaria de informar que escrevi uma versão do
MemoryCache
que 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
fonte
2n + 20
tamanho em relação aos bytes, onden
é o comprimento da string. Isso se deve principalmente ao suporte Unicode.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.
fonte
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()); }
fonte
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.
fonte
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 sobrecargafonte
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()); } }
fonte
MemoryCache
. Eu me pergunto por que esse exemplo funciona.