Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Von Sébastien Ros und Rick Anderson
Die Arbeitsspeicherverwaltung ist auch in einem verwalteten Framework wie .NET eine komplexe Angelegenheit. Das Analysieren und Beheben von Speicherproblemen kann eine Herausforderung darstellen. Speicherlecks und Gc-Probleme (Garbage Collection) sind häufig darauf zurückzuführen, dass sie nicht wissen, wie der Speicherverbrauch in .NET funktioniert, oder dass Sie den Prozess zur Messung der Nutzung nicht verstehen.
In diesem Artikel werden gängige Speichernutzungsmuster veranschaulicht, die problematisch sein können, und alternative Ansätze vorgeschlagen.
Erkunden Sie die "Garbage Collection" (GC) in .NET
Der GC-Prozess weist Heap-Segmente zu, wobei jedes Segment ein zusammenhängender Speicherbereich ist. Objekte, die im Heap platziert werden, werden in eine von drei Generationen unterteilt: 0, 1 oder 2. Die Generierung bestimmt die Häufigkeit der GC-Versuche, Speicher für verwaltete Objekte freizugeben, auf die die App nicht mehr verweist. Die GC befasst sich häufiger mit den niedriger nummerierten Generationen.
Objekte werden basierend auf ihrer Lebensdauer von einer Generation in eine andere verschoben. Wenn Objekte länger leben, werden sie in eine höhere Generation verschoben. Wie bereits erwähnt, wird der GC bei höheren Generationen seltener ausgeführt. Kurzlebige Objekte verbleiben immer in Gen 0. Beispielsweise sind Objekte, auf die während der Lebensdauer einer Webanforderung verwiesen wird, kurzlebig. Singletons auf Anwendungsebene migrieren in der Regel zu Gen 2.
Wenn eine ASP.NET Core App startet, wird der GC-Prozess ausgeführt:
- Reserviert Arbeitsspeicher für die anfänglichen Heapsegmente.
- Committet einen kleinen Teil des Arbeitsspeichers, wenn die Laufzeit geladen wird.
Die obigen Speicherbelegungen erfolgen aus Leistungsgründen. Der Leistungsvorteil ergibt sich aus Heapsegmenten im zusammenhängenden Arbeitsspeicher.
Überprüfen Sie die Einschränkungen bei der Verwendung von GC.Collect.
Im Allgemeinen sollten ASP.NET Core Apps in der Produktion nicht explizit die Methode GC.Collect verwenden. Das Auslösen von Garbage Collections zu suboptimalen Zeiten kann die Leistung erheblich verringern.
GC.Collect ist nützlich, wenn Speicherverluste untersucht werden. Durch Aufrufen von GC.Collect() wird ein blockierter Garbage Collection-Zyklus ausgelöst, der versucht, alle Objekte zurückzufordern, auf die von verwaltetem Code nicht zugegriffen werden kann. Dies ist nützlich, um die Größe der erreichbaren Liveobjekte im Heap zu verstehen und das Wachstum der Speichergröße im Laufe der Zeit nachzuverfolgen.
Analysieren der Speicherauslastung einer App
Dedizierte Tools können dabei helfen, die Speicherauslastung zu analysieren, einschließlich:
- Zählen von Objektverweisen.
- Messen, wie viel Auswirkung die GC auf die CPU-Auslastung hat.
- Messen des für jede Generation verwendeten Speicherplatzes.
Verwenden Sie die folgenden Tools, um die Arbeitsspeicherauslastung zu analysieren:
- dotnet-trace-Hilfsprogramm (kann auf Produktionsmaschinen verwendet werden)
- Analysieren der Arbeitsspeicherauslastung ohne Visual Studio-Debugger
- Messen der Speicherauslastung in Visual Studio
Erkennen von Speicherproblemen
Über den Task-Manager können Sie näherungsweise erfahren, wie viel Arbeitsspeicher eine ASP.NET-App verwendet. Der Arbeitsspeicherwert im Task-Manager:
- Stellt die Vom ASP.NET Prozess verwendete Arbeitsspeichermenge dar.
- ** Enthält die aktiven Objekte der App und andere Speicherverbraucher wie z. B. die native Arbeitsspeicherauslastung.
Wenn der Arbeitsspeicherwert im Task-Manager unbegrenzt zunimmt und nie abflacht, tritt bei der App ein Arbeitsspeicherverlust auf. In den folgenden Abschnitten werden verschiedene Muster der Arbeitsspeicherauslastung veranschaulicht und erläutert.
Erkunden Sie die Muster-App zur Anzeige der Speichernutzung
Die Beispiel-App „MemoryLeak“ ist auf GitHub verfügbar. Für die MemoryLeak-App gilt Folgendes:
- Sie enthält einen Diagnosecontroller, der Echtzeitdaten für Arbeitsspeicher und GC für die App sammelt.
- Sie verfügt über eine Indexseite, auf der die Daten für Arbeitsspeicher und GC angezeigt werden. Die Indexseite wird jede Sekunde aktualisiert.
- Sie enthält einen API-Controller, der verschiedene Auslastungsmuster für den Arbeitsspeicher bereitstellt.
- Kann verwendet werden, um Speichernutzungsmuster von ASP.NET Core Apps anzuzeigen, es ist jedoch kein unterstütztes Tool.
Führen Sie MemoryLeak aus. Der zugeordnete Arbeitsspeicher nimmt langsam zu, bis eine GC erfolgt. Der Arbeitsspeicher erhöht sich, da das Tool ein benutzerdefiniertes Objekt zum Erfassen von Daten zuweist. Die folgende Abbildung zeigt die MemoryLeak Index-Seite, wenn ein GC der Generation 0 erfolgt. Das Diagramm zeigt 0 RPS (Requests Per Second, Anforderungen pro Sekunde), da vom API-Controller keine API-Endpunkte aufgerufen wurden.
Das Diagramm zeigt zwei Werte für die Arbeitsspeicherauslastung an:
- Zugewiesen: Die Speichermenge, die von verwalteten Objekten belegt wird.
- Arbeitssatz: Der Satz von Seiten im virtuellen Adressbereich des Prozesses, der derzeit im physischen Speicher gespeichert ist. Der angezeigte Arbeitssatz ist der gleiche Wert, der auch im Task-Manager angezeigt wird. Weitere Informationen finden Sie unter "Arbeitssatz".
Vorübergehende Objekte
Die folgende API erstellt eine 20-KB-Zeichenfolgeninstanz und gibt sie an den Client zurück. Bei jeder Anforderung wird ein neues Objekt im Arbeitsspeicher zugeordnet und in die Antwort geschrieben. Zeichenfolgen werden in .NET als UTF-16-Zeichen gespeichert, sodass jedes Zeichen 2 Bytes im Arbeitsspeicher benötigt.
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
Das folgende Diagramm wird mit einer relativ kleinen Last generiert, die zeigt, wie GC sich auf Speicherzuweisungen auswirkt.
Das Diagramm veranschaulicht die folgenden Details:
- 4K RPS (Anforderungen pro Sekunde)
- GC-Sammlungen der Generation 0 treten etwa alle 2 Sekunden auf.
- Konstanter Arbeitssatz, ca. 500 MB
- CPU-Auslastung beträgt 12%
- Stabiler Arbeitsspeicherverbrauch und Freigabe (durch GC)
Das folgende Diagramm wird beim maximalen Durchsatz verwendet, den der Computer verarbeiten kann.
Das Diagramm veranschaulicht die folgenden Details:
- 22 K RPS
- GC-Sammlungen der Generation 0 treten mehrmals pro Sekunde auf
- Gen 1 Collections triggern, weil die App deutlich mehr Arbeitsspeicher pro Sekunde zuweist
- Konstanter Arbeitssatz, ca. 500 MB
- CPU ist 33%
- Stabiler Arbeitsspeicherverbrauch und Freigabe (durch GC)
- Die CPU (33 %) ist nicht übermäßig ausgelastet, sodass GC mit einer hohen Anzahl von Allokationen Schritt halten kann
Workstation GC im Vergleich zu Server GC
Der .NET Garbage Collector verfügt über zwei verschiedene Modi:
- Workstation GC: Optimiert für den Desktop.
- Server GC: Die Standard-GC für ASP.NET Core-Apps. Optimiert für den Server.
Der GC-Modus kann explizit in der Projektdatei oder in der runtimeconfig.json Datei der veröffentlichten App festgelegt werden. Das folgende Markup zeigt die Festlegung von ServerGarbageCollection in der Projektdatei:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Wenn ServerGarbageCollection in der Projektdatei geändert wird, muss die App neu kompiliert werden.
Note
Die Server garbage collection ist auf Computern mit einem einzigen Kern nicht verfügbar. Weitere Informationen finden Sie in den Ausführungen zur IsServerGC-Eigenschaft.
Die folgende Abbildung zeigt das Arbeitsspeicherprofil mit 5.000 RPS unter Verwendung der Arbeitsstations-GC.
Die Unterschiede zwischen diesem Diagramm und der Serverversion sind erheblich:
- Arbeitsspeicher fällt von 500 MB auf 70 MB
- GC führt Gen 0-Sammlungen mehrmals pro Sekunde statt alle 2 Sekunden aus.
- GC fällt von 300 MB auf 10 MB
In einer typischen Webserverumgebung ist die CPU-Auslastung wichtiger als der Arbeitsspeicher, daher ist die Server-GC besser. Wenn die Arbeitsspeicherauslastung hoch und die CPU-Auslastung relativ niedrig ist, ist die Arbeitsstations-GC möglicherweise leistungsfähiger. Ein Beispiel ist das Hosten mehrerer Web-Apps mit hoher Dichte, wenn der Arbeitsspeicher knapp ist.
GC mittels Docker und kleiner Containers
Wenn mehrere containerisierte Apps auf demselben Computer ausgeführt werden, ist Workstation GC möglicherweise leistungsfähiger als Server GC. Weitere Informationen finden Sie unter Ausführen von Server GC in einem kleinen Container (Blog) und Ausführen von Server GC in einem kleinen Container-Szenario Teil 1 - Hartes Limit für den GC-Heap (Blog).
Persistente Objektverweise
Die GC kann keine Objekte freigeben, auf die verwiesen wird. Objekte, auf die verwiesen wird, die aber nicht mehr benötigt werden, führen zu einem Arbeitsspeicherverlust. Wenn die App Objekte häufig zuordnet und sie nicht mehr freigeben kann, nachdem sie nicht mehr benötigt werden, erhöht sich die Speicherauslastung im Laufe der Zeit.
Die folgende API erstellt eine 20-KB-Zeichenfolgeninstanz und gibt sie an den Client zurück. Der Unterschied zum vorherigen Beispiel besteht darin, dass ein statisches Element auf diese Instanz verweist, was bedeutet, dass die Instanz nie für die Auflistung verfügbar ist.
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;
}
Der vorangehende Code:
- Zeigt ein typisches Speicherleck.
- Bei häufigen Aufrufen nimmt der App-Arbeitsspeicher zu, bis der Prozess mit einer
OutOfMemory-Ausnahme abstürzt.
Das Diagramm veranschaulicht die folgenden Details:
- Auslastungstests des Endpunkts führen zu einer linearen Erhöhung des
/api/staticstringArbeitsspeichers - GC versucht, Arbeitsspeicher freizugeben, wenn die Speicherbelastung zunimmt, indem eine Gen 2-Collection aufgerufen wird
- GC kann den geleakten Speicher nicht freigeben; Allokation und Working Set nehmen mit der Zeit zu
In einigen Szenarien – beispielsweise bei der Zwischenspeicherung – müssen Objektverweise so lange aufbewahrt werden, bis sie aufgrund der Arbeitsspeicherauslastung freigegeben werden müssen. Für diese Art von Code für die Zwischenspeicherung kann die WeakReference-Klasse verwendet werden. Ein WeakReference-Objekt wird unter Speicherdruck gesammelt. Die Standardimplementierung der IMemoryCache Schnittstelle verwendet WeakReference.
Nativer Speicher
Einige .NET-Objekte basieren auf nativem Speicher, aber nativer Speicher wird vom GC nicht gesammelt. Das .NET-Objekt, das nativen Speicher verwendet, muss es mit systemeigenem Code freigeben.
.NET stellt die Schnittstelle IDisposable bereit, damit Entwickler*innen nativen Arbeitsspeicher freigeben können. Auch wenn die Dispose Methode nicht aufgerufen wird, rufen Dispose ordnungsgemäß implementierte Klassen auf, wenn der Finalizer ausgeführt wird.
Betrachten Sie folgenden Code:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider ist eine verwaltete Klasse, sodass jede Instanz am Ende der Anforderung erfasst wird.
Die folgende Abbildung zeigt das Arbeitsspeicherprofil während eines kontinuierlichen Aufrufs der fileprovider-API.
Das obige Diagramm zeigt ein offensichtliches Problem mit der Implementierung dieser Klasse, da die Arbeitsspeicherauslastung ständig erhöht wird. Dieses Ergebnis ist ein bekanntes Problem, das in GitHub dotnet/aspnetcore Problem #844 nachverfolgt wird.
Das gleiche Leck tritt in den folgenden Szenarien im Benutzercode auf:
- Die Klasse wird nicht ordnungsgemäß freigegeben.
- Das Vergessen, die Methode
Disposeder abhängigen Objekte aufzurufen, die entsorgt werden sollen
Heap für große Objekte
Häufige Speicherzuweisungen/freie Zyklen führen zu fragmentiertem Speicher, insbesondere beim Zuordnen großer Speicherblöcke. Objekte werden in zusammenhängenden Speicherblöcken zugeordnet. Um die Fragmentierung zu minimieren, versucht die GC bei der Freigabe von Arbeitsspeicher diesen zu defragmentieren. Dieser Prozess wird als Komprimierung bezeichnet. Die Komprimierung beinhaltet das Verschieben von Objekten. Das Verschieben großer Objekte führt zu Leistungseinbußen. Aus diesem Grund erstellt die GC eine spezielle Speicherzone für große Objekte, die als Large Object Heap (LOH) bezeichnet wird. Für Objekte, die größer als 85.000 Bytes (ca. 83 KB) sind, gilt Folgendes:
- Positioniert auf dem LOH
- Nicht komprimiert
- Verarbeitet während der Gen 2-Sammlung
Wenn der LOH voll ist, löst der GC einen Gen 2-Sammelprozess aus.
- Gen 2-Sammlungen sind von Natur aus langsam.
- Sie können die Kosten für das Triggern einer Collection für alle anderen Generationen übernehmen.
Der folgende Code komprimiert den LOH sofort:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Informationen zur Verdichtung des LOH finden Sie in der Eigenschaft LargeObjectHeapCompactionMode.
In Containern, die .NET Core 3.0 oder höher verwenden, wird der LOH automatisch komprimiert.
Die folgende API veranschaulicht dieses Verhalten:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
Das folgende Diagramm zeigt das Arbeitsspeicherprofil für das Aufrufen des /api/loh/84975-Endpunkts unter maximaler Last:
Das folgende Diagramm zeigt das Arbeitsspeicherprofil für das Aufrufen des /api/loh/84976-Endpunkts, wobei nur noch ein weiteres Byte zugeordnet wird:
Note
Die byte[] Struktur hat Overheadbytes, weshalb 84.976 Byte das Limit von 85.000 auslösen.
Vergleich der beiden vorherigen Diagramme:
- Der Arbeitssatz ist für beide Szenarien ähnlich, etwa 450 MB
- Unter LOH-Anforderungen (84.975 Bytes) werden meist Gen 0-Sammlungen angezeigt.
- Über LOH-Anfragen generieren konstante Gen 2-Collections, die teuer sind. Mehr CPU ist erforderlich und der Durchsatz sinkt ~ 50%.
Temporäre große Objekte sind problematisch, da sie Gen 2-Sammlungen verursachen.
Minimieren Sie die Verwendung großer Objekte, um maximale Leistung zu erzielen. Teilen Sie große Objekte nach Möglichkeit auf. Beispielsweise teilt Response Caching Middleware in ASP.NET Core die Cacheeinträge in Blöcke unter 85.000 Bytes auf.
Die folgenden Links zeigen den Ansatz von ASP.NET Core, Objekte unter dem LOH-Grenzwert zu halten:
Weitere Informationen finden Sie unter:
HttpClient
Die fehlerhafte Verwendung der HttpClient Klasse kann ein Ressourcenleck verursachen.
Systemressourcen (z. B. Datenbankverbindungen, Sockets, Dateihandles usw.) stellen zwei Probleme dar:
- Sie sind knapper als Arbeitsspeicher.
- Sie sind problematischer, wenn sie durchsickern, als wenn es sich um Speicher handelt.
Erfahrene .NET Entwickler wissen, die methode Dispose für Objekte aufzurufen, die die schnittstelle IDisposable implementieren. Wenn Objekte, die IDisposable implementieren, nicht verworfen werden, führt dies in der Regel zu Speicherverlusten oder Systemressourcenverlusten.
HttpClient implementiert IDisposable, sollte aber nicht bei jedem Aufruf beseitigt werden. Stattdessen sollte HttpClient wiederverwendet werden.
Der folgende Endpunkt erstellt und verwirft bei jeder Anforderung eine neue HttpClient-Instanz:
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
using (var httpClient = new HttpClient())
{
var result = await httpClient.GetAsync(url);
return (int)result.StatusCode;
}
}
Unter Last werden die folgenden Fehlermeldungen protokolliert:
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)
Obwohl die HttpClient Instanzen verworfen werden, dauert es einige Zeit, bis das Betriebssystem die tatsächliche Netzwerkverbindung freigibt. Der Prozess der kontinuierlichen Erstellung neuer Verbindungen führt zu einer Erschöpfung der Ports. Jede Clientverbindung erfordert einen eigenen Clientport.
Eine Möglichkeit, eine Portauslastung zu vermeiden, ist die Wiederverwendung derselben HttpClient-Instanz:
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;
}
Die HttpClient-Instanz wird freigegeben, wenn die App beendet wird. Dieses Beispiel zeigt, dass nicht jede Ressource, die verworfen werden kann, auch nach jeder Verwendung verworfen werden sollte.
In den folgenden Artikeln wird eine bessere Möglichkeit beschrieben, um mit der Lebensdauer einer HttpClient-Instanz umzugehen.
Objektpooling
Das obige Beispiel zeigt, wie die HttpClient-Instanz als statisch festgelegt und von allen Anforderungen wiederverwendet werden kann. Die Wiederverwendung verhindert, dass Ressourcen knapp werden.
Objektpooling ist eine Alternative:
- Es verwendet das Wiederverwendungsmuster.
- Das Design eignet sich ideal für Objekte, die teuer zu erstellen sind.
Ein Pool ist eine Sammlung präinitialisierter Objekte, die über Threads hinweg reserviert und freigegeben werden können. Pools können Zuordnungsregeln wie Grenzwerte, vordefinierte Größen oder Wachstumsraten definieren.
Das NuGet-Paket Microsoft.Extensions.ObjectPool enthält Klassen, die bei der Verwaltung solcher Pools helfen.
Der folgende API-Endpunkt initiiert einen byte-Puffer, der mit zufallsbasierten Zahlen für jede Anforderung gefüllt ist:
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array);
return array;
}
Im folgenden Diagramm wird das Aufrufen der vorherigen API mit moderater Last angezeigt:
Das Diagramm zeigt, dass Gen 0 Collections etwa einmal pro Sekunde stattfinden.
Der Code kann optimiert werden, indem der byte Puffer durch die Nutzung der ArrayPool<T-Klasse> gepoolt wird. Eine statische Instanz wird über Anfragen hinweg wiederverwendet.
Die Unterschiede bei diesem Ansatz unterscheiden sich darin, dass ein pooliertes Objekt von der API zurückgegeben wird:
- Das Objekt befindet sich außerhalb Ihrer Kontrolle, sobald Sie aus der Methode zurückkehren.
- Sie können das Objekt nicht freigeben.
So richten Sie die Entsorgung des Objekts ein:
- Kapseln Sie das Poolarray in ein Objekt, das verworfen werden kann.
- Registrieren Sie das gruppierte Objekt mithilfe der HttpContext.Response.RegisterForDispose-Methode .
RegisterForDispose übernimmt das Aufrufen Dispose des Zielobjekts, sodass das Objekt erst nach Abschluss der HTTP-Anforderung freigegeben wird.
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;
}
Das Anwenden der gleichen Last wie die nicht gepoolte Version führt zum folgenden Diagramm:
Der Hauptunterschied liegt in den zugewiesenen Bytes und infolgedessen weniger Gen 0-Sammlungen.