Casi todo lo que usamos a diario en Internet —buscar una web, pagar con tarjeta, ver una serie— funciona gracias a decenas o miles de ordenadores que cooperan sin que lo notemos. Esta lección establece el vocabulario y las ideas fundamentales sobre las que se construye el resto del curso: qué es exactamente un sistema distribuido, por qué se construyen, qué características lo distinguen de un programa "normal" y de qué piezas está hecho. Además, presentaremos el caso práctico que nos acompañará durante todo el curso, Kilómetro Cero, un marketplace de productores locales que empieza como una aplicación sencilla en un único servidor y que, por culpa de su propio éxito, va a tener que convertirse en una plataforma distribuida.

Entender bien estos fundamentos es importante porque los sistemas distribuidos no son simplemente "varios ordenadores": introducen una forma de pensar distinta, en la que el fallo parcial, la concurrencia y la ausencia de una verdad única son la norma y no la excepción.

Contenido

  1. ¿Qué es un sistema distribuido?
  2. ¿Por qué existen los sistemas distribuidos?
  3. Características clave
  4. Transparencia: ocultar la distribución
  5. Componentes de un sistema distribuido
  6. Ejemplos cotidianos
  7. Presentación de Kilómetro Cero: el monolito inicial
  8. Ejemplo de código: simulando nodos que fallan de forma independiente
  9. Errores comunes y consejos
  10. Ejercicios
  11. Conclusión

  1. ¿Qué es un sistema distribuido?

Existen muchas definiciones, pero dos son especialmente útiles porque capturan aspectos complementarios:

  • Definición formal (Andrew Tanenbaum): "Un sistema distribuido es una colección de ordenadores independientes que aparece ante sus usuarios como un único sistema coherente." Esta definición subraya el objetivo: cooperación con apariencia de unidad. El usuario de un buscador no sabe (ni le importa) cuántas máquinas han participado en su consulta.
  • Definición irónica (Leslie Lamport): "Un sistema distribuido es aquel en el que el fallo de una máquina que ni siquiera sabías que existía puede dejar tu propio ordenador inutilizable." Esta definición subraya el problema: la dependencia oculta y el fallo parcial. En un sistema distribuido, tu programa puede dejar de funcionar por causas completamente ajenas a él.

Ambas definiciones son ciertas al mismo tiempo, y la tensión entre ellas es la esencia de esta disciplina: queremos que muchas máquinas parezcan una sola, pero cada máquina puede fallar por separado.

Conviene fijar una distinción sencilla:

Sistema Dónde se ejecuta Cómo se comunica Modo de fallo habitual
Centralizado (un proceso) Una máquina Llamadas a funciones en memoria Todo o nada: el proceso funciona o cae
Concurrente en una máquina Una máquina, varios hilos o procesos Memoria compartida, tuberías Todo o nada (con condiciones de carrera)
Distribuido Varias máquinas Mensajes por red Parcial: unas partes fallan y otras no

La diferencia esencial no es la cantidad de ordenadores, sino que la única forma de comunicarse es enviando mensajes por una red que puede retrasarlos o perderlos, y que cada nodo puede fallar mientras los demás siguen funcionando.

  1. ¿Por qué existen los sistemas distribuidos?

Nadie construye un sistema distribuido por gusto: son más difíciles de diseñar, probar y operar que un programa en una sola máquina. Se construyen porque hay motivos que lo justifican:

  1. Escala. Una sola máquina tiene un límite de CPU, memoria, disco y conexiones de red. Cuando la demanda supera ese límite, la única salida es repartir el trabajo entre varias máquinas.
  2. Geografía. Los usuarios están repartidos por el mundo, y la velocidad de la luz impone una latencia mínima. Servir a cada usuario desde un centro de datos cercano exige tener máquinas en varios lugares. Además, algunas fuentes de datos están inherentemente distribuidas (sensores, dispositivos móviles, sucursales).
  3. Disponibilidad y tolerancia a fallos. Si el servicio corre en una sola máquina, esa máquina es un punto único de fallo. Con varias máquinas, el sistema puede seguir funcionando aunque alguna caiga.
  4. Coste. A partir de cierto tamaño, muchas máquinas modestas son mucho más baratas que una única máquina enorme (y una máquina enorme, al final, también tiene un límite). Además, permite pagar solo por la capacidad que se usa.
  5. Organización. Las empresas grandes tienen muchos equipos; que cada equipo sea dueño de su parte del sistema, con su propio ciclo de despliegue, requiere que esas partes estén separadas.

En la lección 01-03 analizaremos en detalle las ventajas y los costes de distribuir. Por ahora basta con tener claro que la distribución siempre es una respuesta a un problema concreto, nunca un fin en sí misma.

  1. Características clave

Todo sistema distribuido, independientemente de su tamaño, comparte tres características fundamentales. Son consecuencias directas de "varias máquinas que se comunican por mensajes".

3.1 Concurrencia

Los nodos se ejecutan simultáneamente y de forma independiente. En Kilómetro Cero, mientras Ana está añadiendo un queso al carrito desde Barcelona, Marc está pagando un pedido desde Valencia y un repartidor está enviando su posición desde Zaragoza. No hay un "turno": todo ocurre a la vez, y los recursos compartidos (por ejemplo, el stock de un producto) deben coordinarse explícitamente.

3.2 Ausencia de reloj global

Cada máquina tiene su propio reloj, y estos relojes nunca están perfectamente sincronizados. Si el nodo A registra un evento a las 10:00:00.120 y el nodo B otro a las 10:00:00.118, no podemos afirmar con seguridad cuál sucedió antes: la diferencia puede ser menor que el error de sincronización. Esto tiene consecuencias profundas para cualquier cosa que dependa de "quién fue primero". Lo estudiaremos a fondo en la lección 01-05.

3.3 Fallos independientes

En un programa monolítico, si algo falla, falla todo, y normalmente lo detectas de inmediato (una excepción, un proceso muerto). En un sistema distribuido:

  • Un nodo puede caerse mientras el resto sigue en marcha (fallo parcial).
  • La red puede perder mensajes, duplicarlos o retrasarlos.
  • Un nodo puede estar tan lento que sea indistinguible de uno caído.
  • Y lo peor: no siempre hay forma de saber qué ha pasado. Si envías una petición y no recibes respuesta, ¿la petición no llegó? ¿Llegó, se procesó y se perdió la respuesta? ¿El nodo está caído o simplemente sobrecargado?

Esta incertidumbre es la raíz de la mayoría de dificultades del curso. Diseñar sistemas distribuidos consiste, en buena parte, en asumir que el fallo parcial es inevitable y decidir cómo comportarse cuando ocurre.

  1. Transparencia: ocultar la distribución

Recordemos la definición de Tanenbaum: el sistema debe aparecer como uno solo. Cada uno de los aspectos de la distribución que se ocultan al usuario (o al programador) recibe el nombre de transparencia. Las más importantes:

Transparencia Qué oculta Ejemplo en Kilómetro Cero
De acceso Las diferencias en la representación de datos y en cómo se accede a un recurso local o remoto El código que consulta el catálogo se escribe igual tanto si el catálogo está en el mismo proceso como si está en otra máquina
De ubicación Dónde está físicamente un recurso La app llama a catalogo.buscar("queso") sin saber en qué servidor ni en qué ciudad se ejecuta
De replicación Que existen varias copias del mismo recurso El cliente ve "el catálogo", aunque haya cinco copias sirviéndolo
De fallo Que un componente ha fallado y se ha recuperado (o se ha sustituido) Si una copia del catálogo cae, las peticiones van a otra sin que Ana lo note
De escalado Que el sistema crece o decrece en tamaño Añadir servidores durante una campaña no cambia la forma de usar la plataforma
De concurrencia Que varios usuarios comparten el mismo recurso Ana y Marc consultan el mismo stock sin interferirse
De migración Que un recurso se mueve de sitio mientras se usa Una sesión pasa de un servidor a otro sin cortar la conexión

Es importante entender que la transparencia total es un ideal, no una realidad. Ocultar completamente la distribución tiene costes (latencia, complejidad) y, en muchos casos, es contraproducente: si el programador no sabe que una llamada viaja por la red, escribirá código que asume que nunca falla y que siempre responde rápido. Veremos exactamente este problema en la lección 01-04, dedicada a las falacias de la computación distribuida.

  1. Componentes de un sistema distribuido

Podemos describir cualquier sistema distribuido en función de cuatro tipos de piezas:

flowchart LR
    subgraph Nodo_A["Nodo A"]
        A1[Aplicación]
        A2[Middleware]
        A3[Sistema operativo]
    end
    subgraph Nodo_B["Nodo B"]
        B1[Aplicación]
        B2[Middleware]
        B3[Sistema operativo]
    end
    A2 <-- "Protocolos (mensajes)" --> B2
    A3 <-- "Red" --> B3
  • Nodos. Cada una de las máquinas (físicas o virtuales, contenedores incluidos) que ejecutan una parte del sistema. Un nodo tiene su propia CPU, memoria, disco y reloj. Entre los nodos no hay memoria compartida: solo mensajes.
  • Red. El medio por el que viajan los mensajes: cables, conmutadores, enrutadores, enlaces inalámbricos, Internet. La red es un componente más del sistema, con sus propios fallos y limitaciones, aunque a menudo se olvida.
  • Protocolos. Las reglas que definen cómo se estructuran e intercambian los mensajes: formato, orden, qué hacer ante un error. Desde los protocolos de transporte (TCP, UDP) hasta los de aplicación (HTTP, gRPC, MQTT), se estudian en el Módulo 2.
  • Middleware. La capa de software que se sitúa entre las aplicaciones y el sistema operativo/red y que proporciona las transparencias: localización de servicios, llamadas remotas, colas de mensajes, replicación... Es lo que permite que el programador de aplicaciones no tenga que trabajar directamente con sockets y bytes. Buena parte del curso (RPC, colas, bases de datos distribuidas, orquestadores) trata precisamente sobre middleware.

  1. Ejemplos cotidianos

Para convencerse de que los sistemas distribuidos están en todas partes, basta con mirar cuatro cosas que hacemos cada día:

Sistema Qué distribuye Transparencias que ofrece Qué pasaría en un fallo parcial
DNS Una base de datos jerárquica de nombres, replicada en miles de servidores de todo el mundo Ubicación, replicación, fallo Tu resolvedor pregunta a otro servidor; casi nunca lo notas
La Web Contenido servido por millones de servidores, con cachés (CDN) cerca de cada usuario Ubicación, replicación, escalado Una web concreta no carga, pero el resto de Internet sigue funcionando
Banca electrónica Cuentas, transacciones y cajeros repartidos en muchos sistemas que deben mantener coherencia Acceso, concurrencia, replicación Puedes consultar saldo pero no hacer transferencias (degradación parcial)
Streaming de vídeo Copias de cada película en servidores repartidos geográficamente, más los sistemas de recomendación y facturación Ubicación, replicación, escalado Cae la recomendación, pero puedes seguir viendo tu serie

Fíjate en la última columna: en todos los casos, el sistema está diseñado para degradarse parcialmente en lugar de caer del todo. Esa es una de las promesas de los sistemas distribuidos... y también una de las cosas más difíciles de conseguir.

  1. Presentación de Kilómetro Cero: el monolito inicial

A lo largo del curso trabajaremos con Kilómetro Cero, un marketplace online ficticio que conecta productores locales con consumidores de varias ciudades. Entre sus productores están Huerta La Vega (verduras de temporada), Quesería Montblanc (quesos artesanos) y Bodega Roble Alto (vinos). Entre sus clientes, Ana, Marc y Lucía.

7.1 La arquitectura de partida

Kilómetro Cero nació como cualquier proyecto sensato: un monolito. Una única aplicación web en Python, con todos los módulos en el mismo proceso, hablando con una única base de datos PostgreSQL, todo en un único servidor.

flowchart TB
    U1[Ana - navegador] --> S
    U2[Marc - app móvil] --> S
    U3[Lucía - navegador] --> S
    subgraph S["Servidor único (8 vCPU, 32 GB RAM)"]
        direction TB
        subgraph APP["Aplicación Python (un proceso)"]
            C[catalogo]
            P[pedidos]
            I[inventario]
            PA[pagos]
            R[reparto]
            AN[analitica]
        end
        DB[(PostgreSQL)]
        APP --> DB
    end

Esta arquitectura tiene enormes virtudes, y conviene decirlo claramente para no caer en el prejuicio de que "monolito" es sinónimo de "mal diseño":

  • Simplicidad. Una llamada del módulo pedidos al módulo inventario es una simple llamada a una función Python: no hay red, ni serialización, ni timeouts.
  • Transacciones sencillas. Descontar stock, crear el pedido y registrar el pago ocurren en una única transacción de PostgreSQL: o se hace todo o no se hace nada.
  • Despliegue trivial. Un único artefacto que se instala en un único servidor.
  • Depuración directa. Un único registro de logs, una única traza de pila.

En una máquina con 8 vCPU y 32 GB de RAM, Kilómetro Cero atiende sin problemas a sus primeras dos ciudades: unos 300 pedidos al día y picos de 40 peticiones por segundo.

7.2 El primer síntoma: la "Semana de la Vendimia"

En septiembre, Kilómetro Cero lanza su primera gran campaña de temporada: la Semana de la Vendimia, con ofertas de Bodega Roble Alto y otras bodegas, promocionada en redes sociales en cinco ciudades. El lunes a las 10:00 se abre la campaña y ocurre lo siguiente:

Hora Peticiones/s Síntoma
09:55 45 Normal
10:02 380 Las páginas del catálogo tardan 3-4 s en cargar
10:06 610 PostgreSQL agota su límite de conexiones; los pagos empiezan a fallar
10:09 700+ El proceso Python se queda sin memoria y el servidor deja de responder
10:10 0 Caída total. Ni catálogo, ni pedidos, ni el seguimiento de repartidores

Observa un detalle revelador: el pico de tráfico lo provoca la gente mirando el catálogo de vinos, pero lo que cae es todo, incluido el seguimiento de los repartidores, que no tiene nada que ver con la campaña. En un monolito, todos los módulos comparten el mismo destino: la misma CPU, la misma memoria, el mismo pool de conexiones a la base de datos.

Este incidente será el punto de partida de la evolución de Kilómetro Cero que iremos siguiendo durante el curso. Conviene resistir la tentación de saltar ya a la solución: primero (lecciones 01-02 a 01-05) necesitamos entender qué significa distribuir y qué nuevos problemas aparecen; en la lección 01-06 dibujaremos la arquitectura objetivo.

  1. Ejemplo de código: simulando nodos que fallan de forma independiente

Vamos a construir una pequeña simulación en Python para sentir la diferencia entre un fallo total y un fallo parcial. No necesitamos redes reales: representaremos cada nodo con una clase, y la "red" con llamadas a métodos. Es una simplificación enorme, pero suficiente para ilustrar la idea.

El escenario: Kilómetro Cero tiene tres nodos que atienden pedidos (pedidos-1, pedidos-2 y pedidos-3) detrás de un repartidor de carga muy sencillo. Uno de los nodos va a fallar en mitad de la campaña.

import random
from dataclasses import dataclass, field


@dataclass
class Pedido:
    """Un pedido de un cliente de Kilómetro Cero."""
    id: int
    cliente: str
    producto: str
    productor: str


class NodoCaidoError(Exception):
    """Se lanza cuando intentamos hablar con un nodo que ha fallado."""


@dataclass
class NodoPedidos:
    """Simula una máquina que ejecuta el servicio 'pedidos'.

    Cada nodo tiene su propio estado (los pedidos que ha atendido) y su
    propia 'salud'. No comparte memoria con los demás nodos: si queremos
    saber algo de él, tenemos que 'enviarle un mensaje' (llamar a un método).
    """
    nombre: str
    activo: bool = True
    atendidos: list = field(default_factory=list)

    def procesar(self, pedido: Pedido) -> str:
        if not self.activo:
            # En la realidad no habría excepción: simplemente no llegaría
            # respuesta. Aquí lo modelamos como una excepción para simplificar.
            raise NodoCaidoError(f"{self.nombre} no responde")
        self.atendidos.append(pedido.id)
        return f"{self.nombre} confirmó el pedido {pedido.id} de {pedido.cliente}"

    def fallar(self) -> None:
        """Simula que la máquina se apaga de golpe."""
        self.activo = False


class RepartidorDeCarga:
    """Envía cada pedido a un nodo distinto, por turnos (round-robin)."""

    def __init__(self, nodos: list[NodoPedidos]):
        self.nodos = nodos
        self._siguiente = 0

    def enviar(self, pedido: Pedido) -> str:
        nodo = self.nodos[self._siguiente % len(self.nodos)]
        self._siguiente += 1
        return nodo.procesar(pedido)


def generar_pedidos(cantidad: int) -> list[Pedido]:
    clientes = ["Ana", "Marc", "Lucía"]
    productos = [
        ("Tomates de temporada", "Huerta La Vega"),
        ("Queso curado", "Quesería Montblanc"),
        ("Vino tinto crianza", "Bodega Roble Alto"),
    ]
    pedidos = []
    for i in range(1, cantidad + 1):
        producto, productor = random.choice(productos)
        pedidos.append(Pedido(i, random.choice(clientes), producto, productor))
    return pedidos


if __name__ == "__main__":
    random.seed(7)  # para que la ejecución sea reproducible

    nodos = [NodoPedidos("pedidos-1"), NodoPedidos("pedidos-2"), NodoPedidos("pedidos-3")]
    balanceador = RepartidorDeCarga(nodos)

    correctos, fallidos = 0, 0
    for pedido in generar_pedidos(12):
        if pedido.id == 5:
            print(">>> ¡El nodo pedidos-2 se ha caído! <<<")
            nodos[1].fallar()
        try:
            print(balanceador.enviar(pedido))
            correctos += 1
        except NodoCaidoError as e:
            print(f"ERROR al procesar el pedido {pedido.id}: {e}")
            fallidos += 1

    print(f"\nResultado: {correctos} pedidos correctos, {fallidos} fallidos")
    for nodo in nodos:
        estado = "activo" if nodo.activo else "CAÍDO"
        print(f"  {nodo.nombre} ({estado}): atendió {nodo.atendidos}")

Analicemos el programa paso a paso:

  1. Pedido es un simple contenedor de datos: identificador, cliente, producto y productor. Usamos @dataclass para no tener que escribir el constructor a mano.
  2. NodoPedidos representa una máquina. Lo esencial es que tiene estado propio (atendidos) y salud propia (activo). Cuando activo es False, cualquier intento de usarlo lanza NodoCaidoError. Esto modela el fallo independiente: un nodo puede caer sin que los demás se enteren.
  3. RepartidorDeCarga reparte los pedidos entre los nodos por turnos. Es la pieza que, en un sistema real, sería un balanceador de carga; aquí es un simple contador.
  4. En el bucle principal, al llegar al pedido 5 apagamos pedidos-2. A partir de ahí, uno de cada tres pedidos falla, pero los otros dos tercios siguen procesándose con normalidad.

La salida (abreviada) es algo así:

pedidos-1 confirmó el pedido 1 de Ana
pedidos-2 confirmó el pedido 2 de Lucía
pedidos-3 confirmó el pedido 3 de Ana
pedidos-1 confirmó el pedido 4 de Ana
>>> ¡El nodo pedidos-2 se ha caído! <<<
ERROR al procesar el pedido 5: pedidos-2 no responde
pedidos-3 confirmó el pedido 6 de Lucía
pedidos-1 confirmó el pedido 7 de Ana
ERROR al procesar el pedido 8: pedidos-2 no responde
...
Resultado: 9 pedidos correctos, 3 fallidos
  pedidos-1 (activo): atendió [1, 4, 7, 10]
  pedidos-2 (CAÍDO): atendió [2]
  pedidos-3 (activo): atendió [3, 6, 9, 12]

Comparémoslo con el monolito de la Semana de la Vendimia: allí, un único fallo dejaba 0 pedidos correctos. Aquí, el sistema sigue atendiendo el 67 % de los pedidos. Eso es un fallo parcial: peor que "todo funciona", mucho mejor que "nada funciona".

Pero fíjate también en lo que la simulación no resuelve, y que anticipa temas de lecciones futuras:

  • El balanceador sigue enviando pedidos a pedidos-2 aunque esté caído. Necesitaría detectar el fallo y dejar de usarlo (lección 07-03).
  • Los pedidos 5, 8 y 11 se han perdido. ¿Deberíamos reintentarlos en otro nodo? ¿Y si pedidos-2 sí llegó a procesar el pedido antes de caer? (lecciones 02-05 y 07-04).
  • El estado de pedidos-2 (el pedido 2) se ha quedado atrapado en un nodo caído. ¿Cómo lo recuperamos? (Módulo 3, replicación).
  • En la simulación sabemos con certeza que el nodo ha caído porque lanza una excepción. En la realidad, lo único que veríamos es que no responde, y tendríamos que decidir cuánto esperar (lección 01-04).

Errores Comunes y Consejos

  • Confundir "distribuido" con "muchos servidores". Lo que define un sistema distribuido es que las partes solo se comunican por mensajes y pueden fallar por separado. Dos procesos en la misma máquina que hablan por sockets ya presentan muchos de los problemas de un sistema distribuido (aunque no todos).
  • Pensar que la transparencia total es deseable. Ocultar completamente que una llamada es remota lleva a escribir código que ignora la latencia y los fallos. Los buenos diseños hacen la distribución cómoda, no invisible.
  • Distribuir demasiado pronto. Kilómetro Cero hizo bien empezando como monolito: le permitió lanzar rápido y con pocos costes. La distribución llega cuando hay un problema concreto (escala, disponibilidad, organización) que la justifica. Distribuir "por si acaso" añade complejidad sin beneficio.
  • Olvidar que la red es un componente. Al dibujar arquitecturas es habitual poner cajas (los nodos) y flechas (la comunicación) y tratar las flechas como si fueran gratuitas e infalibles. Las flechas fallan, tardan y cuestan dinero.
  • Asumir que un nodo que no responde está caído. Puede estar sobrecargado, puede estar aislado por un problema de red, o puede haber procesado la petición y haberse perdido solo la respuesta. Esta ambigüedad reaparecerá una y otra vez en el curso.
  • Consejo: cuando diseñes cualquier interacción entre dos componentes, hazte siempre tres preguntas: ¿qué pasa si el otro no responde?, ¿qué pasa si responde muy tarde? y ¿qué pasa si mi mensaje llegó pero la respuesta se perdió?. Si no tienes respuesta para las tres, el diseño no está terminado.

Ejercicios

Ejercicio 1: Clasificar sistemas

Para cada uno de los siguientes casos, indica si se trata de un sistema distribuido según la definición de esta lección y justifica en una o dos frases cuál de las características clave (concurrencia, ausencia de reloj global, fallos independientes) aplica y cuál no:

  1. Un programa de hoja de cálculo que usa varios hilos para recalcular fórmulas en paralelo.
  2. Una aplicación móvil de Kilómetro Cero que guarda el carrito en el teléfono y lo sincroniza con el servidor cuando hay cobertura.
  3. Un script de copia de seguridad que copia archivos de un disco a otro en el mismo ordenador.
  4. Tres repartidores que envían su posición cada 5 segundos al servicio reparto.

Ejercicio 2: Transparencias en la Semana de la Vendimia

Imagina que, tras el incidente, Kilómetro Cero añade dos servidores más para el catálogo y un balanceador de carga delante. Indica qué transparencias (de la tabla del apartado 4) se están proporcionando a los clientes (Ana, Marc, Lucía) y cuáles no se proporcionan a los desarrolladores del módulo pedidos, que ahora debe llamar al catálogo por red en lugar de con una llamada de función.

Ejercicio 3: Mejorar la simulación

Modifica la simulación del apartado 8 para que el RepartidorDeCarga salte los nodos caídos: cuando un nodo lance NodoCaidoError, debe reintentar el mismo pedido en el siguiente nodo activo. Si ningún nodo está activo, debe lanzar una excepción SinNodosDisponiblesError. Comprueba que, con pedidos-2 caído, los 12 pedidos se procesan correctamente.

Soluciones

Solución 1:

  1. No es distribuido. Hay concurrencia (varios hilos), pero todos comparten memoria y el mismo reloj, y si el proceso falla, fallan todos los hilos a la vez. Es un sistema concurrente centralizado.
  2. Sí es distribuido. El teléfono y el servidor son nodos independientes que solo se comunican por mensajes, con relojes distintos, y el teléfono puede quedarse sin cobertura (fallo de red) mientras el servidor sigue funcionando. Fíjate en que aparecerán problemas de coherencia (¿qué carrito es el "bueno" si se modificó en ambos sitios?), que trataremos en el Módulo 3.
  3. No es distribuido. Un único proceso en una única máquina. Si el disco destino falla, es un fallo de hardware local, pero no hay nodos independientes comunicándose por mensajes.
  4. Sí es distribuido. Cada repartidor es un nodo con su reloj (el del móvil), envía mensajes por una red poco fiable (4G) y puede desconectarse de forma independiente. El servicio reparto debe asumir que las posiciones pueden llegar tarde, desordenadas o no llegar.

Solución 2:

A los clientes se les proporciona transparencia de replicación (ven "el catálogo", aunque haya tres copias), de ubicación (no saben a qué servidor les envía el balanceador), de fallo (si un servidor de catálogo cae, el balanceador los envía a otro) y de escalado (añadir el tercer servidor no cambió nada para ellos).

A los desarrolladores de pedidos no se les proporciona plenamente transparencia de acceso: antes llamaban a una función local y ahora tienen que hacer una petición por red, manejar timeouts y errores de conexión, y serializar datos. Tampoco tienen transparencia de fallo completa: si el balanceador o los tres servidores de catálogo caen, pedidos recibe un error y tiene que decidir qué hacer. Esa es precisamente la lección: la distribución se puede ocultar al usuario final, pero rara vez se puede ocultar del todo al programador.

Solución 3:

class SinNodosDisponiblesError(Exception):
    """Ningún nodo del clúster está activo."""


class RepartidorDeCargaResiliente(RepartidorDeCarga):
    """Igual que RepartidorDeCarga, pero salta los nodos caídos."""

    def enviar(self, pedido: Pedido) -> str:
        intentos = 0
        while intentos < len(self.nodos):
            nodo = self.nodos[self._siguiente % len(self.nodos)]
            self._siguiente += 1
            intentos += 1
            try:
                return nodo.procesar(pedido)
            except NodoCaidoError:
                # Este nodo no responde: probamos con el siguiente.
                continue
        raise SinNodosDisponiblesError(
            f"No hay nodos activos para el pedido {pedido.id}"
        )

Basta con sustituir RepartidorDeCarga(nodos) por RepartidorDeCargaResiliente(nodos) en el programa principal. Con pedidos-2 caído, la salida final mostrará 12 pedidos correctos, 0 fallidos, con los pedidos repartidos entre pedidos-1 y pedidos-3.

Reflexión adicional: esta solución asume que si un nodo lanza NodoCaidoError es seguro reintentar en otro. En la simulación es cierto, porque el nodo caído no ha hecho nada. En un sistema real, la petición podría haber llegado y haberse procesado antes de que se perdiera la respuesta; reintentar a ciegas podría crear el pedido dos veces. La solución a esto (idempotencia) se estudia en la lección 02-05.

Conclusión

En esta lección hemos fijado los cimientos del curso. Un sistema distribuido es un conjunto de nodos independientes que cooperan intercambiando mensajes por una red y que se presentan ante el usuario como un único sistema. Se construyen por necesidad —escala, geografía, disponibilidad, coste, organización— y no por gusto, porque introducen tres características que cambian por completo la forma de razonar: concurrencia real, ausencia de un reloj global y fallos parciales e independientes. Para hacer la distribución llevadera, el middleware ofrece distintas transparencias (acceso, ubicación, replicación, fallo, escalado), que siempre son parciales y tienen coste.

Hemos conocido a Kilómetro Cero en su estado inicial —un monolito Python con PostgreSQL en un único servidor— y hemos visto cómo la Semana de la Vendimia lo tumba por completo, incluidos módulos que nada tenían que ver con la campaña. La simulación de nodos ha mostrado el lado positivo (el sistema sigue atendiendo pedidos cuando un nodo cae) y también ha dejado a la vista los problemas que aún no sabemos resolver.

En la siguiente lección, Modelos de Sistemas Distribuidos, veremos las distintas formas de organizar los nodos (cliente-servidor, multicapa, peer-to-peer, servicios), los supuestos que podemos hacer sobre su comunicación y sus fallos, y empezaremos a dibujar cómo podría reorganizarse Kilómetro Cero.

Curso de Arquitecturas Distribuidas

Módulo 1: Introducción a los Sistemas Distribuidos

Módulo 2: Comunicación en Sistemas Distribuidos

Módulo 3: Consistencia y Replicación

Módulo 4: Almacenamiento Distribuido

Módulo 5: Computación Distribuida

Módulo 6: Seguridad en Sistemas Distribuidos

Módulo 7: Monitoreo y Mantenimiento

Módulo 8: Casos de Estudio y Aplicaciones

© Copyright 2026. Todos los derechos reservados