Share via


Antimönstret synkrona I/O

Blockering av anropstråden medan I/O slutförs kan minska prestandan och påverka den lodräta skalbarheten.

Problembeskrivning

En åtgärd för synkrona I/O blockerar anropstråden medan I/O slutförs. Anropstråden försätts i vänteläge och kan inte utföra användbart arbete under det här intervallet, och bearbetningsresurser slösas.

Vanliga exempel på I/O är:

  • Hämta eller spara data i en databas eller någon annan typ av beständig lagring.
  • Skicka en begäran till en webbtjänst.
  • Publicera ett meddelande eller hämta ett meddelande från en kö.
  • Skriva till eller läsa från en lokal fil.

Det här antimönstret inträffar normalt eftersom:

  • Det verkar vara det mest intuitiva sättet att utföra en åtgärd.
  • Programmet kräver ett svar från en begäran.
  • Programmet använder ett bibliotek som bara har synkrona metoder för I/O.
  • Ett externt bibliotek utför åtgärder för synkrona I/O internt. Ett enda synkron I/O-anrop kan blockera en hel anropskedja.

Följande kod laddar upp en fil till Azure Blob Storage. Det finns två platser där koden blockerar att vänta på synkrona I/O, metoden CreateIfNotExists och metoden UploadFromStream.

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);
}

Här är ett exempel på att vänta på ett svar från en extern tjänst. Metoden GetUserProfile anropar en fjärrtjänst som returnerar en UserProfile.

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();
    }
}

Du hittar den fullständiga koden för båda exemplen här.

Åtgärda problemet

Ersätt synkrona I/O-åtgärder med asynkrona åtgärder. Det frigör den aktuella tråden att fortsätta utföra meningsfullt arbete istället för blockering och hjälper till att förbättra användningen av beräkningsresurser. Att utföra I/O asynkront är särskilt effektivt för att hantera en oväntad ökning av begäranden från klientprogram.

Många bibliotek har både synkrona och asynkrona versioner av metoderna. Använd de asynkrona versionerna när det är möjligt. Här är den asynkrona versionen av det tidigare exemplet som laddar upp en fil till 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);
}

Operatorn await returnerar kontrollen till anropsmiljön medan den asynkrona åtgärden utförs. Koden efter den här instruktionen fungerar som en fortsättning som körs när den asynkrona åtgärden har slutförts.

En väl utformad tjänst bör även ha asynkrona åtgärder. Här är en asynkron version av webbtjänsten som returnerar användarprofiler. Metoden GetUserProfileAsync är beroende av att ha en asynkron version av användarprofiltjänsten.

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();
    }
}

För bibliotek som inte har asynkrona versioner av åtgärder kan det vara möjligt att skapa asynkrona omslutningar runt utvalda synkrona metoder. Var försiktig när du använder det här tillvägagångssättet. Svarstiden kan förbättras för tråden som anropar den asynkrona omslutningen men den förbrukar faktiskt mer resurser. En extra tråd kan skapas och det finns overhead kopplat till att synkronisera arbetet som utförs av den här tråden. Vissa kompromisser diskuteras i det här blogginlägget: Should I expose asynchronous wrappers for synchronous methods? (Ska jag exponera asynkrona omslutningar för synkrona metoder?)

Här är ett exempel på en asynkron omslutning runt en synkron metod.

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

Nu kan anropskoden invänta omslutningen:

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

Att tänka på

  • I/O-åtgärderna som förväntas vara kortlivade och sannolikt inte orsakar konkurrens kan fungera bättre som synkrona åtgärder. Ett exempel kan vara att läsa små filer på en SSD-enhet. Arbetet med att skicka en uppgift till en annan tråd, och synkronisera med den tråden när uppgiften slutförs, kan uppväga fördelarna med asynkrona I/O. Men de här fallen är relativt sällsynta och de flesta I/O-åtgärderna bör utföras asynkront.

  • Att förbättra I/O-prestandan kan orsaka att andra delar av systemet kan bli flaskhalsar. Till exempel kan avblockering av trådar resultera i ett ökat antal samtidiga begäranden till delade resurser, vilket i sin tur leder till resurserna tar slut eller begränsas. Om det blir ett problem kan du behöva skala ut antalet webbservrar eller partitionera datalager för att minska konkurrensen.

Identifiera problemet

För användarna kan det verka som om programmet inte svarar då och då. Programmet kan misslyckas med tidsgränsundantag. De här felen kan även returnera HTTP 500-fel (intern server). På servern kan inkommande klientbegäranden blockeras tills en tråd blir tillgänglig, vilket resulterar i överdrivna kölängder för begäranden som visas som HTTP 503-fel (tjänsten är inte tillgänglig).

Du kan göra följande för att identifiera problemet:

  1. Övervaka produktionssystemet och fastställa om de blockerade trådarna hindrar dataflödet.

  2. Om begäranden blockeras på grund av bristen på trådar granskar du programmet för att fastställa vilka åtgärder som kan utföra I/O synkront.

  3. Utför kontrollerad belastningstestning av varje åtgärd som utför synkrona I/O, för att ta reda på om de åtgärderna påverkar systemprestanda.

Exempeldiagnos

I följande avsnitt används stegen på exempelprogrammet som beskrivs ovan.

Övervaka webbserverprestanda

För Azure-webbprogram och -webbroller är det värt att övervaka IIS-webbserverns prestanda. Var särskilt uppmärksam på kölängden för begäranden för att fastställa om begäranden blockeras i väntan på tillgängliga trådar under perioder med hög aktivitet. Du kan samla in den här informationen genom att aktivera Azure Diagnostics. Mer information finns i:

Instrumentera programmet för att se hur begäranden hanteras när de har godkänts. Att spåra flödet för en begäran kan hjälpa till att identifiera om den utför långsamma anrop och blockerar den aktuella tråden. Trådprofilering kan också lyfta fram begäranden som blockeras.

Belastningstesta programmet

I följande diagram visas prestandan för den synkrona metoden GetUserProfile som visas ovan, under varierande belastning på upp till 4 000 samtidiga användare. Programmet är ett ASP.NET-program som körs i en Azure Cloud Service-webbroll.

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

Den synkrona åtgärden är hårdkodad att vila i 2 sekunder, simulera synkrona I/O, så att den minsta svarstiden är något över 2 sekunder. När belastningen når cirka 2 500 samtidiga användare når den genomsnittliga svarstiden en platå, trots att mängden begäranden per sekund fortsätter att öka. Observera att skalan för de här två måtten är logaritmisk. Antalet begäranden per sekund fördubblas mellan den här punkten och slutet av testet.

Isolerat är det inte nödvändigtvis tydligt från det här testet om de synkrona I/O är ett problem. Under tyngre belastning kan programmet nå en brytpunkt när webbservern inte längre kan bearbeta begäranden i tid, vilket leder till att klientprogrammen får tidsgränsundantag.

Inkommande begäranden placeras i kö av webbservern och ges till en tråd som körs i ASP.NET-trådpoolen. Eftersom varje åtgärd utför I/O synkront blockeras tråden tills åtgärden slutförs. När arbetsbelastningen ökar allokeras och blockeras till sist alla ASP.NET-trådar i trådpoolen. Då måste ytterligare inkommande begäranden vänta i kön på en tillgänglig tråd. När kön växer börjar begäranden att uppnå tidsgränsen.

Implementera lösningen och verifiera resultatet

I nästa diagram visas resultatet från belastningstestet av kodens asynkrona version.

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

Dataflödet är mycket högre. Under samma tidslängd som det föregående testet hanterar systemet en nästan tiofaldig ökning i dataflödet, mätt i begäranden per sekund. Dessutom är den genomsnittliga svarstiden relativt konstant och förblir ungefär 25 gånger mindre än det föregående testet.