MemoryCacheは構成のメモリ制限に従わない


87

私は、.NET 4.0で働いているMemoryCacheのアプリケーションにクラスと最大キャッシュサイズを制限しようとしているが、私のテストでは、キャッシュが実際の制限に従うことであることを表示されません。

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

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

キャッシュをパージするスレッドはx秒ごとに起動され、ポーリング間隔やその他の文書化されていない変数にも依存するため、これらの値は概算であり、ハード制限ではないことを理解しています。ただし、これらの差異を考慮しても、テストアプリでCacheMemoryLimitMegabytesPhysicalMemoryLimitPercentageを一緒に、または単独で設定した後、最初のアイテムがキャッシュから削除されると、キャッシュサイズに非常に一貫性がなくなります。確かに、各テストを10回実行し、平均値を計算しました。

これらは、3GBのRAMを搭載した32ビットのWindows 7PCで以下のサンプルコードをテストした結果です。キャッシュのサイズは、各テストで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が構成されたメモリ制限に従わないのはなぜですか?


2
forループが間違っている、i ++なし
xiaoyifang 2013年

4
このバグのMSConnect
Bruno Brant

3
Microsoftが(2014年9月現在)上記のリンクされた接続チケットにかなり徹底的な応答を追加したことは注目に値します。そのTLDRは、MemoryCacheすべての操作でこれらの制限を本質的にチェックするのではなく、動的内部タイマーに基づいて定期的に行われる内部キャッシュのトリミング時にのみ制限が尊重されることです。
ダスティ

5
MemoryCache.CacheMemoryLimitのドキュメントを更新したようです:「MemoryCacheは、新しいアイテムがMemoryCacheインスタンスに追加されるたびに、CacheMemoryLimitを即座に適用しません。MemoryCacheから余分なアイテムを削除する内部ヒューリスティックは徐々に適用します...」msdn.microsoft .com / en-us / library /…
Sully

1
@ Zeus、MSFTが問題を取り除いたと思います。いずれにせよ、MSFTは私と話し合った後、問題を解決しました。そこでは、制限はPoolingTimeの期限が切れた後にのみ適用されると言われました。
ブルーノブラント

回答:


100

うわー、それで私はリフレクターを使って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が発生したと仮定すると、問題2が発生します。つまり、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がこの時点で完全に無効になっていると仮定します。これを10MBに設定してから、キャッシュを最大2 GBまでいっぱいにして、アイテムの削除を行わずにメモリ不足の例外を解消できます。


4
良い答えありがとうございます。私はこれで何が起こっているのかを理解しようとするのをあきらめ、代わりにアイテムの入出力をカウントし、必要に応じて手動で.Trim()を呼び出すことによってキャッシュサイズを管理します。System.Runtime.Cachingは広く使用されているようで、大きなバグはないと思っていたので、私のアプリにとっては簡単な選択だと思いました。
カナコース2011

3
ワオ。だから私はSOが大好きです。ポーリング時間が10秒と短く、キャッシュメモリの制限が1 MBであったにもかかわらず、まったく同じ動作に遭遇し、テストアプリを作成し、PCを何度もクラッシュさせることができました。すべての洞察に感謝します。
ブルーノブラント

7
質問の中でそれについて言及したことは知っていますが、完全を期すために、ここでもう一度言及します。私はこれについてConnectで問題を開きました。connect.microsoft.com/VisualStudio/feedback/details/806334/…–
Bruno Brant

1
外部サービスデータにMemoryCacheを使用していますが、MemoryCacheにガベージを挿入してテストすると、コンテンツ自動トリムされますが、パーセント制限値を使用している場合に限ります。絶対サイズは、少なくともメモリプロファイラーを使用する場合は、サイズを制限するものではありません。whileループではテストされていませんが、より「現実的な」使用法によってテストされています(これはバックエンドシステムであるため、オンデマンドでキャッシュにデータを挿入できるWCFサービスを追加しました)。
2014年

これはまだ.NETCoreの問題ですか?
Павле

29

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

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

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

https://github.com/haneytron/sharpmemorycache


2
これは意図したとおりに機能し、1000文字の文字列のロードでキャッシュを埋めるジェネレーターでテストされています。ただし、100MBのようにキャッシュに追加すると、実際には200〜300MBがキャッシュに追加されます。これは非常に奇妙なことです。多分私が数えていないいくつかの耳にした。
Karl Cassar 2014年

5
.NETの@KarlCassar文字列は、2n + 20バイトに関しておおよそのサイズnです。ここで、は文字列の長さです。これは主にUnicodeのサポートによるものです。
ヘイニー

4

@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番目のPASSWORD()ごとにThread.Sleep(1)を使用すると、同じ効果が得られます。

まあ、それは問題のそれほど深い調査ではありませんが、多くの新しい要素が追加されている間、MemoryCacheのスレッドがクリーニングのための十分なCPU時間を取得していないように見えます。


4

私もこの問題に遭遇しました。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());
}

3

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

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

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

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


4
更新:.NET Framework 4.5を使用していますが、問題が修正されていません。キャッシュは、マシンをクラッシュさせるほど大きくなる可能性があります。
ブルーノブラント

質問:あなたが言及した接続記事へのリンクはありますか?
ブルーノブラント


3

バグではないことが判明しました。あなたがする必要があるのは、制限を適用するためにプーリングの期間を設定することだけです。プーリングを設定しないままにすると、トリガーされないようです。テストしただけで、ラッパーは必要ありません。または追加のコード:

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

        };

PollingIntervalキャッシュの増加速度に基づいて「」の値を設定します。キャッシュの増加が速すぎる場合は、ポーリングチェックの頻度を増やします。それ以外の場合は、チェックの頻度を低くしてオーバーヘッドを発生させないようにします。


1

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

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

トリミングされる、またはトリミングされないということですか?
カナコース2012

うん、それはトリミングされます。奇妙なことに、人々が抱えていると思われるすべての問題を考えるとMemoryCache。なぜこのサンプルが機能するのだろうか。
ダニエル・リドストロム

1
私はそれに従いません。例を繰り返してみましたが、キャッシュは無期限に大きくなります。
ブルーノブラント

紛らわしいサンプルクラス: "Statlock"、 "ItemCount"、 "size"は役に立ちません... NameValueCollection(3)は2つのアイテムしか保持しませんか?...実際、sizelimitプロパティとpollIntervalプロパティを使用してキャッシュを作成しました。項目を「立ち退かない」の問題は触れていません...
ベルンハルト
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.