Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Separare l'elaborazione back-end da un host front-end quando l'elaborazione back-end deve essere eseguita in modo asincrono, ma il front-end richiede una risposta chiara.
Contesto e problema
Nello sviluppo di applicazioni moderne, le applicazioni client spesso dipendono dalle API remote per fornire la logica di business e comporre funzionalità. Molte applicazioni eseguono codice in un Web browser e altri ambienti ospitano anche il codice client. Le API possono essere correlate direttamente all'applicazione o che operano come servizi condivisi da un servizio esterno. La maggior parte delle chiamate API usa HTTP o HTTPS e segue la semantica REST.
Nella maggior parte dei casi, le API per un'applicazione client rispondono in circa 100 millisecondi (ms) o meno. Molti fattori possono influire sulla latenza della risposta:
- Stack di hosting dell'applicazione
- Componenti di sicurezza
- Posizione geografica relativa del chiamante e del back-end
- Infrastruttura di rete
- Carico attuale
- Dimensioni del payload della richiesta
- Lunghezza coda di elaborazione
- Tempo per l'elaborazione della richiesta da parte del back-end
Questi fattori possono aggiungere latenza alla risposta. È possibile attenuare alcuni fattori aumentando il back-end. Altri fattori, ad esempio l'infrastruttura di rete, non sono al di fuori del controllo dello sviluppatore dell'applicazione. La maggior parte delle API risponde abbastanza rapidamente per restituire la risposta sulla stessa connessione. Il codice dell'applicazione può effettuare una chiamata API sincrona in modo non bloccante per dare l'aspetto dell'elaborazione asincrona. È consigliabile usare questo approccio per le operazioni associate a input e output (I/O).
In alcuni scenari, il back-end esegue operazioni prolungate e può richiedere alcuni secondi. In altri scenari, il backend esegue operazioni in background a lungo termine per alcuni minuti o per periodi estesi. In questi casi, non è possibile attendere il completamento del lavoro prima di inviare una risposta. Questa situazione può creare un problema per i modelli di richiesta-risposta sincroni. Per indicazioni sulla progettazione dell'elaborazione back-end, vedere Processi in background.
Alcune architetture risolvono questo problema usando un broker di messaggi per separare le fasi di richiesta e risposta. Molti sistemi ottengono questa separazione tramite il modello di livellamento del caricoQueue-Based. Questa separazione consente al processo client e all'API back-end di essere ridimensionati in modo indipendente. Introduce anche una maggiore complessità quando il client richiede una notifica di esito positivo perché anche questo passaggio deve diventare asincrono.
Molte delle stesse considerazioni che si applicano alle applicazioni client si applicano anche alle chiamate API REST da server a server nei sistemi distribuiti, ad esempio in un'architettura di microservizi.
Soluzione
Una soluzione a questo problema consiste nell'usare il polling HTTP. Il polling funziona bene per il codice lato client quando gli endpoint di callback non sono disponibili o quando le connessioni persistenti aggiungono troppa complessità. Anche quando i callback sono possibili, le librerie e i servizi aggiuntivi necessari possono aumentare la complessità.
I passaggi seguenti descrivono la soluzione:
L'applicazione client effettua una chiamata sincrona all'API per attivare un'operazione a esecuzione prolungata sul back-end.
L'API risponde in modo sincrono il più rapidamente possibile. Restituisce un codice di stato HTTP 202 (accettato) per confermare che ha ricevuto la richiesta di elaborazione.
Annotazioni
L'API deve convalidare la richiesta e l'azione da eseguire prima di avviare il processo a esecuzione prolungata. Se la richiesta non è valida, rispondere immediatamente con un codice di errore come HTTP 400 (richiesta non valida).
La risposta include un riferimento alla posizione che punta a un endpoint che il client può utilizzare per controllare il risultato dell'operazione a lunga durata.
L'API esegue l'offload dell'elaborazione in un altro componente, ad esempio una coda di messaggi.
Per ogni chiamata riuscita all'endpoint di stato, l'endpoint restituisce HTTP 200 (OK). Mentre il lavoro è in corso, l'endpoint di stato restituisce una risorsa che indica tale stato. Il corpo della risposta dello stato deve includere informazioni sufficienti per il client per comprendere lo stato corrente dell'operazione.
Al termine del lavoro, l'endpoint di stato restituisce una risorsa che indica il completamento o il reindirizzamento a un altro URL della risorsa. Ad esempio, se l'operazione asincrona crea una nuova risorsa, l'endpoint di stato reindirizza all'URL per tale risorsa.
Il diagramma seguente mostra un flusso tipico.
Il client invia una richiesta e riceve una risposta HTTP 202 (accettata).
Il client invia una richiesta HTTP GET all'endpoint di stato. Il lavoro è ancora in sospeso, quindi questa chiamata restituisce HTTP 200.
A un certo punto, il lavoro viene completato e l'endpoint di stato restituisce HTTP 303 (vedere Altro) per reindirizzare alla risorsa.
Il client recupera la risorsa nell'URL specificato.
Problemi e considerazioni
Quando si decide come implementare questo modello, tenere presente quanto segue:
Esistono più modi per implementare questo modello su HTTP e i servizi upstream non usano sempre la stessa semantica. Ad esempio, alcune implementazioni non usano un endpoint di stato separato. Il client esegue invece il polling diretto dell'URL della risorsa di destinazione e riceve HTTP 404 (Non trovato) fino a quando non viene creata la risorsa. Questa risposta ha senso perché la risorsa non esiste ancora. Tuttavia, questo approccio può essere ambiguo se anche 404 viene restituito per ID richiesta non validi. Un endpoint di stato dedicato che restituisce HTTP 200 con un corpo di stato, come descritto in questo modello, evita tale ambiguità.
Una risposta HTTP 202 indica dove il client esegue il polling e la frequenza. Dovrebbe includere le seguenti intestazioni.
Intestazione Descrizione Note LocationURL a cui il client controlla lo stato della risposta Questo URL può essere un token di firma di accesso condiviso. Il modello Valet Key funziona bene quando questa posizione richiede il controllo di accesso. Il modello si applica anche quando il polling delle risposte deve essere trasferito a un altro back-end. Retry-AfterStima di quando l'elaborazione sarà completata Questa intestazione è progettata per impedire ai client di polling di inviare troppe richieste al back-end. Prendere in considerazione il comportamento previsto del client quando si progetta questa risposta. Un client controllato può seguire esattamente questi valori di risposta. I client creati da altri utenti, inclusi i client creati tramite strumenti senza codice o con poco codice come Azure Logic Apps, possono applicare la propria gestione per HTTP 202.
Prendere in considerazione l'inclusione dei campi seguenti nella risposta all'endpoint di stato.
Campo Descrizione Note statusStato corrente dell'operazione, ad esempio Pending, Running, Succeeded, Failed o Canceled. Usare un set coerente e documentato di valori terminal e non terminal. createdAtOra in cui l'operazione è stata accettata. Consente ai clienti di rilevare operazioni non aggiornate o abbandonate. lastUpdatedAtOra dell'ultimo aggiornamento dello stato. Consente ai client di distinguere un'operazione bloccata da una che sta procedendo attivamente. percentCompleteIndicatore di stato facoltativo. Utile quando il back-end può stimare significativamente lo stato di avanzamento. errorOggetto errore strutturato quando lo stato è Failed. Prendere in considerazione l'uso del formato RFC 9457 per coerenza. Potrebbe essere necessario usare un proxy di elaborazione per modificare le intestazioni o il payload della risposta, a seconda dei servizi sottostanti usati.
Se l'endpoint di stato viene reindirizzato dopo il completamento, usare HTTP 303 (vedere Altro).If the status endpoint redirects after completion, use HTTP 303 (See Other). Un valore 303 indica al client di emettere una richiesta GET all'URL di reindirizzamento, indipendentemente dal metodo di richiesta originale. Questo comportamento è la semantica corretta per questo modello perché il client sta recuperando una risorsa risultato distinta, non inviando nuovamente l'operazione originale. HTTP 302 (Trovato) non garantisce una modifica del metodo; alcuni client replayno il metodo originale sul reindirizzamento, che può causare effetti collaterali imprevisti, ad esempio richieste POST duplicate.
Dopo che il server elabora correttamente la richiesta, la risorsa specificata dall'intestazione
Locationrestituisce un codice di stato HTTP come 200, 201 (Creato) o 204 (Nessun contenuto).Se si verifica un errore durante l'elaborazione, rendere persistente l'errore nell'URL della risorsa specificato dall'intestazione
Locatione restituire un codice di stato 4xx da tale risorsa che corrisponde all'errore. Usare un formato di errore strutturato, ad esempio RFC 9457 (Dettagli del problema per le API HTTP) in modo che i client possano analizzare e gestire gli errori a livello di codice.La risorsa di stato e i risultati archiviati usano l'archiviazione e il calcolo. Definire un criterio di conservazione per pulirli dopo un periodo ragionevole e valutare la possibilità di comunicare la finestra di conservazione ai client tramite un'intestazione
Expiressulla risposta di stato.Le soluzioni non implementano tutti questo modello allo stesso modo e alcuni servizi includono intestazioni aggiuntive o alternative. Ad esempio, Azure Resource Manager usa una variante modificata di questo modello. Per ulteriori informazioni, vedere le operazioni asincrone del Resource Manager.
I client legacy potrebbero non supportare questo modello. In tal caso, potrebbe essere necessario posizionare una facciata sull'API asincrona per nascondere l'elaborazione asincrona dal client originale. Ad esempio, App per la logica supporta questo modello in modo nativo ed è possibile usarlo come livello di integrazione tra un'API asincrona e un client che effettua chiamate sincrone. Per altre informazioni, vedere Comportamento asincrono di richiesta-risposta in App per la logica di Azure.
In alcuni scenari, potrebbe essere necessario fornire ai client un modo per annullare una richiesta a esecuzione prolungata. In tal caso, esponi un'operazione DELETE sulla risorsa endpoint dello stato. Questa richiesta deve inoltrare un'istruzione di annullamento al componente di elaborazione back-end. Dopo che il back-end gestisce l'annullamento, deve aggiornare la risorsa di stato in modo da riflettere lo stato annullato. Questo processo consente di evitare che il lavoro incompleto consumi le risorse a tempo indeterminato. Valutare se l'operazione supporta il rollback parziale o è meglio considerata come transazione di compensazione.
È consigliabile far in modo che i client forniscano una chiave di idempotenza (ad esempio, in un'intestazione
Idempotency-Keydi richiesta) quando si invia la richiesta iniziale. Se il back-end riceve una chiave duplicata, deve restituire la risorsa di stato esistente anziché accodare un secondo elemento di lavoro. Questo approccio protegge da errori di rete che causano il client a ripetere una richiesta POST che il server ha già accettato. È particolarmente importante in questo modello perché il client non ha modo di distinguere una risposta persa da una richiesta che non è mai stata ricevuta.
Annotazioni
Questo modello descrive il polling HTTP, in cui il client invia periodicamente nuove richieste per controllare lo stato. Il polling lungo è una tecnica correlata ma distinta: il client invia una richiesta e il server mantiene aperta la connessione fino a quando non sono disponibili nuovi dati o si verifica un timeout. Il polling lungo riduce la latenza di risposta rispetto al polling periodico, ma introduce complessità per la gestione delle connessioni e i timeout.
Quando usare questo modello
Usare questo modello quando:
Lavori con codice lato client, come le applicazioni browser, e questi vincoli rendono difficile fornire endpoint di callback, o le connessioni di lunga durata aggiungono troppa complessità.
Viene chiamato un servizio che utilizza solo il protocollo HTTP e il servizio di ritorno non può inviare callback a causa delle restrizioni del firewall dal lato del client.
È possibile eseguire l'integrazione con carichi di lavoro che non supportano meccanismi di callback moderni, ad esempio WebSocket o webhook.
Questo modello potrebbe non essere adatto quando:
È invece possibile usare un servizio compilato per le notifiche asincrone, ad esempio Azure Event Grid.
Le risposte devono essere trasmesse in tempo reale al client. Considerare Server-Sent Events (SSE), che forniscono un canale push unidirezionale, leggero e nativo HTTP dal server al client senza che il client debba eseguire il polling.
Il client deve raccogliere molti risultati e la latenza di questi risultati è importante. Si consideri invece un broker di messaggi.
Sono disponibili connessioni di rete persistenti sul lato server, ad esempio WebSocket o SignalR. È possibile usare queste connessioni per notificare al chiamante il risultato.
La progettazione di rete supporta porte aperte per ricevere callback asincroni o webhook.
Progettazione del carico di lavoro
Un architetto deve valutare come usare il modello asincrono Request-Reply nella progettazione del carico di lavoro per soddisfare gli obiettivi e i principi trattati nei pilastri del framework Azure Well-Architected.
| Pilastro | Come questo modello supporta gli obiettivi di pilastro |
|---|---|
| l'efficienza delle prestazioni consente al carico di lavoro soddisfare in modo efficiente le richieste tramite ottimizzazioni di ridimensionamento, dati e codice. | È possibile migliorare la velocità di risposta e la scalabilità separando le fasi di richiesta e risposta per i processi che non richiedono una risposta immediata. Un approccio asincrono aumenta la concorrenza e consente al server di pianificare il lavoro man mano che la capacità diventa disponibile. - PE:05 Ridimensionamento e partizionamento - PE:07 Codice e infrastruttura |
Come per qualsiasi decisione di progettazione, prendere in considerazione compromessi rispetto agli obiettivi degli altri pilastri che questo modello potrebbe introdurre.
Esempio
Il codice seguente mostra estratti da un'applicazione che usa Azure Functions per implementare questo modello. Questa soluzione ha tre funzioni:
- API endpoint asincrono
- Endpoint di stato
- Funzione back-end che accetta elementi di lavoro in coda e li esegue
Questo esempio è disponibile in GitHub.
L'implementazione utilizza l'identità gestita per autenticarsi con Azure Service Bus e Azure Blob Storage, evitando così di archiviare stringhe di connessione o chiavi dell'account. Le dipendenze vengono registrate in Program.cs usando DefaultAzureCredential e inserite tramite costruttori primari.
Funzione AsyncProcessingWorkAcceptor
La AsyncProcessingWorkAcceptor funzione implementa un endpoint che accetta un task da un'applicazione client e lo accoda per l'elaborazione.
La funzione genera un ID di richiesta e lo aggiunge come metadati al messaggio della coda.
La risposta HTTP include un'intestazione
Locationche indirizza a un endpoint di stato e un'intestazioneRetry-Afterche indica un intervallo di polling. L'ID richiesta viene visualizzato nel percorso 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);
}
}
La funzione AsyncProcessingBackgroundWorker
La AsyncProcessingBackgroundWorker funzione legge l'operazione dalla coda, la elabora in base al payload del messaggio e scrive il risultato in un account di archiviazione.
public class AsyncProcessingBackgroundWorker(BlobContainerClient _blobContainerClient)
{
[Function("AsyncProcessingBackgroundWorker")]
public async Task Run(
[ServiceBusTrigger("outqueue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage message)
{
// Perform an actual action against the blob data source for the async readers to be able to check against.
// This is where your actual 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);
}
}
}
Funzione AsyncOperationStatusChecker
La AsyncOperationStatusChecker funzione implementa l'endpoint di stato. Questa funzione controlla lo stato della richiesta:
Se la richiesta viene completata, la funzione restituisce HTTP 303 (vedere Altro), reindirizzando il client a un URL della chiave di controllo per il risultato.
Se la richiesta è in sospeso, la funzione restituisce un codice HTTP 200 che include lo stato corrente.
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 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
}
Passaggi successivi
- App per la logica di Azure: comportamento asincrono di richiesta-risposta.
- Per le procedure consigliate generali per la progettazione di un'API Web, vedere Progettazione api Web.