Introducción
Los sistemas distribuidos son aquellos en los que los componentes ubicados en diferentes máquinas se comunican y coordinan sus acciones mediante el intercambio de mensajes. Estos sistemas presentan desafíos únicos, como la latencia de red, la tolerancia a fallos y la consistencia de datos. Los patrones de diseño en sistemas distribuidos proporcionan soluciones probadas para abordar estos desafíos.
Objetivos del Tema
- Comprender los desafíos específicos de los sistemas distribuidos.
- Aprender los patrones de diseño más comunes utilizados en sistemas distribuidos.
- Aplicar estos patrones para mejorar la robustez, escalabilidad y mantenibilidad de los sistemas distribuidos.
Desafíos en Sistemas Distribuidos
- Latencia de Red: El tiempo que tarda un mensaje en viajar de un nodo a otro puede afectar el rendimiento del sistema.
- Tolerancia a Fallos: Los sistemas distribuidos deben ser capaces de manejar fallos de hardware y software.
- Consistencia de Datos: Mantener la consistencia de datos en diferentes nodos es crucial y puede ser complicado.
- Escalabilidad: Los sistemas deben ser capaces de manejar un aumento en la carga de trabajo sin degradar el rendimiento.
- Seguridad: La comunicación entre nodos debe ser segura para evitar ataques y accesos no autorizados.
Patrones de Diseño Comunes en Sistemas Distribuidos
- Patrón de Orquestación
Descripción
La orquestación implica la coordinación centralizada de los componentes del sistema. Un orquestador central gestiona las interacciones entre los servicios.
Ejemplo
class Orchestrator: def __init__(self): self.service_a = ServiceA() self.service_b = ServiceB() def execute(self): result_a = self.service_a.perform_task() result_b = self.service_b.perform_task(result_a) return result_b class ServiceA: def perform_task(self): # Lógica del servicio A return "Result from Service A" class ServiceB: def perform_task(self, input): # Lógica del servicio B return f"Result from Service B with {input}" # Uso del orquestador orchestrator = Orchestrator() result = orchestrator.execute() print(result)
Ventajas
- Control centralizado de las interacciones.
- Facilita la implementación de lógica compleja.
Desventajas
- Punto único de fallo.
- Puede convertirse en un cuello de botella.
- Patrón de Coreografía
Descripción
La coreografía implica la coordinación descentralizada de los componentes del sistema. Cada servicio conoce su rol y se comunica directamente con otros servicios según sea necesario.
Ejemplo
class ServiceA: def perform_task(self): # Lógica del servicio A return "Result from Service A" class ServiceB: def perform_task(self, input): # Lógica del servicio B return f"Result from Service B with {input}" # Uso de coreografía service_a = ServiceA() result_a = service_a.perform_task() service_b = ServiceB() result_b = service_b.perform_task(result_a) print(result_b)
Ventajas
- No hay un punto único de fallo.
- Mejor escalabilidad.
Desventajas
- Mayor complejidad en la coordinación.
- Difícil de depurar y mantener.
- Patrón de Circuit Breaker
Descripción
El patrón de Circuit Breaker se utiliza para evitar que una aplicación intente realizar una operación que probablemente fallará. Actúa como un interruptor que corta el flujo cuando detecta fallos repetidos.
Ejemplo
class CircuitBreaker: def __init__(self, failure_threshold, recovery_timeout): self.failure_threshold = failure_threshold self.recovery_timeout = recovery_timeout self.failure_count = 0 self.last_failure_time = None self.state = "CLOSED" def call(self, func, *args, **kwargs): if self.state == "OPEN": if (time.time() - self.last_failure_time) > self.recovery_timeout: self.state = "HALF-OPEN" else: raise Exception("Circuit is open") try: result = func(*args, **kwargs) self.reset() return result except Exception as e: self.record_failure() raise e def reset(self): self.failure_count = 0 self.state = "CLOSED" def record_failure(self): self.failure_count += 1 self.last_failure_time = time.time() if self.failure_count >= self.failure_threshold: self.state = "OPEN" # Uso del Circuit Breaker import time def unreliable_service(): if time.time() % 2 == 0: raise Exception("Service failed") return "Service succeeded" circuit_breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=5) for _ in range(10): try: result = circuit_breaker.call(unreliable_service) print(result) except Exception as e: print(e) time.sleep(1)
Ventajas
- Previene fallos en cascada.
- Mejora la resiliencia del sistema.
Desventajas
- Puede introducir latencia adicional.
- Requiere configuración y ajuste fino.
- Patrón de Event Sourcing
Descripción
El patrón de Event Sourcing almacena el estado de un sistema como una secuencia de eventos. Cada cambio en el estado se representa como un evento, y el estado actual se reconstruye reproduciendo estos eventos.
Ejemplo
class EventStore: def __init__(self): self.events = [] def save_event(self, event): self.events.append(event) def get_events(self): return self.events class Account: def __init__(self, event_store): self.event_store = event_store self.balance = 0 def apply_event(self, event): if event["type"] == "deposit": self.balance += event["amount"] elif event["type"] == "withdraw": self.balance -= event["amount"] def deposit(self, amount): event = {"type": "deposit", "amount": amount} self.event_store.save_event(event) self.apply_event(event) def withdraw(self, amount): event = {"type": "withdraw", "amount": amount} self.event_store.save_event(event) self.apply_event(event) def get_balance(self): return self.balance # Uso del Event Sourcing event_store = EventStore() account = Account(event_store) account.deposit(100) account.withdraw(50) print(f"Balance actual: {account.get_balance()}") print(f"Eventos: {event_store.get_events()}")
Ventajas
- Historial completo de cambios.
- Facilita la auditoría y el debugging.
Desventajas
- Complejidad en la gestión de eventos.
- Puede requerir mucho almacenamiento.
Ejercicio Práctico
Ejercicio 1: Implementar un Circuit Breaker
Objetivo: Implementar un Circuit Breaker para un servicio que puede fallar y probar su funcionamiento.
Instrucciones:
- Implementa una clase
CircuitBreaker
con los métodoscall
,reset
yrecord_failure
. - Simula un servicio que falla aleatoriamente.
- Usa el Circuit Breaker para llamar al servicio y maneja los fallos adecuadamente.
Código Base:
import time import random class CircuitBreaker: def __init__(self, failure_threshold, recovery_timeout): self.failure_threshold = failure_threshold self.recovery_timeout = recovery_timeout self.failure_count = 0 self.last_failure_time = None self.state = "CLOSED" def call(self, func, *args, **kwargs): if self.state == "OPEN": if (time.time() - self.last_failure_time) > self.recovery_timeout: self.state = "HALF-OPEN" else: raise Exception("Circuit is open") try: result = func(*args, **kwargs) self.reset() return result except Exception as e: self.record_failure() raise e def reset(self): self.failure_count = 0 self.state = "CLOSED" def record_failure(self): self.failure_count += 1 self.last_failure_time = time.time() if self.failure_count >= self.failure_threshold: self.state = "OPEN" def unreliable_service(): if random.random() < 0.5: raise Exception("Service failed") return "Service succeeded" # Uso del Circuit Breaker circuit_breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=5) for _ in range(10): try: result = circuit_breaker.call(unreliable_service) print(result) except Exception as e: print(e) time.sleep(1)
Solución
El código base ya proporciona una implementación funcional del Circuit Breaker. Puedes ajustarlo y probarlo para diferentes configuraciones de failure_threshold
y recovery_timeout
.
Conclusión
En esta sección, hemos explorado varios patrones de diseño utilizados en sistemas distribuidos, incluyendo Orquestación, Coreografía, Circuit Breaker y Event Sourcing. Estos patrones ayudan a abordar los desafíos únicos de los sistemas distribuidos, mejorando su robustez, escalabilidad y mantenibilidad. Al aplicar estos patrones, los desarrolladores pueden construir sistemas más resilientes y eficientes.
En el siguiente módulo, exploraremos cómo aplicar estos patrones en arquitecturas modernas, como microservicios y sistemas distribuidos, para maximizar su efectividad y eficiencia.
Curso de Patrones de Diseño de Software
Módulo 1: Introducción a los Patrones de Diseño
- ¿Qué son los Patrones de Diseño?
- Historia y Origen de los Patrones de Diseño
- Clasificación de los Patrones de Diseño
- Ventajas y Desventajas de Usar Patrones de Diseño
Módulo 2: Patrones Creacionales
- Introducción a los Patrones Creacionales
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
Módulo 3: Patrones Estructurales
Módulo 4: Patrones de Comportamiento
- Introducción a los Patrones de Comportamiento
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
Módulo 5: Aplicación de Patrones de Diseño
- Cómo Seleccionar el Patrón Adecuado
- Ejemplos Prácticos de Uso de Patrones
- Patrones de Diseño en Proyectos Reales
- Refactorización Usando Patrones de Diseño
Módulo 6: Patrones de Diseño Avanzados
- Patrones de Diseño en Arquitecturas Modernas
- Patrones de Diseño en Microservicios
- Patrones de Diseño en Sistemas Distribuidos
- Patrones de Diseño en Desarrollo Ágil