Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Desacoplar o processamento backend de um host frontend quando o processamento backend precisa de ser executado de forma assíncrona, mas o frontend necessita de uma resposta clara.
Contexto e problema
No desenvolvimento moderno de aplicações, as aplicações cliente dependem frequentemente de APIs remotas para fornecer lógica de negócio e compor funcionalidades. Muitas aplicações executam código num navegador web, e outros ambientes também alojam código cliente. As APIs podem estar diretamente relacionadas com a aplicação ou operar como serviços partilhados a partir de um serviço externo. A maioria das chamadas de API utiliza HTTP ou HTTPS e segue a semântica REST.
Na maioria dos casos, as APIs de uma aplicação cliente respondem em cerca de 100 milissegundos (ms) ou menos. Muitos fatores podem afetar a latência de resposta:
- A pilha de alojamento da aplicação
- Componentes de segurança
- A localização geográfica relativa do chamador e do back-end
- Infraestrutura de rede
- Carga de corrente
- O tamanho da carga útil do pedido
- Comprimento da fila de processamento
- O tempo para o back-end processar o pedido
Estes fatores podem adicionar latência à resposta. Podes mitigar alguns fatores escalando o back-end. Outros fatores, como a infraestrutura de rede, estão fora do controlo do programador da aplicação. A maioria das APIs responde rapidamente o suficiente para que a resposta retorne pela mesma ligação. O código de aplicação pode fazer uma chamada de API síncrona de forma não bloqueante para dar a aparência de processamento assíncrono. Recomendamos esta abordagem para operações ligadas à entrada e saída (I/O).
Em alguns cenários, o backend faz um trabalho de longa duração e demora alguns segundos. Noutros cenários, o back-end realiza trabalho de fundo prolongado que dura minutos ou até mais. Nestes casos, não pode esperar que o trabalho termine antes de enviar uma resposta. Esta situação pode criar um problema para padrões síncronos de pedido-resposta.
Algumas arquiteturas resolvem esse problema usando um agente de mensagens para separar os estágios de solicitação e resposta. Muitos sistemas conseguem esta separação através do padrão de Nivelamento de Carga com Fila. Esta separação permite que o cliente processe e a API back-end escale de forma independente. Também introduz complexidade adicional quando o cliente necessita de notificação de sucesso, porque esse passo também tem de se tornar assíncrono.
Muitas das mesmas considerações que se aplicam às aplicações cliente também se aplicam a chamadas de API REST entre servidores em sistemas distribuídos, como numa arquitetura de microserviços.
Solução
Uma solução para esse problema é usar sondagem HTTP. O polling funciona bem para código do lado do cliente quando os endpoints de callback não estão disponíveis ou quando ligações de longa duração acrescentam demasiada complexidade. Mesmo quando os callbacks são possíveis, as bibliotecas e serviços adicionais que necessitam podem aumentar a complexidade.
Os passos seguintes descrevem a solução:
A aplicação cliente faz uma chamada síncrona à API para desencadear uma operação de longa duração no backend.
A API responde de forma síncrona o mais rápido possível. Devolve um código de estado HTTP 202 (Aceite) para confirmar que recebeu o pedido de processamento.
Observação
A API valida o pedido e a ação a realizar antes de iniciar o processo de longa duração. Se o pedido não for válido, responda imediatamente com um código de erro como HTTP 400 (Pedido Mau).
A resposta inclui uma referência de localização que aponta para um endpoint que o cliente pode consultar para verificar o resultado da operação de longa duração.
A API transfere o processamento para outro componente, como uma fila de mensagens.
Para uma chamada bem-sucedida ao endpoint de estado, o endpoint devolve HTTP 200 (OK). Enquanto o trabalho está em andamento, o endpoint devolve um recurso que indica esse estado. Quando o trabalho termina, o endpoint devolve um recurso que indica conclusão ou redireciona para outra URL de recurso. Por exemplo, se a operação assíncrona criar um novo recurso, o endpoint de estado redireciona para a URL desse recurso.
O diagrama seguinte mostra um fluxo típico.
O cliente envia um pedido e recebe uma resposta HTTP 202.
O cliente envia uma solicitação HTTP GET para o endpoint de status. O trabalho está pendente, por isso esta chamada devolve HTTP 200.
O trabalho termina e o endpoint de estado devolve HTTP 302 (Encontrado) para redirecionar para o recurso.
O cliente busca o recurso na URL especificada.
Problemas e considerações
Considere os seguintes pontos ao decidir como implementar este padrão:
Existem várias formas de implementar este padrão sobre HTTP, e os serviços a montante nem sempre usam a mesma semântica. Por exemplo, a maioria dos serviços devolve HTTP 404 (Não Encontrado) de um método GET quando um processo remoto não está completo, em vez de HTTP 202. De acordo com a semântica REST padrão, HTTP 404 é a resposta correta porque o resultado da chamada ainda não existe.
Uma resposta HTTP 202 indica onde o cliente consulta e com que frequência. Inclui os seguintes cabeçalhos.
Cabeçalho Descrição Notes LocationUma URL que o cliente consulta para o estado da resposta Este URL pode ser um token de assinatura de acesso partilhado. O padrão Valet Key funciona bem quando este local precisa de controlo de acesso. O padrão também se aplica quando o response polling precisa de ser transferido para outro backend. Retry-AfterUma estimativa de quando o processamento será concluído Este cabeçalho impede que os clientes de sondagem enviem demasiados pedidos para o backend. Considere o comportamento esperado do cliente ao desenhar esta resposta. Um cliente que você controla pode seguir estes valores de resposta exatamente. Clientes criados por outros, incluindo clientes construídos usando ferramentas no-code ou low-code como o Azure Logic Apps, podem aplicar o seu próprio tratamento para HTTP 202.
Pode precisar de usar um proxy de processamento para ajustar os cabeçalhos de resposta ou a carga útil, dependendo dos serviços subjacentes que utiliza.
Se o endpoint de estado redirecionar após a conclusão, HTTP 302 ou HTTP 303 (ver Outros) são códigos de retorno válidos, dependendo da semântica que suporta.
Depois de o servidor processar o pedido, o recurso que o
Locationcabeçalho especifica devolve um código de estado HTTP como 200, 201 (Criado) ou 204 (Sem Conteúdo).Se ocorrer um erro durante o processamento, registe o erro na URL do recurso especificada pelo cabeçalho
Locatione devolva um código de estado 4xx desse recurso que corresponda à falha.As soluções nem todas implementam este padrão da mesma forma, e alguns serviços incluem cabeçalhos extra ou alternativos. Por exemplo, o Azure Resource Manager utiliza uma variante modificada deste padrão. Para mais informações, veja operações assíncronas do Resource Manager.
Os clientes legados podem não suportar este padrão. Nesse caso, pode ser necessário colocar um proxy de processamento sobre a API assíncrona para esconder o processamento assíncrono do cliente original. Por exemplo, o Logic Apps suporta este padrão de forma nativa, e pode usá-lo como camada de integração entre uma API assíncrona e um cliente que faz chamadas síncronas. Para mais informações, veja Executar tarefas de longa duração com o padrão de ação do webhook.
Em alguns cenários, pode ser necessário oferecer uma forma para os clientes cancelarem um pedido de longa duração. Nesse caso, o serviço de back-end deve suportar algum tipo de instrução de cancelamento.
Quando utilizar este padrão
Utilize este padrão quando:
Trabalhas com código do lado do cliente, como aplicações de navegador, e essas restrições tornam os endpoints de callback difíceis de fornecer, ou ligações de longa duração acrescentam demasiada complexidade.
Chamas um serviço que usa apenas o protocolo HTTP e o serviço de retorno não pode enviar callbacks devido às restrições do firewall do lado do cliente.
Integra-se com arquiteturas legadas que não suportam mecanismos modernos de callback como WebSockets ou webhooks.
Este padrão pode não ser adequado quando:
Pode usar um serviço criado para notificações assíncronas, como o Azure Event Grid.
As respostas devem ser transmitidas em tempo real para o cliente.
O cliente precisa de recolher muitos resultados e a latência desses resultados é importante. Em vez disso, considere um padrão de barramento de serviço.
Estão disponíveis ligações persistentes de rede do lado do servidor, como WebSockets ou SignalR. Pode usar estas ligações para notificar o ouvinte do resultado.
O design de rede suporta portas abertas para receber chamadas de retorno assíncronas ou webhooks.
Design da carga de trabalho
Um arquiteto deve avaliar como pode utilizar o padrão Asynchronous Request-Reply no design da sua carga de trabalho para alcançar os objetivos e princípios nos pilares do Azure Well-Architected Framework.
| Pilar | Como esse padrão suporta os objetivos do pilar |
|---|---|
| A Eficiência de Desempenho ajuda sua carga de trabalho a atender às demandas de forma eficiente por meio de otimizações em escala, dados e código. | Melhora a capacidade de resposta e escalabilidade ao desacoplar as fases de pedido e resposta para processos que não exigem resposta imediata. Uma abordagem assíncrona aumenta a concorrência e permite ao servidor agendar o trabalho à medida que a capacidade se torna disponível. - PE:05 Dimensionamento e particionamento - PE:07 Código e infraestrutura |
Como em qualquer decisão de design, considere as compensações em relação aos objetivos dos outros pilares que este padrão possa introduzir.
Exemplo
O código seguinte mostra excertos de uma aplicação que utiliza o Azure Functions para implementar este padrão. Esta solução tem três funções:
- O endpoint assíncrono da API
- O ponto final de estado
- Uma função de back-end que processa itens de trabalho em fila e executa-os
Este exemplo está disponível em GitHub.
Função AsyncProcessingWorkAcceptor
A AsyncProcessingWorkAcceptor função implementa um endpoint que aceita trabalho de uma aplicação cliente e coloca-o numa fila de espera para processamento.
A função gera um ID de solicitação e o adiciona como metadados à mensagem de fila.
A resposta HTTP inclui um
Locationcabeçalho que aponta para um endpoint de status. O ID do pedido aparece no caminho da 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}");
}
}
Função AsyncProcessingBackgroundWorker
A AsyncProcessingBackgroundWorker função lê a operação da fila, processa-a com base na carga útil da mensagem e grava o resultado numa conta de armazenamento.
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);
}
}
}
Função AsyncOperationStatusChecker
A AsyncOperationStatusChecker função implementa o ponto de extremidade de status. Esta função verifica o estado do pedido:
Se o pedido for concluído, a função devolve uma chave valet para a resposta ou redireciona a chamada imediatamente para a URL da chave de valet.
Se o pedido estiver pendente, a função devolve um código HTTP 200 que inclui o estado atual.
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
}
A classe seguinte CloudBlockBlobExtensions fornece um método de extensão que o verificador de estado utiliza para gerar um identificador uniforme de recurso (URI) de assinatura de acesso partilhada por delegação do utilizador para o blob de resultados.
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();
}
}