Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Frikoppla backend-bearbetning från en frontendvärd när backend-bearbetning måste köras asynkront, men frontenden behöver ett klart svar.
Kontext och problem
I modern programutveckling är klientprogram ofta beroende av fjärranslutna API:er för att tillhandahålla affärslogik och skapa funktioner. Många program kör kod i en webbläsare och andra miljöer är också värdar för klientkod. API:erna kan relatera direkt till programmet eller fungera som delade tjänster från en extern tjänst. De flesta API-anrop använder HTTP eller HTTPS och följer REST-semantik.
I de flesta fall svarar API:er för ett klientprogram på cirka 100 millisekunder (ms) eller mindre. Många faktorer kan påverka svarsfördröjningen:
- Programmets värdstack
- Säkerhetskomponenter
- Den relativa geografiska platsen för anroparen och backend-systemet
- Nätverksinfrastruktur
- Aktuell belastning
- Storleken på begäransdatabördan
- Behandlingskölängd
- Tiden för serverdelen att bearbeta begäran
Dessa faktorer kan öka svarstiden. Du kan minimera vissa faktorer genom att skala ut serverdelen. Andra faktorer, till exempel nätverksinfrastruktur, ligger utanför programutvecklarens kontroll. De flesta API:er svarar tillräckligt snabbt för att svaret ska returneras via samma anslutning. Programkod kan göra ett synkront API-anrop på ett icke-blockerande sätt för att ge utseendet av asynkron bearbetning. Vi rekommenderar den här metoden för in- och utdata (I/O)-bundna åtgärder.
I vissa scenarier utför serverdelen långvarigt arbete och tar några sekunder. I andra scenarier utför serverdelen tidskrävande bakgrundsarbete i minuter eller under längre perioder. I dessa fall kan du inte vänta tills arbetet har slutförts innan du skickar ett svar. Den här situationen kan skapa ett problem för synkrona mönster för begäran-svar. För vägledning om hur du utformar bakgrundsbehandling, se Bakgrundsjobb.
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. Många system uppnår denna separation genom kö-baserade mönstret för belastningsutjämning. Med den här separationen kan klientprocessen och serverdels-API:et skalas oberoende av varandra. Det ger också extra komplexitet när klienten behöver ett meddelande om lyckad åtgärd eftersom det steget också måste bli asynkront.
Många av de överväganden som gäller 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. Pollning fungerar bra för kod på klientsidan när återuppringningsslutpunkter inte är tillgängliga eller när långlivade anslutningar lägger till för mycket komplexitet. Även om återanrop är möjliga kan de extra bibliotek och tjänster som de behöver öka komplexiteten.
Följande steg beskriver lösningen:
Klientprogrammet gör ett synkront anrop till API:et för att utlösa en tidskrävande åtgärd på serverdelen.
API:et svarar synkront så snabbt som möjligt. Den returnerar en HTTP 202-statuskod (godkänd) för att bekräfta att den tog emot begäran om bearbetning.
Anmärkning
API:et bör verifiera begäran och den åtgärd som ska utföras innan den startar den tidskrävande processen. Om begäran inte är giltig 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 kontrollera 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 returnerar slutpunkten HTTP 200 (OK). Medan arbetet pågår returnerar statusslutpunkten en resurs som anger det tillståndet. Statussvarstexten bör innehålla tillräckligt med information för att klienten ska förstå åtgärdens aktuella tillstånd.
När arbetet är klart returnerar statusslutpunkten en resurs som anger slutförande eller omdirigeringar till en annan resurs-URL. Om den asynkrona åtgärden till exempel skapar en ny resurs omdirigeras statusslutpunkten till URL:en för resursen.
Följande diagram visar ett typiskt flöde.
Klienten skickar en begäran och tar emot ett HTTP 202-svar (godkänt).
Klienten skickar en HTTP GET-begäran till statusslutpunkten. Det här anropet returnerar HTTP 200 eftersom arbetet väntar.
Vid något tillfälle slutförs arbetet och statusslutpunkten returnerar HTTP 303 (se Annat) för att omdirigera till resursen.
Klienten hämtar resursen på den angivna URL:en.
Problem och överväganden
Tänk på följande när du bestämmer hur du ska implementera det här mönstret:
Det finns flera sätt att implementera det här mönstret via HTTP, och överordnade tjänster använder inte alltid samma semantik. Vissa implementeringar använder till exempel inte en separat statusslutpunkt. I stället avsöker klienten målresursens URL direkt och tar emot HTTP 404 (hittades inte) tills resursen har skapats. Det här svaret genereras eftersom resursen inte finns ännu. Den här metoden kan dock vara oklar eftersom ogiltiga begärande-ID:er också returnerar HTTP 404. En dedikerad statusslutpunkt som returnerar HTTP 200 med en statustext, enligt beskrivningen i det här mönstret, undviker den här förvirringen.
Ett HTTP 202-svar anger var klienten avsöker och hur ofta. Den bör innehålla följande rubriker.
Header Beskrivning Notes LocationEn URL som klienten söker efter svarsstatus Den här URL:en kan vara en SAS-token (signatur för delad åtkomst). Valet Key-mönstret fungerar bra när den här platsen behöver åtkomstkontroll. Mönstret gäller även när svarssökningen måste flyttas till en annan serverdel. Retry-AfterEn uppskattad slutförandetid för bearbetning Den här rubriken hjälper pollingklienter att undvika att skicka för många begäranden till back-end. Överväg förväntat klientbeteende när du utformar det här svaret. En klient som du styr kan följa dessa svarsvärden exakt. Klienter som andra skapar, inklusive klienter som skapats med hjälp av verktyg utan kod eller låg kod som Azure Logic Apps, kan använda sin egen hantering för HTTP 202.
Överväg att inkludera följande fält i statusslutpunktssvaret.
Fält Beskrivning Notes statusDet aktuella tillståndet för åtgärden, till exempel Väntar, Körs, Lyckades, Misslyckades eller Avbryts Använder en konsekvent, dokumenterad uppsättning terminal- och icke-terminella värden createdAtDen tid då åtgärden accepterades Hjälper klienter att identifiera inaktuella eller övergivna åtgärder lastUpdatedAtDen tid då statusen senast uppdaterades Hjälper klienter att skilja mellan fördröjda och pågående åtgärder percentCompleteEn valfri förloppsindikator Användbart när backend kan uppskatta förloppet errorEtt strukturerat felobjekt när statusen misslyckades För konsekvens bör du överväga att använda RFC 9457-formatet . Du kan behöva använda en bearbetningsproxy för att justera svarshuvudena eller nyttolasten, beroende på de underliggande tjänster som du använder.
Om statusslutpunkten omdirigeras efter slutförandet använder du HTTP 303 (se annat). A 303 instruerar klienten att utfärda en GET-begäran till omdirigerings-URL:en, oavsett den ursprungliga begärandemetoden. Det här beteendet är det korrekta semantiska för det här mönstret eftersom klienten hämtar en tydlig resultatresurs, inte skickar in den ursprungliga åtgärden på nytt. HTTP 302 (hittades) garanterar inte någon metodändring. Vissa klienter spelar upp den ursprungliga metoden vid omdirigering. Det här beteendet kan orsaka oavsiktliga biverkningar, till exempel duplicerade POST-begäranden.
När servern har bearbetat begäran returnerar resursen
Locationsom rubriken anger en HTTP-statuskod som 200, 201 (skapad) eller 204 (inget innehåll).Om ett fel uppstår under bearbetningen bevarar du felet på resurs-URL:en som
Locationhuvudet anger och returnerar en 4xx-statuskod från resursen som matchar felet. Använd ett strukturerat felformat, till exempel RFC 9457 (probleminformation för HTTP-API:er), så att klienter programmässigt kan parsa och hantera fel.Statusresursen och alla lagrade resultat förbrukar lagring och beräkning. Definiera en retentionsprincip för att rensa dem efter en rimlig tidsperiod. Om du vill informera klienterna om kvarhållningsfönstret kan du lägga till en
Expiresrubrik i statussvaret.Lösningar implementerar inte alla det här mönstret på samma sätt, och vissa tjänster innehåller extra eller alternativa rubriker. Till exempel använder Azure Resource Manager en modifierad variant av det här mönstret. Mer information finns i Resource Manager asynkrona åtgärder.
Ä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. Logic Apps stöder till exempel det här mönstret internt och du kan använda det som ett integreringslager mellan ett asynkront API och en klient som gör synkrona anrop. Mer information finns i Asynkront beteende för begärandesvar i Logic Apps.
Om du vill ange ett sätt för klienter att avbryta en tidskrävande begäran exponerar du en DELETE-åtgärd för statusslutpunktsresursen. Den här begäran bör vidarebefordra en instruktion om annullering till serverdelsbearbetningskomponenten. Efter att back end har hanterat annulleringen bör den uppdatera statusresursen för att återspegla ett annullerat tillstånd. Den här processen hjälper till att förhindra att ofullständigt arbete förbrukar resurser på obestämd tid. Avgör om åtgärden stöder partiell återställning eller kräver en kompenserande transaktion.
Du kan kräva att klienter anger en idempotensnyckel, till exempel i ett
Idempotency-Keybegärandehuvud, när de skickar den första begäran. Om back-end tar emot en duplicerad nyckel bör den returnera den befintliga statusresursen i stället för att ange ett andra arbetsobjekt. Den här metoden skyddar mot nätverksfel som gör att klienten försöker igen med en POST som servern redan har accepterat. Det är särskilt viktigt i det här mönstret eftersom klienten inte kan skilja mellan ett förlorat svar och en begäran som aldrig togs emot.
Anmärkning
Det här mönstret beskriver HTTP-avsökning, där klienten regelbundet utfärdar nya begäranden för att kontrollera status. Vid lång avsökning skickar klienten en begäran och servern håller anslutningen öppen tills nya data är tillgängliga eller en tidsgräns uppnås. Lång avsökning minskar svarsfördröjningen jämfört med periodisk avsökning, men det ger komplexitet kring anslutningshantering och tidsgränser.
När du ska använda det här mönstret
Använd det här mönstret i sådana här scenarier:
Du arbetar med kod på klientsidan, till exempel webbläsarprogram, och dessa begränsningar gör det svårt att tillhandahålla återanropsslutpunkter, eller att långvariga anslutningar lägger till för mycket komplexitet.
Du anropar en tjänst som endast använder HTTP-protokollet och returtjänsten kan inte skicka återanrop på grund av brandväggsbegränsningar på klientsidan.
Du integrerar med arbetsflöden som inte stöder moderna återkopplingsmekanismer 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. Överväg att använda Server-Sent-händelser (SSE), som tillhandahåller en enkel, HTTP-intern, enkelriktad pushkanal från server till klient utan att klienten behöver fråga.
Klienten måste samla in många resultat och svarstiden för dessa resultat är viktig. Överväg att använda en meddelandekö i stället.
Beständiga nätverksanslutningar på serversidan som WebSockets eller SignalR är tillgängliga. Du kan använda dessa anslutningar för att meddela anroparen om resultatet.
Nätverksdesignen stöder öppna portar för att ta emot asynkrona återanrop eller webhooks.
Design av arbetsbelastning
En arkitekt bör utvärdera hur de kan använda det asynkrona Request-Reply mönstret i arbetsbelastningens design för att uppfylla de mål och principer som beskrivs i Azure Well-Architected Framework-pelarna.
| Grundpelare | Så här stöder det här mönstret pelarmål |
|---|---|
| Prestandaeffektivitet hjälper din arbetsbelastning effektivt uppfylla kraven genom optimering av skalning, data och kod. | Du förbättrar svarstiden och skalbarheten genom att avkoda begärande- och svarsfaserna för processer som inte kräver ett omedelbart svar. En asynkron metod ökar samtidigheten och låter servern schemalägga arbete när kapaciteten blir tillgänglig. - PE:05 Skalning och partitionering - PE:07 Kod och infrastruktur |
Som med alla designbeslut bör du överväga kompromisser mot målen för de andra pelarna som det här mönstret kan införa.
Exempel
Följande kod visar utdrag från ett program som använder Azure Functions för att implementera det här mönstret. Den här lösningen har tre funktioner:
- Den asynkrona API-slutpunkten
- Statusslutpunkten
- En serverdelsfunktion som tar köade arbetsobjekt och kör dem
Det här exemplet finns på GitHub.
Implementeringen använder hanterad identitet för att autentisera med Azure Service Bus och Azure Blob Storage, vilket förhindrar lagring av anslutningssträngar eller kontonycklar. Beroenden registreras i Program.cs genom att använda DefaultAzureCredential och injiceras genom primära konstruktorer.
Funktionen AsyncProcessingWorkAcceptor
Funktionen AsyncProcessingWorkAcceptor implementerar en slutpunkt som accepterar arbete från ett klientprogram och köar det för bearbetning.
Funktionen genererar ett begärande-ID och lägger till det som metadata i kömeddelandet.
HTTP-svaret innehåller en
Locationrubrik som pekar på en statusslutpunkt och ettRetry-Afterhuvud som föreslår ett avsökningsintervall. Begärande-ID:t visas i URL-sökvägen.
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);
}
}
Funktionen AsyncProcessingBackgroundWorker
Funktionen AsyncProcessingBackgroundWorker läser åtgärden från kön, bearbetar den baserat på meddelandets nyttolast och skriver resultatet till ett lagringskonto.
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);
}
}
}
Funktionen AsyncOperationStatusChecker
Funktionen AsyncOperationStatusChecker implementerar statusslutpunkten. Den här funktionen kontrollerar statusen för begäran:
Om begäran slutförs returnerar funktionen HTTP 303 (se Övrigt) och omdirigerar klienten till en valet-nyckel-URL för resultatet.
Om begäran väntar returnerar funktionen en HTTP 200-kod som innehåller det aktuella tillståndet.
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
}