¿Cómo ocurren las fallas en cascada en un sistema distribuido, y cómo evitas realmente que una tumbe todo?
Una sola dependencia lenta en el fondo de la cadena de llamadas puede agotar en silencio cada pool de hilos por encima de ella hasta que todo el sistema parece caído.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo ocurren las fallas en cascada en un sistema distribuido, y cómo evitas realmente que una tumbe todo?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Qué es realmente una falla en cascada
Una falla en cascada es lo que ocurre cuando una falla —a menudo pequeña y localizada— en un solo componente dispara fallas en los componentes que dependen de él, los cuales a su vez disparan fallas en sus dependientes, hasta que una degradación que empezó en un servicio profundo y poco notable termina tumbando todo el sistema. Lo insidioso es que el disparador con frecuencia no es siquiera una caída total: un servicio que simplemente está lento suele ser mucho más peligroso que uno que está limpiamente caído, porque una falla dura falla rápido, mientras que una dependencia lenta invita a que todos los que la llaman se queden esperando, consumiendo recursos todo ese tiempo.
#Cómo ocurre la propagación mecánicamente
Imagina cuatro servicios en una cadena de llamadas: un API Gateway llama al Servicio B, que llama al Servicio C, que llama al Servicio D. El Servicio D desarrolla un pico de latencia —una consulta lenta a la base de datos downstream, una pausa de garbage collection, contención de disco, no importa cuál sea la causa—. El Servicio C llama a D de forma síncrona sin timeout configurado, así que cada petición que entra a C y toca a D ahora se queda esperando ahí. Esas peticiones mantienen hilos abiertos, o conexiones de un pool de tamaño fijo, y en cuestión de segundos el propio pool de C queda completamente agotado por peticiones que están todas esperando a una dependencia que no responde. Ahora C mismo parece lento o no responder ante el Servicio B —exactamente por la misma razón que D parecía lento ante C— y los recursos de B empiezan a agotarse de la misma forma. Esto sigue escalando hasta que el propio API Gateway queda agotado, y ahora absolutamente todas las peticiones al sistema fallan o se cuelgan, incluso las que no tienen nada que ver con lo que originalmente se ralentizó en las profundidades de la cadena.
Dos cosas empeoran esto en la práctica. Primero, la lógica de reintento ingenua: cuando quien llama ve un timeout o un error, un reintento sin condiciones reenvía la petición de inmediato, lo que agrega más carga a una dependencia que ya está sufriendo —una "tormenta de reintentos" que puede convertir un tropiezo recuperable en una caída total—. Segundo, la infraestructura compartida: si los mensajes de B, C y D fluyen todos por el mismo clúster de colas o el mismo pool de conexiones hacia un datastore compartido, un atraso causado por un consumidor lento puede agotar disco o memoria en infraestructura de la que también dependen servicios no relacionados —un efecto de "vecino ruidoso" que expande el radio de impacto hacia los lados, no solo hacia arriba en la cadena de llamadas—.
#Las mitigaciones centrales
- Timeouts en cada llamada de red. Ninguna llamada debería poder esperar indefinidamente. Los timeouts deben configurarse de forma agresiva en relación con el propio SLA de quien llama, e idealmente el presupuesto de tiempo se propaga hacia abajo en la cadena de llamadas como un deadline (esto es justo lo que da gratis la propagación de deadlines de gRPC), de modo que una petición que ya gastó 150ms de su presupuesto de 200ms no deje que una llamada downstream intente durante otros 500ms.
- Circuit breakers. Después de una tasa configurada de fallas o respuestas lentas hacia una dependencia específica, el breaker se abre: las llamadas siguientes fallan de inmediato (o recurren a datos en caché o por defecto) sin siquiera intentar la llamada de red, lo que detiene la acumulación de más carga sobre una dependencia que ya sufre y le da espacio para recuperarse. El breaker deja pasar periódicamente un pequeño número de llamadas en un estado "medio abierto" para verificar si la dependencia se recuperó antes de cerrarse completamente de nuevo.
- Load shedding (descarte de carga). Cuando un servicio se acerca a la saturación, rechaza deliberadamente una porción de las peticiones entrantes —idealmente las de menor prioridad primero— en vez de aceptarlas todas y que cada petición se degrade o el proceso simplemente se caiga. Servir bien al 80% de las peticiones es mejor que intentar el 100% y no entregar casi nada.
- Bulkheads (mamparos). Particionar recursos —pools de hilos, pools de conexiones, incluso instancias de servicio o node pools separados— por dependencia o por inquilino, de modo que una dependencia lenta o fallida solo pueda agotar el pool que se le asignó, no el pool compartido del que también depende cualquier otro camino de código. El nombre viene de los compartimentos estancos del casco de un barco que evitan que una inundación en una sección hunda toda la nave.
- Backpressure, presupuestos de reintento y jitter. Los reintentos deben estar acotados (un número pequeño de intentos), con backoff exponencial y jitter para que muchos clientes reintentando a la vez no se sincronicen en otro pico de carga, y un presupuesto de reintentos a nivel de sistema debería limitar la fracción total de tráfico extra que los reintentos pueden agregar.
#Uniendo todo
Ninguno de estos mecanismos es alternativa de los demás: un servicio maduro típicamente los tiene todos: un timeout agresivo en cada llamada saliente, un circuit breaker envolviendo cada una de esas llamadas, un bulkhead separando el pool de recursos de cada dependencia downstream, y load shedding en el borde para cuando todo lo anterior sigue sin ser suficiente. La razón por la que los entrevistadores insisten tanto en este tema es que es exactamente el tipo de cosa fácil de enunciar en abstracto y fácil de saltarse en un sistema real bajo presión de fechas de entrega, y suele ser la pieza faltante en el retro después de una caída real.