Share via


Synchroon I/O-antipatroon

Als u de aanroepende thread blokkeert terwijl I/O wordt voltooid, verminderen mogelijk de prestaties en wordt de verticale schaalbaarheid mogelijk beïnvloed.

Beschrijving van het probleem

Een synchrone I/O-bewerking blokkeert de aanroepende thread terwijl de I/O wordt voltooid. De aanroepende thread krijgt een wachtstatus en kan geen nuttig werk uitvoeren gedurende deze periode, waardoor verwerkingsbronnen worden verspild.

Algemene voorbeelden van I/O-bewerkingen zijn:

  • Gegevens ophalen uit of persistent maken in een database of ander type permanente opslag.
  • Een aanvraag verzenden naar een webservice.
  • Een bericht plaatsen of ophalen uit een wachtrij.
  • Schrijven naar of lezen van een lokaal bestand.

Enkele veelvoorkomende oorzaken voor dit antipatroon:

  • Het lijkt de meest intuïtieve manier om een bewerking uit te voeren.
  • De toepassing vereist een reactie van een aanvraag.
  • De toepassing gebruikt een bibliotheek met alleen synchrone methoden voor I/O.
  • Een externe bibliotheek voert intern synchrone I/O-bewerkingen uit. Één synchrone I/O-aanroep kan een aanroepketen blokkeren.

Met de volgende code wordt een bestand geüpload naar Azure Blob Storage. Er zijn twee manieren waarop de codeblokken wachten op synchrone I/O-bewerkingen, de CreateIfNotExists-methode en de UploadFromStream-methode.

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

container.CreateIfNotExists();
var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    blockBlob.UploadFromStream(fileStream);
}

Hier volgt een voorbeeld waarin er wordt gewacht op een reactie van een externe service. De GetUserProfile-methode roept een externe service aan, die een UserProfile retourneert.

public interface IUserProfileService
{
    UserProfile GetUserProfile();
}

public class SyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public SyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is a synchronous method that calls the synchronous GetUserProfile method.
    public UserProfile GetUserProfile()
    {
        return _userProfileService.GetUserProfile();
    }
}

U vindt de volledige code voor deze voorbeelden hier.

Het probleem oplossen

Vervang synchrone I/O-bewerkingen door asynchrone bewerkingen. Hierdoor kan de huidige thread doorgaan met zinvol werk en raakt niet geblokkeerd, en helpt bij het verbeteren van het gebruik van rekenbronnen. Het asynchroon uitvoeren van I/O-bewerkingen is bijzonder efficiënt voor het verwerken van een onverwachte piek in aanvragen vanuit clienttoepassingen.

Veel bibliotheken bieden synchrone en asynchrone versies van methoden. Gebruik indien mogelijk de asynchrone versies. Hier volgt de asynchrone versie van het vorige voorbeeld, waarbij een bestand wordt geüpload naar Azure Blob Storage.

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

await container.CreateIfNotExistsAsync();

var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    await blockBlob.UploadFromStreamAsync(fileStream);
}

De await-operator retourneert het beheer aan de aanroepende omgeving terwijl de asynchrone bewerking wordt uitgevoerd. De code na deze instructie fungeert als een voortzetting die wordt uitgevoerd als de asynchrone bewerking is voltooid.

Een goed ontworpen service moet ook asynchrone bewerkingen bieden. Hier volgt een asynchrone versie van de webservice die gebruikersprofielen retourneert. De GetUserProfileAsync-methode vereist een asynchrone versie van de gebruikersprofielservice.

public interface IUserProfileService
{
    Task<UserProfile> GetUserProfileAsync();
}

public class AsyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public AsyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is a synchronous method that calls the Task based GetUserProfileAsync method.
    public Task<UserProfile> GetUserProfileAsync()
    {
        return _userProfileService.GetUserProfileAsync();
    }
}

Voor bibliotheken die geen asynchrone versies van bewerkingen bieden, is het mogelijk om asynchrone wrappers te maken rond geselecteerde synchrone methoden. Wees voorzichtig wanneer u deze aanpak gebruik. De reactietijd op de thread die de asynchrone wrapper aanroept, kan worden verbeterd, maar er worden in werkelijkheid meer bronnen verbruikt. Er kan een extra thread worden gemaakt en er komt overhead kijken bij het synchroniseren van het werk dat door deze thread wordt uitgevoerd. In dit blogbericht worden een aantal voordelen beschreven: Should I expose asynchronous wrappers for synchronous methods? (Moet ik asynchrone wrappers gebruiken voor synchrone methoden?)

Hier volgt een voorbeeld van een asynchrone wrapper rond een synchrone methode.

// Asynchronous wrapper around synchronous library method
private async Task<int> LibraryIOOperationAsync()
{
    return await Task.Run(() => LibraryIOOperation());
}

De aanroepende code kan nu wachten op de wrapper:

// Invoke the asynchronous wrapper using a task
await LibraryIOOperationAsync();

Overwegingen

  • I/O-bewerkingen die naar verwachting een zeer korte levensduur hebben en waarschijnlijk niet leiden tot conflicten, werken mogelijk beter als synchrone bewerkingen. Een voorbeeld is het lezen van kleine bestanden op een SSD-station. De overhead voor het verzenden van een taak voor een andere thread en het synchroniseren met die thread wanneer de taak is voltooid, kan meer voordelen bieden dan asynchrone I/O-bewerkingen. Deze gevallen zijn echter relatief zeldzaam, en de meeste I/O-bewerkingen moeten asynchroon worden gedaan.

  • Het verbeteren van de I/O-prestaties, kan ertoe leiden dat andere onderdelen van het systeem knelpunten worden. Het deblokkeren van threads kan bijvoorbeeld leiden tot een hoger aantal gelijktijdige aanvragen bij gedeelde bronnen, wat weer kan leiden tot overbelaste bronnen of aanvraagbeperkingen. Als dit een probleem wordt, moet u mogelijk het aantal webservers uitschalen of gegevensarchieven partitioneren om het aantal conflicten te verminderen.

Het probleem vaststellen

Voor gebruikers lijkt het alsof de toepassing periodiek niet responsief is. De toepassing kan mislukken met time-outuitzonderingen. Deze fouten kunnen ook HTTP 500-fouten (interne serverfouten) retourneren. Op de server worden binnenkomende aanvragen van clients mogelijk geblokkeerd totdat een thread beschikbaar is. Dit resulteert in overmatig lange aanvraagwachtrijen, die de vorm aannemen van HTTP 503-fouten (service niet beschikbaar).

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

  1. Controleer het productiesysteem en bepaal of geblokkeerde werkthreads de doorvoer beperken.

  2. Als aanvragen worden geblokkeerd vanwege een gebrek aan threads, controleert u de toepassing om te bepalen welke bewerkingen mogelijk I/O synchroon uitvoeren.

  3. Voer gecontroleerde belastingstests uit voor elke bewerking die synchrone I/O uitvoert, om erachter te komen of deze bewerkingen van invloed zijn op de systeemprestaties.

Voorbeeld van diagnose

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

Prestaties van de webserver bewaken

Voor Azure-webtoepassingen en webrollen is het de moeite waard om de prestaties van de IIS-webserver te bewaken. Let met name op de lengte van de aanvraagwachtrij om te achterhalen of aanvragen tijdens perioden met veel activiteit worden geblokkeerd omdat ze wachten op beschikbare threads. U kunt deze informatie verzamelen door Azure Diagnostics in te schakelen. Zie voor meer informatie:

Instrumenteer de toepassing om te zien hoe aanvragen worden verwerkt wanneer ze zijn geaccepteerd. Het traceren van de stroom van een aanvraag kan helpen bij het bepalen of deze aanroepen langzaam uitvoert en de huidige thread blokkeert. Bij het profileren van de thread kan ook duidelijk worden welke aanvragen worden geblokkeerd.

De belasting van de toepassing testen

In de volgende grafiek ziet u de prestaties van de synchrone GetUserProfile-methode die we eerder hebben besproken, bij verschillende belasting van maximaal 4.000 gelijktijdige gebruikers. De toepassing is een ASP.NET-toepassing die wordt uitgevoerd in een Azure Cloud Service-webrol.

Performance chart for the sample application performing synchronous I/O operations

In de code is vastgelegd dat de synchrone bewerking 2 seconden in de slaapstand gaat om synchrone I/O-bewerkingen te simuleren. De minimale reactietijd is daardoor iets meer dan 2 seconden. Als de belasting ongeveer 2500 gelijktijdige gebruikers bereikt, bereikt de gemiddelde reactietijd een limiet, terwijl aantal aanvragen per seconde blijft toenemen. Houd er rekening mee dat de schaal voor deze twee metingen logaritmisch is. Het aantal aanvragen per seconde verdubbelt tussen dit punt en het einde van de test.

Deze test alleen wijst niet per se uit of de synchrone I/O-bewerkingen een probleem vormen. De toepassing kan bij hogere belasting een kantelpunt bereiken, waarop de webserver aanvragen niet langer tijdig kan verwerken, waardoor clienttoepassingen time-outuitzonderingen ontvangen.

Inkomende aanvragen worden in de wachtrij geplaatst door de IIS-webserver en doorgestuurd naar een thread die wordt uitgevoerd in de ASP.NET-threadgroep. Omdat elke bewerking I/O synchroon uitvoert, wordt de thread geblokkeerd tot de bewerking is voltooid. Naarmate de werkbelasting toeneemt, worden uiteindelijk alle ASP.NET-threads in de threadgroep toegewezen en geblokkeerd. Op dat moment moeten verdere inkomende aanvragen wachten in de wachtrij op een beschikbare thread. Naarmate de lengte van de wachtrij toeneemt, verlopen steeds meer verzoeken.

De oplossing implementeren en het resultaat controleren

Het volgende diagram toont de resultaten van de belastingstest van de asynchrone versie van de code.

Performance chart for the sample application performing asynchronous I/O operations

De doorvoer is veel hoger. Binnen de duur van de vorige test verwerkt het systeem bijna tien keer zoveel doorvoer, gemeten in aanvragen per seconde. Bovendien is de gemiddelde reactietijd relatief constant en blijft ongeveer 25 keer korter dan in de vorige test.