Delen via


Antipatroon Onjuiste instantiëring

Soms worden er voortdurend nieuwe exemplaren van een klasse gemaakt, wanneer het is bedoeld om eenmaal te worden gemaakt en vervolgens gedeeld. Dit gedrag kan de prestaties schaden en wordt een onjuiste instantiërings antipatroon genoemd. Een antipatroon is een veelvoorkomende reactie op een terugkerend probleem dat meestal ineffectief is en zelfs contraproductief kan zijn.

Beschrijving van het probleem

Veel bibliotheken bieden abstracties van externe bronnen. Intern beheren deze klassen meestal hun eigen verbindingen met de resource, waarbij fungeren als brokers die clients kunnen gebruiken voor toegang tot de resource. Hier volgen enkele voorbeelden van broker-klassen die relevant zijn voor Azure-toepassingen:

  • System.Net.Http.HttpClient. Communiceren met een webservice via HTTP.
  • Microsoft.ServiceBus.Messaging.QueueClient. Berichten plaatsen en ophalen uit een Service Bus-wachtrij.
  • Microsoft.Azure.Documents.Client.DocumentClient. Maakt verbinding met een Azure Cosmos DB-exemplaar.
  • StackExchange.Redis.ConnectionMultiplexer. Verbinding maken met Redis, met inbegrip van Azure Cache voor Redis.

Deze klassen zijn bedoeld om één keer te worden gemaakt, waarna ze gedurende de levensduur van een toepassing steeds opnieuw kunnen worden gebruikt. Het is echter een algemene misverstand dat deze klassen alleen moeten worden verkregen wanneer dat nodig is en dan weer snel moeten worden vrijgegeven. (De hier vermelde .NET-bibliotheken zijn toevallig .NET-bibliotheken, maar het patroon is niet uniek voor .NET.) In het volgende ASP.NET voorbeeld wordt een exemplaar gemaakt van HttpClient communicatie met een externe service. U vindt het complete voorbeeld hier.

public class NewHttpClientInstancePerRequestController : ApiController
{
    // This method creates a new instance of HttpClient and disposes it for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        using (var httpClient = new HttpClient())
        {
            var hostName = HttpContext.Current.Request.Url.Host;
            var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
            return new Product { Name = result };
        }
    }
}

In een webtoepassing is deze techniek niet schaalbaar. Er wordt voor elke gebruikersaanvraag een nieuw object HttpClient gemaakt. Onder zware belasting is het mogelijk dat de webserver alle beschikbare sockets verbruikt, wat resulteert in SocketException-fouten.

Dit probleem is niet beperkt tot de klasse HttpClient. Andere klassen die resources inpakken of duur zijn om te maken, kunnen vergelijkbare problemen veroorzaken. In het volgende voorbeeld wordt een instantie van de klasse ExpensiveToCreateService gemaakt. Hier is het probleem niet per se het verbruik van alle sockets, maar gewoon hoe lang het duurt om elke instantie te maken. Het steeds opnieuw maken en vernietigen van instanties van deze klasse kan nadelige gevolgen hebben voor de schaalbaarheid van het systeem.

public class NewServiceInstancePerRequestController : ApiController
{
    public async Task<Product> GetProductAsync(string id)
    {
        var expensiveToCreateService = new ExpensiveToCreateService();
        return await expensiveToCreateService.GetProductByIdAsync(id);
    }
}

public class ExpensiveToCreateService
{
    public ExpensiveToCreateService()
    {
        // Simulate delay due to setup and configuration of ExpensiveToCreateService
        Thread.SpinWait(Int32.MaxValue / 100);
    }
    ...
}

Onjuiste instantiërings antipatroon oplossen

Als de klasse die de externe resource inpakt, kan worden gedeeld en thread-veilig is, maakt u een gedeelde singleton-instantie of een groep herbruikbare instanties van de klasse.

In het volgende voorbeeld wordt een statische HttpClient-instantie gebruikt, om de verbinding zo voor alle aanvragen te delen.

public class SingleHttpClientInstanceController : ApiController
{
    private static readonly HttpClient httpClient;

    static SingleHttpClientInstanceController()
    {
        httpClient = new HttpClient();
    }

    // This method uses the shared instance of HttpClient for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        var hostName = HttpContext.Current.Request.Url.Host;
        var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
        return new Product { Name = result };
    }
}

Overwegingen

  • Het belangrijkste element van dit antipatroon is het herhaaldelijk maken en vernietigen van instanties van een deelbaar object. Als een klasse niet kan worden gedeeld (niet-thread-veilig is), is dit antipatroon niet van toepassing.

  • Het type gedeelde resource kan bepalen of u een singleton moet gebruiken of een groep moet maken. De klasse HttpClient is ontworpen om te worden gedeeld, niet om in een groep te worden opgenomen. Andere objecten ondersteunen mogelijk wel groepen, zodat de werkbelasting kan worden verdeeld over meerdere instanties.

  • Objecten die u deelt met meerdere aanvragen moeten thread-veilig zijn. De klasse HttpClient is ontworpen om op deze manier te worden gebruikt, maar andere klassen bieden mogelijk geen ondersteuning voor gelijktijdige aanvragen. Raadpleeg de beschikbare documentatie voor meer informatie.

  • Ga zorgvuldig te werk bij het instellen van eigenschappen voor gedeelde objecten, aangezien dit kan leiden tot racevoorwaarden. Als u bijvoorbeeld DefaultRequestHeaders instelt voor de klasse HttpClient voorafgaand aan elke aanvraag, kan er een racevoorwaarde ontstaan. Stel dergelijke kenmerken eenmaal in (bijvoorbeeld tijdens het opstarten) en maak afzonderlijke exemplaren als u andere instellingen wilt configureren.

  • Sommige resourcetypen zijn schaars en mogen niet worden vastgehouden. Databaseverbindingen zijn hier een voorbeeld van. Het vasthouden van een geopende databaseverbinding die niet vereist is, kan tot gevolg hebben dat andere gelijktijdige gebruikers geen toegang kunnen krijgen tot de database.

  • In .NET Framework worden veel objecten die verbindingen met externe resources opzetten, gemaakt met behulp van statische factorymethoden van andere klassen die deze verbindingen beheren. Deze objecten zijn bedoeld om te worden opgeslagen en hergebruikt, in plaats van steeds te worden verwijderd en opnieuw gemaakt. In Azure Service Bus wordt het object QueueClient bijvoorbeeld gemaakt via een MessagingFactory-object. MessagingFactory zorgt intern voor het beheer van de verbindingen. Raadpleeg Aanbevolen procedures voor verbeterde prestaties met behulp va Service Bus-berichten voor meer informatie.

Antipatroon voor onjuiste instantiëring detecteren

Symptomen van dit probleem zijn een afname van de doorvoer of een hogere foutenlast, samen met een of meer van de volgende factoren:

  • Een toename van het aantal uitzonderingen die wijzen op verbruik van resources, zoals sockets, databaseverbindingen, bestandsingangen, enzovoort.
  • Toegenomen geheugengebruik en garbagecollection.
  • Een toename in activiteit op het netwerk, schijven of databases.

U kunt de volgende stappen uitvoeren om dit probleem te identificeren:

  1. Monitor de processen van het productiesysteem om vast te stellen wanneer de respons langer duurt of het systeem een storing geeft als gevolg van te weinig resources.
  2. Bekijk de telemetriegegevens die zijn vastgelegd op deze punten om te bepalen welke bewerkingen mogelijk resource-intensieve objecten maken en vernietigen.
  3. Voer voor elke verdachte bewerking een belastingstest uit in een testomgeving, niet in het productiesysteem.
  4. Controleer de broncode en bekijk hoe de brokerobjecten worden beheerd.

Bekijk stack-traces voor bewerkingen die langzaam worden uitgevoerd of die uitzonderingen genereren wanneer het systeem belast wordt. Met deze informatie kunt u bepalen op welke wijze deze bewerkingen gebruikmaken van resources. Uitzonderingen kunnen helpen om te bepalen of fouten het gevolg zijn van het opraken van gedeelde resources.

Voorbeeld van diagnose

In de volgende secties worden deze stappen toegepast op de voorbeeldtoepassing die eerder is beschreven.

Punten identificeren waar vertragingen of fouten optreden

De volgende afbeelding bevat resultaten die zijn gegenereerd met New Relic APM en laat de bewerkingen zien waarvoor een slechte reactietijd is geregistreerd. In dit geval is het zinvol om eens goed te kijken naar de methode GetProductAsync in de controller NewHttpClientInstancePerRequest. U kunt ook zien dat de frequentie van fouten hoger wordt tijdens de uitvoering van deze bewerkingen.

Het dashboard van New Relic met gegevens voor de voorbeeldtoepassing die voor elke aanvraag een nieuwe instantie van een object HttpClient maakt

Telemetriegegevens onderzoeken en correlaties vinden

In de volgende afbeelding ziet u gegevens die zijn vastgelegd met behulp van thread-profilering, voor dezelfde periode als in de vorige afbeelding. Het systeem heeft aanzienlijk wat tijd nodig voor het openen van socketverbindingen en zelfs nog meer tijd om deze weer te sluiten en om socket-uitzonderingen af te handelen.

De thread-profiler van New Relic met gegevens voor de voorbeeldtoepassing wanneer deze voor elke aanvraag een nieuwe instantie van een object HttpClient maakt

Belastingstests uitvoeren

Gebruik belastingstests om de normale bewerkingen te simuleren die gebruikers kunnen uitvoeren. Dit kan helpen om vast te stellen welke onderdelen van een systeem te maken krijgen met resource-uitputting, en dus prestatieverlies, onder verschillende belastingen. Voer deze tests uit in een gecontroleerde omgeving, niet in het productiesysteem. In het volgende diagram ziet u de doorvoer van aanvragen die worden verwerkt door de controller NewHttpClientInstancePerRequest op het moment dat de gebruikersbelasting wordt verhoogd naar 100 gelijktijdige gebruikers.

Doorvoer van de voorbeeldtoepassing wanneer deze voor elke aanvraag een nieuwe instantie van een object HttpClient maakt

Het aantal aanvragen dat per seconde wordt verwerkt, neemt eerst nog evenredig toe met de stijgende werkbelasting. Bij ongeveer 30 gebruikers is er echter een limiet bereikt voor het aantal geslaagde aanvragen en begint het systeem uitzonderingen te genereren. Vanaf dat punt stijgt het aantal uitzonderingen evenredig met de toenemende gebruikersbelasting.

Tijdens de belastingstest zijn deze uitzonderingen gemeld als HTTP 500-fouten (interne serverfouten). Onderzoek van de telemetrie toont aan dat deze fouten zijn veroorzaakt doordat er geen sockets meer beschikbaar zijn voor het systeem, waardoor er steeds meer HttpClient-objecten werden gemaakt.

In het volgende diagram ziet u een vergelijkbare test voor een controller die het aangepaste object ExpensiveToCreateService maakt.

Doorvoer van de voorbeeldtoepassing wanneer deze voor elke aanvraag een nieuwe instantie van het object ExpensiveToCreateService maakt

Deze tijd genereert de controller geen uitzonderingen, maar de doorvoer bereikt nog steeds een plafond terwijl de gemiddelde responstijd met een factor 20 oploopt. (De grafiek maakt gebruik van een logaritmische schaal voor reactietijd en doorvoer.) Telemetrie liet zien dat het maken van nieuwe exemplaren van het ExpensiveToCreateService probleem de belangrijkste oorzaak van het probleem was.

De oplossing implementeren en het resultaat controleren

Na enkele aanpassingen om de methode GetProductAsync toe te wijzen aan één enkele gedeelde instantie van HttpClient, laat een tweede belastingstest zien dat de prestaties zijn verbeterd. Er worden geen fouten gemeld en het systeem kan overweg met een toenemende belasting van maximaal 500 aanvragen per seconde. De gemiddelde responstijd is gehalveerd, vergeleken met de vorige test.

Doorvoer van de voorbeeldtoepassing wanneer deze voor elke aanvraag dezelfde instantie van een object HttpClient hergebruikt

Ter vergelijking ziet u in de volgende afbeelding de telemetriegegevens van de stack-trace. Deze keer wordt de meeste tijd besteed aan het uitvoeren van echte taken, in plaats van het steeds openen en sluiten van sockets.

De thread-profiler van New Relic met gegevens voor de voorbeeldtoepassing wanneer deze voor alle aanvragen één enkele instantie van een object HttpClient maakt

In het volgende diagram ziet u een vergelijkbare belastingstest die wordt uitgevoerd met behulp van een gedeelde instantie van het ExpensiveToCreateService-object. Ook hier stijgt het aantal verwerkte aanvragen evenredig met de gebruikersbelasting, terwijl de gemiddelde responstijd laag blijft.

Grafiek die een vergelijkbare belastingtest weergeeft met een gedeelde instantie van het object ExpensiveToCreateService.