MemoryCache không tuân theo giới hạn bộ nhớ trong cấu hình


87

Tôi đang làm việc với lớp .NET 4.0 MemoryCache trong một ứng dụng và cố gắng giới hạn kích thước bộ nhớ cache tối đa, nhưng trong các thử nghiệm của tôi, có vẻ như bộ nhớ cache không thực sự tuân theo các giới hạn.

Tôi đang sử dụng các cài đặt, theo MSDN , được cho là để giới hạn kích thước bộ nhớ cache:

  1. CacheMemoryLimitMegabyte : Kích thước bộ nhớ tối đa, tính bằng megabyte, mà một phiên bản của đối tượng có thể tăng lên. "
  2. PhysicalMemoryLimitPercentage : "Phần trăm bộ nhớ vật lý mà bộ nhớ đệm có thể sử dụng, được biểu thị dưới dạng giá trị số nguyên từ 1 đến 100. Giá trị mặc định là 0, điều này cho biết cácphiên bản MemoryCache quản lý bộ nhớ 1 của riêng chúngdựa trên dung lượng bộ nhớ được cài đặt trên máy vi tính." 1. Điều này không hoàn toàn chính xác - bất kỳ giá trị nào dưới 4 đều bị bỏ qua và thay thế bằng 4.

Tôi hiểu rằng các giá trị này là gần đúng và không phải là giới hạn cứng vì chuỗi xóa bộ nhớ cache được kích hoạt sau mỗi x giây và cũng phụ thuộc vào khoảng thời gian bỏ phiếu và các biến không có tài liệu khác. Tuy nhiên, ngay cả khi tính đến những khác biệt này, tôi vẫn thấy kích thước bộ nhớ cache hoàn toàn không nhất quán khi mục đầu tiên bị loại bỏ khỏi bộ nhớ cache sau khi đặt CacheMemoryLimitMegabytesPhysicalMemoryLimitPercentage cùng nhau hoặc riêng lẻ trong một ứng dụng thử nghiệm. Để chắc chắn, tôi đã chạy mỗi bài kiểm tra 10 lần và tính toán con số trung bình.

Đây là kết quả của việc kiểm tra đoạn mã ví dụ dưới đây trên PC chạy Windows 7 32 bit có RAM 3GB. Kích thước của bộ nhớ cache được lấy sau lần gọi đầu tiên đến CacheItemRemoved () trong mỗi lần kiểm tra. (Tôi biết kích thước thực của bộ nhớ đệm sẽ lớn hơn kích thước này)

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

Đây là ứng dụng thử nghiệm:

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

Tại sao MemoryCache không tuân theo giới hạn bộ nhớ đã định cấu hình?


2
Vòng lặp for là sai, mà không i ++
xiaoyifang

4
Tôi đã thêm một báo cáo MS Connect cho lỗi này (có thể ai đó đã làm, nhưng dù sao ...) connect.microsoft.com/VisualStudio/feedback/details/806334/...
Bruno Brant

3
Điều đáng chú ý là Microsoft hiện đã (tính đến tháng 9 năm 2014) đã thêm một phản hồi khá kỹ lưỡng về phiếu kết nối được liên kết ở trên. TLDR của nó là MemoryCache vốn dĩ không kiểm tra các giới hạn này trên mọi hoạt động, mà là các giới hạn chỉ được tuân thủ khi cắt bộ nhớ cache bên trong, định kỳ dựa trên bộ định thời nội bộ động.
Bụi bặm

5
Có vẻ như họ đã cập nhật tài liệu cho MemoryCache.CacheMemoryLimit: "MemoryCache không thực thi CacheMemoryLimit ngay lập tức mỗi khi một mục mới được thêm vào một phiên bản MemoryCache. Kinh nghiệm nội bộ loại bỏ các mục bổ sung khỏi MemoryCache sẽ thực hiện dần dần ..." msdn.microsoft .com / en-us / library /…
Sully

1
@Zeus, tôi nghĩ MSFT đã loại bỏ vấn đề. Trong mọi trường hợp, MSFT đã đóng vấn đề sau một số cuộc thảo luận với tôi, nơi họ nói với tôi rằng giới hạn chỉ được áp dụng sau khi PoolingTime đã hết hạn.
Bruno Brant

Câu trả lời:


100

Chà, vì vậy tôi đã dành quá nhiều thời gian để tìm hiểu xung quanh CLR với gương phản xạ, nhưng tôi nghĩ rằng cuối cùng tôi đã xử lý tốt những gì đang xảy ra ở đây.

Các cài đặt đang được đọc một cách chính xác, nhưng có vẻ như có một vấn đề sâu xa trong chính CLR có vẻ như nó sẽ làm cho cài đặt giới hạn bộ nhớ về cơ bản là vô dụng.

Mã sau được phản ánh trong DLL System.Runtime.Caching, cho lớp CacheMemoryMonitor (có một lớp tương tự giám sát bộ nhớ vật lý và xử lý cài đặt khác, nhưng đây là lớp quan trọng hơn):

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

Điều đầu tiên bạn có thể nhận thấy là nó thậm chí không cố gắng xem xét kích thước của bộ nhớ cache cho đến sau khi thu thập rác Gen2, thay vào đó chỉ giảm giá trị kích thước được lưu trữ hiện có trong cacheSizeSamples. Vì vậy, bạn sẽ không bao giờ có thể bắn trúng mục tiêu, nhưng nếu phần còn lại hoạt động, ít nhất chúng ta cũng sẽ nhận được một phép đo kích thước trước khi chúng ta gặp rắc rối thực sự.

Vì vậy, giả sử một Gen2 GC đã xảy ra, chúng tôi gặp phải vấn đề 2, đó là ref2.ApproximateSize thực hiện một công việc kinh khủng là thực sự ước tính kích thước của bộ nhớ cache. Lướt qua CLR rác, tôi thấy rằng đây là System.SizeReference và đây là những gì nó đang làm để lấy giá trị (IntPtr là một xử lý đối với chính đối tượng MemoryCache):

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

Tôi giả định rằng tuyên bố kỳ lạ đó có nghĩa là nó đi sâu vào các cửa sổ không được quản lý hạ cánh vào thời điểm này và tôi không biết làm thế nào để bắt đầu tìm hiểu xem nó làm gì ở đó. Từ những gì tôi quan sát được, mặc dù nó thực hiện một công việc kinh khủng khi cố gắng ước lượng kích thước của tổng thể.

Điều đáng chú ý thứ ba là cuộc gọi tới manager.UpdateCacheSize, nghe có vẻ như nó phải làm một cái gì đó. Thật không may trong bất kỳ mẫu bình thường nào về cách thức hoạt động của điều này, s_memoryCacheManager sẽ luôn là null. Trường được thiết lập từ thành viên tĩnh công khai ObjectCache.Host. Điều này sẽ khiến người dùng bối rối nếu anh ta chọn và tôi thực sự có thể làm cho thứ này hoạt động giống như nó phải làm bằng cách kết hợp triển khai IMemoryCacheManager của riêng tôi, đặt nó thành ObjectCache. . Tuy nhiên, tại thời điểm đó, có vẻ như bạn cũng có thể chỉ thực hiện triển khai bộ nhớ cache của riêng mình và thậm chí không bận tâm đến tất cả những thứ này, đặc biệt là vì tôi không biết nếu đặt lớp của riêng bạn thành ObjectCache.

Tôi phải tin rằng ít nhất một phần của điều này (nếu không phải là một vài phần) chỉ là một lỗi thẳng. Thật vui khi nghe ai đó ở MS về thỏa thuận với thứ này.

Phiên bản TLDR của câu trả lời khổng lồ này: Giả sử rằng CacheMemoryLimitMegabyte đã hoàn toàn bị phá sản tại thời điểm này. Bạn có thể đặt nó thành 10 MB, sau đó tiến hành lấp đầy bộ nhớ cache thành ~ 2GB và xóa ngoại lệ bộ nhớ mà không vấp phải thao tác xóa mục.


4
Một câu trả lời tốt cảm ơn bạn. Tôi đã từ bỏ việc cố gắng tìm hiểu điều gì đang xảy ra với điều này và thay vào đó bây giờ quản lý kích thước bộ nhớ cache bằng cách đếm các mục vào / ra và gọi .Trim () theo cách thủ công nếu cần. Tôi nghĩ System.Runtime.Caching là một lựa chọn dễ dàng cho ứng dụng của tôi vì nó dường như được sử dụng rộng rãi và tôi nghĩ do đó sẽ không có bất kỳ lỗi lớn nào.
Canacourse

3
Chà. Đó là lý do tại sao tôi yêu SO. Tôi đã gặp phải hành vi tương tự chính xác, viết một ứng dụng thử nghiệm và quản lý để làm hỏng máy tính của mình nhiều lần mặc dù thời gian bỏ phiếu chỉ còn 10 giây và giới hạn bộ nhớ đệm là 1MB. Cảm ơn vì tất cả những hiểu biết sâu sắc.
Bruno Brant

7
Tôi biết tôi chỉ đề cập đến nó ở đó trong câu hỏi nhưng, vì lợi ích hoàn chỉnh, tôi sẽ đề cập lại nó ở đây. Tôi đã gặp sự cố tại Connect cho việc này. connect.microsoft.com/VisualStudio/feedback/details/806334/…
Bruno Brant,

1
Tôi đang sử dụng MemoryCache cho dữ liệu dịch vụ bên ngoài và khi tôi kiểm tra bằng cách đưa rác vào MemoryCache, nó sẽ tự động cắt nội dung, nhưng chỉ khi sử dụng giá trị giới hạn phần trăm. Kích thước tuyệt đối không có tác dụng gì đối với kích thước giới hạn, ít nhất là khi inpsaging với trình biên dịch bộ nhớ. Không được thử nghiệm trong vòng lặp while, mà bởi cách sử dụng "thực tế" hơn (đó là một hệ thống phụ trợ, vì vậy tôi đã thêm một dịch vụ WCF cho phép tôi đưa dữ liệu vào bộ nhớ đệm theo yêu cầu).
Svend

Đây có còn là sự cố trong .NET Core không?
Павле

29

Tôi biết câu trả lời này là muộn điên rồ, nhưng muộn còn hơn không. Tôi muốn cho bạn biết rằng tôi đã viết một phiên bản MemoryCachegiải quyết các vấn đề của Bộ sưu tập Gen 2 một cách tự động cho bạn. Do đó, nó sẽ cắt bất cứ khi nào khoảng thời gian bỏ phiếu cho thấy áp lực bộ nhớ. Nếu bạn đang gặp sự cố này, hãy thử!

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

Bạn cũng có thể tìm thấy nó trên GitHub nếu bạn tò mò về cách tôi đã giải quyết nó. Mã này hơi đơn giản.

https://github.com/haneytron/sharpmemorycache


2
Điều này hoạt động như dự định, đã được thử nghiệm với một trình tạo bộ nhớ đệm với vô số chuỗi 1000 ký tự. Mặc dù, việc cộng thêm 100MB vào bộ nhớ đệm sẽ làm tăng thực sự 200 - 300MB vào bộ đệm, điều mà tôi thấy khá lạ. Có lẽ một số cuộc nghe lén tôi không tính.
Karl Cassar

5
Các chuỗi @KarlCassar trong .NET có 2n + 20kích thước gần đúng với byte, trong đó nlà độ dài của chuỗi. Điều này chủ yếu là do hỗ trợ Unicode.
Haney

4

Tôi đã thực hiện một số thử nghiệm với ví dụ về @Canacourse và sửa đổi của @woany và tôi nghĩ rằng có một số lệnh gọi quan trọng chặn việc dọn dẹp bộ nhớ đệm.

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

Nhưng tại sao việc sửa đổi @woany dường như giữ bộ nhớ ở mức cũ? Thứ nhất, RemovedCallback không được thiết lập và không có đầu ra bảng điều khiển hoặc chờ đầu vào có thể chặn luồng của bộ nhớ cache.

Thứ hai ...

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

Một Thread.Sleep (1) sau mỗi ~ 1000 AddItem () sẽ có cùng tác dụng.

Chà, đây không phải là một cuộc điều tra sâu về vấn đề, nhưng có vẻ như luồng của MemoryCache không có đủ thời gian cho CPU để dọn dẹp, trong khi nhiều yếu tố mới được thêm vào.


4

Tôi cũng gặp phải vấn đề này. Tôi đang lưu vào bộ nhớ đệm các đối tượng đang được kích hoạt vào quy trình của mình hàng chục lần mỗi giây.

Tôi đã tìm thấy cấu hình và cách sử dụng sau đây giải phóng các mục sau mỗi 5 giây hầu hết thời gian .

App.config:

Hãy lưu ý đến cacheMemoryLimitMegabyte . Khi điều này được đặt thành 0, quy trình thanh lọc sẽ không hoạt động trong một thời gian hợp lý.

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

Thêm vào bộ nhớ cache:

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

Xác nhận xóa bộ nhớ cache đang hoạt động:

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

3

Tôi (rất may) đã tình cờ xem được bài viết hữu ích này ngày hôm qua khi lần đầu tiên cố gắng sử dụng MemoryCache. Tôi nghĩ rằng đó sẽ là một trường hợp đơn giản của việc đặt giá trị và sử dụng các lớp nhưng tôi gặp phải các vấn đề tương tự như đã nêu ở trên. Để thử và xem điều gì đang xảy ra, tôi đã trích xuất nguồn bằng ILSpy và sau đó thiết lập một bài kiểm tra và chuyển qua mã. Mã thử nghiệm của tôi rất giống với mã ở trên nên tôi sẽ không đăng nó. Từ các thử nghiệm của mình, tôi nhận thấy rằng phép đo kích thước bộ nhớ cache không bao giờ đặc biệt chính xác (như đã đề cập ở trên) và việc triển khai hiện tại sẽ không bao giờ hoạt động đáng tin cậy. Tuy nhiên, phép đo vật lý vẫn ổn và nếu bộ nhớ vật lý được đo ở mọi cuộc thăm dò thì đối với tôi, có vẻ như mã sẽ hoạt động đáng tin cậy. Vì vậy, tôi đã loại bỏ kiểm tra thu thập rác thế hệ 2 trong MemoryCacheSt Statistics;

Trong một kịch bản thử nghiệm, điều này rõ ràng tạo ra sự khác biệt lớn vì bộ nhớ cache bị tấn công liên tục nên các đối tượng không bao giờ có cơ hội chuyển sang thế hệ 2. Tôi nghĩ chúng ta sẽ sử dụng bản dựng đã sửa đổi của dll này trong dự án của mình và sử dụng MS chính thức xây dựng khi .net 4.5 ra mắt (mà theo bài viết kết nối đã đề cập ở trên nên có bản sửa lỗi trong đó). Về mặt logic, tôi có thể hiểu tại sao kiểm tra gen 2 đã được đưa ra nhưng trong thực tế, tôi không chắc liệu nó có ý nghĩa hay không. Nếu bộ nhớ đạt đến 90% (hoặc bất kỳ giới hạn nào mà nó đã được đặt) thì không thành vấn đề nếu bộ sưu tập gen 2 có xảy ra hay không, các mục sẽ bị loại bỏ bất kể.

Tôi đã để mã thử nghiệm của mình chạy trong khoảng 15 phút với phần trăm physicalMemoryLimitPercentage được đặt thành 65%. Tôi thấy mức sử dụng bộ nhớ vẫn ở mức 65-68% trong quá trình thử nghiệm và thấy mọi thứ được loại bỏ đúng cách. Trong thử nghiệm của mình, tôi đã đặt pollingInterval thành 5 giây, vật lý là 65 giây và vật lýMemoryLimitPercentage thành 0 để mặc định điều này.

Theo lời khuyên trên; việc triển khai IMemoryCacheManager có thể được thực hiện để loại bỏ mọi thứ khỏi bộ nhớ cache. Tuy nhiên, nó sẽ gặp phải vấn đề kiểm tra gen 2 được đề cập. Mặc dù, tùy thuộc vào kịch bản, đây có thể không phải là vấn đề trong mã sản xuất và có thể hoạt động phù hợp với mọi người.


4
Cập nhật: Tôi đang sử dụng .NET framework 4.5 và không có cách nào sự cố được khắc phục. Bộ nhớ đệm có thể phát triển đủ lớn để làm hỏng máy.
Bruno Brant

Một câu hỏi: bạn có liên kết đến bài báo kết nối mà bạn đã đề cập không?
Bruno Brant


3

Hóa ra nó không phải là một lỗi, tất cả những gì bạn cần làm là đặt khoảng thời gian gộp để thực thi các giới hạn, có vẻ như nếu bạn không đặt gộp, nó sẽ không bao giờ kích hoạt. Tôi chỉ cần thử nghiệm nó và không cần trình bao bọc hoặc bất kỳ mã bổ sung nào:

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

        };

Đặt giá trị của " PollingInterval" dựa trên tốc độ phát triển của bộ nhớ cache, nếu bộ nhớ cache phát triển quá nhanh, hãy tăng tần suất kiểm tra thăm dò, nếu không, hãy giữ kiểm tra không thường xuyên để không gây ra chi phí.


1

Nếu bạn sử dụng lớp đã sửa đổi sau và theo dõi bộ nhớ qua Trình quản lý tác vụ trên thực tế sẽ bị cắt:

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

Bạn đang nói nó có hay không được cắt tỉa?
Canacourse

Đúng, nó được cắt tỉa. Thật kỳ lạ, xem xét tất cả những vấn đề mà mọi người dường như gặp phải MemoryCache. Tôi tự hỏi tại sao mẫu này hoạt động.
Daniel Lidström

1
Tôi không làm theo nó. Tôi đã thử lặp lại ví dụ, nhưng bộ nhớ cache vẫn phát triển vô thời hạn.
Bruno Brant

Một lớp ví dụ khó hiểu: "Statlock", "ItemCount", "size" là vô dụng ... NameValueCollection (3) chỉ chứa được 2 mục? ... Thực tế là bạn đã tạo một bộ nhớ cache với các thuộc tính sizelimit và pollInterval, không có gì hơn! Vấn đề "không trục xuất" các mặt hàng không được chạm vào ...
Bernhard
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.