[c#] MemoryCache는 구성의 메모리 제한을 따르지 않습니다.

응용 프로그램에서 .NET 4.0 MemoryCache 클래스로 작업 하고 최대 캐시 크기를 제한하려고 시도하고 있지만 테스트에서 캐시가 실제로 제한을 따르는 것으로 나타나지 않습니다.

MSDN에 따르면 캐시 크기를 제한하는 설정을 사용하고 있습니다.

  1. CacheMemoryLimitMegabytes : 개체 인스턴스가 증가 할 수있는 최대 메모리 크기 (MB)입니다. “
  2. PhysicalMemoryLimitPercentage : “캐시가 사용할 수있는 실제 메모리의 백분율로, 1에서 100 사이의 정수 값으로 표시됩니다. 기본값은 0입니다. 이는 MemoryCache 인스턴스가 컴퓨터에 설치된 메모리 양에 따라자체 메모리 1을 관리함을 나타냅니다. 컴퓨터.” 1. 이것은 완전히 정확하지 않습니다. 4 미만의 값은 무시되고 4로 대체됩니다.

캐시를 제거하는 스레드가 x 초마다 실행되고 폴링 간격 및 기타 문서화되지 않은 변수에 따라 달라지기 때문에 이러한 값은 근사치이며 하드 제한이 아님을 이해합니다. 그러나 이러한 차이를 고려하더라도 CacheMemoryLimitMegabytesPhysicalMemoryLimitPercentage를 함께 설정 하거나 테스트 앱에서 단일 항목으로 설정 한 후 첫 번째 항목이 캐시에서 제거 될 때 캐시 크기가 매우 일치하지 않습니다 . 각 테스트를 10 번 실행하고 평균 수치를 계산했습니다.

다음은 RAM이 3GB 인 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 가 구성된 메모리 제한을 따르지 않는 이유는 무엇 입니까?



답변

와, 그래서 저는 반사경으로 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 선언이이 시점에서 관리되지 않는 창 땅으로 뛰어 드는 것을 의미한다고 가정하고 있으며, 거기에서 무엇을하는지 알아내는 방법을 모르겠습니다. 내가 관찰 한 것에서 전체적인 크기를 근사화하려는 끔찍한 일을합니다.

세 번째로 눈에 띄는 것은 무언가를해야하는 것처럼 들리는 manager.UpdateCacheSize에 대한 호출입니다. 불행히도 이것이 어떻게 작동하는지에 대한 일반적인 샘플에서 s_memoryCacheManager는 항상 null입니다. 이 필드는 공용 정적 멤버 ObjectCache.Host에서 설정됩니다. 이것은 사용자가 선택하면 엉망이 될 수 있도록 노출되며, 실제로 내 자신의 IMemoryCacheManager 구현을 함께 slopping하고 ObjectCache.Host로 설정 한 다음 샘플을 실행하여 예상대로이 작업을 수행 할 수있었습니다. . 하지만 그 시점에서 자신의 캐시 구현을 만드는 것이 좋을 것 같습니다. 특히 자신의 클래스를 ObjectCache.Host (정적,

나는 이것의 적어도 일부 (몇 부분이 아니라면)는 단지 똑 바른 버그라고 믿어야한다. MS의 누군가로부터이 일에 대한 거래가 무엇인지 듣는 것이 좋을 것입니다.

이 거대한 답변의 TLDR 버전 : CacheMemoryLimitMegabytes가이 시점에서 완전히 버스트되었다고 가정합니다. 10MB로 설정 한 다음 캐시를 ~ 2GB로 채우고 항목 제거 트리핑없이 메모리 부족 예외를 날려 버릴 수 있습니다.


답변

나는이 대답이 늦게 미친다는 것을 알고 있지만 결코 늦지 않는 것보다 낫습니다. MemoryCacheGen 2 Collection 문제를 자동으로 해결 하는 버전을 작성했음을 알려 드립니다. 따라서 폴링 간격이 메모리 부족을 나타낼 때마다 트리밍됩니다. 이 문제가 발생하는 경우 시도해보십시오!

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

내가 어떻게 해결했는지 궁금하다면 GitHub에서 찾을 수도 있습니다. 코드는 다소 간단합니다.

https://github.com/haneytron/sharpmemorycache


답변

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

Thread.Sleep (1)은 ~ 1000 번째 AddItem ()마다 동일한 효과를 갖습니다.

음, 문제에 대한 심층 조사는 아니지만 MemoryCache의 스레드가 정리를위한 충분한 CPU 시간을 얻지 못하고 많은 새로운 요소가 추가되는 것처럼 보입니다.


답변

이 문제도 발생했습니다. 초당 수십 번 내 프로세스로 실행되는 개체를 캐싱하고 있습니다.

나는 다음과 같은 구성을 발견했고 사용량은 대부분 5 초마다 항목을 해제합니다 .

App.config :

cacheMemoryLimitMegabytes를 기록해 둡니다 . 이 값을 0으로 설정하면 적절한 시간 내에 퍼징 루틴이 실행되지 않습니다.

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


답변

나는 (고맙게도) 어제 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 세대 수표 문제로 인해 어려움을 겪을 것입니다. 시나리오에 따라 이것은 프로덕션 코드에서 문제가되지 않을 수 있으며 사람들에게 충분할 수 있습니다.


답변

그것은 버그가 아니라는 것이 밝혀졌습니다. 당신이해야 할 일은 제한을 적용하기 위해 풀링 시간 범위를 설정하는 것입니다. 풀링을 설정하지 않으면 트리거되지 않는 것 같습니다. 나는 그것을 테스트했으며 래퍼 할 필요가 없습니다. 또는 추가 코드 :

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

        };

PollingInterval캐시가 얼마나 빨리 증가하는지에 따라 ” ” 값을 설정하십시오 . 캐시가 너무 빨리 증가하면 폴링 검사 빈도를 늘리십시오. 그렇지 않으면 오버 헤드를 일으키지 않도록 검사를 자주하지 마십시오.


답변

다음과 같은 수정 된 클래스를 사용하고 작업 관리자를 통해 메모리를 모니터링하면 실제로 정리됩니다.

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