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
- ¿Qué es un sistema distribuido?
- ¿Por qué existen los sistemas distribuidos?
- Características clave
- Transparencia: ocultar la distribución
- Componentes de un sistema distribuido
- Ejemplos cotidianos
- Presentación de Kilómetro Cero: el monolito inicial
- Ejemplo de código: simulando nodos que fallan de forma independiente
- Errores comunes y consejos
- Ejercicios
- Conclusión
- ¿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.
- ¿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:
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
pedidosal móduloinventarioes 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.
- 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:
Pedidoes un simple contenedor de datos: identificador, cliente, producto y productor. Usamos@dataclasspara no tener que escribir el constructor a mano.NodoPedidosrepresenta una máquina. Lo esencial es que tiene estado propio (atendidos) y salud propia (activo). CuandoactivoesFalse, cualquier intento de usarlo lanzaNodoCaidoError. Esto modela el fallo independiente: un nodo puede caer sin que los demás se enteren.RepartidorDeCargareparte 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.- 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-2aunque 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-2sí 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:
- Un programa de hoja de cálculo que usa varios hilos para recalcular fórmulas en paralelo.
- 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.
- Un script de copia de seguridad que copia archivos de un disco a otro en el mismo ordenador.
- 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:
- 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.
- 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.
- 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.
- 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
repartodebe 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
- Conceptos Básicos de Sistemas Distribuidos
- Modelos de Sistemas Distribuidos
- Ventajas y Desafíos de los Sistemas Distribuidos
- Las Falacias de la Computación Distribuida
- Tiempo, Relojes y Ordenación de Eventos
- Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero
Módulo 2: Comunicación en Sistemas Distribuidos
- Protocolos de Comunicación
- RPC y RMI
- gRPC y Serialización de Datos
- Mensajería y Colas de Mensajes
- Patrones de Comunicación Asíncrona
Módulo 3: Consistencia y Replicación
- Modelos de Consistencia
- El Teorema CAP y PACELC
- Algoritmos de Consenso
- Replicación de Datos
- Transacciones Distribuidas y Sagas
Módulo 4: Almacenamiento Distribuido
- Particionado de Datos y Hashing Consistente
- Sistemas de Archivos Distribuidos
- Almacenamiento de Objetos
- Bases de Datos Distribuidas
- Cachés Distribuidos
Módulo 5: Computación Distribuida
- Modelos de Computación Distribuida
- MapReduce y Hadoop
- Spark y Computación en Memoria
- Procesamiento de Flujos de Datos
- Planificación de Trabajos y Pipelines de Datos
Módulo 6: Seguridad en Sistemas Distribuidos
- Autenticación y Autorización
- Cifrado y Protección de Datos
- Gestión de Identidades
- Seguridad entre Servicios: mTLS y Gestión de Secretos
- Puertas de Enlace, Limitación de Tasa y Auditoría
Módulo 7: Monitoreo y Mantenimiento
- Monitoreo de Sistemas Distribuidos
- Logs Centralizados y Trazabilidad Distribuida
- Gestión de Fallos y Recuperación
- Patrones de Resiliencia: Timeouts, Reintentos y Circuit Breaker
- Automatización y Orquestación
- Pruebas en Sistemas Distribuidos e Ingeniería del Caos
