MemoryCache, yapılandırmada bellek sınırlarına uymaz


87

Bir uygulamada .NET 4.0 MemoryCache sınıfıyla çalışıyorum ve maksimum önbellek boyutunu sınırlamaya çalışıyorum, ancak testlerimde önbelleğin aslında sınırlara uyuyor gibi görünmüyor.

MSDN'ye göre önbellek boyutunu sınırlaması gereken ayarları kullanıyorum :

  1. CacheMemoryLimitMegabytes : Bir nesnenin bir örneğinin büyüyebileceği megabayt cinsinden maksimum bellek boyutu. "
  2. PhysicalMemoryLimitPercentage : "Önbelleğin kullanabileceği fiziksel bellek yüzdesi, 1'den 100'e kadar bir tamsayı olarak ifade edilir. Varsayılan sıfırdır; bu, MemoryCache örneklerinin cihazda yüklü bellek miktarına görekendi belleğini 1 yönettiğinigösterir. bilgisayar." 1. Bu tamamen doğru değildir - 4'ün altındaki herhangi bir değer göz ardı edilir ve 4 ile değiştirilir.

Önbelleği temizleyen iş parçacığı her x saniyede bir tetiklendiğinden ve ayrıca yoklama aralığına ve diğer belgelenmemiş değişkenlere bağlı olduğundan, bu değerlerin yaklaşık olduğunu ve kesin sınırlar olmadığını anlıyorum. Ancak, bu farklılıkları hesaba katarsak bile , bir test uygulamasında CacheMemoryLimitMegabytes ve PhysicalMemoryLimitPercentage'ı birlikte veya tekil olarak ayarladıktan sonra ilk öğe önbellekten çıkarıldığında çılgınca tutarsız önbellek boyutları görüyorum . Emin olmak için her testi 10 kez çalıştırdım ve ortalama rakamı hesapladım.

Bunlar, aşağıdaki örnek kodun 3 GB RAM ile 32 bit Windows 7 bilgisayarda test edilmesinin sonuçlarıdır. Önbelleğin boyutu , her testte ilk CacheItemRemoved () çağrısından sonra alınır . (Önbellek boyutunun bundan daha büyük olacağının farkındayım)

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

İşte test uygulaması:

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

MemoryCache neden yapılandırılmış bellek sınırlarına uymuyor ?


2
for döngüsü yanlış, i ++ olmadan
xiaoyifang

4
Bu hata için bir MS Connect raporu ekledim (belki başka biri zaten yaptı, ancak yine de ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

3
Microsoft'un şimdi (9/2014 itibariyle) yukarıda bağlantısı verilen bağlantı biletine oldukça kapsamlı bir yanıt eklediğini belirtmek gerekir. Bunun TLDR'si, MemoryCache'nin doğası gereği bu sınırları her işlemde kontrol etmemesidir , bunun yerine sınırlara yalnızca dinamik dahili zamanlayıcılara dayanan periyodik olan dahili önbellek kırpmada saygı duyulur.
Tozlu

5
Görünüşe göre MemoryCache.CacheMemoryLimit için dokümanları güncellediler: "MemoryCache, bir MemoryCache örneğine her yeni öğe eklendiğinde CacheMemoryLimit'i anında zorlamaz. MemoryCache'den fazladan öğeleri çıkaran dahili buluşsal yöntem bunu kademeli olarak yapar ..." msdn.microsoft .com / en-us / library /…
Sully

1
@Zeus, MSFT'nin sorunu kaldırdığını düşünüyorum. Her halükarda, MSFT benimle biraz tartıştıktan sonra konuyu kapattı ve burada limitin yalnızca Havuzlama Süresi sona erdikten sonra uygulandığını söylediler.
Bruno Brant

Yanıtlar:


100

Vay canına, bu yüzden CLR'de reflektörle dolaşmak için tamamen çok fazla zaman harcadım, ama sonunda burada neler olup bittiğini iyi bir şekilde ele aldığımı düşünüyorum.

Ayarlar doğru bir şekilde okunuyor, ancak CLR'nin kendisinde, bellek limiti ayarını esasen işe yaramaz hale getirecek gibi görünen köklü bir sorun var gibi görünüyor.

Aşağıdaki kod, CacheMemoryMonitor sınıfı için System.Runtime.Caching DLL'sinden yansıtılır (fiziksel belleği izleyen ve diğer ayarlarla ilgilenen benzer bir sınıf vardır, ancak bu daha önemli olanıdır):

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

Fark edebileceğiniz ilk şey, bir Gen2 çöp toplama işlemine kadar önbelleğin boyutuna bakmaya bile çalışmaması, bunun yerine cacheSizeSamples'daki mevcut depolanmış boyut değerine geri dönmesidir. Yani hedefi asla tam olarak vuramayacaksınız, ancak gerisi işe yararsa, en azından gerçekten başımız belaya girmeden önce bir boyut ölçümü alırdık.

Öyleyse, bir Gen2 GC'nin meydana geldiğini varsayarsak, sorun 2 ile karşılaşıyoruz, bu ref2.ApproximateSize, önbelleğin boyutuna yaklaşmak gibi korkunç bir iş çıkarıyor. CLR önemsiz yere giriş yapmak, bunun bir System.SizedReference olduğunu ve değeri elde etmek için yaptığı şeyin bu olduğunu buldum (IntPtr, MemoryCache nesnesinin kendisinin bir tanıtıcısıdır):

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

Harici bildirimin, bu noktada yönetilmeyen pencerelere daldığı anlamına geldiğini varsayıyorum ve orada ne yaptığını bulmaya nasıl başlayacağım hakkında hiçbir fikrim yok. Yine de gözlemlediklerime göre, genel şeyin boyutunu yaklaşık olarak tahmin etmeye çalışmak korkunç bir iş yapıyor.

Üçüncü dikkat çeken şey, bir şey yapması gerektiği gibi görünen manager.UpdateCacheSize çağrısıdır. Ne yazık ki bunun nasıl çalışması gerektiğine dair herhangi bir normal örnekte s_memoryCacheManager her zaman boş olacaktır. Alan, genel statik üye ObjectCache.Host'tan ayarlanır. Bu, kullanıcının isterse uğraşmasına maruz kalır ve aslında bu şeyi olması gerektiği gibi, kendi IMemoryCacheManager uygulamamı bir araya getirerek, onu ObjectCache.Host olarak ayarlayarak ve ardından örneği çalıştırarak yapabildim. . Yine de bu noktada, kendi önbellek uygulamanızı yapabilir ve tüm bunlarla uğraşmayabilirsiniz, özellikle de kendi sınıfınızı ObjectCache.Host (statik,

Bunun en azından bir kısmının (birkaç parçası değilse) sadece basit bir hata olduğuna inanmak zorundayım. MS'teki birinden bu şeyle olan anlaşmanın ne olduğunu duymak güzel olurdu.

Bu dev cevabın TLDR versiyonu: CacheMemoryLimitMegabytes'ın bu noktada tamamen bozulmuş olduğunu varsayalım. Bunu 10 MB olarak ayarlayabilir ve ardından önbelleği ~ 2GB'a kadar doldurmaya devam edebilir ve öğe kaldırmayı açmadan bir bellek dışı istisnasını patlatabilirsiniz.


4
Güzel bir cevap teşekkür ederim. Bununla neler olup bittiğini anlamaya çalışmaktan vazgeçtim ve bunun yerine şimdi öğeleri içeri / dışarı sayarak ve gerektiğinde manuel olarak .Trim () 'i çağırarak önbellek boyutunu yönetiyorum. System.Runtime.Caching uygulamasının yaygın olarak kullanıldığı için kolay bir seçim olduğunu düşündüm ve bu nedenle herhangi bir büyük hatanın olmayacağını düşündüm.
Canacourse

3
Vay. Bu yüzden SO seviyorum. Tamamen aynı davranışla karşılaştım, bir test uygulaması yazdım ve yoklama süresi 10 saniye kadar düşük ve önbellek limiti 1MB olmasına rağmen bilgisayarımı defalarca çökertmeyi başardım. Tüm görüşler için teşekkürler.
Bruno Brant

7
Soruda bundan az önce bahsettiğimi biliyorum, ama bütünlük aşkına, burada tekrar bahsedeceğim. Bunun için Connect'te bir sorun açtım. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

1
Ben dış hizmet verisi için MemoryCache kullanarak, ve ben MemoryCache içine çöp enjekte ederek test ederken, bu does yüzde limiti değerini kullanarak yalnızca otomatik Döşeme içeriği ancak. Mutlak boyut, en azından bir bellek profili oluşturucu ile çalışırken, boyutu sınırlamak için hiçbir şey yapmaz. Bir süre döngüsünde test edilmedi, ancak daha "gerçekçi" kullanımlarla (bu bir arka uç sistemidir, bu nedenle talep üzerine önbelleklere veri enjekte etmeme izin veren bir WCF hizmeti ekledim).
Svend

Bu hala .NET Core'da bir sorun mu?
Павле

29

Bu cevabın çok geç olduğunu biliyorum, ama geç olması hiç olmamasından iyidir. MemoryCacheGen 2 Koleksiyonu sorunlarını sizin için otomatik olarak çözen bir sürüm yazdığımı bilmenizi isterim. Bu nedenle, yoklama aralığı bellek baskısını gösterdiğinde kırpar. Bu sorunu yaşıyorsanız, bir deneyin!

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

Nasıl çözdüğümü merak ediyorsanız, GitHub'da da bulabilirsiniz. Kod biraz basit.

https://github.com/haneytron/sharpmemorycache


2
Bu, amaçlandığı gibi çalışır, önbelleği 1000 karakterlik dizelerle dolduran bir jeneratörle test edilmiştir. Her ne kadar önbelleğe 100MB gibi olması gerekenleri eklemek aslında önbelleğe 200 - 300MB ekliyor ki bunu oldukça garip buldum. Belki saymadığım bazı kulak misafiri olur.
Karl Cassar

5
NET'teki @KarlCassar dizeleri baytlara göre kabaca 2n + 20boyuttadır, burada ndizenin uzunluğu. Bu çoğunlukla Unicode desteğinden kaynaklanmaktadır.
Haney

5

Ben de bu sorunla karşılaştım. Saniyede onlarca kez sürecime ateşlenen nesneleri önbelleğe alıyorum.

Aşağıdaki yapılandırmayı buldum ve kullanım çoğu zaman öğeleri her 5 saniyede bir serbest bırakır .

App.config:

CacheMemoryLimitMegabytes'ı not alın . Bu sıfıra ayarlandığında, temizleme rutini makul bir süre içinde çalışmayacaktır.

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

Önbelleğe ekleniyor:

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

Önbellek kaldırma işleminin çalıştığını onaylama:

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

4

@Canacourse örneğiyle ve @woany'nin değiştirilmesiyle bazı testler yaptım ve önbelleğin temizlenmesini engelleyen bazı kritik çağrılar olduğunu düşünüyorum.

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

Fakat @woany'nin değiştirilmesi neden hafızayı aynı seviyede tutuyor gibi görünüyor? İlk olarak, RemovedCallback ayarlanmamıştır ve bellek önbelleğinin iş parçacığını engelleyebilecek bir konsol çıkışı veya giriş için bekleme yoktur.

İkincisi ...

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

Bir Thread.Sleep (1) her ~ 1000. AddItem () aynı etkiye sahip olacaktır.

Pekala, bu sorunun çok derin bir araştırması değil, ancak MemoryCache'nin iş parçacığı temizleme için yeterli CPU zamanı almıyor gibi görünüyor ve birçok yeni öğe ekleniyor.


3

Ben (neyse ki) dün MemoryCache'yi ilk kez kullanmaya çalışırken bu yararlı gönderiye rastladım. Değerleri belirleme ve sınıfları kullanma konusunda basit bir durum olacağını düşündüm, ancak yukarıda özetlenen benzer sorunlarla karşılaştım. Neler olduğunu denemek ve görmek için ILSpy kullanarak kaynağı çıkardım ve ardından bir test ayarladım ve kodda adım adım ilerledim. Test kodum yukarıdaki koda çok benziyordu, bu yüzden onu göndermeyeceğim. Testlerimden, önbellek boyutunun ölçümünün hiçbir zaman özellikle doğru olmadığını (yukarıda belirtildiği gibi) ve mevcut uygulamanın asla güvenilir bir şekilde çalışmayacağını fark ettim. Bununla birlikte, fiziksel ölçüm iyiydi ve fiziksel bellek her ankette ölçülürse, o zaman bana kodun güvenilir bir şekilde çalışacağını düşündü. Bu yüzden, MemoryCacheStatistics içindeki gen 2 çöp toplama denetimini kaldırdım;

Bir test senaryosunda, önbellek sürekli olarak vurulduğundan, nesnelerin hiçbir zaman 2. nesle ulaşma şansı olmadığı için bu açıkça büyük bir fark yaratır. Sanırım bu dll'nin değiştirilmiş yapısını projemizde ve resmi MS'yi kullanacağız. .net 4.5 çıktığında build (yukarıda bahsedilen bağlantı makalesine göre içinde düzeltme olmalıdır). Mantıksal olarak gen 2 kontrolünün neden uygulandığını anlayabiliyorum ama pratikte bunun mantıklı olup olmadığından emin değilim. Bellek% 90'a (veya ayarlanmış herhangi bir sınıra) ulaşırsa, gen 2 koleksiyonunun oluşup oluşmadığı önemli olmamalıdır, öğeler ne olursa olsun çıkarılmalıdır.

PhysicalMemoryLimitPercentage% 65 olarak ayarlıyken test kodumu yaklaşık 15 dakika çalışır durumda bıraktım. Test sırasında bellek kullanımının% 65-68 arasında kaldığını gördüm ve eşyaların düzgün şekilde tahliye edildiğini gördüm. Bunu varsayılan olarak test etmek için pollingInterval değerini 5 saniyeye, PhysicalMemoryLimitPercentage değerini 65 ve PhysicalMemoryLimitPercentage değerini 0 olarak ayarladım.

Yukarıdaki tavsiyelere uyarak; Bir şeyleri önbellekten çıkarmak için bir IMemoryCacheManager uygulaması yapılabilir. Bununla birlikte, bahsedilen gen 2 kontrol sorunundan muzdarip olacaktır. Yine de senaryoya bağlı olarak bu üretim kodunda sorun olmayabilir ve insanlar için yeterince çalışabilir.


4
Güncelleme: .NET framework 4.5 kullanıyorum ve hiçbir şekilde sorun düzeltilmedi. Önbellek, makineyi çökertecek kadar büyüyebilir.
Bruno Brant

Bir soru: Bahsettiğiniz bağlantı makalesine bağlantı var mı?
Bruno Brant


3

Bunun bir hata olmadığı ortaya çıktı, yapmanız gereken tek şey, havuzlama zaman aralığını sınırları zorlamak için ayarlamaktır, görünüşe göre havuzlamayı ayarlanmadan bırakırsanız, asla tetiklenmez. Sadece test ettim ve sarmalayıcılara gerek yok veya herhangi bir ekstra kod:

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

        };

PollingIntervalÖnbelleğin ne kadar hızlı büyüdüğüne bağlı olarak " " değerini ayarlayın , eğer çok hızlı büyürse, yoklama denetimlerinin sıklığını artırın, aksi takdirde denetimleri ek yüke neden olmamak için çok sık tutmayın.


1

Aşağıdaki değiştirilmiş sınıfı kullanırsanız ve belleği Görev Yöneticisi aracılığıyla izlerseniz, aslında kırpılır:

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

Kesildiğini mi yoksa kesilmediğini mi söylüyorsun?
Canacourse

Evet, kırpılıyor. İnsanların yaşadığı tüm sorunları düşündüğümüzde garip MemoryCache. Bu örneğin neden işe yaradığını merak ediyorum.
Daniel Lidström

1
Ben onu takip etmiyorum. Örneği tekrarlamayı denedim, ancak önbellek hala süresiz olarak büyüyor.
Bruno Brant

Kafa karıştırıcı bir örnek sınıf: "Statlock", "ItemCount", "size" işe yaramaz ... NameValueCollection (3) yalnızca 2 öğe barındırıyor mu? ... Aslında sizelimit ve pollInterval özelliklerine sahip bir önbellek oluşturdunuz, daha fazlası değil! Öğelerin "tahliye edilmemesi" sorununa dokunulmadı ...
Bernhard
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.