Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Koppel de back-endverwerking los van een front-endhost wanneer de back-endverwerking asynchroon moet worden uitgevoerd, terwijl de front-end wel een duidelijk antwoord nodig heeft.
Context en probleem
Bij het ontwikkelen van moderne toepassingen zijn clienttoepassingen vaak afhankelijk van externe API's om bedrijfslogica te bieden en functionaliteit op te stellen. Veel toepassingen voeren code uit in een webbrowser en andere omgevingen hosten ook clientcode. De API's kunnen rechtstreeks betrekking hebben op de toepassing of werken als gedeelde services van een externe service. De meeste API-aanroepen maken gebruik van HTTP of HTTPS en volgen REST-semantiek.
In de meeste gevallen reageren API's voor een clienttoepassing in ongeveer 100 milliseconden (ms) of minder. Veel factoren kunnen van invloed zijn op de reactielatentie:
- De hostingstack van de toepassing
- Beveiligingsonderdelen
- De relatieve geografische locatie van de beller en de back-end
- Netwerkinfrastructuur
- Huidige belasting
- De grootte van de inhoud van de aanvraag
- Lengte van de verwerkingswachtrij
- De tijd voor het verwerken van de aanvraag door de back-end
Deze factoren kunnen latentie toevoegen aan het antwoord. U kunt enkele factoren beperken door de back-end uit te schalen. Andere factoren, zoals netwerkinfrastructuur, vallen buiten het beheer van de ontwikkelaar van de toepassing. De meeste API's reageren snel genoeg om het antwoord via dezelfde verbinding te retourneren. Toepassingscode kan een synchrone API-aanroep op een niet-blokkerende manier maken om het uiterlijk van asynchrone verwerking te geven. We raden deze aanpak aan voor invoer- en uitvoergebonden bewerkingen (I/O).
In sommige scenario's voert de back-end langdurige processen uit die enkele seconden duren. In andere scenario's doet de back-end langdurig achtergrondwerk voor enkele minuten of langere perioden. In dergelijke gevallen kunt u niet wachten totdat het werk is voltooid voordat u een antwoord verzendt. In deze situatie kan een probleem ontstaan voor synchrone aanvraag-antwoordpatronen.
Sommige architecturen lossen dit probleem op met behulp van een berichtenbroker om de aanvraag- en antwoordfasen te scheiden. Veel systemen bereiken deze verdeling via het Wachtrij-Georiënteerd patroon voor Load Leveling. Met deze scheiding kunnen het clientproces en de back-end-API onafhankelijk worden geschaald. Het introduceert ook extra complexiteit wanneer de client geslaagde meldingen vereist, omdat die stap ook asynchroon moet worden.
Veel van dezelfde overwegingen die van toepassing zijn op clienttoepassingen, zijn ook van toepassing op SERVER-naar-server REST API-aanroepen in gedistribueerde systemen, zoals in een microservicesarchitectuur.
Solution
Een oplossing voor dit probleem is het gebruik van HTTP-polling. Polling werkt goed voor code aan de clientzijde wanneer callback-eindpunten niet beschikbaar zijn of wanneer langlopende verbindingen te veel complexiteit toevoegen. Zelfs wanneer callbacks mogelijk zijn, kunnen de extra bibliotheken en services die ze nodig hebben, de complexiteit verhogen.
In de volgende stappen wordt de oplossing beschreven:
De clienttoepassing roept de API synchroon aan om een langlopende bewerking op de back-end te activeren.
De API reageert synchroon zo snel mogelijk. Er wordt een HTTP 202-statuscode (Geaccepteerd) geretourneerd om te bevestigen dat deze de aanvraag voor verwerking heeft ontvangen.
Opmerking
De API valideert de aanvraag en de actie die moet worden uitgevoerd voordat het langdurige proces wordt gestart. Als de aanvraag niet geldig is, reageert u onmiddellijk met een foutcode zoals HTTP 400 (Ongeldige aanvraag).
Het antwoord bevat een locatieverwijzing die verwijst naar een eindpunt dat de client kan peilen om het resultaat van de langdurige bewerking te controleren.
De API verlegt de verwerking naar een ander onderdeel, zoals een berichtenwachtrij.
Voor een geslaagde aanroep naar het statuseindpunt retourneert het eindpunt HTTP 200 (OK). Terwijl het werk wordt uitgevoerd, retourneert het eindpunt een resource die de status aangeeft. Wanneer het werk is voltooid, retourneert het eindpunt een resource die aangeeft dat deze is voltooid of wordt omgeleid naar een andere resource-URL. Als met de asynchrone bewerking bijvoorbeeld een nieuwe resource wordt gemaakt, wordt het statuseindpunt omgeleid naar de URL voor die resource.
In het volgende diagram ziet u een typische stroom.
De client verzendt een aanvraag en ontvangt een HTTP 202-antwoord.
De client verzendt een HTTP GET-aanvraag naar het statuseindpunt. Het werk is in behandeling, dus deze aanroep retourneert HTTP 200.
Het werk wordt voltooid en het statuseindpunt retourneert HTTP 302 (Gevonden) om om te leiden naar de resource.
De client haalt de resource op bij de opgegeven URL.
Problemen en overwegingen
Houd rekening met de volgende punten wanneer u besluit hoe u dit patroon implementeert:
Er bestaan meerdere manieren om dit patroon te implementeren via HTTP en upstream-services gebruiken niet altijd dezelfde semantiek. De meeste services retourneren bijvoorbeeld HTTP 404 (Niet gevonden) van een GET-methode wanneer een extern proces niet is voltooid, in plaats van HTTP 202. Volgens de standaard REST-semantiek is HTTP 404 het juiste antwoord omdat het resultaat van de aanroep nog niet bestaat.
Een HTTP 202-antwoord geeft aan waar de client pollt en hoe vaak. Het bevat de volgende headers.
Koptekst Beschrijving Aantekeningen LocationEen URL die door de client wordt gecontroleerd op een antwoordstatus Deze URL kan een shared access Signature-token zijn. Het valetsleutelpatroon werkt goed wanneer deze locatie toegangsbeheer nodig heeft. Het patroon geldt ook wanneer de polling van antwoorden naar een andere back-end moet verhuizen. Retry-AfterEen schatting van wanneer de verwerking wordt voltooid Deze header voorkomt dat polling-clients te veel aanvragen naar de back-end verzenden. Houd rekening met het verwachte clientgedrag wanneer u dit antwoord ontwerpt. Een client die u bepaalt, kan deze antwoordwaarden exact volgen. Clients die anderen maken, inclusief clients die zijn gebouwd met behulp van hulpprogramma's zonder code of hulpprogramma's met weinig code, zoals Azure Logic Apps, kunnen hun eigen verwerking toepassen voor HTTP 202.
Mogelijk moet u een verwerkingsproxy gebruiken om de antwoordheaders of payload aan te passen, afhankelijk van de onderliggende services die u gebruikt.
Als het statuseindpunt na voltooiing wordt omgeleid, zijn HTTP 302 of HTTP 303 (Zie overige) geldige retourcodes, afhankelijk van de semantiek die u ondersteunt.
Nadat de server de aanvraag heeft verwerkt, retourneert de resource die de
Locationheader opgeeft een HTTP-statuscode zoals 200, 201 (Gemaakt) of 204 (Geen inhoud).Als er een fout optreedt tijdens de verwerking, houdt u de fout op in de resource-URL die de
Locationheader opgeeft en retourneert u een 4xx-statuscode van die resource die overeenkomt met de fout.Oplossingen implementeren dit patroon niet allemaal op dezelfde manier en sommige services bevatten extra of alternatieve headers. Azure Resource Manager gebruikt bijvoorbeeld een gewijzigde variant van dit patroon. Voor meer informatie zie Resource Manager asynchrone bewerkingen.
Oudere clients ondersteunen dit patroon mogelijk niet. In dat geval moet u mogelijk een verwerkingsproxy plaatsen via de asynchrone API om de asynchrone verwerking van de oorspronkelijke client te verbergen. Logic Apps ondersteunt dit patroon bijvoorbeeld systeemeigen en u kunt dit gebruiken als een integratielaag tussen een asynchrone API en een client die synchrone aanroepen uitvoert. Zie Langlopende taken uitvoeren met het webhookactiepatroon voor meer informatie.
In sommige scenario's wilt u mogelijk een manier bieden voor clients om een langlopende aanvraag te annuleren. In dat geval moet de back-endservice een vorm van annuleringsinstructie ondersteunen.
Wanneer gebruikt u dit patroon?
Gebruik dit patroon wanneer:
U werkt met code aan de clientzijde, zoals browsertoepassingen, en deze beperkingen maken callback-eindpunten moeilijk te bieden, of langdurige verbindingen zorgen voor te veel complexiteit.
U roept een service aan die alleen gebruikmaakt van het HTTP-protocol en de retourservice kan geen callbacks verzenden vanwege firewallbeperkingen aan de clientzijde.
U integreert met verouderde architecturen die geen ondersteuning bieden voor moderne callbackmechanismen, zoals WebSockets of webhooks.
Dit patroon is mogelijk niet geschikt wanneer:
U kunt in plaats daarvan een service gebruiken die is gebouwd voor asynchrone meldingen, zoals Azure Event Grid.
Antwoorden moeten in realtime naar de client worden gestreamd.
De client moet veel resultaten verzamelen en de latentie van deze resultaten is belangrijk. Overweeg in plaats daarvan een Service Bus-patroon.
Permanente netwerkverbindingen aan de serverzijde, zoals WebSockets of SignalR, zijn beschikbaar. U kunt deze verbindingen gebruiken om de beller op de hoogte te stellen van het resultaat.
Het netwerkontwerp ondersteunt open poorten voor het ontvangen van asynchrone callbacks of webhooks.
Werklastontwerp
Een architect moet evalueren hoe ze het Asynchrone Request-Reply-patroon kunnen gebruiken in het ontwerp van hun workload om de doelstellingen en principes aan te pakken die worden behandeld in de pijlers van het Azure Well-Architected Framework.
| Pilaar | Hoe dit patroon ondersteuning biedt voor pijlerdoelen |
|---|---|
| Prestatie-efficiëntie helpt uw workload efficiënt te voldoen aan de vereisten door middel van optimalisaties in schalen, gegevens en code. | U verbetert de reactiesnelheid en schaalbaarheid door de aanvraag- en antwoordfasen te ontkoppelen voor processen waarvoor geen onmiddellijke reactie nodig is. Een asynchrone benadering verhoogt de gelijktijdigheid en zorgt ervoor dat de serverplanning werkt wanneer de capaciteit beschikbaar komt. - PE:05 Schalen en partitioneren - PE:07 Code en infrastructuur |
Net als bij elke ontwerpbeslissing kunt u rekening houden met de doelstellingen van de andere pijlers die dit patroon mogelijk introduceert.
Voorbeeld
De volgende code toont fragmenten van een toepassing die gebruikmaakt van Azure Functions om dit patroon te implementeren. Deze oplossing heeft drie functies:
- Het asynchrone API-eindpunt
- Het statuseindpunt
- Een back-endfunctie die werkitems in de wachtrij neemt en deze uitvoert
Dit voorbeeld is beschikbaar op GitHub.
De functie AsyncProcessingWorkAcceptor
Met de AsyncProcessingWorkAcceptor functie wordt een eindpunt geïmplementeerd dat werk van een clienttoepassing accepteert en in de wachtrij zet voor verwerking.
De functie genereert een aanvraag-id en voegt deze toe als metagegevens aan het wachtrijbericht.
Het HTTP-antwoord bevat een
Locationheader die verwijst naar een statuseindpunt. De aanvraag-id wordt weergegeven in het URL-pad.
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}");
}
}
De functie AsyncProcessingBackgroundWorker
De AsyncProcessingBackgroundWorker functie leest de bewerking uit de wachtrij, verwerkt deze op basis van de nettolading van het bericht en schrijft het resultaat naar een opslagaccount.
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);
}
}
}
De functie AsyncOperationStatusChecker
De AsyncOperationStatusChecker functie implementeert het statuseindpunt. Met deze functie wordt de status van de aanvraag gecontroleerd:
Als de aanvraag is voltooid, retourneert de functie een valetsleutel naar het antwoord of stuurt de aanroep onmiddellijk door naar de valetsleutel-URL.
Als de aanvraag in behandeling is, retourneert de functie een HTTP 200-code die de huidige status bevat.
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
}
De volgende CloudBlockBlobExtensions klasse biedt een uitbreidingsmethode die door de statuscontrole wordt gebruikt voor het genereren van een Shared Access Signature Uniform Resource Identifier (URI) voor de resultaatblob.
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();
}
}