Patrones de Sistemas Distribuidos
Construyendo sistemas resilientes, tolerantes a fallos y escalables
A medida que la tecnología evoluciona y la demanda de sistemas escalables y resilientes aumenta, comprender los patrones fundamentales que gobiernan los sistemas distribuidos se vuelve primordial.
Desde gestionar la comunicación entre nodos hasta asegurar la tolerancia a fallos y la consistencia, este artículo explora patrones de diseño esenciales que permiten a los desarrolladores arquitectar sistemas distribuidos robustos. Ya sea que estés construyendo microservicios o implementando aplicaciones cloud-native, dominar estos patrones te equipará con las herramientas para enfrentar las complejidades de la computación distribuida.
python -m pip install flask flask-limiter
python -m pip install pybreaker
El Patrón Throttling
Throttling es un patrón importante que necesitamos usar en las aplicaciones y APIs actuales. En este contexto, throttling significa controlar la tasa de solicitudes que un usuario (o servicio cliente) puede enviar a un servicio o API dado en un período de tiempo determinado, para proteger los recursos del servicio de ser sobreutilizados.
Por ejemplo, podemos limitar el número de solicitudes de usuario para una API a 1,000 por día. Una vez que se alcanza ese límite, la siguiente solicitud es manejada enviando un mensaje de error con el código de estado HTTP 429 al usuario con un mensaje indicando que hay demasiadas solicitudes.
Ejemplos del Mundo Real
- Gestión de tráfico en autopistas: Los semáforos o límites de velocidad regulan el flujo de vehículos
- Venta de entradas para conciertos: El sitio web limita el número de entradas que cada usuario puede comprar para evitar colapsos
- Uso de electricidad: Algunas compañías ofrecen planes con diferentes tarifas según el uso en horas pico
- Línea de buffet: Los clientes están limitados a tomar un plato a la vez para asegurar equidad
Casos de Uso
Implementación con Flask-Limiter
Veamos un ejemplo de throttling tipo rate-limit usando una aplicación web mínima desarrollada con Flask y su extensión Flask-Limiter:
from flask import Flask from flask_limiter import Limiter from flask_limiter.util import get_remote_address # Configurar la aplicación Flask app = Flask(__name__) # Definir el Limiter con límites por defecto limiter = Limiter( get_remote_address, app=app, default_limits=["100 per day", "10 per hour"], storage_uri="memory://", strategy="fixed-window", ) # Ruta con límites por defecto @app.route("/limited") def limited_api(): return "Welcome to our API!" # Ruta con límite personalizado más restrictivo @app.route("/more_limited") @limiter.limit("2/minute") def more_limited_api(): return "Welcome to our expensive, thus very limited, API!" if __name__ == "__main__": app.run(debug=True)
Press CTRL+C to quit
Al acceder a http://127.0.0.1:5000/limited, verás el mensaje de bienvenida. Sin embargo, si presionas Refresh repetidamente, en la 10ª vez el contenido cambiará:
10 per 1 hour
Para la ruta /more_limited, el límite es de solo 2 solicitudes por minuto, demostrando cómo diferentes endpoints pueden tener diferentes políticas de throttling.
Redis para implementaciones en producción. Consulta la documentación oficial para opciones avanzadas.
El Patrón Retry
El reintento es un enfoque cada vez más necesario en el contexto de sistemas distribuidos. Piensa en microservicios o infraestructuras basadas en la nube donde los componentes colaboran entre sí pero no son desarrollados o desplegados/operados por los mismos equipos.
En su operación diaria, partes de una aplicación cloud-native pueden experimentar lo que se llaman fallos transitorios — pequeños problemas que pueden parecer bugs pero no se deben a tu aplicación en sí, sino a restricciones fuera de tu control como la red o el rendimiento del servidor externo.
Ejemplos del Mundo Real
- Hacer una llamada telefónica: Si la llamada no conecta porque la línea está ocupada o hay un problema de red, reintentamos marcar después de un breve retraso
- Retirar dinero de un ATM: Si la transacción falla por congestión de red, esperamos un momento y volvemos a intentar
- Biblioteca Retrying (Python): Simplifica agregar comportamiento de reintento a funciones
- Biblioteca Pester (Go): Proporciona funcionalidad similar para desarrolladores Go
Casos de Uso
Implementación con Decorador
Implementaremos el patrón Retry para una conexión a base de datos usando un decorador para manejar el mecanismo de reintento:
import logging import random import time # Configurar logging para observabilidad logging.basicConfig(level=logging.DEBUG) def retry(attempts): """Decorador para reintentar automáticamente la ejecución.""" def decorator(func): def wrapper(*args, **kwargs): for _ in range(attempts): try: logging.info("Retry happening") return func(*args, **kwargs) except Exception as e: time.sleep(1) logging.debug(e) return "Failure after all attempts" return wrapper return decorator @retry(attempts=3) def connect_to_database(): """Simula una conexión a base de datos que puede fallar.""" if random.randint(0, 1): raise Exception("Temporary Database Error") return "Connected to Database" # Código de prueba if __name__ == "__main__": for i in range(1, 6): logging.info(f"Connection attempt #{i}") print(f"--> {connect_to_database()}")
INFO:root:Retry happening
--> Connected to Database
INFO:root:Connection attempt #2
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
INFO:root:Retry happening
DEBUG:root:Temporary Database Error
--> Failure after all attempts
INFO:root:Connection attempt #3
INFO:root:Retry happening
--> Connected to Database
El Patrón Circuit Breaker
Un enfoque para la tolerancia a fallos involucra reintentos, como acabamos de ver. Pero cuando un fallo debido a la comunicación con un componente externo es probable que sea de larga duración, usar un mecanismo de reintento puede afectar la capacidad de respuesta de la aplicación.
Podríamos estar desperdiciando tiempo y recursos intentando repetir una solicitud que probablemente fallará. Aquí es donde otro patrón puede ser útil: el Circuit Breaker.
Con el patrón Circuit Breaker, envuelves una llamada a función frágil, o un punto de integración con un servicio externo, en un objeto especial (circuit breaker) que monitorea los fallos. Una vez que los fallos alcanzan cierto umbral, el circuit breaker se "dispara" y todas las llamadas subsecuentes retornan con un error, sin que la llamada protegida se realice en absoluto.
Ejemplos del Mundo Real
- Circuitos eléctricos: Un disyuntor que protege el circuito de sobrecargas
- E-commerce checkout: Si el gateway de pagos está caído, el circuit breaker detiene más intentos de pago
- APIs con rate-limit: Cuando una API alcanza su límite, un circuit breaker puede detener solicitudes adicionales
Estados del Circuit Breaker
Operación Normal
fallos >= threshold
Fallos Inmediatos
éxito
Prueba de Recuperación
timeout expirado
Implementación con PyBreaker
Usaremos la biblioteca pybreaker para mostrar un ejemplo de implementación del patrón Circuit Breaker:
import pybreaker from datetime import datetime import random from time import sleep # Crear circuit breaker: abre después de 2 fallos consecutivos # Se reinicia después de 5 segundos de timeout breaker = pybreaker.CircuitBreaker( fail_max=2, reset_timeout=5 ) @breaker def fragile_function(): """Simula una función frágil que puede fallar aleatoriamente.""" if not random.choice([True, False]): print(" / OK", end="") else: print(" / FAIL", end="") raise Exception("This is a sample Exception") def main(): while True: print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), end="") try: fragile_function() except Exception as e: print(f" / {type(e)} {e}", end="") finally: print("") sleep(1) if __name__ == "__main__": main()
2025-01-17 10:30:02 / FAIL / <class 'Exception'> This is a sample Exception
2025-01-17 10:30:03 / FAIL / <class 'Exception'> This is a sample Exception
2025-01-17 10:30:04 / <class 'pybreaker.CircuitBreakerError'> Circuit "fragile_function" OPEN
2025-01-17 10:30:05 / <class 'pybreaker.CircuitBreakerError'> Circuit "fragile_function" OPEN
2025-01-17 10:30:06 / <class 'pybreaker.CircuitBreakerError'> Circuit "fragile_function" OPEN
2025-01-17 10:30:07 / <class 'pybreaker.CircuitBreakerError'> Circuit "fragile_function" OPEN
2025-01-17 10:30:08 / <class 'pybreaker.CircuitBreakerError'> Circuit "fragile_function" OPEN
2025-01-17 10:30:09 / OK (Circuit CLOSED again)
2025-01-17 10:30:10 / OK
fragile_function() fallan inmediatamente con CircuitBreakerError sin intentar ejecutar la operación. Después del timeout de 5 segundos, se permite que la siguiente llamada pase. Si tiene éxito, el circuito se cierra; si falla, se abre de nuevo.
Otros Patrones de Sistemas Distribuidos
Hay muchos más patrones de sistemas distribuidos que los que hemos cubierto. Entre otros patrones que los desarrolladores y arquitectos pueden usar están los siguientes:
CQRS (Command Query Responsibility Segregation)
Este patrón separa las responsabilidades de lectura y escritura de datos, permitiendo acceso a datos optimizado y escalabilidad al adaptar modelos de datos y operaciones a casos de uso específicos.
Two-Phase Commit
Este protocolo de transacción distribuida asegura atomicidad y consistencia a través de múltiples recursos participantes coordinando un proceso de commit en dos fases: una fase de preparación seguida de una fase de commit.
Saga
Una saga es una secuencia de transacciones locales que juntas forman una transacción distribuida, proporcionando un mecanismo de compensación para mantener consistencia ante fallos parciales o transacciones abortadas.
Sidecar
El patrón Sidecar involucra desplegar servicios auxiliares adicionales junto a los servicios principales para mejorar funcionalidad, como agregar monitoreo, logging o características de seguridad sin modificar directamente la aplicación principal.
Service Registry
Este patrón centraliza la gestión y descubrimiento de servicios dentro de un sistema distribuido, permitiendo que los servicios se registren y descubran dinámicamente, facilitando comunicación y escalabilidad.
Bulkhead
Inspirado en el diseño de barcos, el patrón Bulkhead particiona recursos o componentes dentro de un sistema para aislar fallos y prevenir que fallos en cascada impacten otras partes del sistema, mejorando así la tolerancia a fallos y resiliencia.
Resumen de Patrones
01 // Throttling
Controla la tasa de solicitudes para proteger recursos del servicio y manejar cargas de forma eficiente.
02 // Retry
Maneja fallos transitorios reintentando operaciones fallidas con una estrategia de backoff apropiada.
03 // Circuit Breaker
Previene fallos en cascada deteniendo llamadas a servicios que probablemente fallarán.
04+ // Patrones Avanzados
CQRS, Saga, Bulkhead, Sidecar y más para arquitecturas distribuidas complejas.