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

  1. Latencia de Red: El tiempo que tarda un mensaje en viajar de un nodo a otro puede afectar el rendimiento del sistema.
  2. Tolerancia a Fallos: Los sistemas distribuidos deben ser capaces de manejar fallos de hardware y software.
  3. Consistencia de Datos: Mantener la consistencia de datos en diferentes nodos es crucial y puede ser complicado.
  4. Escalabilidad: Los sistemas deben ser capaces de manejar un aumento en la carga de trabajo sin degradar el rendimiento.
  5. Seguridad: La comunicación entre nodos debe ser segura para evitar ataques y accesos no autorizados.

Patrones de Diseño Comunes en Sistemas Distribuidos

  1. 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.

  1. 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.

  1. 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.

  1. 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:

  1. Implementa una clase CircuitBreaker con los métodos call, reset y record_failure.
  2. Simula un servicio que falla aleatoriamente.
  3. 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.

© Copyright 2024. Todos los derechos reservados