A principios de los años noventa, Peter Deutsch, ingeniero de Sun Microsystems, recopiló una lista de suposiciones falsas que los programadores hacen, casi sin darse cuenta, cuando escriben por primera vez software que se comunica por red. James Gosling (creador de Java) completó la lista hasta las ocho falacias de la computación distribuida que hoy son un clásico. Su valor está en que cada una de ellas es tan natural que la damos por cierta al escribir código y solo descubrimos que era falsa cuando el sistema falla en producción.
Esta lección repasa las ocho falacias una a una. Para cada una veremos qué significa, cómo se manifiesta concretamente en Kilómetro Cero y qué medida de diseño la contrarresta, señalando la lección del curso donde esa medida se desarrolla. Terminaremos con un ejemplo de código en el que una llamada "ingenua" del servicio pedidos al servicio inventario se enfrenta a una red que pierde paquetes y tarda un tiempo variable, y veremos cómo un simple timeout cambia por completo su comportamiento.
Contenido
- Origen y sentido de las falacias
- Falacia 1: la red es fiable
- Falacia 2: la latencia es cero
- Falacia 3: el ancho de banda es infinito
- Falacia 4: la red es segura
- Falacia 5: la topología no cambia
- Falacia 6: hay un único administrador
- Falacia 7: el coste de transporte es cero
- Falacia 8: la red es homogénea
- Tabla resumen: falacia, síntoma, contramedida, lección
- Ejemplo de código:
pedidosllama ainventarioen una red imperfecta - Errores comunes y consejos
- Ejercicios
- Conclusión
- Origen y sentido de las falacias
Las falacias no son errores de programación: son errores de modelo mental. Cuando en el monolito de Kilómetro Cero el módulo pedidos llamaba a inventario.descontar_stock(producto_id, 1), esa llamada era una función Python: siempre llegaba, siempre respondía en microsegundos, nadie podía interceptarla y no costaba dinero. Al separar pedidos e inventario en dos servicios, la línea de código puede seguir siendo casi idéntica (inventario.descontar_stock(...) a través de un cliente RPC), pero todas esas propiedades han desaparecido. Si el programador no cambia su modelo mental, escribirá código correcto para el monolito e incorrecto para el sistema distribuido.
Por eso conviene recorrer las ocho falacias con un ejemplo concreto en la cabeza. Nuestro ejemplo será, principalmente, la interacción pedidos → inventario al confirmar un pedido, y el flujo de posiciones que los repartidores envían al servicio reparto por 4G.
- Falacia 1: la red es fiable
Qué significa creerla. Que todo mensaje enviado llega a su destino, exactamente una vez y sin corromperse.
La realidad. Los paquetes se pierden (congestión, cables defectuosos, conmutadores que se reinician), se duplican (reintentos de capas inferiores) y llegan desordenados. TCP oculta parte de esto (retransmite y reordena), pero no puede hacer nada si el enlace cae por completo, si el otro extremo se reinicia o si un cortafuegos empieza a descartar conexiones. Y, lo más importante, cuando una petición no obtiene respuesta, el emisor no puede saber si se perdió la petición o la respuesta.
En Kilómetro Cero. Al confirmar el pedido de Ana, pedidos envía a inventario "descuenta una unidad de queso curado". Si la petición se pierde, el stock no se descuenta y el pedido se confirma sin existencias. Si la petición llega, inventario descuenta, pero la respuesta se pierde, pedidos cree que ha fallado; si reintenta, se descontarán dos unidades.
Contramedidas. Confirmaciones y reintentos, pero con operaciones idempotentes (que puedan repetirse sin efectos adicionales) e identificadores únicos de petición (lección 02-05); colas de mensajes con entrega garantizada (02-04); y estrategias de reintento con espera exponencial (07-04).
- Falacia 2: la latencia es cero
Qué significa creerla. Que una llamada remota tarda lo mismo que una llamada local, es decir, prácticamente nada.
La realidad. Una llamada a función en memoria tarda nanosegundos. Una llamada por red dentro del mismo centro de datos tarda entre 0,5 y 2 ms; entre ciudades europeas, 20-40 ms; entre continentes, 100-200 ms; por 4G desde un móvil, entre 50 y 500 ms, con enorme variabilidad. Es decir, una llamada remota es entre 10.000 y 1.000.000 de veces más lenta que una local. Y el problema se agrava cuando las llamadas se encadenan: el patrón "N+1" (hacer una llamada por cada elemento de una lista) que en el monolito era un descuido tolerable, en un sistema distribuido convierte una página en un desastre.
En Kilómetro Cero. La página del carrito de Lucía muestra 15 productos. Si la nueva versión de pedidos hace una llamada a catalogo por cada producto para obtener su nombre y precio, son 15 llamadas × 20 ms = 300 ms solo en esperas de red, en serie. Y las posiciones de los repartidores llegan con retrasos variables de 80 a 900 ms, de modo que la posición "actual" en el mapa siempre tiene cierta antigüedad y a veces llega desordenada.
Contramedidas. Diseñar interfaces de grano grueso (una llamada que devuelve 15 productos, no 15 llamadas); paralelizar las llamadas independientes; cachear (04-05); situar los datos cerca de quien los usa (replicación geográfica, 03-04); comunicación asíncrona para lo que no necesita respuesta inmediata (02-04). Y, en cualquier caso, medir la latencia de cada llamada (07-01).
- Falacia 3: el ancho de banda es infinito
Qué significa creerla. Que se puede enviar cualquier cantidad de datos sin que el tamaño importe.
La realidad. Aunque el ancho de banda ha crecido enormemente, sigue siendo finito y compartido. Además, la latencia y el ancho de banda interactúan: enviar 10 MB por un enlace de 100 Mb/s tarda casi un segundo, por muy baja que sea la latencia. Y en redes móviles, el ancho de banda es escaso, variable y, a menudo, de pago por volumen.
En Kilómetro Cero. La primera versión del servicio catalogo devolvía, en cada búsqueda, los productos completos con sus fotos codificadas en base64 dentro del JSON: 2 MB por respuesta. Con 400 búsquedas/s durante la campaña, son 800 MB/s, más de lo que da la interfaz de red. Otro caso: la app del repartidor enviaba, en cada actualización de posición, todo el historial del día en lugar de solo la posición nueva.
Contramedidas. Enviar solo lo necesario (paginación, campos seleccionables); formatos de serialización compactos como Protocol Buffers (02-03); compresión; separar los datos grandes (fotos) en un almacenamiento de objetos y enviar solo referencias (04-03); agregar mensajes pequeños en lotes cuando el volumen lo justifique.
- Falacia 4: la red es segura
Qué significa creerla. Que solo los componentes legítimos pueden leer o enviar mensajes en la red.
La realidad. Cualquier mensaje que atraviesa una red puede ser leído, modificado o fabricado por quien tenga acceso a esa red. Y "la red interna" es un concepto cada vez más difuso: contenedores, nubes públicas, redes de proveedores, portátiles de empleados, dispositivos móviles. En un monolito, la llamada de pedidos a inventario no podía ser interceptada porque no salía del proceso. Ahora sí.
En Kilómetro Cero. Si inventario acepta cualquier petición de "descuenta stock" sin verificar quién la envía, cualquiera que llegue a la red interna (un contenedor comprometido, un empleado descontento) puede vaciar el stock de Bodega Roble Alto. Si las posiciones de los repartidores viajan sin cifrar, un atacante en la misma wifi puede seguir sus movimientos, o inyectar posiciones falsas.
Contramedidas. Cifrado en tránsito (TLS) para todas las comunicaciones, incluidas las internas (06-02); autenticación mutua entre servicios con mTLS (06-04); autenticación y autorización de usuarios con tokens (06-01); gestión segura de secretos (06-04); una puerta de enlace que centralice controles (06-05). Principio general: confianza cero, no se confía en una petición por venir de "dentro".
- Falacia 5: la topología no cambia
Qué significa creerla. Que las máquinas están siempre en la misma dirección IP, que el mismo nodo siempre está en el mismo sitio y que el mapa de la red es estable.
La realidad. En cualquier sistema moderno, los nodos aparecen y desaparecen constantemente: escalado automático, reinicios, despliegues, fallos de hardware, migraciones entre zonas. Un contenedor puede tener una dirección IP distinta cada vez que arranca. Los enlaces de red cambian de ruta. El código que tiene direcciones fijas escritas ("inventario está en 10.0.3.17") funciona hasta el primer redespliegue.
En Kilómetro Cero. Durante la Semana del Queso Artesano se añaden 6 instancias de catalogo y, al terminar, se retiran. pedidos necesita encontrar las instancias vivas en cada momento. Y cuando inventario se redespliega con una nueva versión, sus contenedores cambian de IP: si pedidos tenía la dirección antigua, empieza a fallar sin que nada esté "roto".
Contramedidas. Descubrimiento de servicios (registro dinámico de instancias y resolución por nombre, no por dirección) y orquestadores que lo gestionan automáticamente (07-05); balanceadores de carga que enrutan a las instancias sanas; comprobaciones de salud que retiran las instancias que no responden (07-03); diseñar los servicios para que no dependan de la identidad de la máquina en la que corren.
- Falacia 6: hay un único administrador
Qué significa creerla. Que una sola persona (o equipo) conoce, controla y configura toda la red y todos los sistemas implicados.
La realidad. En cuanto el sistema crece, intervienen múltiples partes: el equipo de infraestructura, cada equipo de servicio, el proveedor de nube, la pasarela de pagos externa, los operadores de telefonía móvil de los repartidores. Nadie tiene la visión completa. Un cambio de configuración que hace un equipo puede romper a otro; una actualización del proveedor externo puede cambiar el comportamiento de una API; una política de red de la nube puede bloquear un puerto.
En Kilómetro Cero. La pasarela de pagos externa anuncia que deja de admitir una versión antigua de TLS, y pagos deja de funcionar un martes por la mañana sin que nadie de Kilómetro Cero haya tocado nada. El equipo de reparto cambia el formato de un evento, y analitica (de otro equipo) empieza a descartar mensajes.
Contramedidas. Contratos explícitos y versionados entre servicios (02-03); configuración centralizada y auditada; automatización de la infraestructura para que la configuración sea reproducible (07-05); observabilidad para detectar rápidamente los cambios de comportamiento (Módulo 7); acuerdos de nivel de servicio con los proveedores externos y diseño que tolere sus fallos (07-04).
- Falacia 7: el coste de transporte es cero
Qué significa creerla. Que mover datos por la red no cuesta nada, ni en dinero ni en recursos de computación.
La realidad. Hay dos costes. El primero es de computación: para enviar un objeto por la red hay que serializarlo (convertirlo a bytes), y al recibirlo, deserializarlo; para conjuntos de datos grandes, este trabajo puede consumir más CPU que la propia lógica de negocio. El segundo es económico: los proveedores de nube cobran por el tráfico que sale de sus centros de datos (y a veces entre zonas), y las redes móviles cobran por volumen. Lo que en el monolito era gratis (pasar un objeto Python de un módulo a otro), ahora tiene un precio.
En Kilómetro Cero. El servicio analitica se diseña inicialmente para consultar cada noche todos los pedidos del día pidiéndoselos a pedidos en JSON: 2 millones de registros serializados, transmitidos y deserializados, cada noche. Un mes después, la factura de tráfico entre zonas de la nube sorprende a dirección, y el proceso tarda más en serializar que en calcular.
Contramedidas. Formatos binarios eficientes (02-03); llevar el cálculo a donde están los datos en lugar de mover los datos al cálculo, que es el principio de MapReduce y Spark (05-02, 05-03); flujos de eventos que se procesan a medida que ocurren en lugar de volcados masivos (05-04); ubicar los servicios que más se hablan en la misma zona.
- Falacia 8: la red es homogénea
Qué significa creerla. Que todos los nodos usan el mismo hardware, el mismo sistema operativo, el mismo lenguaje, las mismas versiones de bibliotecas y el mismo formato de datos.
La realidad. Cualquier sistema real es una mezcla: distintos lenguajes, versiones distintas del mismo servicio conviviendo durante un despliegue, diferentes representaciones de los números (¿cómo se codifica un decimal? ¿y una fecha?), distintos juegos de caracteres, distintos tamaños de red (una interfaz de 10 Gb/s en el centro de datos frente a 4G en el móvil).
En Kilómetro Cero. El servicio reparto recibe posiciones de una app Android (que envía el timestamp en milisegundos), de una app iOS (que lo envía en segundos con decimales) y de un dispositivo GPS de furgoneta (que lo envía como texto en un formato propio). Durante un despliegue gradual, la versión 1 y la versión 2 de inventario conviven, y la 2 devuelve un campo nuevo que la versión antigua de pedidos no espera. Un precio de 9,90 € viaja como 9.9 (coma flotante) y llega como 9.899999.
Contramedidas. Formatos de intercambio con esquema explícito e independientes del lenguaje (02-03); reglas de compatibilidad hacia delante y hacia atrás en las APIs (añadir campos sin romper a los clientes antiguos); tipos de datos precisos para dinero (decimales, no flotantes) y fechas (UTC con zona explícita); contenedores para homogeneizar el entorno de ejecución (07-05).
- Tabla resumen: falacia, síntoma, contramedida, lección
| # | Falacia | Síntoma en Kilómetro Cero | Contramedida principal | Lección |
|---|---|---|---|---|
| 1 | La red es fiable | Pedidos confirmados sin stock, o stock descontado dos veces | Reintentos con idempotencia, colas con entrega garantizada | 02-04, 02-05, 07-04 |
| 2 | La latencia es cero | Carrito que tarda 300 ms por 15 llamadas encadenadas; posiciones de repartidores desordenadas | Interfaces de grano grueso, caché, asincronía, medición | 02-04, 04-05, 07-01 |
| 3 | El ancho de banda es infinito | Respuestas de 2 MB con fotos en base64 | Paginación, serialización compacta, almacenamiento de objetos | 02-03, 04-03 |
| 4 | La red es segura | Cualquiera en la red interna puede vaciar el stock | TLS, mTLS, autenticación entre servicios, confianza cero | 06-01, 06-02, 06-04, 06-05 |
| 5 | La topología no cambia | Direcciones IP escritas en el código que fallan tras cada despliegue | Descubrimiento de servicios, comprobaciones de salud, orquestación | 07-03, 07-05 |
| 6 | Hay un único administrador | La pasarela de pagos cambia y pagos deja de funcionar sin tocar nada |
Contratos versionados, infraestructura como código, observabilidad | 02-03, 07-05, Módulo 7 |
| 7 | El coste de transporte es cero | Factura de tráfico y CPU disparadas por volcados nocturnos en JSON | Formatos binarios, llevar el cálculo a los datos, flujos de eventos | 02-03, 05-02, 05-04 |
| 8 | La red es homogénea | Timestamps en tres formatos; precios con errores de redondeo; versiones incompatibles | Esquemas explícitos, compatibilidad de APIs, tipos precisos | 02-03, 07-05 |
- Ejemplo de código:
pedidos llama a inventario en una red imperfecta
pedidos llama a inventario en una red imperfectaVamos a simular, sin sockets reales, las falacias 1 y 2: una red que pierde peticiones con cierta probabilidad y que tarda un tiempo variable en entregarlas. Sobre esa red, el servicio pedidos intentará descontar stock en inventario. Primero con un cliente ingenuo (que asume que la red es fiable y rápida) y después con uno que usa un timeout.
Usaremos asyncio para modelar la espera: asyncio.sleep representará el tiempo de viaje del mensaje.
import asyncio
import random
import time
class RedImperfecta:
"""Simula una red con pérdida de paquetes y latencia variable.
- prob_perdida: probabilidad de que un mensaje no llegue nunca.
- latencia_min / latencia_max: segundos que tarda un mensaje en llegar
(elegidos al azar en ese rango, en cada envío).
"""
def __init__(self, prob_perdida: float, latencia_min: float, latencia_max: float):
self.prob_perdida = prob_perdida
self.latencia_min = latencia_min
self.latencia_max = latencia_max
async def transmitir(self, mensaje: dict) -> dict:
"""Entrega el mensaje tras una latencia aleatoria... o nunca."""
if random.random() < self.prob_perdida:
# El paquete se ha perdido. En la red real no pasa NADA: no hay
# error, no hay excepción, simplemente nunca llega respuesta.
await asyncio.sleep(float("inf"))
await asyncio.sleep(random.uniform(self.latencia_min, self.latencia_max))
return mensaje
class ServicioInventario:
"""Un inventario muy simple, con el stock de cada producto."""
def __init__(self):
self.stock = {"queso-curado": 5, "tomate-rosa": 20, "tinto-crianza": 12}
self.peticiones_atendidas = 0
async def descontar(self, producto: str, cantidad: int) -> dict:
self.peticiones_atendidas += 1
if self.stock.get(producto, 0) < cantidad:
return {"ok": False, "motivo": "sin stock"}
self.stock[producto] -= cantidad
return {"ok": True, "stock_restante": self.stock[producto]}
class ClientePedidosIngenuo:
"""Llama a inventario como si fuera una función local."""
def __init__(self, red: RedImperfecta, inventario: ServicioInventario):
self.red = red
self.inventario = inventario
async def confirmar_pedido(self, cliente: str, producto: str) -> str:
peticion = {"op": "descontar", "producto": producto, "cantidad": 1}
# Viaje de ida por la red, procesamiento, y viaje de vuelta.
await self.red.transmitir(peticion)
respuesta = await self.inventario.descontar(producto, 1)
await self.red.transmitir(respuesta)
if respuesta["ok"]:
return f"Pedido de {cliente} confirmado ({respuesta['stock_restante']} uds. quedan)"
return f"Pedido de {cliente} rechazado: {respuesta['motivo']}"
class ClientePedidosConTimeout(ClientePedidosIngenuo):
"""Igual que el ingenuo, pero no espera más de 'timeout' segundos."""
def __init__(self, red, inventario, timeout: float):
super().__init__(red, inventario)
self.timeout = timeout
async def confirmar_pedido(self, cliente: str, producto: str) -> str:
try:
return await asyncio.wait_for(
super().confirmar_pedido(cliente, producto), timeout=self.timeout
)
except asyncio.TimeoutError:
return (f"Pedido de {cliente}: SIN RESPUESTA de inventario en "
f"{self.timeout} s (¿se descontó el stock? no lo sabemos)")
async def procesar_lote(cliente_pedidos, pedidos: list[tuple[str, str]], nombre: str):
"""Procesa varios pedidos, uno detrás de otro, y mide cuánto tarda."""
print(f"\n--- {nombre} ---")
inicio = time.monotonic()
for cliente, producto in pedidos:
t0 = time.monotonic()
resultado = await cliente_pedidos.confirmar_pedido(cliente, producto)
print(f"[{time.monotonic() - t0:5.2f} s] {resultado}")
print(f"Total: {time.monotonic() - inicio:.2f} s; "
f"inventario atendió {cliente_pedidos.inventario.peticiones_atendidas} peticiones")
async def main():
random.seed(16)
pedidos = [("Ana", "queso-curado"), ("Marc", "queso-curado"),
("Lucía", "tinto-crianza"), ("Ana", "tomate-rosa")]
# Escenario A: red casi perfecta. El cliente ingenuo parece funcionar bien.
red_buena = RedImperfecta(prob_perdida=0.0, latencia_min=0.01, latencia_max=0.03)
await procesar_lote(ClientePedidosIngenuo(red_buena, ServicioInventario()),
pedidos, "A: cliente ingenuo, red buena")
# Escenario B: red con 25 % de pérdida y latencia de hasta 1,5 s.
red_mala = RedImperfecta(prob_perdida=0.25, latencia_min=0.05, latencia_max=1.5)
# B1: cliente ingenuo. Lo protegemos con un límite global de 6 s porque,
# si no, el programa se quedaría colgado PARA SIEMPRE en el primer paquete perdido.
try:
await asyncio.wait_for(
procesar_lote(ClientePedidosIngenuo(red_mala, ServicioInventario()),
pedidos, "B1: cliente ingenuo, red mala"),
timeout=6.0,
)
except asyncio.TimeoutError:
print("!!! El lote entero se ha quedado colgado: el cliente ingenuo espera "
"indefinidamente un paquete que nunca llegará")
# B2: cliente con timeout de 1 s por llamada.
inventario = ServicioInventario()
await procesar_lote(ClientePedidosConTimeout(red_mala, inventario, timeout=1.0),
pedidos, "B2: cliente con timeout, red mala")
print(f"Stock final en inventario: {inventario.stock}")
if __name__ == "__main__":
asyncio.run(main())Ejecutémoslo y analicemos la salida (los tiempos exactos varían ligeramente entre máquinas):
--- A: cliente ingenuo, red buena ---
[ 0.04 s] Pedido de Ana confirmado (4 uds. quedan)
[ 0.05 s] Pedido de Marc confirmado (3 uds. quedan)
[ 0.03 s] Pedido de Lucía confirmado (11 uds. quedan)
[ 0.05 s] Pedido de Ana confirmado (19 uds. quedan)
Total: 0.17 s; inventario atendió 4 peticiones
--- B1: cliente ingenuo, red mala ---
[ 2.37 s] Pedido de Ana confirmado (4 uds. quedan)
!!! El lote entero se ha quedado colgado: el cliente ingenuo espera indefinidamente un paquete que nunca llegará
--- B2: cliente con timeout, red mala ---
[ 1.00 s] Pedido de Ana: SIN RESPUESTA de inventario en 1.0 s (¿se descontó el stock? no lo sabemos)
[ 0.98 s] Pedido de Marc confirmado (3 uds. quedan)
[ 1.00 s] Pedido de Lucía: SIN RESPUESTA de inventario en 1.0 s (¿se descontó el stock? no lo sabemos)
[ 1.00 s] Pedido de Ana: SIN RESPUESTA de inventario en 1.0 s (¿se descontó el stock? no lo sabemos)
Total: 3.98 s; inventario atendió 3 peticiones
Stock final en inventario: {'queso-curado': 3, 'tomate-rosa': 20, 'tinto-crianza': 11}Qué nos enseña cada parte:
RedImperfecta.transmitires el corazón de la simulación. Fíjate en cómo se modela la pérdida: no con una excepción, sino conawait asyncio.sleep(float("inf")), una espera infinita. Esto es fiel a la realidad: la red no avisa de que ha perdido un paquete. Es la falacia 1 en estado puro.- Escenario A. Con una red casi perfecta, el cliente ingenuo funciona bien, y esto es precisamente lo peligroso: el código pasa todas las pruebas en el portátil del desarrollador y en el entorno de pruebas, donde la red es buena.
- Escenario B1. Con un 25 % de pérdida, el primer pedido tarda casi 2,4 segundos (falacia 2: la latencia no es cero, y aquí es de hasta 1,5 s por trayecto) y el segundo se queda colgado para siempre. Hemos tenido que envolver el lote en un
wait_forde 6 segundos solo para que el programa termine. En un servidor real, esto se traduce en hilos o conexiones bloqueadas que se acumulan hasta agotar los recursos, exactamente lo que le pasó al monolito en la Semana de la Vendimia. - Escenario B2. El cliente con timeout nunca se cuelga: cada llamada tarda como máximo 1 segundo, y el lote termina en 4 segundos. Pero observa con atención las últimas líneas:
pedidossolo tiene constancia de un pedido confirmado (el de Marc), y sin embargoinventarioatendió 3 peticiones: el stock de queso curado bajó de 5 a 3 (dos descuentos, no uno) y el de vino de 12 a 11, aunque el pedido de Lucía se dio por fallido. Es decir: el pedido de queso de Ana y el de vino de Lucía sí llegaron a inventario y descontaron stock; lo que se perdió fue la respuesta, no la petición. El pedido de tomate de Ana, en cambio, no llegó nunca. Desdepedidoslos tres casos son indistinguibles.
El timeout resuelve el problema del bloqueo, pero deja abierta la pregunta más importante: cuando expira, ¿la operación se hizo o no? El mensaje "¿se descontó el stock? no lo sabemos" es literalmente cierto. Resolver esa ambigüedad exige idempotencia y reintentos seguros (02-05), y decidir cuándo dejar de intentarlo para no sobrecargar a un servicio enfermo es el papel del circuit breaker (07-04). Esta lección se detiene en el timeout, que es la contramedida mínima e imprescindible: ninguna llamada remota debe hacerse jamás sin un límite de tiempo.
Errores Comunes y Consejos
- Probar solo en redes buenas. El escenario A demuestra que un código que ignora las falacias funciona perfectamente en desarrollo. Hay que probar con pérdida de paquetes y latencia inyectadas (la lección 07-06 trata la ingeniería del caos precisamente para esto).
- Poner timeouts "generosos" para evitar falsos fallos. Un timeout de 60 segundos no protege de nada: 60 segundos de conexiones bloqueadas bajo carga es una caída. El timeout debe ser algo mayor que la latencia normal de la operación (por ejemplo, el percentil 99), no "lo bastante grande para que nunca salte".
- Tratar el timeout como "la operación no se hizo". Como muestra el escenario B2, tras un timeout la operación puede haberse ejecutado. Cualquier reintento debe ser seguro frente a duplicados.
- Escribir direcciones IP o nombres de host en el código. Falacia 5. Toda dirección debe venir de configuración o de un sistema de descubrimiento.
- Suponer que "interno" significa "seguro". Falacia 4. Cifrar y autenticar también entre servicios propios.
- Usar
floatpara dinero o timestamps sin zona horaria. Falacia 8. Son las dos fuentes más habituales de discrepancias entre servicios escritos por equipos distintos. - Consejo: cuando revises código que hace una llamada remota, comprueba la "lista de las tres T": ¿tiene Timeout?, ¿es Tolerante a duplicados (idempotente)?, ¿está Trazada (se registra cuánto tardó y si falló)? Si falta alguna, la llamada no está lista para producción.
Ejercicios
Ejercicio 1: Diagnóstico de falacias
Para cada uno de los siguientes incidentes de Kilómetro Cero, indica qué falacia (o falacias) se creyó cierta y qué contramedida aplicarías:
- Tras migrar
inventarioa contenedores,pedidosfalla cada mañana a las 6:00, justo cuando se redespliegainventariocon los datos actualizados de los productores. - En un mercado rural, la app del repartidor consume 400 MB de datos móviles al día y la batería dura la mitad.
- Un desarrollador escribe un bucle que, para cada uno de los 300 pedidos del día, llama a
pagospara comprobar su estado. El informe tarda 2 minutos. - Un análisis de seguridad muestra que un contenedor de
analitica(que solo debería leer) puede enviar peticiones de "descontar stock" ainventarioy son aceptadas.
Ejercicio 2: Ajustar el timeout
Usando la simulación del apartado 11, con la red_mala (pérdida 25 %, latencia 0,05-1,5 s por trayecto), razona y luego comprueba experimentalmente:
- ¿Qué timeout hace falta para que ninguna petición que sí llega se dé por perdida (es decir, para que el timeout solo salte por pérdida real)?
- ¿Qué proporción de las llamadas fallará por pérdida real, teniendo en cuenta que cada llamada hace dos trayectos (ida y vuelta)?
- Modifica
mainpara ejecutar 200 pedidos con el cliente con timeout y contar cuántos se confirman, cuántos dan timeout y cuántas peticiones atiende realmenteinventario. Compara los dos últimos números y explica la diferencia.
Ejercicio 3: Un reintento ingenuo
Añade a ClientePedidosConTimeout un parámetro reintentos de forma que, tras un timeout, vuelva a intentar la misma llamada hasta ese número de veces. Ejecuta el lote de 4 pedidos con reintentos=3 y observa el stock final de queso-curado. ¿Qué ha pasado y por qué? (No hace falta resolverlo: solo diagnosticarlo. La solución se estudia en 02-05).
Soluciones
Solución 1:
- Falacia 5 (la topología no cambia).
pedidostiene cacheada (o configurada) la dirección de los contenedores antiguos deinventario, que desaparecen en el redespliegue. Contramedida: descubrimiento de servicios y resolución por nombre en cada llamada, con comprobaciones de salud (07-05, 07-03). - Falacias 3 y 7 (ancho de banda infinito, coste de transporte cero), y probablemente la 2 (envía demasiado a menudo). La app envía demasiados datos o demasiadas veces. Contramedida: enviar solo la posición nueva (no el historial), en formato compacto, con una frecuencia adaptada (más baja cuando el repartidor está parado), y agrupar posiciones cuando no hay urgencia (02-03, 08-02).
- Falacia 2 (la latencia es cero). Es el patrón N+1: 300 llamadas × 400 ms = 2 minutos. Contramedida: una llamada de grano grueso a
pagosque devuelva el estado de los 300 pedidos de una vez, o mejor, quepagospublique los cambios de estado como eventos quepedidosya tenga almacenados localmente (02-04). - Falacia 4 (la red es segura).
inventariono autentica al llamante ni comprueba sus permisos. Contramedida: autenticación entre servicios con mTLS y autorización por identidad del servicio (06-04); principio de mínimo privilegio.
Solución 2:
- Cada llamada hace dos trayectos, cada uno de hasta 1,5 s, así que el peor caso de una llamada que sí llega es de 3 s. Con un timeout de 3 s (o algo más, por el tiempo de procesamiento), ningún timeout sería un "falso positivo". Pero fíjate en el precio: cada pérdida real bloquea al cliente 3 segundos. Esta tensión (timeout corto = falsos fallos; timeout largo = bloqueos largos) no tiene solución perfecta.
- Una llamada se completa solo si ambos trayectos tienen éxito: 0,75 × 0,75 = 0,5625. Es decir, el 43,75 % de las llamadas fallará por pérdida real. De esas, la mitad aproximadamente (0,25 × 0,75 / 0,4375 ≈ 43 %) habrán llegado a
inventarioy perdido solo la respuesta.
async def experimento(n: int = 200):
random.seed(11)
red = RedImperfecta(prob_perdida=0.25, latencia_min=0.05, latencia_max=1.5)
inventario = ServicioInventario()
inventario.stock["tomate-rosa"] = 10_000 # que no se agote
cliente = ClientePedidosConTimeout(red, inventario, timeout=3.0)
confirmados = timeouts = 0
for _ in range(n):
r = await cliente.confirmar_pedido("Ana", "tomate-rosa")
if "confirmado" in r:
confirmados += 1
else:
timeouts += 1
print(f"confirmados={confirmados} timeouts={timeouts} "
f"atendidas por inventario={inventario.peticiones_atendidas} "
f"stock descontado={10_000 - inventario.stock['tomate-rosa']}")Con 200 pedidos se obtiene confirmados=109 timeouts=91 atendidas por inventario=144 stock descontado=144 (los valores concretos pueden variar ligeramente según la temporización de cada máquina). La diferencia entre las peticiones atendidas (144) y las confirmadas (109) son los 35 pedidos que sí descontaron stock pero cuyo cliente cree que fallaron; y el 45,5 % de timeouts observado coincide con el 43,75 % teórico. En un sistema real, esos 35 clientes verían un error, probablemente volverían a intentarlo y el stock se descontaría dos veces. (El experimento tarda unos minutos porque las esperas son reales; puedes reducir las latencias proporcionalmente para acelerarlo).
Solución 3:
class ClientePedidosConReintentos(ClientePedidosConTimeout):
def __init__(self, red, inventario, timeout: float, reintentos: int):
super().__init__(red, inventario, timeout)
self.reintentos = reintentos
async def confirmar_pedido(self, cliente: str, producto: str) -> str:
for intento in range(1, self.reintentos + 1):
resultado = await super().confirmar_pedido(cliente, producto)
if "SIN RESPUESTA" not in resultado:
return resultado + f" (intento {intento})"
return f"Pedido de {cliente}: fallido tras {self.reintentos} intentos"Al ejecutar el lote de 4 pedidos con reintentos=3, es habitual ver que el stock descontado de queso-curado es superior al número de pedidos de queso confirmados: por ejemplo, con la semilla 16, los dos pedidos de queso terminan como "fallidos tras 3 intentos" y, sin embargo, el stock ha bajado de 5 a 2 (tres descuentos que nadie ha confirmado). Cada vez que una petición llega a inventario y se pierde la respuesta, el reintento vuelve a descontar. El reintento convierte la pérdida de mensajes en duplicación de efectos. La solución (que cada petición lleve un identificador único y que inventario recuerde cuáles ya ha procesado, es decir, idempotencia) se desarrolla en la lección 02-05.
Conclusión
Las ocho falacias de Deutsch y Gosling son un catálogo de los errores de modelo mental que cometemos al pasar de llamadas locales a llamadas por red: creer que la red es fiable, que la latencia es cero, que el ancho de banda es infinito, que la red es segura, que la topología no cambia, que hay un único administrador, que transportar datos no cuesta y que todo es homogéneo. Para cada una hemos visto un síntoma concreto en Kilómetro Cero y la contramedida que el curso desarrollará más adelante, resumidas en la tabla del apartado 10.
La simulación ha mostrado la lección más importante de forma tangible: un código que ignora las falacias funciona perfectamente en una red buena y se cuelga para siempre en una red mala; un timeout evita el bloqueo, pero deja sin respuesta la pregunta de si la operación se ejecutó, y un reintento ingenuo convierte esa duda en duplicados. Ninguna llamada remota debería hacerse sin timeout, y ninguna operación con efectos debería reintentarse sin ser idempotente.
Hay una falacia implícita que no está en la lista de Deutsch pero que subyace a muchos problemas: la de que todos los nodos comparten la misma noción del tiempo. Cuando Ana y Marc compran la última unidad de queso desde dos ciudades distintas, ¿quién fue primero? La siguiente lección, Tiempo, Relojes y Ordenación de Eventos, muestra por qué esa pregunta no tiene una respuesta obvia y qué herramientas (relojes lógicos y vectoriales) nos permiten responderla de forma útil.
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
