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


87

응용 프로그램에서 .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 가 구성된 메모리 제한을 따르지 않는 이유는 무엇 입니까?


2
루프 ++ 내가하지 않고, 잘못
xiaoyifang

4
이 버그에 대한 MS Connect 보고서를 추가했습니다 (다른 사람이 이미 수행했지만 어쨌든 ...) connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

3
Microsoft가 현재 (2014 년 9 월 현재) 위에 링크 된 연결 티켓에 대해 상당히 철저한 응답을 추가했다는 점은 주목할 가치가 있습니다. 그것의 TLDR은 MemoryCache 본질적으로 모든 작업에서 이러한 제한을 확인하는 것이 아니라 동적 내부 타이머를 기반으로주기적인 내부 캐시 트리밍에서만 제한이 적용된다는 것입니다.
Dusty

5
MemoryCache.CacheMemoryLimit에 대한 문서를 업데이트 한 것 같습니다. "MemoryCache는 MemoryCache 인스턴스에 새 항목이 추가 될 때마다 CacheMemoryLimit을 즉시 적용하지 않습니다. MemoryCache에서 추가 항목을 제거하는 내부 휴리스틱은 점진적으로 수행합니다 ..." msdn.microsoft .com / en-us / library /…
Sully

1
@Zeus, MSFT가 문제를 제거했다고 생각합니다. 어쨌든 MSFT는 나와 몇 가지 토론을 한 후 문제를 종결했으며 제한은 PoolingTime이 만료 된 후에 만 ​​적용된다고 말했습니다.
Bruno Brant는

답변:


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

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

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

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


4
좋은 답변 감사합니다. 나는 이것으로 무슨 일이 일어나고 있는지 알아 내려고 포기하고 대신 항목을 입 / 출력하고 필요에 따라 수동으로 .Trim ()을 호출하여 캐시 크기를 관리합니다. System.Runtime.Caching은 널리 사용되는 것처럼 보이므로 내 앱에 대한 쉬운 선택이라고 생각했고 따라서 큰 버그가 없을 것이라고 생각했습니다.
Canacourse 2011 년

3
와. 그래서 내가 그렇게 좋아합니다. 폴링 시간이 10 초로 짧고 캐시 메모리 제한이 1MB 임에도 불구하고 똑같은 동작을 겪고 테스트 앱을 작성하고 내 PC가 여러 번 충돌했습니다. 모든 통찰력에 감사드립니다.
Bruno Brant

7
방금 질문에서 언급했지만 완전성을 위해 여기서 다시 언급하겠습니다. 이에 대해 Connect에서 문제를 열었습니다. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant

1
나는 외부 서비스 데이터에 대한 MemoryCache을 사용하고, 나는이 MemoryCache에 쓰레기를 주입하여 테스트 할 때, 그것은 않는 비율 제한 값을 사용하는 경우에만 자동 트림 컨텐츠를하지만. 절대 크기는 최소한 메모리 프로파일 러로 감염 될 때 크기를 제한하지 않습니다. while 루프에서 테스트되지는 않았지만보다 "현실적인"사용으로 테스트되었습니다 (백엔드 시스템이므로 필요에 따라 캐시에 데이터를 삽입 할 수있는 WCF 서비스를 추가했습니다).
Svend

.NET Core에서 여전히 문제입니까?
Павле

29

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

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의 길이입니다. 이것은 대부분 유니 코드 지원 때문입니다.
Haney

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

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

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


4

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

나는 다음과 같은 구성을 발견했고 사용량은 대부분 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());
}

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를 사용하고 있으며 문제가 해결되지 않았습니다. 캐시는 시스템을 손상시킬만큼 커질 수 있습니다.
Bruno Brant

질문 : 언급 한 연결 기사에 대한 링크가 있습니까?
Bruno Brant 2013 년


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

당신은 그것이 다듬어 지거나되지 않는다고 말하는 것입니까?
Canacourse

네, 잘립니다. 사람들이 가지고있는 것처럼 보이는 모든 문제를 고려하면 이상합니다 MemoryCache. 이 샘플이 작동하는 이유가 궁금합니다.
Daniel Lidström 2013-06-26

1
나는 그것을 따르지 않는다. 예제를 반복 해 보았지만 캐시는 여전히 무한정 커집니다.
Bruno Brant 2013 년

혼란스러운 예제 클래스 : "Statlock", "ItemCount", "size"는 쓸모가 없습니다 ... NameValueCollection (3)은 2 개의 항목 만 보유합니까? ... 사실 sizelimit 및 pollInterval 속성을 사용하여 캐시를 만들었습니다. 더 이상은 없습니다! 항목을 "퇴거하지"문제는 건드리지 않는다 ...
베른하르트
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.