Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Oddělení back-endového zpracování od front-endového hostitele, když back-endové zpracování musí běžet asynchronně, ale front-end potřebuje jasnou odpověď.
Kontext a problém
V moderním vývoji aplikací klientské aplikace často závisejí na vzdálených rozhraních API, aby poskytovaly obchodní logiku a funkci vytváření. Mnoho aplikací spouští kód ve webovém prohlížeči a další prostředí také hostuje klientský kód. API můžou souviset přímo s aplikací nebo pracovat jako sdílené služby prostřednictvím externí služby. Většina volání rozhraní API používá HTTP nebo HTTPS a sleduje sémantiku REST.
Ve většině případů rozhraní API klientské aplikace reagují přibližně v 100 milisekundách (ms) nebo méně. Latence odpovědi může ovlivnit mnoho faktorů:
- Hostovací vrstva aplikace
- Součásti zabezpečení
- Relativní zeměpisné umístění volajícího a zázemí
- Síťová infrastruktura
- Aktuální zatížení
- Velikost datové části požadavku
- Délka fronty zpracování
- Doba zpracování požadavku back-endem
Tyto faktory můžou do odpovědi přidat latenci. Některé faktory můžete zmírnit horizontálním navýšením kapacity back-endu. Další faktory, jako je síťová infrastruktura, jsou mimo kontrolu vývojáře aplikací. Většina rozhraní API reaguje dostatečně rychle, aby se odpověď vrátila přes stejné připojení. Kód aplikace může provést synchronní volání rozhraní API neblokujícím způsobem, aby vyvolal dojem asynchronního zpracování. Tento přístup doporučujeme pro operace vázané na vstup/výstup.
V některých scénářích backend provádí úlohy, které trvají dlouhou dobu a několik sekund. V jiných scénářích back-end provádí dlouhodobé úkoly na pozadí po několik minut nebo delší období. V těchto případech nemůžete počkat, až se práce dokončí, než odešlete odpověď. Tato situace může vytvořit problém pro synchronní vzory odpovědí na žádosti. Pokyny k návrhu back-endového zpracování najdete v tématu Úlohy na pozadí.
Některé architektury tento problém řeší pomocí zprostředkovatele zpráv k oddělení fází požadavků a odpovědí. Mnoho systémů toho dosahuje prostřednictvím modelu vyrovnávání zatíženíQueue-Based. Toto oddělení umožňuje nezávisle škálovat klientské procesy a back-endové rozhraní API. Navíc představuje větší složitost, když klient vyžaduje oznámení o úspěchu, protože tento krok se musí také stát asynchronním.
Mnoho stejných aspektů, které platí pro klientské aplikace, platí také pro volání rozhraní REST API mezi servery v distribuovaných systémech, jako je architektura mikroslužeb.
Řešení
Jedno z řešení tohoto problému je použít HTTP polling. Dotazování funguje dobře pro kód na straně klienta, když koncové body zpětného volání nejsou k dispozici nebo když dlouhotrvající připojení přidávají příliš mnoho složitosti. I když jsou zpětná volání možná, další knihovny a služby, které vyžadují, mohou zvýšit složitost.
Následující kroky popisují řešení:
Klientská aplikace provádí synchronní volání rozhraní API pro aktivaci dlouhotrvající operace na back-endu.
Rozhraní API reaguje synchronně co nejrychleji. Vrátí stavový kód HTTP 202 (Accepted), který potvrdí, že obdržel žádost o zpracování.
Poznámka:
Rozhraní API by mělo ověřit požadavek a provedení akce, než začne dlouho trvající proces. Pokud požadavek není platný, okamžitě odpovězte kódem chyby, jako je HTTP 400 (Chybný požadavek).
Odpověď obsahuje referenci na umístění, jenž ukazuje na koncový bod, který klient může dotazovat pro kontrolu výsledku dlouhotrvající operace.
Rozhraní API přesouvá zpracování na jinou komponentu, jako je fronta zpráv.
Při každém úspěšném volání stavového koncového bodu vrátí HTTP 200 (OK). Zatímco probíhá práce, koncový bod stavu vrátí prostředek, který označuje tento stav. Text odpovědi na stav by měl obsahovat dostatek informací, aby klient porozuměl aktuálnímu stavu operace.
Po dokončení práce vrátí stavový koncový bod prostředek, který označuje dokončení, nebo přesměruje na jinou adresu URL prostředku. Pokud například asynchronní operace vytvoří nový prostředek, koncový bod stavu přesměruje na adresu URL daného prostředku.
Následující diagram znázorňuje typický tok.
Klient odešle požadavek a obdrží odpověď HTTP 202 (Přijato).
Klient odešle požadavek HTTP GET do koncového bodu stavu. Toto volání vrátí http 200, protože práce čeká na vyřízení.
V určitém okamžiku se práce dokončí a koncový bod stavu vrátí http 303 (viz ostatní) pro přesměrování na prostředek.
Klient načte prostředek na zadanou adresu URL.
Problémy a důležité informace
Při rozhodování o implementaci tohoto modelu zvažte následující body:
Existuje několik způsobů implementace tohoto modelu přes protokol HTTP a upstreamové služby vždy nepoužívají stejnou sémantiku. Například některé implementace nepoužívají samostatný koncový bod stavu. Místo toho klient přímo dotazuje URL cílového prostředku a obdrží stavový kód HTTP 404 (Nenalezeno), dokud není prostředek vytvořen. Tato odpověď se vygeneruje, protože prostředek ještě neexistuje. Tento přístup ale může být nejasný, protože ID neplatných požadavků také vrací HTTP 404. Vyhrazený koncový bod stavu, který vrací HTTP 200 s textem stavu, jak je popsáno v tomto vzoru, zabrání této nejasnosti.
Odpověď HTTP 202 označuje, kde se klient dotazuje a jak často. Měla by obsahovat následující hlavičky.
Header Description Poznámky LocationAdresa URL, kterou klient dotazuje na stav odpovědi Tato adresa URL může být token sdíleného přístupového podpisu (SAS). Vzor Valet Key funguje dobře, když toto umístění potřebuje řízení přístupu. Tento model platí také v případech, kdy se dotazování odpovědí musí přesunout na jiný back-end. Retry-AfterOdhadovaná doba dokončení zpracování Tato hlavička pomáhá klientům při načítání dat omezit počet požadavků odesílaných do back-endu. Při návrhu této odpovědi zvažte očekávané chování klienta. Klient, který řídíte, může přesně postupovat podle těchto hodnot odpovědí. Klienti vytvoření jinými, včetně klientů vytvořených pomocí nástrojů na bázi bezkódového nebo nízkokódového přístupu, jako je Azure Logic Apps, mohou použít své vlastní zpracování pro HTTP 202.
Zvažte zahrnutí následujících polí do odpovědi koncového bodu stavu.
Pole Description Poznámky statusAktuální stav operace, například Čeká na vyřízení, Spuštění, Úspěch, Selhání nebo Zrušeno Používá konzistentní, zdokumentovanou sadu terminálových a netermiálních hodnot. createdAtČas přijetí operace Pomáhá klientům zjišťovat zastaralé nebo opuštěné operace. lastUpdatedAtČas poslední aktualizace stavu Pomáhá klientům rozlišovat mezi zastavenými a probíhajícími operacemi. percentCompleteVolitelný indikátor průběhu Užitečné, když back-end dokáže odhadnout průběh errorStrukturovaný objekt chyby, když je stav Selhání Pro konzistenci zvažte použití formátu RFC 9457 . Možná budete muset použít zpracovatelný proxy server k úpravě hlaviček odpovědi nebo datové části v závislosti na tom, které základní služby používáte.
Pokud se koncový bod stavu po dokončení přesměruje, použijte HTTP 303 (viz ostatní). A 303 dává klientovi pokyn odeslat požadavek GET na přesměrovací URL bez ohledu na původní metodu požadavku. Toto chování je správná sémantika pro tento vzor, protože klient načítá jedinečný prostředek výsledku a znovu neodesílá původní operaci. HTTP 302 (Nalezeno) nezaručuje změnu metody. Někteří klienti přehrají původní metodu při přesměrování. Toto chování může způsobit nežádoucí vedlejší účinky, například duplicitní požadavky POST.
Po úspěšném zpracování požadavku server vrátí prostředek, který
Locationhlavička určuje, stavový kód HTTP, například 200, 201 (vytvořeno) nebo 204 (bez obsahu).Pokud během zpracování dojde k chybě, uchovejte chybu na URL adrese prostředku, kterou určuje hlavička
Location, a vraťte stavový kód 4xx z daného prostředku, který odpovídá selhání. Použijte strukturovaný formát chyby, například RFC 9457 (Podrobnosti o problému pro rozhraní HTTP API), aby klienti mohli programově analyzovat a zpracovávat chyby.Stavový prostředek a všechny uložené výsledky spotřebovávají úložiště a výpočetní prostředky. Definujte zásady uchování dat, které zajistí jejich vyčištění po přiměřené době. Chcete-li informovat klienty o okně uchovávání informací, můžete do odpovědi na stav přidat
Expireshlavičku.Řešení neimplementují tento vzor stejným způsobem a některé služby obsahují další nebo alternativní hlavičky. Například Azure Resource Manager používá upravenou variantu tohoto modelu. Další informace najdete v tématu Resource Manager asynchronní operace.
Starší klienti nemusí tento model podporovat. V takovém případě možná budete muset umístit vrstvu přes asynchronní rozhraní API k zakrytí asynchronního zpracování před původním klientem. Logic Apps například nativně podporuje tento model a můžete ho použít jako integrační vrstvu mezi asynchronním rozhraním API a klientem, který provádí synchronní volání. Další informace najdete v tématu Chování asynchronní odpovědi na požadavek v Logic Apps.
Pokud chcete klientům poskytnout způsob, jak zrušit dlouhotrvající požadavek, zpřístupněte operaci DELETE na prostředku stavu koncového bodu. Tento požadavek by měl předat pokyn ke zrušení součásti pro zpracování back-endu. Jakmile back-end zpracuje zrušení, měl by aktualizovat stav prostředku, aby odrážel zrušený stav. Tento proces pomáhá zabránit tomu, aby nedokončená práce využívala prostředky neomezeně dlouho. Určete, jestli operace podporuje částečné vrácení zpět nebo vyžaduje kompenzační transakci.
Klienti můžou vyžadovat, aby při odeslání počátečního požadavku zadávali klíč idempotence, například v
Idempotency-Keyhlavičce požadavku. Pokud back-end systém obdrží duplicitní klíč, měl by vrátit existující stavový zdroj, místo aby zařadil do fronty druhou pracovní položku. Tento přístup chrání před selháními sítě, které způsobí, že klient zkusí opakovat post, který server již přijal. Je to zvlášť důležité v tomto vzoru, protože klient nemůže rozlišovat mezi ztracenou odpovědí a požadavkem, který nebyl nikdy přijat.
Poznámka:
Tento model popisuje dotazování HTTP, ve kterém klient pravidelně vydává nové požadavky na kontrolu stavu. V dlouhém dotazování klient odešle požadavek a server uchovává připojení otevřené, dokud nebudou k dispozici nová data nebo dojde k vypršení časového limitu. Dlouhé dotazování snižuje latenci odezvy v porovnání s pravidelným dotazováním, ale představuje složitost správy připojení a časových limitů.
Kdy použít tento vzor
Tento model použijte v těchto případech:
Pracujete s kódem na straně klienta, jako jsou aplikace prohlížeče, a tato omezení ztěžují poskytování callbackových koncovými bodů nebo přidávají přílišnou složitost při dlouhotrvajících připojeních.
Zavoláte službu, která používá pouze protokol HTTP a návratová služba nemůže odesílat zpětná volání kvůli omezením brány firewall na straně klienta.
Integrujete se s úlohami, které nepodporují moderní mechanismy 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í být streamovány do klienta v reálném čase. Zvažte použití Server-Sent Events (SSE), které poskytují lehký, HTTP-native, jednosměrný push kanál z serveru do klienta, aniž by bylo nutné, aby klient prováděl polling.
Klient potřebuje shromáždit mnoho výsledků a latence těchto výsledků je důležitá. Místo toho zvažte použití zprostředkovatele zpráv.
K dispozici jsou trvalá síťová připojení na straně serveru, jako jsou WebSockets nebo SignalR. Pomocí těchto připojení můžete volajícímu oznámit výsledek.
Návrh sítě podporuje otevřené porty pro příjem asynchronních zpětných volání či webhooků.
Návrh úloh
Architekt by měl vyhodnotit, jak může v návrhu své úlohy použít vzor asynchronního Request-Reply k řešení cílů a principů popsaných v pilířích architektury Azure Well-Architected.
| Pilíř | Jak tento model podporuje cíle pilíře |
|---|---|
| efektivita výkonu pomáhá vašim úlohám efektivně splňovat požadavky prostřednictvím optimalizací škálování, dat a kódu. | Zvýšíte rychlost odezvy a škálovatelnost oddělením fází žádosti a odpovědí pro procesy, které nevyžadují okamžitou odpověď. Asynchronní přístup zvyšuje souběžnost a umožňuje serveru plánovat práci, jakmile bude kapacita dostupná. - 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 kompromisy proti cílům ostatních pilířů, které by tento model mohl zavést.
Příklad
Následující kód ukazuje výňatky z aplikace, která k implementaci tohoto vzoru používá Azure Functions. Toto řešení má tři funkce:
- Asynchronní koncový bod rozhraní API
- Koncový bod stavu
- Back-endová funkce, která zpracovává pracovní položky zařazené do fronty a spouští je.
Tato ukázka je dostupná na GitHub.
Implementace používá spravovanou identitu k ověření pomocí Azure Service Bus a Azure Blob Storage, což zabraňuje ukládání připojovacích řetězců nebo klíčů účtu. Závislosti jsou registrovány Program.cs pomocí použití DefaultAzureCredential a jsou vloženy prostřednictvím primárních konstruktorů.
Funkce AsyncProcessingWorkAcceptor
Funkce AsyncProcessingWorkAcceptor implementuje koncový bod, který přijímá práci z klientské aplikace a přidává ji do fronty pro zpracování.
Funkce vygeneruje ID požadavku a přidá ho jako metadata do zprávy fronty.
Odpověď HTTP obsahuje hlavičku
Locationodkazující na koncový bod stavu a hlavičkuRetry-After, která navrhuje interval dotazování. ID požadavku se zobrazí v cestě URL.
public class AsyncProcessingWorkAcceptor(ServiceBusClient _serviceBusClient)
{
[Function("AsyncProcessingWorkAcceptor")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
[FromBody] CustomerPOCO customer)
{
if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
{
return new BadRequestResult();
}
string requestId = Guid.NewGuid().ToString();
string statusUrl = $"https://{Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME")}/api/RequestStatus/{requestId}";
var messagePayload = JsonConvert.SerializeObject(customer);
var message = new ServiceBusMessage(messagePayload);
message.ApplicationProperties.Add("RequestGUID", requestId);
message.ApplicationProperties.Add("RequestSubmittedAt", DateTime.UtcNow);
message.ApplicationProperties.Add("RequestStatusURL", statusUrl);
var sender = _serviceBusClient.CreateSender("outqueue");
await sender.SendMessageAsync(message);
req.HttpContext.Response.Headers["Retry-After"] = "5";
return new AcceptedResult(statusUrl, null);
}
}
Funkce AsyncProcessingBackgroundWorker
Funkce AsyncProcessingBackgroundWorker přečte operaci z fronty, zpracuje ji na základě obsahu zprávy a zapíše výsledek do úložiště.
public class AsyncProcessingBackgroundWorker(BlobContainerClient _blobContainerClient)
{
[Function("AsyncProcessingBackgroundWorker")]
public async Task Run(
[ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
{
// Perform an action against the blob data source for the async readers to check against.
// This is where your service worker processing will be performed.
var requestGuid = message.ApplicationProperties["RequestGUID"].ToString();
string blobName = $"{requestGuid}.blobdata";
var blobClient = _blobContainerClient.GetBlobClient(blobName);
using (MemoryStream memoryStream = new MemoryStream())
using (StreamWriter writer = new StreamWriter(memoryStream))
{
writer.Write(message.Body.ToString());
writer.Flush();
memoryStream.Position = 0;
await blobClient.UploadAsync(memoryStream, overwrite: true);
}
}
}
Funkce AsyncOperationStatusChecker
Funkce AsyncOperationStatusChecker implementuje koncový bod stavu. Tato funkce zkontroluje stav požadavku:
Pokud se požadavek dokončí, funkce vrátí HTTP 303 (Viz jiné) a přesměruje klienta na adresu URL valet key pro výsledek.
Pokud požadavek čeká na vyřízení, vrátí funkce kód HTTP 200, který obsahuje aktuální stav.
public class AsyncOperationStatusChecker(ILogger<AsyncOperationStatusChecker> _logger)
{
[Function("AsyncOperationStatusChecker")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "RequestStatus/{requestId}")] HttpRequest req,
[BlobInput("data/{requestId}.blobdata", Connection = "DataStorage")] BlockBlobClient inputBlob, string requestId)
{
OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");
_logger.LogInformation("Received status request for {RequestId} - OnComplete {OnComplete} - OnPending {OnPending}",
requestId, OnComplete, OnPending);
// Check whether the blob exists.
if (await inputBlob.ExistsAsync())
{
// If the blob exists, the function uses the OnComplete parameter to determine the next action.
return await OnCompleted(OnComplete, inputBlob, requestId, req);
}
else
{
// If the blob doesn't exist, the function uses the OnPending parameter to determine the next action.
switch (OnPending)
{
case OnPendingEnum.OK:
{
// Return an HTTP 200 status code.
return new OkObjectResult(new { status = "In progress", Location = rqs });
}
case OnPendingEnum.Synchronous:
{
// Long polling example: hold the connection open and check for completion
// using exponential backoff. Time out after approximately one minute.
int backoff = 250;
while (!await inputBlob.ExistsAsync() && backoff < 64000)
{
_logger.LogInformation("Synchronous mode {RequestId} - retrying in {Backoff} ms", requestId, backoff);
backoff = backoff * 2;
await Task.Delay(backoff);
}
if (await inputBlob.ExistsAsync())
{
_logger.LogInformation("Synchronous mode {RequestId} - completed after {Backoff} ms", requestId, backoff);
return await OnCompleted(OnComplete, inputBlob, requestId, req);
}
else
{
_logger.LogInformation("Synchronous mode {RequestId} - NOT FOUND after timeout {Backoff} ms", requestId, backoff);
return new NotFoundResult();
}
}
default:
{
throw new InvalidOperationException($"Unexpected value: {OnPending}");
}
}
}
}
private async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string requestId, HttpRequest req)
{
switch (OnComplete)
{
case OnCompleteEnum.Redirect:
{
// Generate a user delegation SAS URI by using managed identity credentials.
BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));
// Return 303 (See Other) to redirect the client to the result resource.
// GenerateUserDelegationSasUri is a custom helper. See the full implementation on GitHub.
req.HttpContext.Response.Headers.Location = GenerateUserDelegationSasUri(inputBlob, userDelegationKey);
return new StatusCodeResult(StatusCodes.Status303SeeOther);
}
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
}