sábado, 17 de enero de 2026

Principios Fundamentales de Diseño en Python - Parte 9 - Patrones de Sistemas Distribuidos

Patrones de Sistemas Distribuidos en Python
Python // Design Patterns

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.

Requerimientos Técnicos
python -m pip install flask flask-limiter python -m pip install pybreaker
PATTERN_01

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

PROTECCIÓN
Asegurar que el sistema entregue servicio continuamente
COSTOS
Optimizar el costo de uso del servicio
ESCALABILIDAD
Manejar ráfagas de actividad de forma controlada
Flujo de Throttling
Cliente
——▶
Rate Limiter
——▶
API/Service
✓ Bajo límite
|
⚠ 429 Too Many

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:

throttling_flaskapp.py
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)
python throttling_flaskapp.py
* Running on http://127.0.0.1:5000
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á:

Respuesta HTTP 429
Too Many Requests
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.

Flask-Limiter soporta múltiples estrategias y backends de almacenamiento como Redis para implementaciones en producción. Consulta la documentación oficial para opciones avanzadas.
PATTERN_02

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

FALLOS TRANSITORIOS
Aliviar el impacto de fallos identificados al comunicarse con componentes externos
MICROSERVICIOS
Asegurar que fallos transitorios no causen que todo el sistema falle
SINCRONIZACIÓN
Manejar la indisponibilidad temporal al sincronizar datos entre sistemas
El enfoque de reintento no es recomendado para manejar fallos causados por errores en la lógica de la aplicación. Si la aplicación experimenta fallos frecuentes, es señal de que el servicio tiene un problema de escalabilidad que debe abordarse.
Flujo del Patrón Retry
Llamada
——▶intento 1
Fallo
——▶espera
Retry
——▶intento 2
Éxito

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:

retry_database_connection.py
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()}")
python retry_database_connection.py
INFO:root:Connection attempt #1
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
Cuando ocurre un error temporal de base de datos, se realiza un reintento. Pueden ocurrir varios intentos hasta el máximo de 3. Si después de 3 intentos fallidos, el resultado es el fallo de la operación.
PATTERN_03

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

Máquina de Estados del Circuit Breaker
CLOSED
Operación Normal
——▶
fallos >= threshold
OPEN
Fallos Inmediatos
◀——
éxito
HALF-OPEN
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:

circuit_breaker.py
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()
python circuit_breaker.py
2025-01-17 10:30:01 / OK
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
Observa cómo después de 2 fallos consecutivos, el circuit breaker se abre y todas las llamadas a 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.
PATTERN_04+

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:

Catálogo de Patrones Adicionales
CQRS
Two-Phase Commit
Saga
Sidecar
Service Registry
Bulkhead

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.

Arquitectura CQRS
Commands
——▶
Write Model
|
Read Model
◀——
Queries

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.

Patrón Saga - Transacciones Compensatorias
T1
T2
T3 ✗
C2
C1

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.

Patrón Bulkhead - Aislamiento de Fallos
Pool A
Pool B
Pool C ✗
◀—▶
Sistema Principal
Cada uno de estos patrones aborda desafíos específicos inherentes a los sistemas distribuidos, ofreciendo estrategias y mejores prácticas para arquitectos y desarrolladores que diseñan soluciones robustas y escalables capaces de operar en entornos dinámicos e impredecibles.

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.

Recuerda: Estos patrones no son soluciones aisladas sino piezas de un rompecabezas mayor. A menudo funcionan mejor cuando se combinan y adaptan para ajustarse a las necesidades específicas y restricciones de tu sistema. La clave está en entender los principios subyacentes para poder adaptarlos y crear un sistema distribuido resiliente y eficiente.

Basado en "Mastering Python Design Patterns" // Kamon Ayeva & Sakis Kasampalis

Capítulo 9: Distributed Systems Patterns