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 spoléhají na vzdálená rozhraní API k poskytování obchodní logiky a vytváření funkcí. 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.
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 ověří požadavek a akci, která se má provést před spuštěním dlouhotrvajícího procesu. 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 úspěšném volání na koncový bod stavu vrátí HTTP 200 (OK). Zatímco probíhá práce, koncový bod vrátí prostředek, který označuje tento stav. Po dokončení práce vrátí koncový bod prostředek, signalizující 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.
Klient odešle požadavek HTTP GET do koncového bodu stavu. Práce čeká na vyřízení, takže toto volání vrátí HTTP 200.
Úloha je dokončena a stavový endpoint vrátí HTTP 302 (Nalezeno) 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 většina služeb vrací http 404 (Nenalezena) z metody GET, pokud vzdálený proces není dokončen, a ne HTTP 202. Podle standardní sémantiky REST je http 404 správná odpověď, protože výsledek volání ještě neexistuje.
Odpověď HTTP 202 označuje, kde se klient dotazuje a jak často. Obsahuje 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. 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-AfterOdhad dokončení zpracování Tato hlavička zabraňuje dotazujícím klientům, aby do backendu odesílaly příliš mnoho požadavků. 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, kteří ostatní vytvářejí, včetně klientů vytvořených pomocí nástrojů bez kódu nebo nízkého kódu, jako je Azure Logic Apps, můžou pro HTTP 202 použít vlastní zpracování.
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, jsou platné návratové kódy HTTP 302 nebo HTTP 303 (viz další) v závislosti na sémantice, kterou podporujete.
Jakmile server zpracuje požadavek, prostředek, který hlavička
Locationurčuje, vrátí stavový kód HTTP, například 200, 201 (vytvořeno) nebo 204 (bez obsahu).Pokud během zpracování dojde k chybě, chyba se uloží na adrese URL prostředku, jak určuje hlavička
Location, a z tohoto prostředku se vrátí stavový kód 4xx, který odpovídá selhání.Ř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 zpracovatelskou proxy nad asynchronním rozhraním API, aby se asynchronní zpracování skrylo 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 naleznete v tématu 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 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 se staršími architekturami, 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.
Klient potřebuje shromáždit mnoho výsledků a latence těchto výsledků je důležitá. Zvažte místo toho vzor s názvem Service Bus.
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.
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
Location, která odkazuje na koncový bod stavu. ID požadavku se zobrazí v cestě URL.
public class AsyncProcessingWorkAcceptor(ServiceBusClient _serviceBusClient)
{
[Function("AsyncProcessingWorkAcceptor")]
public async Task<IActionResult> RunAsync([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, [FromBody] CustomerPOCO customer)
{
if (string.IsNullOrEmpty(customer.id) || string.IsNullOrEmpty(customer.customername))
{
return new BadRequestResult();
}
var reqid = Guid.NewGuid().ToString();
string scheme = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development" ? "http" : "https";
var rqs = $"{scheme}://{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);
var sender = _serviceBusClient.CreateSender("outqueue");
await sender.SendMessageAsync(message);
return new AcceptedResult(rqs, $"Request Accepted for Processing{Environment.NewLine}ProxyStatus: {rqs}");
}
}
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(nameof(AsyncProcessingBackgroundWorker))]
public async Task Run([ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
{
var requestGuid = message.ApplicationProperties["RequestGUID"].ToString();
string blobName = $"{requestGuid}.blobdata";
await _blobContainerClient.CreateIfNotExistsAsync();
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í valet klíč do odpovědi nebo přesměruje volání okamžitě na adresu URL valet-key.
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/{thisGUID}")] HttpRequest req,
[BlobInput("data/{thisGUID}.blobdata", Connection = "DataStorage")] BlockBlobClient inputBlob, string thisGUID)
{
OnCompleteEnum OnComplete = Enum.Parse<OnCompleteEnum>(req.Query["OnComplete"].FirstOrDefault() ?? "Redirect");
OnPendingEnum OnPending = Enum.Parse<OnPendingEnum>(req.Query["OnPending"].FirstOrDefault() ?? "OK");
_logger.LogInformation($"C# HTTP trigger function processed a request for status on {thisGUID} - OnComplete {OnComplete} - OnPending {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, thisGUID);
}
else
{
// If the blob doesn't exist, the function uses the OnPending parameter to determine the next action.
string scheme = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Development" ? "http" : "https";
string rqs = $"{scheme}://{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 back-off period reaches one minute.
int backoff = 250;
while (!await inputBlob.ExistsAsync() && backoff < 64000)
{
_logger.LogInformation($"Synchronous mode {thisGUID}.blob - retrying in {backoff} ms");
backoff = backoff * 2;
await Task.Delay(backoff);
}
if (await inputBlob.ExistsAsync())
{
_logger.LogInformation($"Synchronous Redirect mode {thisGUID}.blob - completed after {backoff} ms");
return await OnCompleted(OnComplete, inputBlob, thisGUID);
}
else
{
_logger.LogInformation($"Synchronous mode {thisGUID}.blob - NOT FOUND after timeout {backoff} ms");
return new NotFoundResult();
}
}
default:
{
throw new InvalidOperationException($"Unexpected value: {OnPending}");
}
}
}
}
private async Task<IActionResult> OnCompleted(OnCompleteEnum OnComplete, BlockBlobClient inputBlob, string thisGUID)
{
switch (OnComplete)
{
case OnCompleteEnum.Redirect:
{
// The typical way to generate a shared access signature token in code requires the storage account key.
// If you need to use a managed identity to control access to your storage accounts in code, which is a recommended best practice, you should do so when possible.
// In this scenario, you don't have a storage account key, so you need to find another way to generate the shared access signatures.
// To generate shared access signatures, use a user delegation shared access signature. This approach lets you sign the shared access signature by using Microsoft Entra ID credentials instead of the storage account key.
BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(7));
// Redirect the shared access signature uniform resource identifier (URI) to blob storage.
return new RedirectResult(inputBlob.GenerateSASURI(userDelegationKey));
}
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ásledující CloudBlockBlobExtensions třída poskytuje metodu rozšíření, kterou nástroj pro kontrolu stavu používá ke generování identifikátoru URI sdíleného přístupového podpisu delegování uživatele pro výsledkový blob.
public static class CloudBlockBlobExtensions
{
public static string GenerateSASURI(this BlockBlobClient inputBlob, UserDelegationKey userDelegationKey)
{
BlobServiceClient blobServiceClient = inputBlob.GetParentBlobContainerClient().GetParentBlobServiceClient();
BlobSasBuilder blobSasBuilder = new BlobSasBuilder()
{
BlobContainerName = inputBlob.BlobContainerName,
BlobName = inputBlob.Name,
Resource = "b",
StartsOn = DateTimeOffset.UtcNow,
ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(10)
};
blobSasBuilder.SetPermissions(BlobSasPermissions.Read);
var blobUriBuilder = new BlobUriBuilder(inputBlob.Uri)
{
Sas = blobSasBuilder.ToSasQueryParameters(userDelegationKey, blobServiceClient.AccountName)
};
// Generate the shared access signature on the blob, which sets the constraints directly on the signature.
Uri sasUri = blobUriBuilder.ToUri();
// Return the URI string for the container, including the shared access signature token.
return sasUri.ToString();
}
}