README · by ansango
← Volver al libro

The trouble with distributed systems: faults y redes

Por qué los sistemas distribuidos fallan: partial failures, unreliable networks, TCP, timeouts y los escenarios más sutiles

~4 min de lectura
Resumen

La observación más importante del libro: los sistemas distribuidos son intrínsecamente poco fiables. No porque la tecnología sea mala, sino porque están compuestos de partes que se comunican por una red que falla. Esta nota cubre los tipos de fallos, las limitaciones de TCP, los timeouts y los patrones que las aplicaciones deben implementar para robustez.

Lo que diferencia un sistema distribuido de uno single-node

El libro abre con la observación más importante del libro (un eco de la que ya usamos en esta wiki):

“Los sistemas distribuidos son aquellos en los que el fallo de un nodo del que nunca has oído hablar impide que tu propio código funcione.”

Sistema single-node:

failure mode:
  - tu código tiene un bug
  - el sistema operativo se cuelga
  - el hardware falla

Sistema distribuido:

failure modes adicionales:
  - cualquier nodo remoto puede fallar
  - la red puede particionarse
  - los mensajes pueden llegar dos veces
  - los mensajes pueden no llegar nunca
  - los mensajes pueden llegar en otro orden
  - un nodo puede estar caído pero el resto no lo sabe

[!note> Diseñar para fallos no es paranoia El libro insiste: en producción, los fallos ocurren. Diseñar asumiendo que todo va a funcionar es diseñar para que todo falle cuando algo falle.

Faults vs failures

El libro distingue dos niveles:

Fault → Failure?

Red se particiona (fault)
  → Node 1 no alcanza a Node 2
  → ¿El sistema sigue funcionando? (failure?)

Fault tolerance: el sistema sigue aunque haya faults.

[!tip> El objetivo es fault tolerance No evitar los faults (imposible), sino evitar las failures (con un buen diseño).

Tipos de fallos

El libro enumera los fallos típicos:

Hardware

Software

Human

Network

Partial failures

El libro considera la partial failure como la propiedad definitoria de los sistemas distribuidos.

Single-node failure:

Nodo cae → sistema deja de funcionar.
Failure: {sistema}.

Distributed partial failure:

Nodo 1 sigue activo.
Nodo 2 cae.
¿El sistema falla? ¿Qué partes?

[!danger> La parcialidad es el problema En un sistema single-node, un fallo es binario: funciona o no. En un sistema distribuido, los fallos son parciales: cada nodo tiene su estado, y la combinación es incierta.

Redes poco fiables

El libro dedica una sección a las redes, que trata como “best effort”.

Ethernet y TCP

TCP es fiable en teoría, pero en la práctica…

TCP guarantees:

  - In-order delivery: los mensajes llegan en orden.
  - No duplication: no llegan duplicados.
  - Error detection: detecta errores y retransmite.

TCP does NOT guarantee:

  - Latency: el mensaje puede tardar 100ms o 10 segundos.
  - Delivery: las conexiones pueden cortarse.
  - No data loss: en la práctica, hay pérdida si la conexión se corta.

Síntomas de la red

Síntomas:                  Causa posible:
  - Sin respuesta             Switch muerto
  - Latencia alta              Subred congestionada
  - Conexión rechazada         Peer caído
  - Conexión cortada           Firewall, NAT timeout

[!tip> La red puede mentir El libro advierte: la red no te dice si el mensaje llegó o no. Una conexión “estable” puede haber sido reseteada por un NAT que no te avisa.

Detección de fallos

El libro describe los mecanismos de detección:

1. Timeouts

Si no recibes respuesta en un tiempo, asume que ha fallado.

# Timeout simple
response = send_with_timeout(request, timeout=5)
if response is None:
    # Posible fallo
    handle_failure()

2. Heartbeats

Probes periódicos a nodos.

Heartbeat:

Coordinator ──► "¿estás vivo?" ──► Node 1
Coordinator ◄── "sí" ──

Ventajas: detecta caídas rápidamente. Limitaciones: la red puede tener falsas alarmas.

3. Acks (acknowledgments)

Esperar reconocimiento.

Ack:

Node 1 ──► Comando ──► Node 2
Node 2 ──► Ack ──► Node 1
Node 1: confirmado

4. Tracing distribuido

Sistemas como Jaeger o Zipkin trazan requests a través del sistema.

Trace:

Request ──► API ──► Service A ──► Service B ──► DB
0ms     5ms       15ms              50ms

[!tip> El tracing es indispensable Sin tracing distribuido, debuggear un sistema distribuido es imposible. OpenTelemetry es el estándar.

Timeouts

El libro dedica páginas al timeout, porque es la decisión más sutil.

El problema del timeout

Si el timeout es muy corto: falsas alarmas. Si el timeout es muy largo: detección de fallos lentos.

Trade-off del timeout:

Timeout 1s:  rápido, muchas falsas alarmas.
Timeout 60s: lento, pocas falsas alarmas.

Adaptive timeouts

Cassandra usa timeouts adaptativos que se ajustan al RTT observado.

Adaptive timeout:

RTT observado: 50ms
Timeout: 50ms × 2 = 100ms

RTT observado: 500ms (red lenta)
Timeout: 500ms × 2 = 1000ms

Phi accrual failure detector

Cassandra usa el phi accrual failure detector, que calcula una probabilidad de fallo basada en los heartbeats.

# Phi accrual simplificado
last_heartbeat = t0
current_time = t1
heartbeat_interval = 10ms  # típico
arrival_interval = current_time - last_heartbeat

# Phi = log(...) en función del arrival interval

[!tip> Phi accrual es elegante En vez de un booleano (vivo/muerto), Phi accrual devuelve un valor continuo (la probabilidad de que el nodo haya caído). El sistema decide el umbral según sus necesidades.

Las trampas del networking

TCP y el “double ack”

Si se retransmite un paquete, el receptor puede recibir dos copias. El emisor puede no darse cuenta y reenviar.

Double ack:

Emisor ──► packet 1 ──► Receptor (ack 1)
Emisor ──► packet 1 (retransmit) ──► Receptor (ack 1)
Emisor: piensa que está hablando con dos receptores.

TCP y los NAT timeouts

Los NAT cierran conexiones después de un tiempo de inactividad. Si el cliente no envía nada, el NAT tira la conexión.

NAT timeout:

Cliente ──► NAT ──► Servidor
         ↑ 5 min idle
NAT: cierra la conexión
Cliente: conexión rota ✗

TCP y los firewalls

Los firewalls pueden rechazar paquetes que no reconocen. A veces la conexión se corta sin aviso.

Patrones para robustez

El libro enumera los patrones que un sistema distribuido debe implementar:

1. Timeouts en todas las operaciones

# Mal
result = other_service.do_thing()

# Bien
result = other_service.do_thing(timeout=5)

2. Retries con backoff

def with_retries(func, max_retries=3):
    for attempt in range(max_retries):
        try:
            return func()
        except (Timeout, ConnectionError):
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # exponential backoff

3. Circuit breaker

Si un servicio falla mucho, deja de llamarlo.

Circuit breaker:

Closed    → normal
Open      → no llamar, devolver error
Half-open → probar una vez, decidir

4. Bulkhead

Aislar los recursos para que un fallo no se propague.

Bulkhead:

Pool A: 10 conexiones a servicio X
Pool B: 10 conexiones a servicio Y

   Pool A cae → Pool B sigue funcionando

5. Idempotency keys

Permitir retries seguros.

def transfer_money(from, to, amount, idempotency_key):
    if cache.exists(f"transfer:{idempotency_key}"):
        return cache.get(f"transfer:{idempotency_key}")
    
    result = do_transfer(from, to, amount)
    cache.set(f"transfer:{idempotency_key}", result)
    return result

Casos de estudio: fallos famosos

El libro menciona algunos casos famosos:

AWS S3 outage (2017)

Operador escribió un comando mal. Se borró mass de objetos. Sistema caído horas.

Lección: los commandos administrativos deben requerir doble confirmación.

GitLab database deletion (2017)

Caché mal configurada. Réplica se borró. Cinco minutos de datos perdidos.

Lección: las copias de seguridad deben ser inmutables.

Knight Capital (2012)

Bug en un deploy automatizado. Una máquina usó código viejo. $440M perdidos en 45 minutos.

Lección: los deploys deben ser canary y reversibles.

[!tip> Los fallos son inevitables El libro no intenta asustar con estos ejemplos. Lo que intenta es mostrar que la historia importa y los buenos diseños deben asumir que los fallos ocurrirán.

Diseño robusto

El libro cierra con un checklist de robustez:

Diseño robusto:

☐ Timeouts en todas las llamadas externas.
☐ Retries con backoff exponencial.
☐ Circuit breakers en servicios remotos.
☐ Bulkheads para aislamiento.
☐ Idempotency keys en operaciones no idempotentes.
☐ Tracing distribuido.
☐ Health checks.
☐ Dead letter queues para mensajes fallidos.
☐ Backups con restore probado.
☐ Chaos engineering (romper cosas a propósito).

Resumen en tres frases

Próximos pasos