Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Door Sébastien Ros en Rick Anderson
Geheugenbeheer is complex, zelfs in een beheerd framework zoals .NET. Het analyseren en begrijpen van geheugenproblemen kan lastig zijn. Dit artikel:
- Was gemotiveerd door veel geheugenlekken en problemen met de GC. De meeste van deze problemen zijn veroorzaakt doordat u niet begrijpt hoe geheugenverbruik werkt in .NET Core of niet begrijpt hoe het wordt gemeten.
- Demonstreert problematisch geheugengebruik en stelt alternatieve benaderingen voor.
Hoe Garbage Collection (GC) werkt in .NET Core
De GC wijst heap-segmenten toe waarbij elk segment een aaneengesloten geheugenbereik is. Objecten die in de heap worden geplaatst, worden onderverdeeld in een van 3 generaties: 0, 1 of 2. De generatie bepaalt de frequentie waarmee de GC probeert geheugen vrij te geven voor beheerde objecten waarnaar niet meer wordt verwezen door de app. Lagere genummerde generaties zijn vaker GC's.
Objecten worden verplaatst van de ene generatie naar de andere op basis van hun levensduur. Naarmate objecten langer leven, worden ze verplaatst naar een hogere generatie. Zoals eerder vermeld, zijn hogere generaties minder vaak GC's. Objecten met korte levensduur blijven altijd in generatie 0. Objecten waarnaar wordt verwezen tijdens de levensduur van een webaanvraag, hebben bijvoorbeeld een korte levensduur. Singletons op toepassingsniveau worden over het algemeen gemigreerd naar generatie 2.
Wanneer een ASP.NET Core-app wordt gestart, wordt de GC:
- Reserveert geheugen voor de eerste heap-segmenten.
- Voert een klein deel van het geheugen door wanneer de runtime wordt geladen.
De voorgaande geheugentoewijzingen worden uitgevoerd voor prestatieverbeteringen. Het prestatievoordeel is afkomstig van heap-segmenten in aaneengesloten geheugen.
GC. Kanttekeningen verzamelen
Over het algemeen zouden ASP.NET Core-apps in productie geen expliciet gebruik moeten maken van GC.Collect. Het induceren van garbage collections op suboptimale momenten kan de prestaties aanzienlijk verminderen.
GC.Collect is handig bij het onderzoeken van geheugenlekken. Het aanroepen van GC.Collect()
activeert een blokkerende garbage-collectioncyclus die probeert alle objecten die ontoegankelijk zijn vanuit beheerde code terug te winnen. Het is een handige manier om inzicht te hebben in de grootte van de bereikbare live-objecten in de heap en de groei van de geheugengrootte in de loop van de tijd bij te houden.
Het geheugengebruik van een app analyseren
Toegewezen hulpprogramma's kunnen helpen bij het analyseren van geheugengebruik:
- Objectverwijzingen tellen
- Meten hoeveel impact de GC heeft op HET CPU-gebruik
- Geheugenruimte meten die voor elke generatie wordt gebruikt
Gebruik de volgende hulpprogramma's om geheugengebruik te analyseren:
- dotnet-trace: kan worden gebruikt op productiemachines.
- Geheugengebruik analyseren zonder het Visual Studio-foutopsporingsprogramma
- Gebruik van profielgeheugen in Visual Studio
Geheugenproblemen detecteren
Taakbeheer kan worden gebruikt om een idee te krijgen van hoeveel geheugen een ASP.NET app gebruikt. De geheugenwaarde van Taakbeheer:
- Vertegenwoordigt de hoeveelheid geheugen die wordt gebruikt door het ASP.NET proces.
- Bevat de levende objecten van de app en andere geheugengebruikers, zoals systeemeigen geheugengebruik.
Als de geheugenwaarde van Task Manager voor onbepaalde tijd toeneemt en nooit platloopt, heeft de app een geheugenlek. In de volgende secties worden verschillende patronen voor geheugengebruik gedemonstreert en uitgelegd.
Voorbeeld van een app voor weergave van geheugengebruik
De MemoryLeak-voorbeeld-app is beschikbaar op GitHub. De MemoryLeak-app:
- Bevat een diagnostische controller die realtime geheugen en GC-gegevens voor de app verzamelt.
- Bevat een indexpagina waarop het geheugen en de GC-gegevens worden weergegeven. De pagina Index wordt elke seconde vernieuwd.
- Bevat een API-controller die verschillende patronen voor geheugenbelasting biedt.
- Is echter geen ondersteund hulpprogramma, maar kan worden gebruikt om geheugengebruikspatronen van ASP.NET Core-apps weer te geven.
Voer MemoryLeak uit. Toegewezen geheugen neemt langzaam toe totdat een GC plaatsvindt. Geheugen neemt toe omdat het hulpprogramma aangepast object toewijst om gegevens vast te leggen. In de volgende afbeelding ziet u de pagina MemoryLeak Index wanneer een Gen 0 GC plaatsvindt. In de grafiek ziet u 0 RPS (Aanvragen per seconde), omdat er geen API-eindpunten van de API-controller zijn aangeroepen.
In de grafiek worden twee waarden weergegeven voor het geheugengebruik:
- Toegewezen: de hoeveelheid geheugen die wordt bezet door beheerde objecten
- Werkset: De set pagina's in de virtuele adresruimte van het proces dat zich momenteel in het fysieke geheugen bevindt. De werkset die wordt weergegeven is dezelfde waarde als die door Taakbeheer wordt weergegeven.
Tijdelijke objecten
De volgende API maakt een tekenreeksinstantie van 20 kB en retourneert deze aan de client. Bij elke aanvraag wordt een nieuw object toegewezen in het geheugen en naar het antwoord geschreven. Tekenreeksen worden opgeslagen als UTF-16 tekens in .NET, zodat elk teken 2 bytes in het geheugen nodig heeft.
[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
return new String('x', 10 * 1024);
}
De volgende grafiek wordt gegenereerd met een relatief kleine belasting om te laten zien hoe geheugentoewijzingen worden beïnvloed door de GC.
In de voorgaande grafiek ziet u:
- 4K RPS (Aanvragen per seconde).
- Generatie 0 GC-verzamelingen vinden ongeveer elke twee seconden plaats.
- De werkset is constant op ongeveer 500 MB.
- De processor is 12%.
- Het geheugenverbruik en de release (via GC) is stabiel.
De volgende grafiek wordt genomen bij de maximale doorvoer die door de machine kan worden verwerkt.
In de voorgaande grafiek ziet u:
- 22 duizend verzoeken per seconde
- Generatie 0 GC-verzamelingen vinden meerdere keren per seconde plaats.
- Verzamelingen van de 1e generatie worden geactiveerd omdat de app aanzienlijk meer geheugen per seconde heeft toegewezen.
- De werkset is constant op ongeveer 500 MB.
- CPU is 33%.
- Het geheugenverbruik en de release (via GC) is stabiel.
- De CPU (33%) is niet overbelast, daarom kan de garbage collection een groot aantal toewijzingen bijhouden.
Werkstation GC versus Server GC
De .NET Garbage Collector heeft twee verschillende modi:
- Werkstation GC: geoptimaliseerd voor het bureaublad.
- Server GC. De standaard-GC voor ASP.NET Core-apps. Geoptimaliseerd voor de server.
De GC-modus kan expliciet worden ingesteld in het projectbestand of in het runtimeconfig.json
bestand van de gepubliceerde app. In de volgende markeringen ziet u de instelling ServerGarbageCollection
in het projectbestand:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
Het wijzigen van ServerGarbageCollection
in het projectbestand vereist dat de app opnieuw wordt opgebouwd.
Notitie: De garbagecollection van de server is niet beschikbaar op computers met één kern. Zie IsServerGC voor meer informatie.
In de volgende afbeelding ziet u het geheugenprofiel onder een 5K RPS met behulp van de Workstation GC.
De verschillen tussen deze grafiek en de serverversie zijn aanzienlijk:
- De werkset daalt van 500 MB tot 70 MB.
- De GC voert generatie 0-verzamelingen meerdere keren per seconde uit in plaats van elke twee seconden.
- GC daalt van 300 MB tot 10 MB.
In een typische webserveromgeving is het CPU-gebruik belangrijker dan geheugen, dus de Server GC is beter. Als het geheugengebruik hoog is en het CPU-gebruik relatief laag is, kan de GC van het werkstation beter presteren. High-density host bijvoorbeeld verschillende web-apps waarbij geheugen schaars is.
GC met Docker en kleine containers
Wanneer meerdere apps in een container worden uitgevoerd op één computer, kan Workstation GC beter presteren dan Server GC. Zie Uitvoeren met Server GC in een kleine container en Uitvoeren met Server GC in een kleine container scenario deel 1 – Vaste limiet voor de GC-heap voor meer informatie.
Permanente objectverwijzingen
De GC kan geen objecten vrij maken waarnaar wordt verwezen. Objecten waarnaar wordt verwezen, maar die niet meer nodig zijn, leiden tot een geheugenlek. Als de app vaak objecten toewijst en ze niet meer vrijgeeft nadat ze niet meer nodig zijn, neemt het geheugengebruik na verloop van tijd toe.
De volgende API maakt een tekenreeksinstantie van 20 kB en retourneert deze aan de client. Het verschil met het vorige voorbeeld is dat naar dit exemplaar wordt verwezen door een statisch lid, wat betekent dat het nooit beschikbaar is voor verzameling.
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;
}
De voorgaande code:
- Is een voorbeeld van een typisch geheugenlek.
- Bij frequente aanroepen neemt het geheugen van de app toe totdat het proces vastloopt met een
OutOfMemory
exception.
In de voorgaande afbeelding:
- Belastingtesten van het
/api/staticstring
eindpunt zorgen voor een lineaire toename van het geheugen. - De GC probeert geheugen vrij te maken naarmate de geheugendruk toeneemt, door een verzameling van de tweede generatie aan te roepen.
- De GC kan het gelekte geheugen niet vrij maken. Toegewezen en werkset nemen toe in de loop van de tijd.
Voor sommige scenario's, zoals caching, moeten objectverwijzingen worden bewaard totdat geheugendruk ervoor zorgt dat ze worden vrijgegeven. De WeakReference klasse kan worden gebruikt voor dit type cachecode. Een WeakReference
object wordt verzameld onder geheugendruk. De standaard implementatie van IMemoryCache gebruik WeakReference
.
Systeemeigen geheugen
Sommige .NET Core-objecten zijn afhankelijk van systeemeigen geheugen. Systeemeigen geheugen kan niet worden verzameld door de GC. Het .NET-object dat systeemeigen geheugen gebruikt, moet het vrij maken met systeemeigen code.
.NET biedt de IDisposable interface waarmee ontwikkelaars systeemeigen geheugen kunnen vrijgeven. Zelfs als Dispose niet wordt aangeroepen, zullen klassen die correct geïmplementeerd zijn Dispose
aanroepen wanneer de finalizer wordt uitgevoerd.
Houd rekening met de volgende code:
[HttpGet("fileprovider")]
public void GetFileProvider()
{
var fp = new PhysicalFileProvider(TempPath);
fp.Watch("*.*");
}
PhysicalFileProvider is een beheerde klasse, dus elk exemplaar wordt verzameld aan het einde van de aanvraag.
In de volgende afbeelding ziet u het geheugenprofiel terwijl u de fileprovider
API continu aanroept.
In de voorgaande grafiek ziet u een duidelijk probleem met de implementatie van deze klasse, omdat het geheugengebruik blijft toenemen. Dit is een bekend probleem dat in dit issue wordt bijgehouden.
Hetzelfde lek kan zich voordoen in de gebruikerscode, op een van de volgende manieren:
- De klasse wordt niet correct vrijgegeven.
- Vergeet de
Dispose
methode aan te roepen van de afhankelijke objecten die moeten worden verwijderd.
Grote object stapel-geheugen
Frequente geheugentoewijzing/vrije cycli kunnen geheugen fragmenteren, met name bij het toewijzen van grote segmenten geheugen. Objecten worden toegewezen in aaneengesloten blokken van geheugen. Om fragmentatie te beperken, probeert de GC het geheugen te defragmenteren wanneer de GC geheugen vrijgeeft. Dit proces wordt compressie genoemd. Compressie omvat het verplaatsen van objecten. Bij het verplaatsen van grote objecten wordt een prestatiestraf opgelegd. Daarom maakt de GC een speciale geheugenzone voor grote objecten, de zogenaamde grote object heap (LOH). Objecten die groter zijn dan 85.000 bytes (ongeveer 83 kB) zijn:
- Geplaatst op de LOH.
- Niet gecomprimeerd.
- Verzameld tijdens generatie 2 GCs.
Wanneer de LOH vol is, activeert de GC een verzameling van de tweede generatie. Verzamelingen van de tweede generatie:
- Zijn inherent traag.
- Daarnaast worden er kosten in rekening gebracht voor het activeren van een verzameling voor alle andere generaties.
Met de volgende code wordt de LOH onmiddellijk gecomprimeerd:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Zie LargeObjectHeapCompactionMode voor meer informatie over het comprimeren van de LOH.
In containers met .NET Core 3.0 of hoger wordt de LOH automatisch gecomprimeerd.
De volgende API die dit gedrag illustreert:
[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
return new byte[size].Length;
}
In de volgende grafiek ziet u het geheugenprofiel voor het aanroepen van het /api/loh/84975
eindpunt, onder maximale belasting:
In de volgende grafiek ziet u het geheugenprofiel voor het aanroepen van het /api/loh/84976
eindpunt, waarbij slechts één byte wordt toegewezen:
Let op: de byte[]
-structuur heeft overhead-bytes. Daarom activeert 84.976 bytes de limiet van 85.000.
De twee voorgaande grafieken vergelijken:
- De werkset is vergelijkbaar voor beide scenario's, ongeveer 450 MB.
- De onderstaande LOH-aanvragen (84.975 bytes) tonen voornamelijk generatie 0-verzamelingen.
- De over LOH-aanvragen genereren constante verzamelingen van generatie 2. Verzamelingen van de tweede generatie zijn duur. Er is meer CPU vereist en de doorvoer daalt bijna 50%.
Tijdelijke grote objecten zijn bijzonder problematisch omdat ze gen2 GCs veroorzaken.
Voor maximale prestaties moet het gebruik van grote objecten worden geminimaliseerd. Splits indien mogelijk grote objecten op. Antwoordcaching-middleware in ASP.NET Core splitst de cachevermeldingen bijvoorbeeld in blokken van minder dan 85.000 bytes.
In de volgende koppelingen ziet u de ASP.NET Kernbenadering om objecten onder de limiet van LOH te houden:
Voor meer informatie, zie:
HttpClient
Onjuist gebruik HttpClient kan leiden tot een resourcelek. Systeembronnen, zoals databaseverbindingen, sockets, bestandsingangen, enzovoort:
- Zijn schaarser dan geheugen.
- Zijn problematischer wanneer ze uitlekken dan geheugen.
Ervaren .NET-ontwikkelaars weten om Dispose aan te roepen op objecten die IDisposable implementeren. Het niet verwijderen van objecten die worden geïmplementeerd door IDisposable
, resulteert doorgaans in lekkend geheugen of lekkende systeembronnen.
HttpClient
implementeert IDisposable
, maar mag niet worden verwijderd bij elke aanroep. In plaats daarvan moet HttpClient
opnieuw worden gebruikt.
Het volgende eindpunt maakt en verwijdert een nieuw HttpClient
exemplaar voor elke aanvraag:
[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
using (var httpClient = new HttpClient())
{
var result = await httpClient.GetAsync(url);
return (int)result.StatusCode;
}
}
Onder laden worden de volgende foutberichten vastgelegd:
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)
Hoewel de HttpClient
exemplaren worden verwijderd, duurt het enige tijd voordat de werkelijke netwerkverbinding door het besturingssysteem wordt vrijgegeven. Door continu nieuwe verbindingen te maken, treden poortenuitputting op. Elke clientverbinding vereist een eigen clientpoort.
Een manier om poortuitputting te voorkomen, is door hetzelfde HttpClient
exemplaar opnieuw te gebruiken.
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;
}
Het HttpClient
exemplaar wordt vrijgegeven wanneer de app stopt. In dit voorbeeld ziet u dat niet elke wegwerpresource na elk gebruik moet worden verwijderd.
Zie het volgende voor een betere manier om de levensduur van een HttpClient
exemplaar te beheren:
Objectgroepering
In het vorige voorbeeld is getoond hoe het HttpClient
exemplaar statisch kan worden gemaakt en opnieuw kan worden gebruikt door alle aanvragen. Hergebruik voorkomt dat de bronnen opraken.
Objectpooling:
- Maakt gebruik van het hergebruikspatroon.
- Is ontworpen voor objecten die duur zijn om te maken.
Een pool is een verzameling vooraf geïnitialiseerde objecten die kunnen worden gereserveerd en vrijgegeven in threads. Pools kunnen toewijzingsregels definiëren, zoals limieten, vooraf gedefinieerde grootten of groeisnelheid.
Het NuGet-pakket Microsoft.Extensions.ObjectPool bevat klassen die helpen bij het beheren van dergelijke pools.
Met het volgende API-eindpunt wordt een byte
buffer geïnstitueerde die wordt gevuld met willekeurige getallen voor elke aanvraag:
[HttpGet("array/{size}")]
public byte[] GetArray(int size)
{
var random = new Random();
var array = new byte[size];
random.NextBytes(array);
return array;
}
In de volgende grafiek ziet u hoe u de voorgaande API aanroept met gemiddelde belasting:
In de voorgaande grafiek worden verzamelingen van generatie 0 ongeveer één keer per seconde uitgevoerd.
De voorgaande code kan worden geoptimaliseerd door de byte
buffer te groeperen met behulp van ArrayPool<T>. Een statisch exemplaar wordt herhaaldelijk gebruikt voor elke aanvraag.
Wat er anders is met deze benadering is dat een gegroepeerd object wordt geretourneerd vanuit de API. Dat betekent:
- Het object valt buiten uw beheer zodra u terugkeert vanuit de methode.
- U kunt het object niet vrijgeven.
Om de afvoer van het object in te stellen:
- De gegroepeerde matrix inkapselen in een wegwerpobject.
- Registreer het gegroepeerde object bij HttpContext.Response.RegisterForDispose.
RegisterForDispose
zorgt ervoor dat Dispose
op het doelobject wordt aangeroepen, zodat dit alleen wordt vrijgegeven wanneer de HTTP-aanvraag is voltooid.
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;
}
Het toepassen van dezelfde belasting als de niet-poolversie resulteert in de volgende grafiek:
Het belangrijkste verschil is toegewezen bytes en als gevolg hiervan zijn er veel minder verzamelingen van generatie 0.