Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Dissociez le traitement back-end d’un hôte frontal lorsque le traitement back-end doit s’exécuter de manière asynchrone, mais le serveur frontal a besoin d’une réponse claire.
Contexte et problème
Dans le développement d’applications modernes, les applications clientes s’appuient souvent sur des API distantes pour fournir une logique métier et composer des fonctionnalités. De nombreuses applications exécutent du code dans un navigateur web, et d’autres environnements hébergent également du code client. Les API peuvent être liées directement à l’application ou fonctionner en tant que services partagés à partir d’un service externe. La plupart des appels d’API utilisent HTTP ou HTTPS et suivent la sémantique REST.
Dans la plupart des cas, les API d’une application cliente répondent en environ 100 millisecondes (ms) ou moins. De nombreux facteurs peuvent affecter la latence de réponse :
- Stack d’hébergement de l’application
- Composants de sécurité
- Emplacement géographique relatif de l’appelant et du serveur principal
- Infrastructure réseau
- Chargement actuel
- Taille de la charge utile de la requête
- Longueur de la file d’attente de traitement
- Heure du traitement de la demande par le serveur principal
Ces facteurs peuvent ajouter une latence à la réponse. Vous pouvez atténuer certains facteurs en effectuant un scale-out du serveur principal. D’autres facteurs, comme l’infrastructure réseau, sont en dehors du contrôle du développeur d’applications. La plupart des API répondent assez rapidement pour que la réponse retourne sur la même connexion. Le code d’application peut effectuer un appel d’API synchrone de manière non bloquante pour donner l’apparence du traitement asynchrone. Nous vous recommandons cette approche pour les opérations liées aux entrées et aux sorties (E/S).
Dans certains scénarios, le serveur principal effectue un travail qui, bien que long, ne prend que quelques secondes. Dans d’autres scénarios, le serveur principal effectue un travail en arrière-plan long pendant des minutes ou pendant des périodes prolongées. Dans ces cas, vous ne pouvez pas attendre la fin du travail avant d’envoyer une réponse. Cette situation peut créer un problème pour les modèles de demande-réponse synchrones.
Certaines architectures résolvent ce problème à l’aide d’un courtier de messages permettant de séparer les étapes de demande et de réponse. De nombreux systèmes obtiennent cette séparation par le biais du modèle d'équilibrage de charge basé sur une file d'attente. Cette séparation permet au processus client et à l'API back-end de monter en charge de manière indépendante. Il introduit également une complexité supplémentaire lorsque le client nécessite une notification de réussite, car cette étape doit également devenir asynchrone.
La plupart des mêmes considérations que celles qui s’appliquent aux applications clientes s’appliquent également aux appels d’API REST serveur à serveur dans des systèmes distribués, comme dans une architecture de microservices.
Solution
Une des solutions à ce problème consiste à utiliser l’interrogation HTTP. La technique de sondage fonctionne bien pour le code côté client lorsque les points de terminaison de rappel ne sont pas disponibles ou lorsque les connexions longues deviennent trop complexes. Même lorsque les callbacks sont possibles, les bibliothèques et services supplémentaires qu'ils nécessitent peuvent augmenter la complexité.
Les étapes suivantes décrivent la solution :
L’application cliente effectue un appel synchrone à l’API pour déclencher une opération de longue durée sur le serveur principal.
L’API répond de manière synchrone aussi rapidement que possible. Elle retourne un code d’état HTTP 202 (accepté) pour confirmer qu’il a reçu la demande de traitement.
Note
L’API valide la requête et l’action à effectuer avant de démarrer le processus de longue durée. Si la demande n’est pas valide, répondez immédiatement avec un code d’erreur tel que HTTP 400 (requête incorrecte).
La réponse inclut une référence d’emplacement qui pointe vers un point de terminaison que le client peut interroger pour vérifier le résultat de l’opération de longue durée.
L’API décharge le traitement vers un autre composant, comme une file d’attente de messages.
Pour un appel réussi au point de terminaison d’état, le point de terminaison retourne HTTP 200 (OK). Pendant que le travail est en cours, le point de terminaison retourne une ressource qui indique cet état. Une fois le travail terminé, le point de terminaison retourne une ressource qui indique l’achèvement ou la redirection vers une autre URL de ressource. Par exemple, si l’opération asynchrone crée une ressource, le point de terminaison d’état redirige vers l’URL de cette ressource.
Le diagramme suivant illustre un flux classique.
Le client envoie une requête et reçoit une réponse HTTP 202.
Le client envoie une requête HTTP GET au point de terminaison d’état. Le travail est en attente. Cet appel retourne donc HTTP 200.
Le travail se termine et le point de terminaison d’état retourne HTTP 302 (Trouvé) pour rediriger vers la ressource.
Le client extrait la ressource à l’URL spécifiée.
Problèmes et considérations
Tenez compte des points suivants lorsque vous décidez comment implémenter ce modèle :
Il existe plusieurs façons d’implémenter ce modèle sur HTTP, et les services en amont n’utilisent pas toujours la même sémantique. Par exemple, la plupart des services retournent HTTP 404 (introuvable) à partir d’une méthode GET lorsqu’un processus distant n’est pas terminé, plutôt que HTTP 202. Selon la sémantique REST standard, HTTP 404 est la réponse correcte, car le résultat de l’appel n’existe pas encore.
Une réponse HTTP 202 indique où le client interroge et à quelle fréquence. Il inclut les en-têtes suivants.
Header Description Remarques LocationURL que le client interroge pour obtenir un état de réponse Cette URL peut être un jeton de signature d’accès partagé. Le modèle de clé valet fonctionne bien lorsque cet emplacement a besoin d’un contrôle d’accès. Le modèle s’applique également lorsque l’interrogation de réponse doit se déplacer vers un autre serveur principal. Retry-AfterUne estimation de la fin du traitement Cet en-tête empêche les clients de sondage d'envoyer trop de requêtes au back-end. Prenez en compte le comportement du client attendu lorsque vous concevez cette réponse. Un client que vous contrôlez peut suivre exactement ces valeurs de réponse. Les clients qu'autorisent d'autres, y compris les clients créés à l’aide d’outils sans code ou low-code comme Azure Logic Apps, peuvent gérer eux-mêmes le HTTP 202.
Vous devrez peut-être utiliser un proxy de traitement pour ajuster les en-têtes de réponse ou la charge utile, en fonction des services sous-jacents que vous utilisez.
Si le point de terminaison d’état est redirigé après l’achèvement, HTTP 302 ou HTTP 303 (voir Autres) sont des codes de retour valides, selon la sémantique que vous prenez en charge.
Une fois que le serveur traite la requête, la ressource spécifiée par l’en-tête
Locationretourne un code d’état HTTP tel que 200, 201 (créé) ou 204 (aucun contenu).Si une erreur se produit pendant le traitement, enregistrez l’erreur à l’URL de ressource spécifiée par l’en-tête
Locationet renvoyez un code d’état 4xx de cette ressource qui correspond à l’échec.Les solutions n’implémentent pas tous ce modèle de la même façon, et certains services incluent des en-têtes supplémentaires ou alternatifs. Par exemple, Azure Resource Manager utilise une variante modifiée de ce modèle. Pour plus d’informations, consultez Resource Manager opérations asynchrones.
Les clients légacy pourraient ne pas prendre en charge ce schéma. Dans ce cas, vous devrez peut-être placer un proxy de traitement sur l’API asynchrone pour masquer le traitement asynchrone du client d’origine. Par exemple, Logic Apps prend en charge ce modèle en mode natif et vous pouvez l’utiliser comme couche d’intégration entre une API asynchrone et un client qui effectue des appels synchrones. Pour plus d’informations, consultez Effectuer des tâches longues avec le modèle d’action webhook.
Dans certains scénarios, vous souhaiterez peut-être offrir aux clients un moyen d’annuler une requête de longue durée. Dans ce cas, le service back-end doit prendre en charge un type d’instruction d’annulation.
Quand utiliser ce modèle
Utilisez ce modèle dans les situations suivantes :
Vous travaillez avec du code côté client, comme les applications de navigateur, et ces contraintes rendent les points d'entrée de rappel difficiles à fournir, ou les connexions de longue durée ajoutent trop de complexité.
Vous appelez un service qui utilise uniquement le protocole HTTP et le service de retour ne peut pas envoyer de rappels en raison de restrictions de pare-feu côté client.
Vous intégrez des architectures héritées qui ne prennent pas en charge les mécanismes de rappel modernes tels que WebSockets ou webhooks.
Ce modèle peut ne pas convenir lorsque :
Vous pouvez utiliser un service créé pour les notifications asynchrones à la place, comme Azure Event Grid.
Les réponses doivent être transmises en temps réel au client.
Le client doit collecter de nombreux résultats et la latence de ces résultats est importante. Envisagez plutôt un modèle Service Bus.
Les connexions réseau persistantes côté serveur telles que WebSockets ou SignalR sont disponibles. Vous pouvez utiliser ces connexions pour avertir l’appelant du résultat.
La conception réseau prend en charge les ports ouverts pour recevoir des rappels asynchrones ou des webhooks.
Conception de la charge de travail
Un architecte doit évaluer comment ils peuvent utiliser le modèle de Request-Reply asynchrone dans la conception de leur charge de travail pour répondre aux objectifs et principes abordés dans les piliers Azure Well-Architected Framework.
| Pilier | Comment ce modèle soutient les objectifs des piliers. |
|---|---|
| L’efficacité des performances permet à votre charge de travail de répondre efficacement aux demandes par le biais d’optimisations de la mise à l’échelle, des données et du code. | Vous améliorez la réactivité et l’évolutivité en découplant les phases de demande et de réponse pour les processus qui ne nécessitent pas de réponse immédiate. Une approche asynchrone augmente la concurrence et permet au serveur de planifier le travail à mesure que la capacité devient disponible. - PE :05 Mise à l’échelle et partitionnement - PE :07 Code et infrastructure |
Comme pour toute décision de conception, envisagez des compromis sur les objectifs des autres piliers que ce modèle pourrait introduire.
Exemple
Le code suivant montre des extraits d’une application qui utilise Azure Functions pour implémenter ce modèle. Cette solution a trois fonctions :
- Point de terminaison d’API asynchrone
- Point de terminaison d’état
- Fonction back-end qui prend les éléments de travail mis en file d’attente et les exécute
Cet exemple est disponible sur GitHub.
Fonction AsyncProcessingWorkAcceptor
La AsyncProcessingWorkAcceptor fonction implémente un point de terminaison qui accepte le travail d’une application cliente et l’met en file d’attente pour le traitement :
La fonction génère un ID de requête et l’ajoute en tant que métadonnée au message de la file d’attente.
La réponse HTTP inclut un
Locationen-tête qui dirige vers un point de terminaison de statut. L’ID de requête s’affiche dans le chemin d’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}");
}
}
Fonction AsyncProcessingBackgroundWorker
La AsyncProcessingBackgroundWorker fonction lit l’opération à partir de la file d’attente, la traite en fonction de la charge utile du message et écrit le résultat dans un compte de stockage.
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);
}
}
}
Fonction AsyncOperationStatusChecker
La fonction AsyncOperationStatusChecker implémente le point de terminaison d’état. Cette fonction vérifie l’état de la requête :
Si la requête est terminée, la fonction retourne une clé de valet vers la réponse ou redirige immédiatement l’appel vers l’URL de la clé de valet.
Si la requête est en attente, la fonction retourne un code HTTP 200 qui inclut l’état actuel.
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
}
La classe suivante CloudBlockBlobExtensions fournit une méthode d'extension qu'utilise le vérificateur de statut pour générer un URI (Uniform Resource Identifier) de signature d'accès partagé de délégation d'utilisateur pour le blob de résultat.
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();
}
}
Étapes suivantes
Ressources associées
- conception d’API web
- Backends for Frontends pattern (Modèle de backends pour frontends)
- Modèle de clé valet