Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Desacoplar el procesamiento de back-end del host front-end cuando el procesamiento de back-end debe ejecutarse asincrónicamente, pero el front-end necesita una respuesta clara.
Contexto y problema
En el desarrollo de aplicaciones modernas, las aplicaciones cliente suelen depender de api remotas para proporcionar lógica de negocios y funcionalidad de redacción. Muchas aplicaciones ejecutan código en un explorador web y otros entornos también hospedan código de cliente. Las API pueden relacionarse directamente con la aplicación o funcionar como servicios compartidos desde un servicio externo. La mayoría de las llamadas API usan HTTP o HTTPS y siguen la semántica de REST.
En la mayoría de los casos, las API de una aplicación cliente responden en unos 100 milisegundos (ms) o menos. Muchos factores pueden afectar a la latencia de respuesta:
- Pila de alojamiento de la aplicación
- Componentes de seguridad
- Ubicación geográfica relativa del autor de la llamada y el backend
- Infraestructura de red
- Carga actual
- Tamaño de la carga de la solicitud
- Longitud de cola de procesamiento
- El tiempo para que el back-end procese la solicitud
Estos factores pueden agregar latencia a la respuesta. Puede mitigar algunos factores ampliando el back-end horizontalmente. Otros factores, como la infraestructura de red, están fuera del control del desarrollador de la aplicación. La mayoría de las API responden rápidamente para que la respuesta vuelva a través de la misma conexión. El código de aplicación puede realizar una llamada API sincrónica de forma no desbloqueada para dar la apariencia del procesamiento asincrónico. Se recomienda este enfoque para las operaciones enlazadas a entrada y salida (E/S).
En algunos escenarios, el back-end realiza tareas que son de larga duración y tarda unos segundos. En otros escenarios, el back-end realiza un trabajo en segundo plano de larga duración durante minutos o durante períodos prolongados. En estos casos, no puede esperar a que finalice el trabajo antes de enviar una respuesta. Esta situación puede crear un problema para patrones de solicitud-respuesta sincrónicos.
Algunas arquitecturas solucionan este problema mediante el uso de un agente de mensajes para separar las fases de solicitud y respuesta. Muchos sistemas logran esta separación a través del patrón de nivelación de carga basado en cola. Esta separación permite que el proceso de cliente y la API de back-end se escalen de forma independiente. También presenta una complejidad adicional cuando el cliente requiere una notificación exitosa, porque ese paso también debe convertirse en asincrónico.
Muchas de las mismas consideraciones que se aplican a las aplicaciones cliente también se aplican a las llamadas API REST de servidor a servidor en sistemas distribuidos, como en una arquitectura de microservicios.
Solución
Una solución a este problema es usar el sondeo HTTP. El sondeo funciona bien para el código del lado del cliente cuando los endpoints de callback no están disponibles o cuando las conexiones de larga duración agregan demasiada complejidad. Incluso cuando los callbacks son posibles, las bibliotecas y servicios adicionales que requieren pueden aumentar la complejidad.
En los pasos siguientes se describe la solución:
La aplicación cliente realiza una llamada sincrónica a la API para desencadenar una operación de larga duración en el back-end.
La API responde sincrónicamente lo más rápido posible. Devuelve un código de estado HTTP 202 (aceptado) para confirmar que recibió la solicitud de procesamiento.
Nota:
La API valida la solicitud y la acción que se va a realizar antes de iniciar el proceso de ejecución prolongada. Si la solicitud no es válida, responda inmediatamente con un código de error como HTTP 400 (solicitud incorrecta).
La respuesta incluye una referencia de ubicación que apunta a un punto de conexión que el cliente puede sondear para comprobar el resultado de la operación de larga duración.
La API descarga el procesamiento en otro componente, como una cola de mensajes.
Para que una llamada al endpoint de estado sea exitosa, el endpoint devuelve HTTP 200 (OK). Mientras el trabajo está en curso, el punto de conexión devuelve un recurso que indica ese estado. Cuando se completa el trabajo, el punto de conexión devuelve un recurso que indica la finalización o redirige a otra dirección URL de recurso. Por ejemplo, si la operación asincrónica crea un nuevo recurso, el punto de conexión de estado redirige a la dirección URL de ese recurso.
En el diagrama siguiente se muestra un flujo típico.
Diagrama que muestra el flujo de solicitud y respuesta para las solicitudes HTTP asincrónicas.
Diagrama de secuencia que muestra un cliente, un punto de conexión de API, un punto de conexión de estado y un URI de recursos. El cliente envía una solicitud POST al punto de conexión de API, que devuelve HTTP 202. A continuación, el cliente envía solicitudes GET repetidas al punto de conexión de estado. La primera respuesta devuelve HTTP 200 y una respuesta posterior devuelve HTTP 302 (encontrado). El cliente sigue el redireccionamiento con una solicitud GET al URI del recurso, que devuelve HTTP 200. El diagrama muestra un patrón de solicitud asincrónico con sondeo y una redirección final al recurso completado.
El cliente envía una solicitud y recibe una respuesta HTTP 202.
El cliente envía la solicitud HTTP GET al endpoint de estado. El trabajo está pendiente, por lo que esta llamada devuelve HTTP 200.
El endpoint de estado devuelve HTTP 302 (Found) para redirigir al recurso, y el trabajo se completa.
El cliente captura el recurso en la dirección URL especificada.
Problemas y consideraciones
Tenga en cuenta los siguientes puntos a medida que decida cómo implementar este patrón:
Existen varias maneras de implementar este patrón a través de HTTP y los servicios ascendentes no siempre usan la misma semántica. Por ejemplo, la mayoría de los servicios devuelven HTTP 404 (no encontrado) desde un método GET cuando no se completa un proceso remoto, en lugar de HTTP 202. Según la semántica de REST estándar, HTTP 404 es la respuesta correcta porque el resultado de la llamada aún no existe.
Una respuesta HTTP 202 indica dónde sondea el cliente y con qué frecuencia. Incluye los siguientes encabezados.
Header Descripción Notas LocationDirección URL que el cliente sondea para obtener un estado de respuesta Esta dirección URL puede ser un token de firma de acceso compartido. El patrón Valet Key funciona bien cuando esta ubicación necesita control de acceso. El patrón también se aplica cuando es necesario mover el sondeo de respuesta a otro back-end. Retry-AfterUna estimación de cuándo se completará el procesamiento Este encabezado impide que los clientes de sondeo envíen demasiadas solicitudes al back-end. Considere el comportamiento esperado del cliente al diseñar esta respuesta. Un cliente bajo su control puede seguir estos valores de respuesta exactamente. Los clientes que otros crean, incluidos los clientes creados mediante herramientas sin código o de poco código, como Azure Logic Apps, pueden aplicar su propio control para HTTP 202.
Es posible que tenga que usar un proxy de procesamiento para ajustar los encabezados de respuesta o la carga, en función de los servicios subyacentes que use.
Si el punto de conexión de estado se redirige después de la finalización, HTTP 302 o HTTP 303 (vea Otro) son códigos de retorno válidos, en función de la semántica que admita.
Una vez que el servidor procesa la solicitud, el recurso que especifica el encabezado devuelve un código de estado HTTP como 200, 201 (creado) o 204 (sin contenido).
Si se produce un error durante el procesamiento, conserve el error en la dirección URL del recurso que el encabezado especifica y devuelva un código de estado 4xx de ese recurso que coincida con el error.
Las soluciones no implementan este patrón de la misma manera y algunos servicios incluyen encabezados adicionales o alternativos. Por ejemplo, Azure Resource Manager usa una variante modificada de este patrón. Para obtener más información, vea Operaciones asincrónicas del Administrador de Recursos.
Es posible que los clientes heredados no soporten este patrón. En ese caso, es posible que tenga que colocar un proxy de procesamiento a través de la API asincrónica para ocultar el procesamiento asincrónico del cliente original. Por ejemplo, Logic Apps admite este patrón de forma nativa y puede usarlo como una capa de integración entre una API asincrónica y un cliente que realiza llamadas sincrónicas. Para obtener más información, consulte Ejecutar tareas de larga duración con el patrón de acción de webhook.
En algunos escenarios, es posible que desee proporcionar a los clientes una forma de cancelar una solicitud de larga duración. En ese caso, el servicio back-end debe admitir alguna forma de instrucción de cancelación.
Cuándo usar este patrón
Use este patrón en los siguientes supuestos:
Trabajas con código del lado del cliente, como aplicaciones de navegador, y esas restricciones dificultan proporcionar puntos de conexión para devoluciones de llamada o hacen que las conexiones prolongadas añadan demasiada complejidad.
Llama a un servicio que usa solo el protocolo HTTP y el servicio de retorno no puede enviar callbacks debido a restricciones de firewall en el lado del cliente.
Se integra con arquitecturas heredadas que no admiten mecanismos de devolución de llamada modernos como WebSockets o webhooks.
Este patrón podría no ser adecuado cuando:
En su lugar, puede usar un servicio creado para notificaciones asincrónicas, como Azure Event Grid.
Las respuestas se deben transmitir en tiempo real al cliente.
El cliente debe recopilar muchos resultados y la latencia de esos resultados es importante. Considere en su lugar un patrón de bus de servicio.
Hay conexiones de red persistentes del lado servidor como WebSockets o SignalR disponibles. Puede usar estas conexiones para notificar al autor de la llamada del resultado.
El diseño de red admite puertos abiertos para recibir llamadas de retorno asincrónicas o webhooks.
Diseño de cargas de trabajo
Un arquitecto debe evaluar cómo pueda usar el patrón asincrónico de solicitud-respuesta en el diseño de su carga de trabajo para abordar los objetivos y principios descritos en los pilares de Azure Well-Architected Framework.
| Fundamento | Cómo apoya este patrón los objetivos de los pilares |
|---|---|
| Eficiencia del rendimiento ayuda a su carga de trabajo a satisfacer eficientemente las demandas mediante optimizaciones en el escalado, los datos y el código. | Para mejorar la capacidad de respuesta y la escalabilidad, desacopla las fases de solicitud y respuesta de los procesos que no requieren una respuesta inmediata. Un enfoque asincrónico aumenta la simultaneidad y permite que la programación del servidor funcione a medida que la capacidad esté disponible. PE:05 Escalado y particionamiento PE:07 Código e infraestructura |
Al igual que con cualquier decisión de diseño, considere el equilibrio con los objetivos de los otros pilares que este patrón podría introducir.
Ejemplo
El código siguiente muestra extractos de una aplicación que usa Azure Functions para implementar este patrón. Esta solución tiene tres funciones:
- El endpoint asincrónico de la API
- El endpoint de estado
- Una función de back-end que toma elementos de trabajo en cola y los ejecuta.
Diagrama de la estructura del patrón de respuesta de solicitud asincrónica en Functions.
En el paso 1, un cliente llama a una API. En el paso 2, la API coloca un mensaje en una cola. En el paso 3, la API devuelve un punto de conexión de estado al cliente. En el paso 4, un trabajador recibe el mensaje de la cola. En el paso 5, el trabajador procesa el mensaje y escribe el resultado en el almacenamiento de blobs. En el paso 6, el cliente llama al punto final de estado. En el paso 7, el punto de conexión de estado comprueba el resultado en el almacenamiento de blobs.
Este ejemplo está disponible en GitHub.
Función AsyncProcessingWorkAcceptor
La función implementa un punto de conexión que acepta tareas de una aplicación cliente y las pone en cola para su procesamiento.
La función genera un identificador de solicitud y lo agrega como metadatos al mensaje en cola.
La respuesta HTTP incluye un encabezado que apunta a un endpoint de estado. El identificador de solicitud aparece en la ruta de acceso de la dirección 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}");
}
}
Función AsyncProcessingBackgroundWorker
La función lee la operación de la cola, la procesa en función de la carga del mensaje y escribe el resultado en una cuenta de almacenamiento.
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);
}
}
}
Función AsyncOperationStatusChecker
La función implementa el endpoint de estado. Esta función comprueba el estado de la solicitud:
Si se completa la solicitud, la función devuelve una clave de valet a la respuesta o redirige la llamada inmediatamente a la dirección URL de la clave de valet.
Si la solicitud está pendiente, la función devuelve un código HTTP 200 que incluye el estado actual.
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 siguiente clase proporciona un método de extensión que el comprobador de estado usa para generar un identificador uniforme de recursos de firma de acceso compartido (URI) de delegación de usuario para el blob resultante.
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();
}
}
Pasos siguientes
- Usar el patrón de acción de sondeo para tareas de larga duración
- Patrón de API HTTP asincrónica
Recursos relacionados
- diseño de la API web
- Patrón Backends for Frontends
- Patrón de clave de valet