Model asynchronní žádosti a odpovědi

Azure
Azure Logic Apps

Oddělte back-endové zpracování od front-endového hostitele – back-endové zpracování back-endu musí být asynchronní, ale front-end pořád potřebuje jasnou odpověď.

Kontext a problém

V moderním vývoji aplikací je běžné, že klientské aplikace – často kód spuštěný ve webovém klientovi (prohlížeči) – závisí na vzdálených rozhraních API, aby poskytovaly obchodní logiku a funkci vytváření. Tato rozhraní API můžou souviset přímo s aplikací nebo můžou být sdílené služby poskytované třetí stranou. Tato volání rozhraní API se obvykle provádí přes protokol HTTP(S) a řídí se sémantikou REST.

Ve většině případů jsou rozhraní API pro klientskou aplikaci navržená tak, aby rychle reagovala na pořadí 100 ms nebo méně. Latence odpovědi může mít vliv na řadu faktorů, mezi které patří:

  • Zásobník hostování aplikace.
  • Součásti zabezpečení.
  • Relativní zeměpisné umístění volajícího a back-endu.
  • Síťová infrastruktura.
  • Aktuální zatížení.
  • Velikost datové části požadavku.
  • Zpracování délky fronty
  • Doba zpracování požadavku back-endem.

Každý z těchto faktorů může do odpovědi přidat latenci. Některé je možné zmírnit horizontálním navýšením kapacity back-endu. Ostatní, jako je síťová infrastruktura, jsou z velké části mimo kontrolu vývojáře aplikací. Většina rozhraní API dokáže rychle reagovat, aby odpovědi dorazily zpět přes stejné připojení. Kód aplikace může provést synchronní volání rozhraní API neblokujícím způsobem, což dává vzhled asynchronního zpracování, což se doporučuje pro vstupně-výstupní operace.

V některých scénářích ale může být práce provedená back-endem dlouhotrvající, v řádu sekund nebo může být proces na pozadí, který se spustí v minutách nebo dokonce hodinách. V takovém případě není možné počkat, než se práce dokončí, než odpoví na požadavek. Tato situace je potenciálním problémem pro jakýkoli synchronní vzor žádosti a odpovědi.

Některé architektury tento problém řeší pomocí zprostředkovatele zpráv k oddělení fází požadavků a odpovědí. Toto oddělení se často dosahuje pomocí modelu vyrovnávání zatížení založeného na frontě. Toto oddělení může klientskému procesu a back-endovému rozhraní API umožnit nezávislé škálování. Toto oddělení ale také přináší další složitost, když klient vyžaduje oznámení o úspěchu, protože tento krok se musí stát asynchronním.

Mnoho stejných aspektů probíraných v klientských aplikacích platí také pro volání rozhraní REST API mezi servery v distribuovaných systémech – například v architektuře mikroslužeb.

Řešení

Jedním zřešeních Dotazování je užitečné pro kód na straně klienta, protože může být obtížné poskytovat koncové body zpětného volání nebo používat dlouhotrvající připojení. I v případě, že jsou možné zpětné volání, můžou dodatečné knihovny a služby, které jsou požadovány, někdy přidat příliš mnoho další složitosti.

  • Klientská aplikace provede synchronní volání rozhraní API, které aktivuje dlouho běžící operaci na back-endu.

  • Rozhraní API reaguje synchronně co nejrychleji. Vrátí stavový kód HTTP 202 (Accepted), který potvrzuje, že požadavek byl přijat ke zpracování.

    Poznámka:

    Rozhraní API by mělo před spuštěním dlouhotrvajícího procesu ověřit požadavek i akci, která se má provést. Pokud je požadavek neplatný, okamžitě odpovězte kódem chyby, jako je HTTP 400 (Chybný požadavek).

  • Odpověď obsahuje odkaz na umístění odkazující na koncový bod, který klient může dotazovat, aby zkontroloval výsledek dlouhotrvající operace.

  • Rozhraní API přesměruje zpracování na jinou komponentu, například frontu zpráv.

  • Pro každé úspěšné volání koncového bodu stavu vrátí HTTP 200. I když práce stále čeká na vyřízení, koncový bod stavu vrátí prostředek, který označuje, že práce stále probíhá. Po dokončení práce může koncový bod stavu vrátit prostředek, který označuje dokončení, nebo přesměrovat na jinou adresu URL prostředku. Pokud například asynchronní operace vytvoří nový prostředek, koncový bod stavu by přesměroval na adresu URL daného prostředku.

Následující diagram znázorňuje typický tok:

Tok požadavků a odpovědí pro asynchronní požadavky HTTP

  1. Klient odešle požadavek a obdrží odpověď HTTP 202 (Přijato).
  2. Klient odešle požadavek HTTP GET do koncového bodu stavu. Práce stále čeká na vyřízení, takže toto volání vrátí http 200.
  3. V určitém okamžiku je práce dokončená a koncový bod stavu vrátí 302 (nalezeno) přesměrování na prostředek.
  4. Klient načte prostředek na zadanou adresu URL.

Problémy a důležité informace

  • Existuje řada možných způsobů, jak tento model implementovat přes PROTOKOL HTTP, a ne všechny upstreamové služby mají stejnou sémantiku. Například většina služeb nevrátí odpověď HTTP 202 zpět z metody GET, když vzdálený proces nedokončil. Po čisté sémantice REST by měly vrátit HTTP 404 (Nenalezena). Tato odpověď dává smysl, když se domníváte, že výsledek volání ještě není k dispozici.

  • Odpověď HTTP 202 by měla naznačovat umístění a frekvenci, které má klient dotazovat na odpověď. Měla by obsahovat následující další hlavičky:

    Hlavička Popis Notes
    Umístění Adresa URL, na které by se klient měl dotazovat na stav odpovědi. Tato adresa URL může být token SAS s vhodným vzorem klíče Valet, pokud toto umístění potřebuje řízení přístupu. Vzor klíče valet je platný také v případech, kdy dotazování odpovědí potřebuje přesměrování zpracování do jiného back-endu.
    Opakovat po Odhad dokončení zpracování Tato hlavička je navržená tak, aby zabránila klientům dotazování v zahlcení back-endu opakováním.
  • V závislosti na použitých podkladových službách možná budete muset použít zpracovatelný proxy server nebo fasádu k manipulaci s hlavičkami odpovědi nebo datovou částí.

  • Pokud se koncový bod stavu při dokončení přesměruje, jsou v závislosti na přesné sémantice, kterou podporujete, vhodné návratové kódy HTTP 302 nebo HTTP 303 .

  • Po úspěšném zpracování by měl prostředek určený hlavičkou Location vrátit odpovídající kód odpovědi HTTP, například 200 (OK), 201 (vytvořeno) nebo 204 (bez obsahu).

  • Pokud během zpracování dojde k chybě, zachote chybu na adrese URL prostředku popsané v hlavičce Umístění a v ideálním případě vraťte odpovídající kód odpovědi klientovi z tohoto prostředku (kód 4xx).

  • Ne všechna řešení tento model implementují stejným způsobem a některé služby budou obsahovat další nebo alternativní hlavičky. Azure Resource Manager například používá upravenou variantu tohoto modelu. Další informace najdete v tématu Asynchronní operace Azure Resource Manageru.

  • Starší klienti nemusí tento model podporovat. V takovém případě možná budete muset umístit fasádu přes asynchronní rozhraní API, aby bylo možné skrýt asynchronní zpracování od původního klienta. Azure Logic Apps například nativně podporuje tento model jako vrstva integrace mezi asynchronním rozhraním API a klientem, který provádí synchronní volání. Viz Provádění dlouhotrvajících úloh pomocí vzoru akce webhooku.

  • V některých scénářích můžete chtít klientům poskytnout způsob, jak zrušit dlouhotrvající požadavek. V takovém případě musí back-endová služba podporovat určitou formu instrukce zrušení.

Kdy se má tento model použít

Tento vzor použijte pro:

  • Kód na straně klienta, například aplikace prohlížeče, kde je obtížné poskytovat koncové body zpětného volání nebo použití dlouhotrvajících připojení zvyšuje příliš mnoho další složitosti.

  • Volání služeb, kde je k dispozici pouze protokol HTTP a návratová služba nemůže aktivovat zpětná volání kvůli omezením brány firewall na straně klienta.

  • Volání služeb, která je potřeba integrovat se staršími architekturami, které nepodporují moderní technologie zpětného volání, jako jsou webSockety nebo webhooky.

Tento vzor nemusí být vhodný v těchto případech:

  • Místo toho můžete použít službu vytvořenou pro asynchronní oznámení, jako je Azure Event Grid.
  • Odpovědi musí do klienta streamovat v reálném čase.
  • Klient potřebuje shromáždit mnoho výsledků a přijímat latenci těchto výsledků je důležité. Zvažte místo toho vzor služby Service Bus.
  • Můžete použít trvalá síťová připojení na straně serveru, jako jsou WebSocket nebo SignalR. Tyto služby lze použít k oznámení volajícímu o výsledku.
  • Návrh sítě umožňuje otevřít porty pro příjem asynchronních zpětných volání nebo webhooků.

Návrh úloh

Architekt by měl vyhodnotit způsob použití vzoru Asynchronní požadavek-odpověď v návrhu úlohy k řešení cílů a principů popsaných v pilířích architektury Azure Well-Architected Framework. Příklad:

Pilíř Jak tento model podporuje cíle pilíře
Efektivita výkonu pomáhá vaší úloze efektivně splňovat požadavky prostřednictvím optimalizací škálování, dat a kódu. Oddělení fází interakce požadavků a odpovědí pro procesy, které nepotřebují okamžité odpovědi, zlepšuje rychlost odezvy a škálovatelnost systémů. Jako asynchronní aplikace můžete maximalizovat souběžnost na straně serveru a naplánovat dokončení práce podle kapacity.

- PE:05 Škálování a dělení
- PE:07 Kód a infrastruktura

Stejně jako u jakéhokoli rozhodnutí o návrhu zvažte jakékoli kompromisy proti cílům ostatních pilířů, které by mohly být s tímto vzorem zavedeny.

Příklad

Následující kód ukazuje výňatky z aplikace, která k implementaci tohoto modelu používá Azure Functions. Řešení obsahuje tři funkce:

  • Asynchronní koncový bod rozhraní API.
  • Koncový bod stavu.
  • Back-endová funkce, která přebírá pracovní položky zařazené do fronty a spouští je.

Obrázek struktury vzoru Odpověď asynchronní žádosti ve funkcích

Logo GitHubu Tato ukázka je k dispozici na GitHubu.

Funkce AsyncProcessingWorkAcceptor

Funkce AsyncProcessingWorkAcceptor implementuje koncový bod, který přijímá práci z klientské aplikace a umístí ji do fronty ke zpracování.

  • Funkce vygeneruje ID požadavku a přidá ho jako metadata do zprávy fronty.
  • Odpověď HTTP obsahuje hlavičku umístění odkazující na koncový bod stavu. ID požadavku je součástí cesty URL.
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}");
    }
}

Funkce AsyncProcessingBackgroundWorker

Funkce AsyncProcessingBackgroundWorker převezme operaci z fronty, pracuje na základě datové části zprávy a zapíše výsledek do účtu úložiště.

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

Funkce AsyncOperationStatusChecker

Funkce AsyncOperationStatusChecker implementuje koncový bod stavu. Tato funkce nejprve zkontroluje, jestli byla žádost dokončena.

  • Pokud byl požadavek dokončen, funkce buď vrátí valet-key do odpovědi, nebo přesměruje volání okamžitě na adresu URL valet-key.
  • Pokud požadavek stále čeká na vyřízení, měli bychom vrátit kód 200 včetně aktuálního stavu.
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
}

Další kroky

Při implementaci tohoto modelu můžou být důležité tyto informace: