Redigera

Dela via


Asynkront mönster för begäran-svar

Azure
Azure Logic Apps

Frikoppla serverdelsbearbetning från en klientdelsvärd, där serverdelsbearbetning måste vara asynkron, men klientdelen fortfarande behöver ett klart svar.

Kontext och problem

I modern programutveckling är det normalt att klientprogram – ofta kod som körs i en webbklient (webbläsare) – är beroende av fjärranslutna API:er för att tillhandahålla affärslogik och skapa funktioner. Dessa API:er kan vara direkt relaterade till programmet eller vara delade tjänster som tillhandahålls av en tredje part. Dessa API-anrop sker vanligtvis via HTTP-protokollet (S) och följer REST-semantik.

I de flesta fall är API:er för ett klientprogram utformade för att svara snabbt, i storleksordningen 100 ms eller mindre. Många faktorer kan påverka svarsfördröjningen, inklusive:

  • Ett programs värdstack.
  • Säkerhetskomponenter.
  • Den relativa geografiska platsen för anroparen och serverdelen.
  • Nätverksinfrastruktur.
  • Aktuell belastning.
  • Storleken på begärandenyttolasten.
  • Bearbeta kölängd.
  • Tiden för serverdelen att bearbeta begäran.

Någon av dessa faktorer kan lägga till svarstid i svaret. Vissa kan minimeras genom att skala ut serverdelen. Andra, till exempel nätverksinfrastruktur, är till stor del utom programutvecklarens kontroll. De flesta API:er kan svara tillräckligt snabbt för att svar ska komma tillbaka via samma anslutning. Programkod kan göra ett synkront API-anrop på ett icke-blockerande sätt, vilket ger utseendet av asynkron bearbetning, vilket rekommenderas för I/O-bundna åtgärder.

I vissa scenarier kan dock det arbete som utförs av serverdelen vara tidskrävande, i sekunders ordning eller vara en bakgrundsprocess som körs i minuter eller till och med timmar. I så fall är det inte möjligt att vänta tills arbetet har slutförts innan du svarar på begäran. Den här situationen är ett potentiellt problem för alla synkrona mönster för begäran-svar.

Vissa arkitekturer löser det här problemet med hjälp av en meddelandekö för att separera stegen för begäran och svar. Den här separationen uppnås ofta med hjälp av mönstret Köbaserad belastningsutjämning. Den här separationen kan göra det möjligt för klientprocessen och serverdels-API:et att skalas oberoende av varandra. Men den här separationen ger också ytterligare komplexitet när klienten behöver ett meddelande om lyckad åtgärd, eftersom det här steget måste bli asynkront.

Många av de överväganden som diskuteras för klientprogram gäller även för REST API-anrop från server till server i distribuerade system , till exempel i en arkitektur för mikrotjänster.

Lösning

En lösning på det här problemet är att använda HTTP-avsökning. Avsökning är användbart för kod på klientsidan, eftersom det kan vara svårt att tillhandahålla återanropsslutpunkter eller använda långvariga anslutningar. Även när återanrop är möjliga kan de extra bibliotek och tjänster som krävs ibland lägga till för mycket extra komplexitet.

  • Klientprogrammet gör ett synkront anrop till API:et, vilket utlöser en tidskrävande åtgärd på serverdelen.

  • API:et svarar synkront så snabbt som möjligt. Den returnerar en HTTP 202-statuskod (godkänd) som bekräftar att begäran har tagits emot för bearbetning.

    Kommentar

    API:et bör verifiera både begäran och den åtgärd som ska utföras innan den tidskrävande processen startas. Om begäran är ogiltig svarar du omedelbart med en felkod som HTTP 400 (felaktig begäran).

  • Svaret innehåller en platsreferens som pekar på en slutpunkt som klienten kan avsöka för att söka efter resultatet av den långvariga åtgärden.

  • API:et avlastar bearbetningen till en annan komponent, till exempel en meddelandekö.

  • För varje lyckat anrop till statusslutpunkten returneras HTTP 200. Medan arbetet fortfarande väntar returnerar statusslutpunkten en resurs som anger att arbetet fortfarande pågår. När arbetet är klart kan statusslutpunkten antingen returnera en resurs som anger slutförande eller omdirigera till en annan resurs-URL. Om den asynkrona åtgärden till exempel skapar en ny resurs omdirigeras statusslutpunkten till URL:en för den resursen.

Följande diagram visar ett typiskt flöde:

Begäran och svarsflöde för asynkrona HTTP-begäranden

  1. Klienten skickar en begäran och tar emot ett HTTP 202-svar (godkänt).
  2. Klienten skickar en HTTP GET-begäran till statusslutpunkten. Arbetet väntar fortfarande, så det här anropet returnerar HTTP 200.
  3. Vid något tillfälle är arbetet slutfört och statusslutpunkten returnerar 302 (hittades) omdirigering till resursen.
  4. Klienten hämtar resursen på den angivna URL:en.

Problem och överväganden

  • Det finns ett antal möjliga sätt att implementera det här mönstret via HTTP och inte alla överordnade tjänster har samma semantik. De flesta tjänster returnerar till exempel inte ett HTTP 202-svar från en GET-metod när en fjärrprocess inte har slutförts. Efter ren REST-semantik bör de returnera HTTP 404 (hittades inte). Det här svaret är meningsfullt när du tänker på att resultatet av anropet inte finns ännu.

  • Ett HTTP 202-svar bör ange platsen och frekvensen som klienten ska söka efter svaret på. Den bör ha följande ytterligare rubriker:

    Header beskrivning Kommentar
    Plats En URL som klienten ska söka efter svarsstatus. Den här URL:en kan vara en SAS-token där Valet-nyckelmönstret är lämpligt om den här platsen behöver åtkomstkontroll. Nyckelmönstret valet är också giltigt när svarssökningen behöver avlastas till en annan serverdel.
    Försök igen efter En uppskattning av när bearbetningen slutförs Det här huvudet är utformat för att förhindra att avsökningsklienter överbelastar serverdelen med återförsök.

    Förväntat klientbeteende måste beaktas när du utformar det här svaret. Även om en klient under din kontroll kan kodas för att uttryckligen respektera dessa svarsvärden, kan klienter som inte har skapats av dig eller som använder en no- eller low-code-metod (till exempel Azure Logic Apps) ha en egen HTTP 202-logikhantering.

  • Du kan behöva använda en bearbetningsproxy eller fasad för att ändra svarshuvudena eller nyttolasten beroende på vilka underliggande tjänster som används.

  • Om statusslutpunkten omdirigeras när den är klar är antingen HTTP 302 eller HTTP 303 lämpliga returkoder, beroende på de exakta semantik som du stöder.

  • När bearbetningen har slutförts ska resursen som anges av platsrubriken returnera en lämplig HTTP-svarskod, till exempel 200 (OK), 201 (skapad) eller 204 (inget innehåll).

  • Om ett fel uppstår under bearbetningen bevarar du felet på resurs-URL:en som beskrivs i platsrubriken och returnerar helst en lämplig svarskod till klienten från resursen (4xx-kod).

  • Alla lösningar implementerar inte det här mönstret på samma sätt och vissa tjänster innehåller ytterligare eller alternativa rubriker. Azure Resource Manager använder till exempel en modifierad variant av det här mönstret. Mer information finns i Async-åtgärder i Azure Resource Manager.

  • Äldre klienter kanske inte stöder det här mönstret. I så fall kan du behöva placera en fasad över det asynkrona API:et för att dölja den asynkrona bearbetningen från den ursprungliga klienten. Azure Logic Apps stöder till exempel det här mönstret internt kan användas som ett integreringslager mellan ett asynkront API och en klient som gör synkrona anrop. Se Utföra långvariga uppgifter med webhook-åtgärdsmönstret.

  • I vissa scenarier kanske du vill tillhandahålla ett sätt för klienter att avbryta en tidskrävande begäran. I så fall måste serverdelstjänsten ha stöd för någon form av avbokningsinstruktion.

När du ska använda det här mönstret

Använd det här mönstret för:

  • Kod på klientsidan, till exempel webbläsarprogram, där det är svårt att tillhandahålla slutpunkter för återanrop eller användning av långvariga anslutningar ger för mycket ytterligare komplexitet.

  • Tjänsten anropar där endast HTTP-protokollet är tillgängligt och returtjänsten inte kan utlösa återanrop på grund av brandväggsbegränsningar på klientsidan.

  • Tjänstanrop som måste integreras med äldre arkitekturer som inte stöder moderna återanropstekniker som WebSockets eller webhooks.

Det här mönstret kanske inte är lämpligt när:

  • Du kan använda en tjänst som skapats för asynkrona meddelanden i stället, till exempel Azure Event Grid.
  • Svar måste strömmas i realtid till klienten.
  • Klienten måste samla in många resultat och svarstiden för dessa resultat är viktig. Överväg ett Service Bus-mönster i stället.
  • Du kan använda beständiga nätverksanslutningar på serversidan, till exempel WebSockets eller SignalR. Dessa tjänster kan användas för att meddela anroparen om resultatet.
  • Med nätverksdesignen kan du öppna portar för att ta emot asynkrona återanrop eller webhooks.

Design av arbetsbelastning

En arkitekt bör utvärdera hur mönstret Asynkron begäran-svar kan användas i deras arbetsbelastningsdesign för att uppfylla de mål och principer som beskrivs i grundpelarna i Azure Well-Architected Framework. Till exempel:

Grundpelare Så här stöder det här mönstret pelarmål
Prestandaeffektivitet hjälper din arbetsbelastning att effektivt uppfylla kraven genom optimeringar inom skalning, data och kod. Avkoppling av begärande- och svarsfaserna i interaktioner för processer som inte behöver omedelbara svar förbättrar systemens svarstider och skalbarhet. Som en asynkron metod kan du maximera samtidigheten på serversidan och schemalägga arbete som ska slutföras som kapaciteten tillåter.

- PE:05 Skalning och partitionering
- PE:07 Kod och infrastruktur

Som med alla designbeslut bör du överväga eventuella kompromisser mot målen för de andra pelarna som kan införas med det här mönstret.

Exempel

Följande kod visar utdrag från ett program som använder Azure Functions för att implementera det här mönstret. Det finns tre funktioner i lösningen:

  • Den asynkrona API-slutpunkten.
  • Statusslutpunkten.
  • En serverdelsfunktion som tar köade arbetsobjekt och kör dem.

Bild av strukturen för mönstret Async-begärandesvar i Functions

GitHub-logotyp Det här exemplet är tillgängligt på GitHub.

Funktionen AsyncProcessingWorkAcceptor

Funktionen AsyncProcessingWorkAcceptor implementerar en slutpunkt som accepterar arbete från ett klientprogram och placerar den i en kö för bearbetning.

  • Funktionen genererar ett begärande-ID och lägger till det som metadata i kömeddelandet.
  • HTTP-svaret innehåller en platsrubrik som pekar på en statusslutpunkt. Begärande-ID:t är en del av URL-sökvägen.
public static class AsyncProcessingWorkAcceptor
{
    [FunctionName("AsyncProcessingWorkAcceptor")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] CustomerPOCO customer,
        [ServiceBus("outqueue", Connection = "ServiceBusConnectionAppSetting")] IAsyncCollector<ServiceBusMessage> OutMessages,
        ILogger log)
    {
        if (String.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
        {
            return new BadRequestResult();
        }

        string reqid = Guid.NewGuid().ToString();

        string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{reqid}";

        var messagePayload = JsonConvert.SerializeObject(customer);
        var message = new ServiceBusMessage(messagePayload);
        message.ApplicationProperties.Add("RequestGUID", reqid);
        message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.Now);
        message.ApplicationProperties.Add("RequestStatusURL", rqs);

        await OutMessages.AddAsync(message);

        return new AcceptedResult(rqs, $"Request Accepted for Processing{Environment.NewLine}ProxyStatus: {rqs}");
    }
}

Funktionen AsyncProcessingBackgroundWorker

Funktionen AsyncProcessingBackgroundWorker hämtar åtgärden från kön, utför en del arbete baserat på meddelandets nyttolast och skriver resultatet till ett lagringskonto.

public static class AsyncProcessingBackgroundWorker
{
    [FunctionName("AsyncProcessingBackgroundWorker")]
    public static async Task RunAsync(
        [ServiceBusTrigger("outqueue", Connection = "ServiceBusConnectionAppSetting")] BinaryData customer,
        IDictionary<string, object> applicationProperties,
        [Blob("data", FileAccess.ReadWrite, Connection = "StorageConnectionAppSetting")] BlobContainerClient inputContainer,
        ILogger log)
    {
        // Perform an actual action against the blob data source for the async readers to be able to check against.
        // This is where your actual service worker processing will be performed

        var id = applicationProperties["RequestGUID"] as string;

        BlobClient blob = inputContainer.GetBlobClient($"{id}.blobdata");

        // Now write the results to blob storage.
        await blob.UploadAsync(customer);
    }
}

Funktionen AsyncOperationStatusChecker

Funktionen AsyncOperationStatusChecker implementerar statusslutpunkten. Den här funktionen kontrollerar först om begäran har slutförts

  • Om begäran har slutförts returnerar funktionen antingen en betjäntnyckel till svaret eller omdirigerar anropet omedelbart till valet-key-URL:en.
  • Om begäran fortfarande väntar bör vi returnera en 200-kod, inklusive det aktuella tillståndet.
public static class AsyncOperationStatusChecker
{
    [FunctionName("AsyncOperationStatusChecker")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{thisGUID}")] HttpRequest req,
        [Blob("data/{thisGuid}.blobdata", FileAccess.Read, Connection = "StorageConnectionAppSetting")] BlockBlobClient inputBlob, string thisGUID,
        ILogger log)
    {

        OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
        OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");

        log.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {OnPending}");

        // Check to see if the blob is present
        if (await inputBlob.ExistsAsync())
        {
            // If it's present, depending on the value of the optional "OnComplete" parameter choose what to do.
            return await OnCompleted(OnComplete, inputBlob, thisGUID);
        }
        else
        {
            // If it's NOT present, then we need to back off. Depending on the value of the optional "OnPending" parameter, choose what to do.
            string rqs = $"http://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{thisGUID}";

            switch (OnPending)
            {
                case OnPendingEnum.OK:
                    {
                        // Return an HTTP 200 status code.
                        return new OkObjectResult(new { status = "In progress", Location = rqs });
                    }

                case OnPendingEnum.Synchronous:
                    {
                        // Back off and retry. Time out if the backoff period hits one minute.
                        int backoff = 250;

                        while (!await inputBlob.ExistsAsync() && backoff < 64000)
                        {
                            log.LogInformation($"Synchronous mode {thisGUID}.blob - retrying in {backoff} ms");
                            backoff = backoff * 2;
                            await Task.Delay(backoff);
                        }

                        if (await inputBlob.ExistsAsync())
                        {
                            log.LogInformation($"Synchronous Redirect mode {thisGUID}.blob - completed after {backoff} ms");
                            return await OnCompleted(OnComplete, inputBlob, thisGUID);
                        }
                        else
                        {
                            log.LogInformation($"Synchronous mode {thisGUID}.blob - NOT FOUND after timeout {backoff} ms");
                            return new NotFoundResult();
                        }
                    }

                default:
                    {
                        throw new InvalidOperationException($"Unexpected value: {OnPending}");
                    }
            }
        }
    }

    private static async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string thisGUID)
    {
        switch (OnComplete)
        {
            case OnCompleteEnum.Redirect:
                {
                    // Redirect to the SAS URI to blob storage

                    return new RedirectResult(inputBlob.GenerateSASURI());
                }

            case OnCompleteEnum.Stream:
                {
                    // Download the file and return it directly to the caller.
                    // For larger files, use a stream to minimize RAM usage.
                    return new OkObjectResult(await inputBlob.DownloadContentAsync());
                }

            default:
                {
                    throw new InvalidOperationException($"Unexpected value: {OnComplete}");
                }
        }
    }
}

public enum OnCompleteEnum
{

    Redirect,
    Stream
}

public enum OnPendingEnum
{

    OK,
    Synchronous
}

Nästa steg

Följande information kan vara relevant när du implementerar det här mönstret: