MemoryCache tidak mematuhi batas memori dalam konfigurasi

87

Saya bekerja dengan kelas .NET 4.0 MemoryCache dalam sebuah aplikasi dan mencoba membatasi ukuran cache maksimum, tetapi dalam pengujian saya tidak tampak bahwa cache sebenarnya mematuhi batas.

Saya menggunakan pengaturan yang, menurut MSDN , seharusnya membatasi ukuran cache:

  1. CacheMemoryLimitMegabytes : Ukuran memori maksimum, dalam megabyte, yang dapat digunakan untuk mengembangkan instance objek. "
  2. PhysicalMemoryLimitPercentage : "Persentase memori fisik yang dapat digunakan cache, dinyatakan sebagai nilai integer dari 1 hingga 100. Standarnya adalah nol, yang menunjukkan bahwainstance MemoryCache mengelola memori 1 mereka sendiriberdasarkan jumlah memori yang diinstal di komputer." 1. Ini tidak sepenuhnya benar - nilai apa pun di bawah 4 akan diabaikan dan diganti dengan 4.

Saya mengerti bahwa nilai-nilai ini adalah perkiraan dan bukan batas yang sulit karena utas yang membersihkan cache dipecat setiap x detik dan juga bergantung pada interval polling dan variabel tidak berdokumen lainnya. Namun, bahkan dengan mempertimbangkan perbedaan ini, saya melihat ukuran cache yang sangat tidak konsisten saat item pertama dikeluarkan dari cache setelah menyetel CacheMemoryLimitMegabytes dan PhysicalMemoryLimitPercentage bersama-sama atau secara tunggal dalam aplikasi pengujian. Untuk memastikan saya menjalankan setiap tes 10 kali dan menghitung angka rata-rata.

Ini adalah hasil pengujian kode contoh di bawah ini pada PC Windows 7 32-bit dengan RAM 3GB. Ukuran cache diambil setelah panggilan pertama ke CacheItemRemoved () pada setiap pengujian. (Saya tahu ukuran cache sebenarnya akan lebih besar dari ini)

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

Berikut adalah aplikasi tesnya:

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

Mengapa MemoryCache tidak mematuhi batas memori yang dikonfigurasi?

Canacourse
sumber
2
for loop salah, tanpa i ++
xiaoyifang
4
Saya telah menambahkan laporan MS Connect untuk bug ini (mungkin orang lain sudah melakukannya, tapi bagaimanapun ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
3
Perlu dicatat bahwa Microsoft sekarang (per 9/2014) menambahkan respons yang cukup menyeluruh pada tiket koneksi yang ditautkan di atas. TLDR-nya adalah bahwa MemoryCache tidak secara inheren memeriksa batas-batas ini pada setiap operasi, melainkan bahwa batas tersebut hanya dipatuhi pada pemangkasan cache internal, yang bersifat berkala berdasarkan pengatur waktu internal dinamis.
Dusty
5
Sepertinya mereka memperbarui dokumen untuk MemoryCache.CacheMemoryLimit: "MemoryCache tidak langsung menerapkan CacheMemoryLimit setiap kali item baru ditambahkan ke instance MemoryCache. Heuristik internal yang mengeluarkan item ekstra dari MemoryCache melakukannya secara bertahap ..." msdn.microsoft .com / en-us / library /…
Sully
1
@ Zeus, saya pikir MSFT telah menghapus masalah tersebut. Bagaimanapun, MSFT menutup masalah setelah beberapa diskusi dengan saya, di mana mereka memberi tahu saya bahwa batas hanya diterapkan setelah PoolingTime kedaluwarsa.
Bruno Brant

Jawaban:

100

Wow, jadi saya menghabiskan terlalu banyak waktu untuk menggali di dalam CLR dengan reflektor, tapi saya rasa akhirnya saya bisa memahami dengan baik apa yang terjadi di sini.

Pengaturan sedang dibaca dengan benar, tetapi tampaknya ada masalah mendalam di CLR itu sendiri yang sepertinya akan membuat pengaturan batas memori pada dasarnya tidak berguna.

Kode berikut tercermin dari System.Runtime.Caching DLL, untuk kelas CacheMemoryMonitor (ada kelas serupa yang memantau memori fisik dan berurusan dengan pengaturan lain, tetapi ini yang lebih penting):

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

Hal pertama yang mungkin Anda perhatikan adalah bahwa ia bahkan tidak mencoba melihat ukuran cache sampai setelah pengumpulan sampah Gen2, alih-alih hanya mengembalikan nilai ukuran tersimpan yang ada di cacheSizeSamples. Jadi Anda tidak akan pernah bisa mencapai target dengan tepat, tapi jika yang lainnya berhasil kita setidaknya akan mendapatkan ukuran ukuran sebelum kita mendapat masalah yang nyata.

Jadi dengan asumsi Gen2 GC telah terjadi, kita mengalami masalah 2, yaitu ref2.ApproximateSize melakukan pekerjaan yang mengerikan untuk benar-benar memperkirakan ukuran cache. Bekerja keras melalui sampah CLR saya menemukan bahwa ini adalah System.SizedReference, dan inilah yang dilakukannya untuk mendapatkan nilai (IntPtr adalah pegangan ke objek MemoryCache itu sendiri):

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

Saya berasumsi bahwa deklarasi eksternal berarti bahwa itu masuk ke jendela yang tidak terkelola pada saat ini, dan saya tidak tahu bagaimana mulai mencari tahu apa fungsinya di sana. Dari apa yang saya amati meskipun itu melakukan pekerjaan yang mengerikan mencoba memperkirakan ukuran keseluruhannya.

Hal ketiga yang terlihat adalah panggilan ke manager.UpdateCacheSize yang sepertinya harus melakukan sesuatu. Sayangnya, dalam contoh normal cara kerja ini, s_memoryCacheManager akan selalu bernilai null. Bidang ini disetel dari ObjectCache.Host anggota statis publik. Ini terbuka bagi pengguna untuk mengacaukannya jika dia memilihnya, dan saya benar-benar dapat membuat hal ini berfungsi seperti yang seharusnya dengan menggabungkan implementasi IMemoryCacheManager saya sendiri, mengaturnya ke ObjectCache.Host, dan kemudian menjalankan sampel . Pada titik itu, sepertinya Anda sebaiknya membuat implementasi cache Anda sendiri dan bahkan tidak peduli dengan semua hal ini, terutama karena saya tidak tahu apakah mengatur kelas Anda sendiri ke ObjectCache.Host (statis,

Saya harus percaya bahwa setidaknya sebagian dari ini (jika bukan beberapa bagian) hanyalah bug langsung. Senang sekali mendengar dari seseorang di MS apa kesepakatannya dengan hal ini.

Versi TLDR dari jawaban raksasa ini: Asumsikan bahwa CacheMemoryLimitMegabytes benar-benar rusak pada saat ini. Anda dapat mengaturnya menjadi 10 MB, dan kemudian melanjutkan untuk mengisi cache hingga ~ 2GB dan menghilangkan pengecualian memori tanpa tersandung penghapusan item.

David Hay
sumber
4
Jawaban yang bagus, terima kasih. Saya menyerah mencoba mencari tahu apa yang terjadi dengan ini dan sebagai gantinya sekarang mengelola ukuran cache dengan menghitung item yang masuk / keluar dan memanggil .Trim () secara manual sesuai kebutuhan. Saya pikir System.Runtime.Caching adalah pilihan yang mudah untuk aplikasi saya karena tampaknya banyak digunakan dan saya pikir karena itu tidak akan ada bug utama.
Canacourse
3
Wow. Itulah mengapa saya menyukai SO. Saya mengalami perilaku yang sama persis, menulis aplikasi pengujian dan berhasil membuat PC saya mogok berkali-kali meskipun waktu pemungutan suara serendah 10 detik dan batas memori cache 1MB. Terima kasih atas semua wawasannya.
Bruno Brant
7
Saya tahu saya baru saja menyebutkannya di atas sana dalam pertanyaan, tetapi demi kelengkapan, saya akan menyebutkannya lagi di sini. Saya telah membuka masalah di Connect untuk ini. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant
1
Saya menggunakan MemoryCache untuk data layanan eksternal, dan ketika saya menguji dengan memasukkan sampah ke dalam MemoryCache, ia melakukan pemotongan otomatis konten, tetapi hanya jika menggunakan nilai batas persentase. Ukuran absolut tidak membatasi ukuran, setidaknya saat berhubungan dengan profiler memori. Tidak diuji dalam loop sementara, tetapi dengan penggunaan yang lebih "realistis" (ini adalah sistem backend, jadi saya telah menambahkan layanan WCF yang memungkinkan saya memasukkan data ke dalam cache sesuai permintaan).
Svend
Apakah ini masih menjadi masalah di .NET Core?
Павле
29

Saya tahu jawaban ini sangat terlambat, tetapi lebih baik terlambat daripada tidak sama sekali. Saya ingin memberi tahu Anda bahwa saya menulis versi MemoryCacheyang menyelesaikan masalah Koleksi Gen 2 secara otomatis untuk Anda. Oleh karena itu, ia memangkas setiap kali polling interval menunjukkan tekanan memori. Jika Anda mengalami masalah ini, cobalah!

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

Anda juga dapat menemukannya di GitHub jika Anda penasaran bagaimana saya menyelesaikannya. Kodenya agak sederhana.

https://github.com/haneytron/sharpmemorycache

Haney
sumber
2
Ini berfungsi sebagaimana mestinya, diuji dengan generator yang mengisi cache dengan banyak string 1000 karakter. Meskipun, menambahkan 100MB ke cache sebenarnya menambahkan 200-300MB ke cache, yang menurut saya cukup aneh. Mungkin beberapa orang mendengar yang tidak saya hitung.
Karl Cassar
5
String @karlCassar di .NET kira-kira 2n + 20berukuran terkait dengan byte, di mana nadalah panjang string. Ini sebagian besar disebabkan oleh dukungan Unicode.
Haney
5

Saya juga mengalami masalah ini. Saya menyimpan objek yang ditembakkan ke dalam proses saya puluhan kali per detik.

Saya telah menemukan konfigurasi dan penggunaan berikut membebaskan item setiap 5 detik sebagian besar waktu .

App.config:

Catat cacheMemoryLimitMegabytes . Jika ini disetel ke nol, rutinitas pembersihan tidak akan diaktifkan dalam waktu yang wajar.

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

Menambahkan ke cache:

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

Mengonfirmasi bahwa penghapusan cache berfungsi:

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

Saya telah melakukan beberapa pengujian dengan contoh @Canacourse dan modifikasi @woany dan menurut saya ada beberapa panggilan penting yang memblokir pembersihan cache memori.

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

Tapi kenapa modifikasi @woany nampaknya menjaga memori pada level yang sama? Pertama, RemovedCallback tidak disetel dan tidak ada keluaran konsol atau menunggu masukan yang dapat memblokir utas cache memori.

Kedua...

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

Sebuah Thread.Sleep (1) setiap ~ 1000 AddItem () akan memiliki efek yang sama.

Yah, ini bukan penyelidikan yang sangat mendalam tentang masalahnya, tetapi sepertinya utas MemoryCache tidak mendapatkan cukup waktu CPU untuk membersihkan, sementara banyak elemen baru ditambahkan.

Jezze
sumber
3

Saya (untungnya) menemukan posting yang berguna ini kemarin ketika pertama kali mencoba menggunakan MemoryCache. Saya pikir ini akan menjadi kasus sederhana dalam menetapkan nilai dan menggunakan kelas tetapi saya mengalami masalah serupa yang diuraikan di atas. Untuk mencoba dan melihat apa yang sedang terjadi, saya mengekstrak sumbernya menggunakan ILSpy dan kemudian menyiapkan tes dan melangkah melalui kode. Kode pengujian saya sangat mirip dengan kode di atas jadi saya tidak akan mempostingnya. Dari pengujian saya, saya memperhatikan bahwa pengukuran ukuran cache tidak pernah terlalu akurat (seperti yang disebutkan di atas) dan mengingat implementasi saat ini tidak akan pernah berfungsi dengan andal. Namun pengukuran fisiknya baik-baik saja dan jika memori fisik diukur pada setiap polling maka menurut saya kode tersebut akan bekerja dengan andal. Jadi, saya menghapus pemeriksaan pengumpulan sampah gen 2 dalam MemoryCacheStatistics;

Dalam skenario pengujian, ini jelas membuat perbedaan besar karena cache dipukul terus-menerus sehingga objek tidak pernah memiliki kesempatan untuk mencapai gen 2. Saya pikir kita akan menggunakan versi modifikasi dll ini pada proyek kita dan menggunakan MS resmi membangun ketika .net 4.5 keluar (yang menurut artikel terhubung yang disebutkan di atas seharusnya ada perbaikan di dalamnya). Secara logis saya dapat melihat mengapa pemeriksaan gen 2 telah diberlakukan tetapi dalam praktiknya saya tidak yakin apakah itu masuk akal. Jika memori mencapai 90% (atau berapa pun batas yang telah ditetapkan) maka tidak masalah apakah koleksi gen 2 telah terjadi atau tidak, item harus dikeluarkan.

Saya membiarkan kode pengujian saya berjalan selama sekitar 15 menit dengan physicalMemoryLimitPercentage yang disetel ke 65%. Saya melihat penggunaan memori tetap antara 65-68% selama pengujian dan melihat hal-hal digusur dengan benar. Dalam pengujian saya, saya menetapkan pollingInterval ke 5 detik, physicalMemoryLimitPercentage ke 65 dan physicalMemoryLimitPercentage ke 0 sebagai default.

Mengikuti saran di atas; implementasi IMemoryCacheManager dapat dilakukan untuk mengeluarkan sesuatu dari cache. Namun itu akan mengalami masalah pemeriksaan gen 2 yang disebutkan. Meskipun, tergantung pada skenarionya, ini mungkin tidak menjadi masalah dalam kode produksi dan dapat bekerja cukup untuk orang-orang.

Ian Gibson
sumber
4
Pembaruan: Saya menggunakan .NET framework 4.5 dan sama sekali tidak memperbaiki masalah. Cache bisa bertambah besar untuk membuat mesin crash.
Bruno Brant
Sebuah pertanyaan: apakah Anda memiliki tautan ke artikel terhubung yang Anda sebutkan?
Bruno Brant
3

Ternyata itu bukan bug, yang perlu Anda lakukan hanyalah mengatur rentang waktu penggabungan untuk memaksakan batasan, sepertinya jika Anda membiarkan penggabungan tidak disetel, itu tidak akan pernah memicu. Saya hanya mengujinya dan tidak perlu membungkusnya atau kode tambahan apa pun:

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

        };

Setel nilai " PollingInterval" berdasarkan seberapa cepat cache berkembang, jika tumbuh terlalu cepat, tingkatkan frekuensi pemeriksaan polling jika tidak, jaga agar pemeriksaan tidak terlalu sering agar tidak menyebabkan overhead.

Tiongkok
sumber
1

Jika Anda menggunakan kelas yang dimodifikasi berikut dan memonitor memori melalui Task Manager sebenarnya dipangkas:

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
sumber
Apakah Anda mengatakan itu tidak atau tidak dipangkas?
Canacourse
Ya, itu dipangkas. Aneh, mengingat semua masalah yang tampaknya dihadapi orang-orang MemoryCache. Saya bertanya-tanya mengapa sampel ini berhasil.
Daniel Lidström
1
Saya tidak mengikutinya. Saya mencoba mengulangi contoh, tetapi cache masih tumbuh tanpa batas.
Bruno Brant
Contoh kelas yang membingungkan: "Statlock", "ItemCount", "size" tidak berguna ... NameValueCollection (3) hanya menampung 2 item? ... Sebenarnya Anda membuat cache dengan properti sizelimit dan pollInterval, tidak lebih! Masalah "tidak menggusur" barang tidak disentuh ...
Bernhard