web-dev-qa-db-ja.com

MemoryCacheは構成のメモリ制限に従いません

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

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

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

キャッシュをパージするスレッドはx秒ごとに起動され、ポーリング間隔やその他の文書化されていない変数にも依存するため、これらの値は概算値であり、厳密な制限ではないことを理解しています。ただし、これらの違いを考慮しても、CacheMemoryLimitMegabytesおよびPhysicalMemoryLimitPercentageテストアプリで一緒にまたは単独で。必ず各テストを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が設定されたメモリ制限に従っていないのはなぜですか?

84
Canacourse

うわー、だから私はリフレクターを使ってCLRを掘り下げて時間を費やしすぎたのですが、ここで何が起こっているのかをうまく把握できたと思います。

設定は正しく読み込まれていますが、CLR自体に根本的な問題があり、メモリ制限の設定が本質的に役に立たないように見えます。

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

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に保存されている既存のサイズ値にフォールバックすることです。そのため、目標をすぐに達成することはできませんが、残りが機能すれば、少なくとも実際のトラブルが発生する前にサイズを測定できます。

そのため、Gen2 GCが発生したと仮定すると、ref2ApproximateSizeが実際にキャッシュのサイズを概算するという恐ろしい仕事をするという問題2に遭遇します。 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に設定するかどうかはわからないため(静的なため、すべてに影響するため、独自のキャッシュ実装を作成し、これらすべてに煩わされることすらしないようです。キャッシュを測定しようとすると、他のことを台無しにする可能性があります。

私は、これの少なくとも一部(2つではないにしても)が単なるバグであることを信じなければなりません。 MSの誰かから、この取引がどのようなものであったかを聞いていただければうれしいです。

この巨大な答えのTLDRバージョン:CacheMemoryLimitMegabytesがこの時点で完全に破壊されたと仮定します。これを10 MBに設定してから、キャッシュを約2 GBまでいっぱいにして、アイテムの削除を行わずにメモリ不足の例外を吹き飛ばすことができます。

96
David Hay

私はこの答えが遅れて狂っていることを知っていますが、決して遅れるよりはましです。 Gen 2 Collectionの問題を自動的に解決するMemoryCacheのバージョンを作成したことをお知らせします。したがって、ポーリング間隔がメモリ不足を示すたびにトリミングされます。この問題が発生している場合は、試してみてください!

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

私がそれをどのように解決したかについて知りたい場合は、GitHubでも見つけることができます。コードはやや単純です。

https://github.com/haneytron/sharpmemorycache

29
Haney

私もこの問題に遭遇しました。 1秒あたり数十回、プロセスに適用されるオブジェクトをキャッシュしています。

次の構成と使用法により、5秒ごとにアイテムが解放されることがわかりましたほとんどの場合

App.config:

cacheMemoryLimitMegabytesに注意してください。これがゼロに設定された場合、パージルーチンは妥当な時間内に起動しませんでした。

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

キャッシュへの追加:

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

キャッシュの削除が機能していることを確認します。

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

昨日、MemoryCacheを初めて使用しようとしたときに、私は(ありがたいことに)この便利な投稿に出くわしました。値を設定してクラスを使用するという単純なケースだと思いましたが、上記で概説した同様の問題に遭遇しました。何が起こっているのかを確認するために、ILSpyを使用してソースを抽出し、テストを設定してコードをステップ実行しました。私のテストコードは上記のコードと非常に似ていたので、投稿しません。私のテストから、キャッシュサイズの測定は(上記のように)特に正確ではなく、現在の実装が確実に機能することはないことに気付きました。しかし、物理的な測定は問題なく、ポーリングごとに物理メモリが測定された場合、コードは確実に機能するように思えました。そのため、MemoryCacheStatistics内のgen 2ガベージコレクションチェックを削除しました。通常の条件下では、最後の測定以降に別のgen 2ガベージコレクションが行われない限り、メモリ測定は行われません。

テストシナリオでは、キャッシュが絶えずヒットしているため、オブジェクトがgen 2に到達する機会がないため、これは明らかに大きな違いをもたらします。このdllの修正ビルドをプロジェクトで使用し、公式のMS .net 4.5が出たときにビルドします(上記の接続記事によると、修正が必要です)。論理的には、なぜ第2世代チェックが実施されたのかはわかりますが、実際にはそれが意味をなすかどうかはわかりません。メモリが90%(または設定されている制限)に達した場合、gen 2コレクションが発生したかどうかは問題ではなく、アイテムは削除されます。

PhysicalMemoryLimitPercentageを65%に設定して、テストコードを約15分間実行したままにしました。テスト中にメモリ使用量が65〜68%のままであることがわかり、適切に削除されることがわかりました。私のテストでは、pollingIntervalを5秒に、physicalMemoryLimitPercentageを65に、physicalMemoryLimitPercentageを0に設定して、これをデフォルトに設定しました。

上記のアドバイスに従ってください。 IMemoryCacheManagerの実装を作成して、キャッシュから物事を排除することができます。ただし、前述のgen 2チェックの問題が発生します。ただし、シナリオによっては、これは実稼働コードでは問題にならず、人々にとって十分に機能する場合があります。

3
Ian Gibson

@Canacourseの例と@woanyの変更を使用していくつかのテストを行いましたが、メモリキャッシュのクリーニングをブロックする重要な呼び出しがいくつかあると思います。

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

しかし、@ woanyの変更がメモリを同じレベルに保つように見えるのはなぜですか?まず、RemovedCallbackが設定されておらず、メモリキャッシュのスレッドをブロックする可能性のあるコンソール出力や入力待機がありません。

第二に...

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

〜1000番目のAddItem()ごとにThread.Sleep(1)は同じ効果があります。

問題の詳細な調査ではありませんが、多くの新しい要素が追加されているにもかかわらず、MemoryCacheのスレッドがクリーニングに十分なCPU時間を取得していないようです。

3
Jezze

バグではなく、制限を強制するためにプーリング時間を設定するだけで、プーリングを設定しないままにしておくと、トリガーされないようです。私はそれをテストし、ラッパーする必要はありませんまたは追加のコード:

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

        };

キャッシュの成長速度に基づいて "PollingInterval"の値を設定します。成長速度が速すぎる場合はポーリングチェックの頻度を増やします。

2
sino

次の変更されたクラスを使用し、タスクマネージャーを介してメモリを監視すると、実際にはトリミングされます。

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