Zdarzenia
19 lis, 23 - 21 lis, 23
Dołącz do sesji online na konferencji Microsoft Ignite, aby rozwinąć swoje umiejętności i pomóc w rozwiązywaniu dzisiejszych złożonych problemów.
Zarejestruj się terazTa przeglądarka nie jest już obsługiwana.
Przejdź na przeglądarkę Microsoft Edge, aby korzystać z najnowszych funkcji, aktualizacji zabezpieczeń i pomocy technicznej.
Autor: Sébastien Ros i Rick Anderson
Zarządzanie pamięcią jest złożone, nawet w strukturze zarządzanej, takiej jak .NET. Analizowanie i zrozumienie problemów z pamięcią może być trudne. W tym artykule:
GC przydziela segmenty sterty, w których każdy segment jest ciągły zakres pamięci. Obiekty umieszczone w stercie są podzielone na jedną z 3 pokoleń: 0, 1 lub 2. Generacja określa częstotliwość, z jaką GC próbuje zwolnić pamięć na zarządzanych obiektach, do których nie odwołuje się już aplikacja. Niższe numerowane generacje to GC częściej.
Obiekty są przenoszone z jednej generacji do innej na podstawie ich okresu istnienia. W miarę dłuższego życia obiektów są przenoszone do wyższej generacji. Jak wspomniano wcześniej, generacje wyższe są GC rzadziej. Obiekty krótkotrwałe zawsze pozostają w generacji 0. Na przykład obiekty, do których odwołuje się okres życia żądania internetowego, są krótkotrwałe. Singletony na poziomie aplikacji zazwyczaj są migrowane do generacji 2.
Po uruchomieniu aplikacji ASP.NET Core GC:
Powyższe alokacje pamięci są wykonywane ze względów wydajności. Zaletą wydajności są segmenty sterty w ciągłej pamięci.
Ogólnie rzecz biorąc, aplikacje ASP.NET Core w środowisku produkcyjnym nie powinny używać GC. Zbierz jawnie. Inducing garbage collections at sub-optimal times can decrease performance znacznie.Inducing garbage collections at sub-optimal times can decrease performance (Inducing garbage collections at sub-optimal times can decrease performance).
GC. Funkcja Collect jest przydatna podczas badania przecieków pamięci. Wywołanie GC.Collect()
wyzwala blokujący cykl odzyskiwania pamięci, który próbuje odzyskać wszystkie obiekty niedostępne z kodu zarządzanego. Jest to przydatny sposób zrozumienia rozmiaru osiągalnych obiektów na żywo w stercie i śledzenia wzrostu rozmiaru pamięci w czasie.
Dedykowane narzędzia mogą pomóc w analizowaniu użycia pamięci:
Użyj następujących narzędzi do analizowania użycia pamięci:
Menedżer zadań może służyć do uzyskania pojęcia, ile pamięci używa aplikacja ASP.NET. Wartość pamięci Menedżera zadań:
Jeśli wartość pamięci Menedżera zadań zwiększa się na czas nieokreślony i nigdy nie spłaszcza się, aplikacja ma przeciek pamięci. W poniższych sekcjach przedstawiono i wyjaśniono kilka wzorców użycia pamięci.
Przykładowa aplikacja MemoryLeak jest dostępna w witrynie GitHub. Aplikacja MemoryLeak:
Uruchom polecenie MemoryLeak. Przydzielona pamięć powoli zwiększa się do momentu wystąpienia GC. Pamięć zwiększa się, ponieważ narzędzie przydziela obiekt niestandardowy do przechwytywania danych. Na poniższej ilustracji przedstawiono stronę indeksu MemoryLeak, gdy wystąpi GC 0. generacji. Wykres przedstawia 0 RPS (żądania na sekundę), ponieważ nie wywołano żadnych punktów końcowych interfejsu API z kontrolera interfejsu API.
Wykres przedstawia dwie wartości użycia pamięci:
Poniższy interfejs API tworzy wystąpienie ciągu 20 KB i zwraca je do klienta. W każdym żądaniu nowy obiekt jest przydzielany w pamięci i zapisywany w odpowiedzi. Ciągi są przechowywane jako znaki UTF-16 na platformie .NET, więc każdy znak przyjmuje 2 bajty w pamięci.
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
Poniższy graf jest generowany przy stosunkowo małym obciążeniu, aby pokazać, w jaki sposób alokacje pamięci mają wpływ na GC.
Powyższy wykres przedstawia następujące elementy:
Poniższy wykres jest pobierany z maksymalną przepływnością, którą można obsłużyć przez maszynę.
Powyższy wykres przedstawia następujące elementy:
Moduł zbierający elementy bezużyteczne platformy .NET ma dwa różne tryby:
Tryb GC można jawnie ustawić w pliku projektu lub w runtimeconfig.json
pliku opublikowanej aplikacji. Następujące znaczniki pokazują ustawienie ServerGarbageCollection
w pliku projektu:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Zmiana ServerGarbageCollection
w pliku projektu wymaga ponownego skompilowania aplikacji.
Uwaga: Odzyskiwanie pamięci serwera nie jest dostępne na maszynach z jednym rdzeniem. Aby uzyskać więcej informacji, zobacz IsServerGC.
Na poniższej ilustracji przedstawiono profil pamięci w ramach 5K RPS przy użyciu kontrolera GC stacji roboczej.
Różnice między tym wykresem a wersją serwera są istotne:
W typowym środowisku serwera internetowego użycie procesora CPU jest ważniejsze niż pamięć, dlatego serwer GC jest lepszy. Jeśli wykorzystanie pamięci jest wysokie, a użycie procesora CPU jest stosunkowo niskie, GC stacji roboczej może być bardziej wydajne. Na przykład wysoka gęstość hostująca kilka aplikacji internetowych, w których pamięć jest niedostępna.
Jeśli na jednej maszynie jest uruchomionych wiele konteneryzowanych aplikacji, GC stacji roboczej może być bardziej wydajne niż GC serwera. Aby uzyskać więcej informacji, zobacz 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 (Uruchamianie z serwerem GC w małym kontenerze) i Running with Server GC in a Small Container Scenario Part 1 – Hard Limit for the GC Heap (Uruchamianie z klastrem GC w małym scenariuszu kontenera — część 1 — twardy limit sterty GC).
GC nie może zwolnić obiektów, do których odwołuje się odwołanie. Obiekty, do których odwołuje się odwołanie, ale nie są już potrzebne, powodują wyciek pamięci. Jeśli aplikacja często przydziela obiekty i nie zwalnia ich po tym, jak nie są już potrzebne, użycie pamięci zwiększy się wraz z upływem czasu.
Poniższy interfejs API tworzy wystąpienie ciągu 20 KB i zwraca je do klienta. Różnica w poprzednim przykładzie polega na tym, że to wystąpienie jest przywoływało statyczny element członkowski, co oznacza, że nigdy nie jest dostępne dla kolekcji.
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;
}
Powyższy kod ma następujące działanie:
OutOfMemory
wyjątkiem.
Na powyższym obrazie:
/api/staticstring
powoduje liniowy wzrost pamięci.Niektóre scenariusze, takie jak buforowanie, wymagają, aby odwołania do obiektów były przechowywane, dopóki ciśnienie pamięci nie wymusza ich zwolnienia. Klasa WeakReference może być używana dla tego typu kodu buforowania. Obiekt WeakReference
jest zbierany pod ciśnieniem pamięci. Domyślna implementacja IMemoryCache funkcji używa WeakReference
metody .
Niektóre obiekty platformy .NET Core bazują na pamięci natywnej. Pamięci natywnej nie można zbierać przez GC. Obiekt .NET używający pamięci natywnej musi zwolnić go przy użyciu kodu natywnego.
Platforma .NET udostępnia IDisposable interfejs umożliwiający deweloperom wydawanie pamięci natywnej. Nawet jeśli Dispose nie jest wywoływana, poprawnie zaimplementowano wywołanie Dispose
klas po uruchomieniu finalizatora.
Spójrzmy na poniższy kod:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider jest klasą zarządzaną, więc każde wystąpienie zostanie zebrane na końcu żądania.
Na poniższej ilustracji przedstawiono profil pamięci podczas ciągłego wywoływania interfejsu fileprovider
API.
Powyższy wykres przedstawia oczywisty problem z implementacją tej klasy, ponieważ stale zwiększa użycie pamięci. Jest to znany problem, który jest śledzony w tym problemie.
Ten sam wyciek może wystąpić w kodzie użytkownika, wykonując jedną z następujących czynności:
Dispose
obiektów zależnych, które powinny być usuwane.Częste przydzielanie pamięci/wolne cykle mogą fragmentować pamięć, szczególnie w przypadku przydzielania dużych fragmentów pamięci. Obiekty są przydzielane w ciągłych blokach pamięci. Aby wyeliminować fragmentację, gdy GC zwalnia pamięć, próbuje go zdefragmentować. Ten proces jest nazywany kompaktowaniem. Kompaktowanie obejmuje przenoszenie obiektów. Przeniesienie dużych obiektów nakłada karę za wydajność. Z tego powodu GC tworzy specjalną strefę pamięci dla dużych obiektów, nazywanych stertą dużych obiektów (LOH). Obiekty, które są większe niż 85 000 bajtów (około 83 KB), to:
Gdy LOH jest pełna, GC wyzwoli kolekcję generacji 2. Kolekcje generacji 2:
Poniższy kod kompaktuje natychmiast LOH:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Zobacz LargeObjectHeapCompactionMode , aby uzyskać informacje na temat kompaktowania LOH.
W kontenerach korzystających z platformy .NET Core 3.0 lub nowszej LOH jest automatycznie kompaktowana.
Następujący interfejs API, który ilustruje to zachowanie:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
Na poniższym wykresie przedstawiono profil pamięci wywoływania punktu końcowego /api/loh/84975
pod maksymalnym obciążeniem:
Na poniższym wykresie przedstawiono profil pamięci wywoływania punktu końcowego /api/loh/84976
, przydzielając tylko jeden bajt:
Uwaga: struktura byte[]
ma bajty narzutu. Dlatego 84 976 bajtów wyzwala limit 85 000.
Porównanie dwóch poprzednich wykresów:
Tymczasowe duże obiekty są szczególnie problematyczne, ponieważ powodują gen2 GCs.
Aby uzyskać maksymalną wydajność, należy zminimalizować użycie dużych obiektów. Jeśli to możliwe, podziel duże obiekty. Na przykład oprogramowanie pośredniczące buforowania odpowiedzi w programie ASP.NET Core dzieli wpisy pamięci podręcznej na bloki mniejsze niż 85 000 bajtów.
Poniższe linki pokazują podejście ASP.NET Core do utrzymania obiektów w ramach limitu LOH:
Aby uzyskać więcej informacji, zobacz:
Nieprawidłowe użycie HttpClient może spowodować wyciek zasobów. Zasoby systemowe, takie jak połączenia bazy danych, gniazda, dojścia plików itp.:
Doświadczeni deweloperzy platformy .NET wiedzą, że wywołają Dispose obiekty implementujące IDisposableelement . Nie dysponowanie obiektów, które implementują IDisposable
, zwykle powoduje wyciek pamięci lub wycieku zasobów systemowych.
HttpClient
implementuje metodę IDisposable
, ale nie należy ich usuwać przy każdym wywołaniu. HttpClient
Zamiast tego należy ponownie użyć.
Następujący punkt końcowy tworzy i usuwa nowe HttpClient
wystąpienie na każdym żądaniu:
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
using (var httpClient = new HttpClient())
{
var result = await httpClient.GetAsync(url);
return (int)result.StatusCode;
}
}
Podczas ładowania rejestrowane są następujące komunikaty o błędach:
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)
Mimo że HttpClient
wystąpienia są usuwane, rzeczywiste połączenie sieciowe może z czasem zostać zwolnione przez system operacyjny. Stale tworząc nowe połączenia, występuje wyczerpanie portów. Każde połączenie klienta wymaga własnego portu klienta.
Jednym ze sposobów zapobiegania wyczerpaniu portów jest ponowne użycie tego samego HttpClient
wystąpienia:
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;
}
Wystąpienie HttpClient
jest zwalniane po zatrzymaniu aplikacji. W tym przykładzie pokazano, że nie każdy jednorazowy zasób powinien zostać usunięty po każdym użyciu.
Aby uzyskać lepszy sposób obsługi okresu istnienia HttpClient
wystąpienia, zobacz następujące informacje:
W poprzednim przykładzie pokazano, w jaki sposób HttpClient
wystąpienie może być statyczne i ponownie używane przez wszystkie żądania. Ponowne użycie zapobiega wyczerpaniu zasobów.
Buforowanie obiektów:
Pula to kolekcja wstępnie zainicjowanych obiektów, które można rezerwować i zwalniać między wątkami. Pule mogą definiować reguły alokacji, takie jak limity, wstępnie zdefiniowane rozmiary lub szybkość wzrostu.
Pakiet NuGet Microsoft.Extensions.ObjectPool zawiera klasy, które ułatwiają zarządzanie takimi pulami.
Następujący punkt końcowy interfejsu API tworzy wystąpienie buforu byte
wypełnionego losowymi liczbami w każdym żądaniu:
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array);
return array;
}
Na poniższym wykresie przedstawiono wywołanie powyższego interfejsu API z umiarkowanym obciążeniem:
Na poprzednim wykresie kolekcje generacji 0 odbywają się mniej więcej raz na sekundę.
Powyższy kod można zoptymalizować, tworząc pulę buforu przy użyciu biblioteki byte
ArrayPool<T>. Wystąpienie statyczne jest ponownie używane w żądaniach.
Czym różni się to od tego podejścia, jest to, że obiekt w puli jest zwracany z interfejsu API. Oznacza to:
Aby skonfigurować usuwanie obiektu:
RegisterForDispose
Program zajmie się wywołaniem Dispose
obiektu docelowego, tak aby został wydany tylko po zakończeniu żądania HTTP.
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;
}
Zastosowanie tego samego obciążenia co wersja niepulowana powoduje wyświetlenie następującego wykresu:
Główna różnica polega na przydzielaniu bajtów i w konsekwencji znacznie mniejszej liczbie kolekcji generacji 0.
Opinia o produkcie ASP.NET Core
ASP.NET Core to projekt typu open source. Wybierz link, aby przekazać opinię:
Zdarzenia
19 lis, 23 - 21 lis, 23
Dołącz do sesji online na konferencji Microsoft Ignite, aby rozwinąć swoje umiejętności i pomóc w rozwiązywaniu dzisiejszych złożonych problemów.
Zarejestruj się terazSzkolenie
Moduł
Zwiększanie wydajności dzięki pamięci podręcznej w projekcie .NET Aspire - Training
W tym module dowiesz się więcej o pamięciach podręcznych w aplikacji natywnej dla chmury platformy .NET Aspire i sposobie ich używania do optymalizacji wydajności mikrousług.