Diseño de sistemas distribuidos tolerantes a fallos: estrategias y mejores prácticas

Descubre cómo diseñar sistemas distribuidos que soporten fallos de red, servidores y datos mediante redundancia, circuit breakers y patrones de resiliencia.

Testing
UnitE2ETDD

Diseño de sistemas distribuidos tolerantes a fallos: estrategias y mejores prácticas

¿Tu empresa está lista para IA? Descargá nuestro checklist gratuito →

Descargar checklist

Introducción

En la era de la computación en la nube y las arquitecturas de microservicios, los sistemas distribuidos se han convertido en la columna vertebral de las aplicaciones modernas. Sin embargo, la distribución introduce nuevos desafíos: fallos de red, caídas de servidores, particiones de datos y latencia variable. Un sistema tolerante a fallos no es aquel que nunca falla, sino el que sigue funcionando correctamente a pesar de los fallos parciales.

En este artículo, exploraremos las estrategias fundamentales para diseñar sistemas distribuidos tolerantes a fallos, desde la redundancia y el balanceo de carga hasta patrones como el circuit breaker y el bulkhead. Además, incluiremos ejemplos prácticos de código para que puedas aplicar estos conceptos en tus propios proyectos.

Estrategias clave para la tolerancia a fallos

1. Redundancia y replicación

La redundancia es la piedra angular de la alta disponibilidad. Consiste en tener múltiples instancias de un componente (servidores, bases de datos, etc.) para que si uno falla, otro pueda tomar el relevo.

  • Redundancia activa-pasiva: Un servidor primario maneja las peticiones mientras uno o más servidores secundarios están en espera. Si el primario falla, el secundario se activa.
  • Redundancia activa-activa: Todas las instancias están activas y comparten la carga. Esto mejora la capacidad y la tolerancia a fallos simultáneamente.

Ejemplo práctico: Configuración de un balanceador de carga con Nginx para distribuir tráfico entre varios servidores de aplicación:

upstream backend {
    server backend1.example.com weight=3;
    server backend2.example.com;
    server backend3.example.com backup;
}

server {
    location / {
        proxy_pass http://backend;
    }
}

2. Health checks y auto-recuperación

Los health checks permiten detectar instancias fallidas y redirigir el tráfico lejos de ellas. Combinados con orquestadores como Kubernetes, las instancias pueden reiniciarse automáticamente.

Kubernetes readiness probe (ejemplo en YAML):

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: myapp:latest
    readinessProbe:
      httpGet:
        path: /health
        port: 8080
      initialDelaySeconds: 5
      periodSeconds: 10

3. Circuit Breaker (interruptor de circuito)

El patrón circuit breaker evita que un fallo en un servicio se propague a otros servicios. Cuando el número de fallos supera un umbral, el circuito se abre y las llamadas subsecuentes fallan inmediatamente, sin intentar la operación real. Tras un tiempo de espera, el circuito pasa a semi-abierto y permite algunas peticiones de prueba.

Implementación con Resilience4j en Java:

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .waitDurationInOpenState(Duration.ofSeconds(10))
    .slidingWindowSize(10)
    .build();

CircuitBreaker circuitBreaker = CircuitBreaker.of("myService", config);

// Uso con decorador
Supplier<String> supplier = circuitBreaker.decorateSupplier(() -> callRemoteService());
Try<String> result = Try.ofSupplier(supplier);

4. Retry con backoff exponencial

Cuando un fallo es transitorio (p.ej., timeout de red), reintentar la operación puede resolverlo. Sin embargo, los reintentos deben implementarse con backoff exponencial y jitter para no sobrecargar el sistema.

Ejemplo en Python con tenacity:

¿Querés un diagnóstico personalizado? Completá el checklist gratuito →

Descargar checklist
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import requests

@retry(stop=stop_after_attempt(3),
       wait=wait_exponential(multiplier=1, min=2, max=10),
       retry=retry_if_exception_type(requests.exceptions.Timeout))
def fetch_data(url):
    response = requests.get(url, timeout=5)
    return response.json()

5. Bulkhead (mamparo)

El patrón bulkhead aísla los recursos de un sistema para que un fallo en un componente no afecte a otros. Por ejemplo, limitar el número de conexiones simultáneas a un servicio externo.

Configuración con Hystrix (deprecado pero ilustrativo):

HystrixCommandProperties.Setter()
    .withExecutionIsolationSemaphoreMaxConcurrentRequests(10)
    .withFallbackIsolationSemaphoreMaxConcurrentRequests(5);

6. Timeouts y límites de velocidad

Establecer timeouts estrictos evita que una petición se bloquee indefinidamente. Además, los límites de velocidad protegen contra ráfagas de tráfico.

Ejemplo con middleware Express en Node.js:

const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // máximo 100 peticiones por ventana
  timeout: 5000, // timeout de 5 segundos
});
app.use('/api/', limiter);

Patrones de diseño para sistemas distribuidos

1. Sagas para transacciones distribuidas

En lugar de usar transacciones ACID (difíciles de implementar en sistemas distribuidos), las sagas dividen una transacción en una secuencia de pasos locales, con compensaciones para deshacerlos.

Ejemplo coreográfico: Servicio de pedidos envía evento "PedidoCreado", servicio de inventario reduce stock, servicio de pagos cobra. Si el pago falla, se emite "PedidoCancelado" y se revierte el inventario.

2. CQRS (Command Query Responsibility Segregation)

Separar las operaciones de escritura (comandos) de las de lectura (consultas). Esto permite optimizar cada lado por separado y escalar independientemente.

3. Event Sourcing

Almacenar el estado como una secuencia de eventos inmutables. Esto facilita la auditoría, la reconstrucción del estado y la tolerancia a fallos (puedes reproducir los eventos).

Herramientas y tecnologías

  • Apache Kafka: Plataforma de streaming distribuido para manejar eventos y mensajes.
  • Kubernetes: Orquestación de contenedores con capacidades nativas de health checks, auto-escalado y reinicio.
  • Resilience4j: Biblioteca ligera para tolerancia a fallos en aplicaciones Java.
  • Consul/HashiCorp: Service mesh y descubrimiento de servicios con health checks.

Enlaces de interés

Conclusión

Diseñar sistemas distribuidos tolerantes a fallos requiere una combinación de patrones, herramientas y buenas prácticas. No existe una solución única; debes evaluar los requisitos de tu sistema y aplicar las estrategias adecuadas. La redundancia, los circuit breakers, los retries con backoff y el aislamiento mediante bulkheads son esenciales para garantizar la resiliencia. Implementa health checks y timeouts para detectar fallos rápidamente. Y no olvides probar tus sistemas con chaos engineering para validar su tolerancia a fallos en condiciones reales.

Recuerda: la tolerancia a fallos no es un destino, sino un proceso continuo de mejora y adaptación.

¿Listo para dar el próximo paso? Evaluá tu empresa con nuestro checklist gratuito →

Descargar checklist

Publicaciones relacionadas