1. ホーム
  2. c#

[解決済み] MemoryCacheが設定上のメモリ制限に従わない

2023-03-13 22:33:09

質問

私は、.NET 4.0を使用しています。 MemoryCache クラスを作成し、最大キャッシュ サイズを制限しようとしていますが、私のテストでは、キャッシュが実際に制限に従っているようには見えません。

私は、以下の設定を使用しています。 MSDN によると によると、キャッシュ サイズを制限することになっています。

  1. キャッシュメモリ制限メガバイト : オブジェクトのインスタンスが成長できる最大メモリサイズ、単位はメガバイトです。
  2. 物理メモリ制限の割合 (PhysicalMemoryLimitPercentage) : キャッシュが使用できる物理メモリの割合で、1 から 100 までの整数値で表されます。デフォルトは 0 で、これは メモリキャッシュ インスタンスは自分自身のメモリを管理します。 1 コンピュータに搭載されているメモリの量に応じて、" 1. これは完全に正しいわけではありません。4 以下の値は無視され、4 に置き換えられます。

キャッシュをパージするスレッドは x 秒ごとに起動され、ポーリング間隔や他の文書化されていない変数にも依存するため、これらの値は概算値であり、厳しい制限値ではないことは理解しています。しかし、これらのばらつきを考慮しても、最初のアイテムが CacheMemoryLimitMegabytes を設定した後、最初のアイテムがキャッシュから追い出されるときに、キャッシュの大きさに乱れが生じます。 物理メモリ制限のパーセンテージ を一緒に、または単体でテストアプリで実行しました。 念のため、各テストを 10 回実行し、平均値を計算しました。

これらは、3GB の RAM を搭載した 32 ビットの Windows 7 PC 上で、以下のサンプル コードをテストした結果です。キャッシュのサイズは、以下の最初の呼び出しの後に取得されます。 CacheItemRemoved() を最初に呼び出した後のものです。(実際のキャッシュのサイズはこれより大きくなることは承知しています)。

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

以下はテストアプリケーションです。

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 は設定されたメモリ制限に従わないのですか?

どのように解決するのですか?

Reflector を使って CLR を掘り下げるのに完全に長い時間を費やしてしまいましたが、ここで起こっていることについてようやく良いハンドルができたようです。

設定は正しく読み込まれていますが、CLR 自体に根深い問題があるようで、メモリ制限の設定が本質的に無意味になっているように見えます。

次のコードは System.Runtime.Caching DLL の CacheMemoryMonitor クラスに反映されています (物理メモリを監視して他の設定を処理する同様のクラスもありますが、より重要なのはこのクラスです)。

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

最初に気づくかもしれませんが、Gen2 ガベージコレクションの後までキャッシュのサイズを見ようともせず、代わりに cacheSizeSamples に保存された既存のサイズ値にフォールバックしています。 そのため、ターゲットを正確にヒットすることはできませんが、残りの部分が機能すれば、実際に問題を起こす前に、少なくともサイズの測定値を得ることができます。

それは ref2.ApproximateSize がキャッシュのサイズを実際に近似するのにひどい仕事をするという問題です。 CLR ジャンクをスロッギングして、これは System.SizedReference であり、これは値を取得するために行っているものであることがわかりました (IntPtr は MemoryCache オブジェクト自体へのハンドルです)。

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

私は、extern宣言がこの時点でアンマネージドウィンドウの土地に潜ることを意味すると仮定しています、そして、私はそれがそこで何をするのかを見つけるために始める方法がわかりません。 私が観察したところでは、全体的なもののサイズを近似させようとする恐ろしい仕事をします。

そこで 3 番目に顕著なことは、何かをするように聞こえる manager.UpdateCacheSize への呼び出しです。 残念ながら、これがどのように動作するかの通常のサンプルでは、s_memoryCacheManager は常に null になります。 このフィールドは、パブリック静的メンバ ObjectCache.Host から設定されます。 このフィールドは、ユーザが好きなように変更できるように公開されています。私は、独自の IMemoryCacheManager の実装を組み合わせて、それを ObjectCache.Host に設定し、サンプルを実行することで、このことを想定通りに動作させることができました。 特に、キャッシュを測定するために、独自のクラスを ObjectCache.Host (static なので、処理中のすべてのクラスに影響します) に設定すると、他のものが台無しになる可能性があるかどうかはわかりません。

私は、この少なくとも一部 (一部ではないにしても) は、単なるストレートなバグであると信じなければなりません。 MS の誰かから、この件に関して何があったのかを聞けるといいですね。

この巨大な答えの TLDR バージョン。 CacheMemoryLimitMegabytes が現時点では完全に破綻していると仮定します。 それを 10 MB に設定し、キャッシュを ~2GB まで満タンにして、アイテム削除のトリップなしでメモリ不足の例外を吹き飛ばすことを続行できます。