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 funcionalidades de composição. 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. Para orientações sobre a conceção do processamento back-end, veja Tarefas em segundo plano.
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 deve validar 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.
Por cada chamada bem-sucedida ao endpoint de estado, o endpoint devolve HTTP 200 (OK). Enquanto o trabalho está em andamento, o endpoint de estado devolve um recurso que indica esse estado. O órgão de resposta ao estado deve incluir informação suficiente para que o cliente compreenda o estado atual da operação.
Quando o trabalho termina, o endpoint de estado devolve um recurso que indica conclusão ou redireciona para outro 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 uma solicitação e recebe uma resposta HTTP 202 (Aceito).
O cliente envia uma solicitação HTTP GET para o endpoint de status. Esta chamada devolve HTTP 200 porque o trabalho está pendente.
Em determinado momento, o trabalho termina e o endpoint de estado devolve HTTP 303 (Ver Outro) 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, algumas implementações não usam um endpoint de estado separado. Em vez disso, o cliente consulta diretamente a URL do recurso de destino e recebe HTTP 404 (Não Encontrado) até que o recurso seja criado. Esta resposta é gerada porque o recurso ainda não existe. No entanto, esta abordagem pode ser pouco clara porque IDs de pedido inválidos também retornam HTTP 404. Um endpoint de estado dedicado que devolve HTTP 200 com um corpo de estado, conforme descrito neste padrão, evita esta confusão.
Uma resposta HTTP 202 indica onde o cliente consulta e com que frequência. Deve incluir os seguintes cabeçalhos.
Cabeçalho Descrição Notes LocationUma URL que o cliente consulta para o estado da resposta Esta URL pode ser um token de assinatura de acesso partilhado (SAS). 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-AfterTempo estimado de conclusão para processamento Este cabeçalho ajuda os clientes de sondagem a evitar enviar 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.
Considere incluir os seguintes campos na resposta do endpoint de estado.
Campo Descrição Notes statusO estado atual da operação, como Pendente, Em Execução, Sucesso, Falhada ou Cancelada Utiliza um conjunto consistente e documentado de valores terminais e não terminais createdAtA altura em que a operação foi aceite Ajuda os clientes a detetar operações obsoletas ou abandonadas lastUpdatedAtA hora em que o estado foi atualizado pela última vez Ajuda os clientes a distinguir entre operações paradas e em curso percentCompleteUm indicador de progresso opcional Útil quando o back-end consegue estimar o progresso errorUm objeto de erro estruturado quando o estado é Falhado Para maior consistência, considere usar o formato RFC 9457 . 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, use HTTP 303 (ver Outro). Um 303 instrui o cliente a emitir um pedido GET para a URL de redirecionamento, independentemente do método original do pedido. Este comportamento é a semântica correta para este padrão porque o cliente está a recuperar um recurso de resultado distinto, não a submeter novamente a operação original. O HTTP 302 (Encontrado) não garante uma alteração de método. Alguns clientes repetem o método original ao redirecionar. Este comportamento pode causar efeitos secundários não intencionais, como pedidos POST duplicados.
Depois de o servidor processar com sucesso 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, persista o erro na URL de recurso especificada pelo
Locationcabeçalho e retorne um código de estado 4xx do recurso que corresponda ao erro. Use um formato estruturado de erro, como o RFC 9457 (Detalhes do Problema para APIs HTTP), para que os clientes possam analisar e gerir falhas programaticamente.O recurso de estado e quaisquer resultados armazenados consomem armazenamento e computação. Defina uma política de retenção para os limpar após um tempo razoável. Para informar os clientes sobre a janela de retenção, pode adicionar um cabeçalho
Expiresà resposta de estado.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, consulte operações assíncronas do Resource Manager.
Os clientes legados podem não suportar este padrão. Nesse caso, talvez seja necessário colocar uma fachada sobre a API assíncrona para ocultar 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 Comportamento de pedido-resposta assíncrono em Aplicações Lógicas.
Para fornecer uma forma aos clientes de cancelar um pedido de longa duração, expõe-se uma operação DELETE no recurso endpoint de estado. Este pedido deve encaminhar uma instrução de cancelamento para o componente de processamento de back-end. Depois de o backend tratar do cancelamento, deve atualizar o recurso de estado para refletir o estado cancelado. Este processo ajuda a evitar que trabalhos incompletos consumam recursos indefinidamente. Determine se a operação suporta reversão parcial ou requer uma transação compensatória.
Pode exigir que os clientes forneçam uma chave de idempotência, por exemplo, num cabeçalho de pedido
Idempotency-Key, quando submetem o pedido inicial. Se o back end receber uma chave duplicada, deve devolver o recurso de estado existente em vez de enfileirar um segundo item de trabalho. Esta abordagem protege contra falhas de rede que levam o cliente a tentar novamente um POST que o servidor já aceitou. É especialmente importante neste padrão porque o cliente não consegue distinguir entre uma resposta perdida e um pedido que nunca foi recebido.
Observação
Este padrão descreve o questionamento HTTP, em que o cliente periodicamente emite novos pedidos para verificar o estado. Em sondagens longas, o cliente envia um pedido e o servidor mantém a ligação aberta até que novos dados estejam disponíveis ou ocorra um timeout. O long polling reduz a latência de resposta em comparação com o polling periódico, mas introduz complexidade em torno da gestão de conexões e timeouts.
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 workloads 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. Considere usar Server-Sent Events (SSEs), que fornecem um canal push unidirecional nativo de HTTP do servidor para o cliente, sem que o cliente precise consultar.
O cliente precisa de recolher muitos resultados, e a latência desses resultados é importante. Considera usar um corretor de mensagens em vez disso.
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 usar o padrão Assíncrono de Request-Reply no design da sua própria carga de trabalho para atender os objetivos e princípios abordados 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.
A implementação utiliza identidade gerida para autenticar com Azure Service Bus e Azure Blob Storage, o que evita armazenar strings de ligação ou chaves de conta. As dependências são registadas em Program.cs usando DefaultAzureCredential e são injetadas através de construtores primários.
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 ponto final de estado e umRetry-Aftercabeçalho que sugere um intervalo de sondagem. O ID do pedido aparece no caminho da 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);
}
}
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("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);
}
}
}
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 HTTP 303 (Ver Outros) e redireciona o cliente para uma URL de valet key para obter o resultado.
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/{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
}