Diseño e Implementación de un Marco de Reintentos en Microservicios - Retry Frameworks

Diseño e Implementación de un Marco de Reintentos en Microservicios - Retry Frameworks

Los microservicios son inherentemente distribuidos, y las fallas de red son comunes. Al llamar a otros microservicios, hay un riesgo de encontrar una falla de red temporal, lo que puede resultar en una solicitud fallida. En algunos casos, estas fallas pueden ser transitorias y podrían resolverse reintentando la solicitud. Sin embargo, en otros casos, el problema podría ser un corte más prolongado o una falla permanente, que puede requerir ser abordado por otros medios.

¿Qué son los marcos de reintentos o "Retry Frameworks"?

Los Retry Frameworks, son sistemas diseñados para gestionar automáticamente los intentos fallidos de realizar una operación, como una solicitud a otro microservicio. Estos frameworks pueden decidir cuándo y cómo volver a intentar una operación fallida, mejorando así la robustez y la confiabilidad del sistema en general.

Diseño del Marco de Reintentos

Al diseñar un marco de reintentos, hay varias consideraciones a tener en cuenta:

Política de Reintentos

Define cuándo y cómo a menudo se debe volver a intentar una solicitud fallida. La política debe estar basada en el tipo de error y las características del servicio que se está llamando.

Ejemplo: Limitar los reintentos a 3 veces y solo para errores específicos como "timeouts" o "error 500".

let retryCount = 0;

async function makeRequest() {
  // Simular una solicitud que podría fallar
}

async function retryRequest() {
  try {
    await makeRequest();
    retryCount = 0; // Restablecer contador si la solicitud tiene éxito
  } catch (error) {
    if (error.code === 'ETIMEDOUT' || error.response.status === 500) {
      if (retryCount < 3) {
        retryCount++;
        retryRequest(); // Reintentar la solicitud
      }
    }
  }
}

Estrategia de Retroceso (Backoff)

Determina cuánto tiempo esperar entre reintentos. Algunas estrategias comunes incluyen retroceso lineal, exponencial y con variación aleatoria ("jitter").

Ejemplo: Empezar con un retraso de 100 ms y doblarlo en cada intento fallido hasta un máximo de 1600 ms.

let retryCount = 0;

async function makeRequest() {
  // Simular una solicitud que podría fallar
}

async function retryRequest() {
  try {
    await makeRequest();
    retryCount = 0; // Restablecer contador si la solicitud tiene éxito
  } catch (error) {
    if (error.code === 'ETIMEDOUT' || error.response.status === 500) {
      if (retryCount < 3) {
        retryCount++;
        const delay = Math.min(1600, Math.pow(2, retryCount) * 100);
        setTimeout(() => retryRequest(), delay); // Reintentar con retraso
      }
    }
  }
}

Rompe-circuito (Circuit Breaker)

Un mecanismo para detectar cuándo un servicio está indisponible y detener futuras solicitudes para evitar la sobrecarga del servicio.

Ejemplo: Después de 5 fallos consecutivos, bloquear más intentos durante 1 minuto.

let consecutiveFailures = 0;
let retryCount = 0;
let isCircuitOpen = false;

async function makeRequest() {
  // Simular una solicitud que podría fallar
}

async function retryRequest() {
  if (isCircuitOpen) {
    console.log("Circuit is open. Skipping request.");
    return;
  }

  try {
    await makeRequest();
    retryCount = 0; // Restablecer contador si la solicitud tiene éxito
  } catch (error) {
    if (error.code === 'ETIMEDOUT' || error.response.status === 500) {
      if (retryCount < 3) {
        retryCount++;
        const delay = Math.min(1600, Math.pow(2, retryCount) * 100);
        setTimeout(() => retryRequest(), delay); // Reintentar con retraso
      } else {
        consecutiveFailures++;
        if (consecutiveFailures >= 5) {
          blockRequestsFor(60 * 1000); // Bloquear nuevas solicitudes
        }
      }
    }
  }
}

function blockRequestsFor(timeMs) {
  isCircuitOpen = true;
  setTimeout(() => {
    isCircuitOpen = false;
    consecutiveFailures = 0; // Restablecer el contador de fallas consecutivas
  }, timeMs);
}

Monitoreo y Alerta

El monitoreo y las alertas son componentes críticos para cualquier sistema de microservicios, y aún más cuando estamos hablando de estrategias de recuperación como los reintentos y los rompe-circuitos. Idealmente, deberías tener monitoreo en tiempo real y alertas configuradas para varios eventos y métricas, algunos ejemplos:

  1. Número de Reintentos: Si el número de reintentos alcanza un umbral específico, esto debería disparar una alerta.

  2. Circuito Abierto: Cuando el circuito se abre debido a múltiples fallas, esto es un evento que definitivamente debería ser monitoreado y alertado.

  3. Tiempo de Respuesta: Si el tiempo de respuesta de un microservicio supera un umbral determinado, esto podría ser un indicativo de problemas y merecer una alerta.

  4. Tasa de Error: Monitorear la tasa de errores generales te ayudará a captar problemas que quizás no se capturen solo con los reintentos o los circuitos abiertos.

Puede que utilices servicios de monitoreo y alerta como Prometheus y Grafana, o tal vez servicios más específicos para aplicaciones como New Relic o Datadog.

Ejemplo: Esta codigo incluye las opciones anteriores + el reporte de errores.


let consecutiveFailures = 0;
let retryCount = 0;
let isCircuitOpen = false;

function sendAlert(message) {
  // Aquí podrías enviar un mensaje a un Slack, correo electrónico, o incluso disparar un webhook.
  console.error(`Alert: ${message}`);
}

async function makeRequest() {
  // Simular una solicitud que podría fallar
}

async function retryRequest() {
  if (isCircuitOpen) {
    console.log("Circuit is open. Skipping request.");
    return;
  }

  try {
    await makeRequest();
    retryCount = 0; // Restablecer contador si la solicitud tiene éxito
  } catch (error) {
    if (error.code === 'ETIMEDOUT' || error.response.status === 500) {
      if (retryCount < 3) {
        retryCount++;
        const delay = Math.min(1600, Math.pow(2, retryCount) * 100);
        setTimeout(() => retryRequest(), delay); // Reintentar con retraso
      } else {
        consecutiveFailures++;
        if (consecutiveFailures >= 5) {
          blockRequestsFor(60 * 1000); // Bloquear nuevas solicitudes
        }
      }
    }

    if (consecutiveFailures >= 5) {
      blockRequestsFor(60 * 1000);
      sendAlert('El servicio esta caido');
    }
  }
}

function blockRequestsFor(timeMs) {
  isCircuitOpen = true;
  setTimeout(() => {
    isCircuitOpen = false;
    consecutiveFailures = 0; // Restablecer el contador de fallas consecutivas
  }, timeMs);
}

El monitoreo y las alertas deberían ser parte de un sistema más grande de observabilidad que también incluya trazabilidad y métricas, para darte una visión completa del estado y la salud de tu aplicación.

Almacenamiento de Reintentos: ¿Dónde y Cuándo?

En muchos casos, los reintentos son manejados en memoria sin necesidad de un almacenamiento persistente. Sin embargo, hay escenarios donde guardar el estado del intento puede ser crítico, como en operaciones de larga duración o sistemas de alta disponibilidad.

En un entorno de microservicios distribuidos, especialmente donde múltiples instancias o nodos de un servicio pueden intentar procesar la misma información, el almacenamiento centralizado para los reintentos se convierte en un componente crucial para evitar problemas como el procesamiento duplicado o la pérdida de datos.

Opciones de Almacenamiento

  • Redis: Excelente para situaciones donde los datos de reintentos necesitan ser accedidos de manera extremadamente rápida y posiblemente desde múltiples servicios.

  • NoSQL: Útil cuando la estructura de los datos de reintentos es variable o cuando se necesita escalar horizontalmente.

  • RDBMS: Puede ser utilizado cuando los reintentos deben ser transaccionales y ACID (Atomicidad, Consistencia, Aislamiento, Durabilidad) es un requisito.

  • RabbitMQ: Apropiado para encolar reintentos y manejarlos de forma asíncrona, especialmente en sistemas con microservicios.

  • Kafka: Utilizado en sistemas más complejos donde los datos de reintentos necesitan ser distribuidos entre múltiples consumidores y tal vez necesiten ser reproducidos.

Conclusión

Un Retry Framework es esencial para cualquier arquitectura de microservicios que aspire a ser robusta y fiable. Desde la elección de una política de reintentos adecuada hasta la implementación de Backoff y Circuit-Breaker, cada aspecto desempeña un papel crucial. Las herramientas de monitoreo y alerta son vitales para mantener el sistema en funcionamiento óptimo. Al considerar estas prácticas recomendadas y herramientas disponibles, las organizaciones pueden diseñar un sistema de microservicios resistente y fiable.

Happy coding! :D


Photo by Grégoire Hervé-Bazin on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?