Antipatrón de carga masiva de reintentos
Cuando un servicio no está disponible o está ocupado, hacer que los clientes reintenten sus conexiones con demasiada frecuencia puede provocar que el servicio tenga dificultades para recuperarse y puede hacer que el problema empeore. Tampoco tiene sentido reintentar indefinidamente, ya que las solicitudes normalmente solo son válidas durante un período de tiempo definido.
Descripción del problema
En la nube, a veces los servicios experimentan problemas y dejan de estar disponibles para los clientes, o tienen que regular o limitar la velocidad de los clientes. Aunque se recomienda que los clientes reintenten las conexiones erróneas con los servicios, es importante que no vuelvan a intentarlo con demasiada frecuencia o durante demasiado tiempo. Es poco probable que los reintentos efectuados en un breve período de tiempo se realicen correctamente, ya que es posible que los servicios no se hayan recuperado. Además, puede aumentar la carga de los servicios si se realizan muchos intentos de conexión mientras se están intentando recuperar, y los intentos de conexión repetidos pueden incluso sobrecargar el servicio y hacer que el problema resultante sea aún peor.
En el ejemplo siguiente se muestra un escenario en el que un cliente se conecta a una API basada en servidor. Si la solicitud no se realiza correctamente, el cliente la vuelve a intentar de inmediato y sigue reintentándolo indefinidamente. Normalmente, este tipo de comportamiento es más sutil que en este ejemplo, pero se aplica el mismo principio.
public async Task<string> GetDataFromServer()
{
while(true)
{
var result = await httpClient.GetAsync(string.Format("http://{0}:8080/api/...", hostName));
if (result.IsSuccessStatusCode) break;
}
// ... Process result.
}
Procedimiento para corregir el problema
Las aplicaciones cliente deben seguir algunos procedimientos recomendados para evitar que se produzca una carga masiva de reintentos.
- Limite el número de reintentos y no siga efectuándolos durante un período de tiempo prolongado. Aunque podría parecer fácil escribir un bucle
while(true)
, seguramente no desea seguir reintentándolo durante un largo período de tiempo, ya que la situación que condujo al inicio de la solicitud ha cambiado probablemente. En la mayoría de las aplicaciones, el reintento durante unos segundos o minutos es suficiente. - Pausa entre reintentos. Si un servicio no está disponible, es poco probable que volver a intentarlo de inmediato tenga éxito. Aumente gradualmente la cantidad de tiempo que espera entre los intentos, por ejemplo, mediante una estrategia de retroceso exponencial.
- Control correcto de los errores. Si el servicio no responde, considere si tiene sentido anular el intento y devolver un error al usuario o al autor de la llamada del componente. Tenga en cuenta estos escenarios de error al diseñar la aplicación.
- Considere la posibilidad de usar el patrón de disyuntor, que está diseñado específicamente para ayudar a evitar las cargas masivas de reintentos.
- Si el servidor proporciona un encabezado de respuesta
retry-after
, asegúrese de que no vuelve a intentarlo hasta que haya transcurrido el período de tiempo especificado. - Use los SDK oficiales al comunicarse con los servicios de Azure. Estos SDK suelen tener directivas de reintento integradas y protecciones que impiden provocar o contribuir a las cargas masivas de reintentos. Si se comunica con un servicio que no tiene un SDK, o en el que el SDK no controla correctamente la lógica de reintentos, considere la posibilidad de usar una biblioteca como Polly (para .NET) o Retry (para JavaScript) para controlar correctamente la lógica de reintentos y evitar escribir el código usted mismo.
- Si la ejecución se va a realizar en un entorno que lo admita, use una malla de servicio (u otra capa de abstracción) para enviar las llamadas salientes. Normalmente, estas herramientas, como Dapr, admiten directivas de reintento y siguen automáticamente los procedimientos recomendados, como el respaldo después de varios intentos. Este enfoque significa que no es imprescindible que escriba código de reintento.
- Considere las solicitudes por lotes y el uso de la agrupación de solicitudes cuando esté disponible. Muchos SDK controlan el procesamiento por lotes de solicitudes y la agrupación de conexiones en su nombre, lo que reduce el número total de intentos de conexión salientes que realiza la aplicación, aunque deberá tener, aún así, cuidado de no reintentar estas conexiones con demasiada frecuencia.
Los servicios también deben protegerse contra las cargas masivas de reintentos.
- Agregue un nivel de puerta de enlace para que pueda apagar las conexiones durante un incidente. Este es un ejemplo del patrón Bulkhead. Azure proporciona muchos servicios de puerta de enlace diferentes para distintos tipos de soluciones, como Front Door, Application Gateway y API Management.
- Limite las solicitudes en la puerta de enlace; esto garantizará que no se acepten tantas solicitudes que los componentes de back-end no puedan seguir funcionando.
- Si tiene un límite, devuelva un encabezado
retry-after
para ayudar a los clientes a comprender cuándo volver a intentar las conexiones.
Consideraciones
- Los clientes deben tener en cuenta el tipo de error devuelto. Algunos tipos de error no indican un error del servicio, sino que el cliente envió una solicitud no válida. Por ejemplo, si una aplicación cliente recibe una respuesta de error
400 Bad Request
, es probable que reintentar la misma solicitud no ayude, ya que el servidor le indica que su solicitud no es válida. - Los clientes deben tener en cuenta la cantidad de tiempo que tiene sentido volver a intentar las conexiones. El período de tiempo que debe realizar los reintentos dependerá de sus requisitos empresariales y de si puede propagar razonablemente un error al usuario o autor de la llamada. En la mayoría de las aplicaciones, el reintento durante unos segundos o minutos es suficiente.
Procedimiento para detectar el problema
Desde la perspectiva del cliente, los síntomas de este problema pueden incluir tiempos de respuesta o de procesamiento muy largos y datos de telemetría que indican intentos repetidos de volver a realizar la conexión.
Desde la perspectiva del servicio, los síntomas podrían incluir un gran número de solicitudes de un cliente en un breve período de tiempo, o un gran número de solicitudes de cliente único mientras se recupera de interrupciones. Los síntomas también pueden incluir problemas al recuperar el servicio, o errores en cascada continuos del servicio después de que se haya reparado un error.
Diagnóstico de ejemplo
En las secciones siguientes se muestra un enfoque para detectar una posible carga masiva de reintentos, tanto en el cliente como en el servicio.
Identificación de la telemetría de cliente
Azure Application Insights registra la telemetría de las aplicaciones y hace que los datos estén disponibles para su consulta y visualización. Se realiza un seguimiento de las conexiones salientes como dependencias, se puede acceder a la información sobre ellas y se pueden representar gráficamente para identificar cuándo un cliente realiza un gran número de solicitudes salientes en el mismo servicio.
El siguiente gráfico se tomó de la pestaña Métricas del portal de Application Insights y muestra la métrica de Errores de dependencias desglosada por el nombre de dependencia remota. Esto muestra un escenario en el que había un gran número (más de 21 000) de intentos de conexión con errores a una dependencia en un breve período de tiempo.
Identificación de la telemetría del servidor
Las aplicaciones de servidor pueden detectar un gran número de conexiones desde un cliente único. En el ejemplo siguiente, Azure Front Door actúa como puerta de enlace para una aplicación y se ha configurado para registrar todas las solicitudes en un área de trabajo de Log Analytics.
La siguiente consulta de Kusto se puede ejecutar en Log Analytics. Identificará las direcciones IP de los clientes que han enviado un gran número de solicitudes a la aplicación en el último día.
AzureDiagnostics
| where ResourceType == "FRONTDOORS" and Category == "FrontdoorAccessLog"
| where TimeGenerated > ago(1d)
| summarize count() by bin(TimeGenerated, 1h), clientIp_s
| order by count_ desc
Al ejecutar esta consulta durante una carga masiva de reintentos, aparece un gran número de intentos de conexión desde una única dirección IP.