Verilerde bellek yönetimi ve atık toplama (GC) ASP.NET Core

Sébastien Ros ve Rick Anderson tarafından

Bellek yönetimi, .NET gibi yönetilen bir çerçevede bile karmaşıktır. Bellek sorunlarını analiz etmek ve anlamak zor olabilir. Bu makalede:

  • Birçok bellek sızıntısı ve GC'nin çalışmama sorundan motivasyonu vardı. Bu sorunların çoğunun nedeni bellek tüketiminin .NET Core'da nasıl çalıştığını anlamama veya nasıl ölçülebilir olduğunu anlamamadır.
  • Sorunlu bellek kullanımını göstermektedir ve alternatif yaklaşımlar önerir.

.NET Core'da atık toplama (GC) nasıl çalışır?

GC, her segmentin bitişik bir bellek aralığı olduğu yığın kesimlerini ayırır. Yığına yerleştirilen nesneler 3 nesilden biri olarak kategorilere ayrılmıştır: 0, 1 veya 2. Oluşturma, GC'nin uygulama tarafından artık başvurulmayan yönetilen nesnelerde belleği serbest bırakma sıklığını belirler. Daha düşük numaralı nesiller daha sık GC'ler olur.

Nesneler yaşam sürelerine göre bir nesilden diğerine taşınır. Nesneler daha uzun süre canlı olarak daha yüksek bir nesle taşınır. Daha önce belirtildiği gibi, daha yüksek nesiller DAHA az sıklıkta GC olur. Kısa süreli nesneler her zaman nesil 0'da kalır. Örneğin, bir web isteğinin ömrü boyunca başvurulan nesneler kısa sürelidir. Uygulama düzeyi tektonlar genellikle 2. nesile geçirilir.

Bir ASP.NET Core başlatıldığında GC:

  • İlk yığın kesimleri için biraz bellek ayırması.
  • Çalışma zamanı yüklendiğinde belleğin küçük bir kısmını işler.

Yukarıdaki bellek ayırmaları performans nedeniyle yapılır. Performans avantajı, bitişik bellekte yığın kesimlerinden gelir.

GC çağrısı. Toplamak

GC çağrısı. Açıkça toplayın:

  • Üretim uygulamaları tarafından ASP.NET Core.
  • Bellek sızıntılarını araştırırken yararlıdır.
  • Araştırmada, belleğin ölçülebilir olması için GC'nin tüm dalgalı nesneleri bellekten kaldırılmış olduğunu doğrular.

Bir uygulamanın bellek kullanımını analiz etme

Ayrılmış araçlar bellek kullanımını analiz etmeye yardımcı olabilir:

  • Sayma nesnesi başvuruları
  • GC'nin CPU kullanımı üzerindeki etkisini ölçme
  • Her nesil için kullanılan bellek alanı ölçümü

Bellek kullanımını analiz etmek için aşağıdaki araçları kullanın:

Bellek sorunlarını algılama

Görev Yöneticisi, bir uygulamanın kullandığı bellek ASP.NET almak için kullanılabilir. Görev Yöneticisi bellek değeri:

  • Veri işlem tarafından kullanılan bellek miktarını ASP.NET.
  • Uygulamanın canlı nesnelerini ve yerel bellek kullanımı gibi diğer bellek tüketicilerini içerir.

Görev Yöneticisi bellek değeri süresiz olarak artarsa ve hiçbir zaman düzlük yoksa, uygulamanın bellek sızıntısı vardır. Aşağıdaki bölümlerde çeşitli bellek kullanım desenleri göster ve açıklanmaktadır.

Örnek görüntü belleği kullanım uygulaması

MemoryLeak örnek uygulaması, GitHub. MemoryLeak uygulaması:

  • Uygulama için gerçek zamanlı bellek ve GC verileri topan bir tanılama denetleyicisi içerir.
  • Bellek ve GC verilerini görüntüleyen bir Dizin sayfası vardır. Dizin sayfası her saniye yenilenir.
  • Çeşitli bellek yükü desenleri sağlayan bir API denetleyicisi içerir.
  • Desteklenen bir araç değildir, ancak uygulamalar için bellek kullanım desenlerini görüntülemek ASP.NET Core kullanılabilir.

MemoryLeak'i çalıştırın. Ayrılan bellek GC oluşana kadar yavaş artar. Araç verileri yakalamak için özel nesne ayırarak bellek artar. Aşağıdaki görüntüde, 0. Nesil GC oluştuğunda MemoryLeak Index sayfası görüntülenir. Grafikte 0 RPS (saniye başına istek sayısı) görüntülenir çünkü API denetleyicisinden hiçbir API uç noktası çağrılmadı.

önceki grafik

Grafikte bellek kullanımı için iki değer görüntülenir:

  • Ayrılmış: Yönetilen nesneler tarafından kapladığı bellek miktarı
  • Çalışma kümesi:Sürecin sanal adres alanı içinde bulunan ve şu anda fiziksel bellekte bulunan sayfalar kümesidir. Gösterilen çalışma kümesi, Görev Yöneticisi'nin görüntüle aynı değerdir.

Geçici nesneler

Aşağıdaki API, 10 KB'lık bir Dize örneği oluşturur ve istemciye döndürür. Her istekte, bellekte yeni bir nesne ayrılır ve yanıta yazılır. Dizeler .NET'te UTF-16 karakter olarak depolanır, bu nedenle her karakter bellekte 2 bayt alır.

[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
    return new String('x', 10 * 1024);
}

Aşağıdaki grafik, bellek ayırmalarının GC'den nasıl etkilene olduğunu göstermek için içinde görece küçük bir yük ile oluşturulur.

önceki grafik

Yukarıdaki grafik şunları gösterir:

  • 4K RPS (saniye başına istek sayısı).
  • Nesil 0 GC koleksiyonları yaklaşık iki saniyede bir gerçekleşir.
  • Çalışma kümesi yaklaşık 500 MB'ta sabittir.
  • CPU %12'dir.
  • Bellek tüketimi ve sürümü (GC aracılığıyla) kararlıdır.

Aşağıdaki grafik, makine tarafından işilebilir en yüksek aktarım hızına göre alınır.

önceki grafik

Yukarıdaki grafik şunları gösterir:

  • 22K RPS
  • Nesil 0 GC koleksiyonları saniye başına birkaç kez gerçekleşir.
    1. nesil koleksiyonlar, uygulamanın saniye başına önemli ölçüde daha fazla bellek ayırmış olduğu için tetiklenir.
  • Çalışma kümesi yaklaşık 500 MB'ta sabittir.
  • CPU %33'tir.
  • Bellek tüketimi ve sürümü (GC aracılığıyla) kararlıdır.
  • CPU (%33) fazla kullanmaz, bu nedenle çöp toplama çok sayıda ayırmaya uygun olabilir.

İş İstasyonu GC ile Sunucu GC karşılaştırması

.NET Çöp Toplayıcısı'nın iki farklı modu vardır:

  • İş istasyonu GC: Masaüstü için en iyi duruma getirilmiş.
  • Sunucu GC. ASP.NET Core uygulamalar için varsayılan GC. Sunucu için en iyi duruma getirilmiş.

GC modu proje dosyasında veya yayımlanan uygulamanın dosyasındakiruntimeconfig.jsaçıkça belirtebilirsiniz. Aşağıdaki işaretleme, proje ServerGarbageCollection dosyasındaki ayarı gösterir:

<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

Proje ServerGarbageCollection dosyasında değişiklik yapmak için uygulamanın yeniden oluşturulmuş olması gerekir.

Not: Sunucu çöp toplama, tek çekirdekli makinelerde kullanılamaz. Daha fazla bilgi için bkz. IsServerGC.

Aşağıdaki görüntüde İş İstasyonu GC kullanılarak 5.000 RPS altındaki bellek profili gösterildi.

önceki grafik

Bu grafikle sunucu sürümü arasındaki farklar önemlidir:

  • Çalışma kümesi 500 MB'tan 70 MB'a düşer.
  • GC, nesil 0 koleksiyonlarını iki saniyede bir yerine saniyede birden çok kez yapar.
  • GC 300 MB'tan 10 MB'a düşer.

Tipik bir web sunucusu ortamında CPU kullanımı bellekten daha önemlidir, bu nedenle Sunucu GC daha iyidir. Bellek kullanımı yüksekse ve CPU kullanımı görece düşükse İş İstasyonu GC daha yüksek performanslı olabilir. Örneğin, belleğin az olduğu birkaç web uygulaması barındıran yüksek yoğunluklu.

Docker ve küçük kapsayıcılar kullanan GC

Bir makinede birden çok kapsayıcılı uygulama çalıştırıldıktan sonra İş İstasyonu GC, Sunucu GC'den daha iyi bir performansa sahip olabilir. Daha fazla bilgi için, bkz. Running with Server GC in a Small Container and Running with Server GC in a Small Container Scenario Part 1 – Hard Limit for the GC Heap.

Kalıcı nesne başvuruları

GC, başvurulan nesneleri serbest bırakamaz. Başvurulan ancak artık gerekli olan nesneler bellek sızıntısına neden olur. Uygulama sık sık nesneleri ayırıyor ve artık ihtiyaç kalmadan serbest bırakamezse, bellek kullanımı zaman içinde artar.

Aşağıdaki API, 10 KB'lık bir Dize örneği oluşturur ve istemciye döndürür. Önceki örnekteki fark, bu örneğine statik bir üye tarafından başvurul olmasıdır ve bu da hiçbir zaman koleksiyon için kullanılamaz.

private static ConcurrentBag<string> _staticStrings = new ConcurrentBag<string>();

[HttpGet("staticstring")]
public ActionResult<string> GetStaticString()
{
    var bigString = new String('x', 10 * 1024);
    _staticStrings.Add(bigString);
    return bigString;
}

Yukarıdaki kod:

  • Tipik bir bellek sızıntısı örneğidir.
  • Sık yapılan çağrılarla, işlem özel durumla birlikte kilit gelene kadar uygulama belleğinin artmasına neden OutOfMemory olur.

önceki grafik

Yukarıdaki görüntüde:

  • Uç noktanın yük /api/staticstring testi bellekte doğrusal bir artışa neden olur.
  • GC, 2. nesil bir koleksiyonu çağırarak bellek baskısı arttıkça belleği serbest etmeye çalışır.
  • GC, sızdırılan belleği serbest bırakamaz. Ayrılan ve çalışma kümesi zaman içinde artar.

Önbelleğe alma gibi bazı senaryolar, bellek baskısı onları serbest bırakmak zorunda bırakıncaya kadar nesne başvurularının tutularak gerçekleşmesini gerektirir. sınıfı, WeakReference bu tür bir önbelleğe alma kodu için kullanılabilir. Bellek WeakReference baskıları altında bir nesne toplanır. Varsayılan uygulaması IMemoryCache WeakReference kullanır.

Yerel bellek

Bazı .NET Core nesneleri yerel belleği kullanır. Yerel bellek GC tarafından toplanmaz. Yerel bellek kullanan .NET nesnesinin yerel kod kullanarak boş olması gerekir.

.NET, geliştiricilerin IDisposable yerel belleği serbest bırakmasına izin veren bir arabirim sağlar. Disposeçağrılmasa bile, sonlandırıcı çalıştırıldıysa doğru Dispose uygulanan sınıflar çağrısını içerir.

Aşağıdaki kodu inceleyin:

[HttpGet("fileprovider")]
public void GetFileProvider()
{
    var fp = new PhysicalFileProvider(TempPath);
    fp.Watch("*.*");
}

PhysicalFileProvider yönetilen bir sınıftır, bu nedenle tüm örnekler isteğin sonunda toplanır.

Aşağıdaki görüntüde, API'yi sürekli olarak çağrıları sırasında bellek fileprovider profili gösterildi.

önceki grafik

Yukarıdaki grafik, bellek kullanımını artırmaya devam etti olarak bu sınıfın uygulanmasıyla ilgili bariz bir sorun gösterir. Bu, bu sorunda izlendiği bilinen bir sorundur.

Kullanıcı kodunda da aşağıdakilerden biri ile aynı sızıntı ortaya olabilir:

  • Sınıfı doğru serbest bırakmama.
  • Atılması gereken Dispose bağımlı nesnelerin yöntemini çağırmayı unutma.

Büyük nesneler yığını

Sık bellek ayırma/serbest döngüler, özellikle büyük bellek öbekleri ayırma sırasında belleği parçalar. Nesneler bitişik bellek bloklarında ayrılır. Parçalanmayı azaltmak için, GC belleği boşaltarak birleştirmeye çalışır. Bu işleme sıkıştırma denir. Sıkıştırma, nesneleri taşımayı içerir. Büyük nesneleri hareket ettiren bir performans cezası oluşturur. Bu nedenle GC, büyük nesneler için büyük nesne yığını (LOH) olarak adlandırılan özel bir bellek bölgesi oluşturur. 85.000 bayttan (yaklaşık 83 KB) büyük nesneler:

  • LOH'a yerleştirilir.
  • Sıkıştırıldı değil.
    1. nesil GC'ler sırasında toplanır.

LOH dolu olduğunda GC, 2. nesil bir koleksiyonu tetikler. 2. nesil koleksiyonlar:

  • Doğal olarak yavaştır.
  • Buna ek olarak, diğer tüm nesillerde bir koleksiyonu tetikleme maliyeti de vardır.

Aşağıdaki kod LOH'ı hemen sıkıştırıyor:

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

LargeObjectHeapCompactionModeLOH'ı sıkıştırma hakkında bilgi için bkz.

.NET Core 3.0 ve sonrası kullanan kapsayıcılarda LOH otomatik olarak sıkıştırılır.

Bu davranışı gösteren aşağıdaki API:

[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
   return new byte[size].Length;
}

Aşağıdaki grafikte, maksimum yük altında uç noktayı /api/loh/84975 çağırmanın bellek profili gösterildi:

önceki grafik

Aşağıdaki grafikte uç noktayı çağırmanın bellek profili /api/loh/84976 ve yalnızca bir byte daha vardır:

önceki grafik

Not: byte[] Yapının ek yük baytları vardır. Bu nedenle 84.976 bayt 85.000 sınırı tetikler.

Yukarıdaki iki grafiği karşılaştırma:

  • Çalışma kümesi yaklaşık 450 MB olmak için her iki senaryo için de benzerdir.
  • LOH istekleri (84.975 bayt) altında çoğunlukla nesil 0 koleksiyonları gösterir.
  • LoH üzerinden istekleri sabit nesil 2 koleksiyonları üretir. 2. nesil koleksiyonlar pahalıdır. Daha fazla CPU gerekir ve aktarım hızı neredeyse %50 düşer.
  1. Nesil GC'lere neden olduğundan geçici büyük nesneler özellikle sorunludur.

En yüksek performans için büyük nesne kullanımının en aza indirilmesi gerekir. Mümkünse büyük nesneleri bölün. Örneğin, yanıt Önbelleğe Alma ara yazılımı ASP.NET Core önbellek girişlerini 85.000 bayttan az bloklara böler.

Aşağıdaki bağlantılarda nesneleri LOH ASP.NET Core tutma yaklaşımı açık bir şekilde açık ve açık bir şekilde açıkmektedir:

Daha fazla bilgi için bkz.

HttpClient

Hatalı bir şekilde HttpClient kullanmak kaynak sızıntısına neden olabilir. Veritabanı bağlantıları, yuvalar, dosya tanıtıcıları gibi sistem kaynakları:

  • Bellekten daha azdır.
  • Sızdırılan bellekten daha sorunludur.

Deneyimli .NET geliştiricileri, uygulayan Dispose nesneleri çağırmayı IDisposable biliyor. Uygulayan nesnelerin elden geçirilmama, IDisposable genellikle bellek sızdırılmış veya sistem kaynaklarının sızdırılmış olarak sonuç verir.

HttpClient , IDisposable 'i uygulayan ancak her çağrıda atılması gereken bir şey değildir. Bunun HttpClient yerine, yeniden kullanılması gerekir.

Aşağıdaki uç nokta her istekte yeni bir HttpClient örnek oluşturur ve atlar:

[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
    using (var httpClient = new HttpClient())
    {
        var result = await httpClient.GetAsync(url);
        return (int)result.StatusCode;
    }
}

Yükleme altında aşağıdaki hata iletileri günlüğe kaydedilir:

fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HLG70PBE1CR1", Request id "0HLG70PBE1CR1:00000031":
      An unhandled exception was thrown by the application.
System.Net.Http.HttpRequestException: Only one usage of each socket address
    (protocol/network address/port) is normally permitted --->
    System.Net.Sockets.SocketException: Only one usage of each socket address
    (protocol/network address/port) is normally permitted
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port,
    CancellationToken cancellationToken)

Örnekler HttpClient atılmasına rağmen, gerçek ağ bağlantısının işletim sistemi tarafından serbest olması biraz zaman alır. Sürekli olarak yeni bağlantılar oluşturarak bağlantı noktalarının tükenmesi oluşur. Her istemci bağlantısı kendi istemci bağlantı noktasını gerektirir.

Bağlantı noktası tükenmesi 'ni önlemenin bir yolu aynı örneği yeniden kullanmaktır HttpClient :

private static readonly HttpClient _httpClient = new HttpClient();

[HttpGet("httpclient2")]
public async Task<int> GetHttpClient2(string url)
{
    var result = await _httpClient.GetAsync(url);
    return (int)result.StatusCode;
}

HttpClientUygulama durdurulduğunda örnek serbest bırakılır. Bu örnek her bir atılabilir kaynağının her bir kullanım sonrasında atılmamalıdır.

Bir örneğin yaşam süresini işlemenin daha iyi bir yolu için aşağıdakilere bakın HttpClient :

Nesne havuzu oluşturma

Önceki örnek, HttpClient Örneğin tüm istekler tarafından nasıl statik hale getirilebilir ve yeniden kullanılabileceğini gösterdi. Yeniden kullanım, kaynak dışı çalışmayı önler.

Nesne havuzu:

  • Yeniden kullanım modelini kullanır.
  • , Oluşturulması pahalı olan nesneler için tasarlanmıştır.

Havuz, iş parçacıkları genelinde ayrılmaları ve yayımlanmaları önceden başlatılmış nesnelerden oluşan bir koleksiyondur. Havuzlar sınırlar, önceden tanımlanmış boyutlar veya büyüme oranı gibi ayırma kuralları tanımlayabilir.

Microsoft. Extensions. objectpool NuGet paketi, bu tür havuzları yönetmeye yardımcı olan sınıflar içerir.

Aşağıdaki API uç noktası byte her istekte rastgele sayılarla doldurulmuş bir arabellek başlatır:

        [HttpGet("array/{size}")]
        public byte[] GetArray(int size)
        {
            var random = new Random();
            var array = new byte[size];
            random.NextBytes(array);

            return array;
        }

Aşağıdaki grafik, önceki API 'nin orta yük ile çağrılmasını gösterir:

önceki grafik

Yukarıdaki grafikte, nesil 0 toplamaları yaklaşık olarak saniyede bir kez gerçekleşir.

Önceki kod, byte arraypool <T> kullanılarak arabelleği havuzlayarak iyileştirilebilir. Bir statik örnek istekler arasında yeniden kullanılır.

Bu yaklaşımla farklı olan özellikler, havuza alınmış bir nesnenin API 'den döndürüldüğü şeydir. Bunun anlamı:

  • Yöntemi, yönteminden geri döndükten hemen sonra Denetim dışındadır.
  • Nesneyi serbest bırakamıyoruz.

Nesnenin elden çıkarılmasını ayarlamak için:

RegisterForDispose , Dispose yalnızca http isteği tamamlandığında serbest bırakılacak şekilde hedef nesneye çağrı yapılır.

private static ArrayPool<byte> _arrayPool = ArrayPool<byte>.Create();

private class PooledArray : IDisposable
{
    public byte[] Array { get; private set; }

    public PooledArray(int size)
    {
        Array = _arrayPool.Rent(size);
    }

    public void Dispose()
    {
        _arrayPool.Return(Array);
    }
}

[HttpGet("pooledarray/{size}")]
public byte[] GetPooledArray(int size)
{
    var pooledArray = new PooledArray(size);

    var random = new Random();
    random.NextBytes(pooledArray.Array);

    HttpContext.Response.RegisterForDispose(pooledArray);

    return pooledArray.Array;
}

Havuza alınmamış sürüm olarak aynı yükün uygulanması aşağıdaki grafiğe neden olur:

önceki grafik

Ana fark ayrılan bayttır ve çok daha az nesil 0 koleksiyonu olarak.

Ek kaynaklar