Módulo 6: Bulkheads y aislamiento
7. El costo del aislamiento
Descripción
Las cinco lecciones anteriores vendieron el bulkhead, y con razón: aísla las fallas, contiene el radio de explosión, devuelve el tráfico sano al 100%. Esta lección es la honestidad del ingeniero: aislar cuesta. No existe una defensa gratis, y el bulkhead paga su precio en tres monedas —más recursos reservados, menos elasticidad, y más complejidad que dimensionar y afinar—. Entender ese costo no es para desanimarte de usar bulkheads; es para que los uses con criterio y no caigas en el error opuesto al de la lección 2: no aislar nada es frágil, pero sobre-particionar —cortar el sistema en demasiados compartimentos diminutos— es igual de frágil, por razones distintas.
El costo central, el que mediremos, es la pérdida de elasticidad. Un pool compartido tiene una virtud que sacrificamos al aislar: cuando una parte del sistema tiene un pico y otra está ociosa, la ociosa le presta su capacidad a la que la necesita. Un pool común absorbe picos porque comparte la capacidad. Al aislar en compartimentos, esa capacidad ociosa queda encerrada: el sub-pool de shipping, aunque esté vacío, no le puede prestar un solo hilo al sub-pool de catalog que está bajo un pico. Aislaste el riesgo, pero también aislaste la capacidad. Y eso, bajo ciertos patrones de carga, hace que el sistema aislado rechace tráfico que el compartido habría servido.
Conexión con el módulo: esta es la lección del contrapeso. Las lecciones 2 a 6 te dieron el beneficio del aislamiento (por dependencia y por clase); aquí medimos su costo, para que la decisión de aislar sea una balanza y no un reflejo. El proyecto de la lección 8 te pedirá justificar no solo que aislaste, sino cuánto —cuántos compartimentos, de qué tamaño—, y esa justificación es imposible sin entender el costo que esta lección cuantifica. Es también la lección que conecta con el módulo 8 (el capstone), donde combinar bulkhead con breaker y degradación exige saber cuánto aislamiento es suficiente y cuánto es demasiado.
La virtud que pierdes: la analogía del estacionamiento
Imagina un edificio de oficinas con un estacionamiento de 120 lugares compartido entre tres empresas que rentan pisos. En un día normal, la empresa A usa 40 lugares, la B usa 30, la C usa 50 —caben holgados—. Pero los días que la empresa A tiene una junta grande y llega con 70 autos, encuentra lugar, porque la B ese día solo trajo 20 y la C 30: los lugares ociosos de B y C absorben el pico de A. El estacionamiento compartido es elástico: la capacidad no usada de unos amortigua los picos de otros.
Ahora divide el estacionamiento en tres secciones cerradas con pluma: 40 lugares exclusivos para A, 40 para B, 40 para C —nadie usa los de nadie—. Esto tiene una ventaja: si la empresa A trae camiones que gotean aceite y ensucian sus 40 lugares, el problema queda en su sección; las de B y C están limpias (aislamiento). Pero tiene un costo cruel: el día de la junta grande de A, sus 40 lugares se llenan y los autos 41 en adelante se quedan afuera, en la calle, mientras las secciones de B y C tienen lugares vacíos que A no puede usar —hay pluma—. El estacionamiento aislado perdió la elasticidad: la capacidad ociosa de B y C ya no amortigua el pico de A. Aislaste la suciedad (el aceite), pero también aislaste el espacio.
Ese es el trato del bulkhead en una imagen. El pool compartido comparte el riesgo (el aceite de A ensucia a todos) y la capacidad (los lugares de A absorben el pico de B). El bulkhead separa ambos: te protege del riesgo compartido, pero renuncias a la capacidad compartida. La pregunta de diseño no es "¿aislar o no?" en abstracto, sino "¿el riesgo que aíslo justifica la elasticidad que pierdo?".
Ejemplo trabajado: el pico sano que el bulkhead rechaza
Medimos el costo de elasticidad con un escenario donde todas las dependencias están sanas —sin cuelgues, sin fallas—, pero una tiene un pico de tráfico. catalog recibe una avalancha (una lectura cada 5 ms, ~40 ms cada una: necesita ~8 hilos de concurrencia), mientras payments y shipping están tranquilos. Comparamos el pool compartido de 12 contra los bulkheads de 4/4/4. Misma semilla. Esta es la salida real:
### COSTO DEL AISLAMIENTO: un pico SANO de catalog, todas las deps OK ###
(catalog en pico; payments y shipping tranquilos y sanos)
catalog bajo el pico:
POOL COMPARTIDO (presta los hilos ociosos): 100.0% (servidas 1201, rechazadas 0)
BULKHEAD (capado a sus 4 hilos) : 50.2% (servidas 601, rechazadas 596)
Qué esperar. Aquí el resultado se invierte respecto a todo el módulo: el pool compartido gana. Bajo el pico sano de catalog, el pool compartido sirve el 100% (1201 requests, cero rechazos), porque catalog toma prestados los hilos ociosos de payments y shipping —que ese día no los necesitan— y así absorbe su pico con los 12 hilos. El bulkhead, en cambio, tiene a catalog capado a sus 4 hilos: por más que payments y shipping tengan sus 8 hilos vacíos, catalog no los puede tocar, así que rechaza 596 de 1197 requests —la mitad de un tráfico perfectamente sano—, con ocho hilos ociosos mirando. El aislamiento que salvó a catalog cuando shipping se colgó (lección 4) es el mismo que ahora lo condena cuando catalog tiene un pico y los demás están ociosos.
Ese es el costo de elasticidad, medido: el bulkhead convierte capacidad ociosa en capacidad inalcanzable. En el pool compartido, ningún hilo se desperdicia: si catalog los necesita y los demás no, catalog los usa. En el bulkhead, los hilos de shipping son de shipping aunque shipping no los use y catalog se esté ahogando. Aislar el riesgo tuvo como precio perder la absorción de picos. La misma pared que contiene la inundación de una dependencia impide que la capacidad ociosa de una amortigüe el pico de otra.
Las tres monedas del costo
El costo de elasticidad es el más sutil, pero no el único. El bulkhead paga en tres monedas:
1. Más recursos reservados. Para darle a cada dependencia un compartimento con margen, reservas hilos (y conexiones, y memoria) por dependencia. Si cada una necesita 4 hilos con margen y tienes 3 dependencias, son 12 hilos reservados, muchos de ellos ociosos la mayor parte del tiempo —porque las tres rara vez pican a la vez—. El pool compartido serviría el mismo tráfico con menos hilos totales, precisamente porque los comparte. Aislar es, en parte, pagar por capacidad que pasa ociosa para tenerla garantizada cuando haga falta.
2. Menos elasticidad. Lo que acabamos de medir: la capacidad ociosa de un compartimento no amortigua el pico de otro. El sistema aislado es más predecible (cada dependencia sabe exactamente qué tiene) pero menos adaptable (no puede reasignar sobre la marcha). Bajo cargas irregulares —picos que van rotando entre dependencias— el pool compartido aprovecha mejor los mismos recursos.
3. Más complejidad de dimensionamiento y operación. Cada compartimento es un tamaño que elegir, medir y afinar. Con un pool compartido tienes un número que dimensionar; con bulkheads por dependencia tienes uno por dependencia, y con bulkheads por clase, uno por clase por dependencia —la combinatoria crece—. Cada uno mal dimensionado es un problema: muy chico rechaza tráfico sano, muy grande desperdicia. Y cada uno hay que reajustarlo cuando el tráfico cambia. El aislamiento cambia "un pool que afinar" por "muchos compartimentos que afinar", y esa carga operativa es real.
El error opuesto: sobre-particionar
La lección 2 mostró el peligro de no aislar nada. Esta lección muestra el peligro opuesto: aislar demasiado. Un sistema cortado en veinte compartimentos diminutos —uno por cada endpoint, uno por cada variante de tráfico— sufre lo peor de las tres monedas: cada compartimento es tan pequeño que rechaza tráfico sano al menor pico (sin margen para absorber varianza), la elasticidad se pierde por completo (nada se presta con nada), y la complejidad de dimensionar veinte números es inmanejable. El sistema sobre-particionado puede rechazar más tráfico sano, en agregado, que uno con un pool compartido bien dimensionado —recreando la fragilidad que el aislamiento venía a evitar, por el otro extremo—.
La regla práctica para no sobre-particionar: aísla a lo largo de los límites de falla reales, no de cualquier distinción imaginable. Dos dependencias que fallan de forma independiente y pueden colgarse merecen compartimentos separados (el riesgo justifica el costo). Pero dos endpoints que llaman a la misma dependencia sana, con el mismo perfil de latencia y sin razón para fallar por separado, probablemente no —aislarlos solo paga el costo sin comprar aislamiento útil—. La pregunta antes de crear un compartimento es siempre: "¿qué falla independiente contiene esta pared, y vale ese aislamiento la elasticidad y la complejidad que cuesta?". Si no puedes nombrar la falla que contiene, la pared no vale su precio.
Cómo recuperar algo de elasticidad sin perder el aislamiento
El trato entre aislamiento y elasticidad no es siempre todo-o-nada; hay diseños intermedios que recuperan algo de elasticidad conservando la contención:
- Compartimentos con un colchón compartido. En vez de repartir los 12 hilos en 4/4/4 rígidos, das a cada dependencia una reserva garantizada (3 cada una = 9) y dejas 3 hilos en un pool común que cualquiera puede tomar cuando su reserva se agota. Así cada dependencia tiene su piso garantizado (aislamiento) pero puede estirarse al colchón compartido bajo un pico (algo de elasticidad). El colchón, al ser pequeño, limita cuánto puede una dependencia rota invadir —contiene el daño sin encerrar toda la capacidad—.
- Límites elásticos con techo. Un compartimento puede tener un tamaño base garantizado y un techo mayor al que crece si hay capacidad libre, cediéndola cuando otra dependencia la reclama. Es más complejo de implementar, pero acerca lo mejor de los dos mundos.
- Aislar solo lo que se cuelga. No todas las dependencias necesitan compartimento. Las que hacen I/O de red y pueden colgarse, sí. Una operación puramente local y rápida, que no puede colgarse esperando a nadie, rara vez necesita su propio bulkhead —dejarla en el pool común preserva elasticidad sin arriesgar contagio, porque no hay nada que contener—.
Estos diseños no eliminan el trato —siempre hay tensión entre aislar y compartir—, pero lo suavizan. La decisión sigue siendo tuya: cuánta elasticidad cedes por cuánto aislamiento ganas, medido contra los patrones de carga y las fallas reales de tu sistema.
Errores comunes
Aislar todo "por si acaso" sin nombrar la falla que contiene. Qué pasa: se crea un compartimento por cada endpoint o cada distinción de tráfico, por prudencia. Por qué pasa: el aislamiento se siente siempre bueno tras las lecciones anteriores. Cómo detectarlo: compartimentos que nunca contuvieron una falla real, muchos de ellos ociosos, y rechazos de tráfico sano en picos que un pool compartido habría absorbido. Cómo corregirlo: para cada compartimento, nombra la falla independiente que contiene; si no puedes, fusiónalo con otro. El aislamiento se justifica por un límite de falla real, no por simetría o prudencia difusa.
Dimensionar los compartimentos sin margen para ahorrar recursos. Qué pasa: para no "desperdiciar" hilos, se dimensiona cada sub-pool justo a la concurrencia promedio (sin margen), y entonces rechaza tráfico sano en cada pico natural. Por qué pasa: se ve la capacidad ociosa como desperdicio. Cómo detectarlo: rechazos frecuentes de tráfico sano con la ocupación promedio del compartimento por debajo del 100% (los rechazos ocurren en los picos, no en promedio). Cómo corregirlo: el margen sobre la concurrencia promedio (el ~2× de la lección 4) no es desperdicio; es lo que absorbe la varianza. Un compartimento sin margen recrea la fragilidad por el lado del rechazo. El costo del aislamiento incluye pagar ese margen.
Olvidar que el pool compartido gana bajo picos rotativos sanos. Qué pasa: se aísla un sistema cuyas dependencias están todas sanas y solo tienen picos que rotan (nunca pican a la vez), y el sistema aislado rechaza más que el compartido. Por qué pasa: se aplicó el aislamiento donde el problema no era la falla sino el pico. Cómo detectarlo: rechazos en dependencias sanas mientras otras están ociosas —la firma exacta de la pérdida de elasticidad—. Cómo corregirlo: si el problema es absorber picos de tráfico sano (no contener fallas), el pool compartido o un diseño con colchón compartido puede ser mejor. El bulkhead es para contener fallas, no para absorber picos; usarlo para lo segundo paga su costo sin cobrar su beneficio.
Ejercicios
Ejercicio 1 — ¿Aislar o no? Para cada par de operaciones, decide si merecen compartimentos separados o si es mejor dejarlas en un pool común, nombrando la falla que el aislamiento contendría (o la ausencia de ella): (a) una llamada HTTP a payments y una llamada HTTP a shipping; (b) dos endpoints que ambos leen de catalog con el mismo perfil de latencia; (c) una lectura rápida de un caché local en memoria y una llamada HTTP a un servicio externo.
Ver solución
-
(a)
paymentsyshipping: compartimentos separados. Falla que contiene: cada uno puede colgarse de forma independiente (red, sobrecarga, despliegue), y si comparten pool, el que se cuelga tumba al otro (lección 2). El riesgo es real y justifica el costo. Aísla. -
(b) dos endpoints que leen de
catalogcon el mismo perfil: probablemente pool común. ¿Qué falla independiente contendría separarlos? Casi ninguna: llaman a la misma dependencia, con la misma latencia, y fallan juntos (sicatalogse cae, se caen los dos). Aislarlos paga el costo (menos elasticidad, dos números que dimensionar) sin comprar aislamiento útil. Déjalos juntos —a menos que uno sea interactivo y el otro lote, en cuyo caso el eje de partición es la clase de tráfico, no el endpoint (lección 6)—. -
(c) caché local vs HTTP externo: separados, pero por una razón asimétrica. La llamada HTTP puede colgarse y necesita su compartimento (con timeout y bulkhead). La lectura de caché local no puede colgarse esperando a nadie (es memoria del proceso), así que no necesita bulkhead propio —y de hecho meterla en un thread pool separado solo añadiría overhead—. Aquí "aislar" significa: la HTTP en su compartimento, la caché local en el flujo directo. No todo necesita el mismo tratamiento; aíslas lo que puede colgarse.
La regla en acción: aíslas donde hay una falla independiente que contener (a, la HTTP de c); no aíslas donde no la hay (b, la caché de c). Nombrar la falla es la prueba.
Ejercicio 2 — El costo de elasticidad, con números. En la medición, bajo el pico sano de catalog, el pool compartido sirvió 100% y el bulkhead 50.2%. Explica exactamente por qué el bulkhead rechazó 596 requests teniendo 8 hilos ociosos, y qué diseño intermedio podría haber servido más sin perder todo el aislamiento.
Ver solución
Por qué rechazó con hilos ociosos: en el bulkhead 4/4/4, catalog está capado a sus 4 hilos. El pico de catalog necesita ~8 hilos de concurrencia (una lectura cada 5 ms, ~40 ms cada una → 40/5 = 8). Con solo 4 hilos disponibles para catalog y una cola acotada, la mitad de las requests no cabe y se rechaza —aunque los 8 hilos de payments y shipping estén completamente ociosos ese día—. La pared del bulkhead impide que catalog toque esos 8 hilos: son de payments y shipping, punto. La capacidad existe pero es inalcanzable. Por eso 596 rechazos con 8 hilos vacíos.
Diseño intermedio: un colchón compartido. En vez de 4/4/4 rígidos, das a cada dependencia una reserva garantizada de 3 hilos (9 en total) y dejas 3 hilos en un pool común que cualquiera puede tomar cuando agota su reserva. Bajo el pico sano de catalog, catalog usaría sus 3 garantizados más los 3 del colchón compartido = 6 hilos, sirviendo mucho más que con 4 (aunque quizás no el 100%, porque el colchón es pequeño). Y conserva aislamiento: si shipping se cuelga, solo puede invadir el colchón de 3 (no los 9 garantizados), así que a catalog le quedan sus 3 pase lo que pase. El colchón cambia un poco de aislamiento (los 3 hilos compartidos son un vector de contagio acotado) por bastante elasticidad (absorbe picos). Es el punto medio entre el pool compartido (toda elasticidad, cero aislamiento) y el bulkhead rígido (todo aislamiento, cero elasticidad).
Ejercicio 3 — La tensión del capstone. En el módulo 8 combinarás bulkhead con circuit breaker. Un compañero propone: "si tengo circuit breaker, no necesito bulkhead: el breaker corta shipping cuando se cuelga, así que nunca llena el pool". Evalúa el argumento considerando el costo y el beneficio de cada uno.
Ver solución
El argumento subestima la ventana de tiempo antes de que el breaker actúe. El circuit breaker no se abre instantáneamente: necesita acumular un número de fallas (o timeouts) para decidir que shipping está muerto. En esos primeros segundos del incidente —mientras shipping empieza a colgarse pero el breaker aún cuenta fallas— shipping sí puede llenar el pool compartido y ahogar a catalog y payments, antes de que el breaker corte. El bulkhead protege durante esa ventana: desde el primer instante, shipping no puede tomar más que su cuota, sin importar cuánto tarde el breaker en abrirse.
Sobre el costo: el bulkhead paga elasticidad y complejidad (esta lección); el breaker paga poco (un contador de fallas y un temporizador). Pero hacen cosas distintas: el breaker corta la fuente (deja de llamar a shipping, ahorrando incluso el timeout), el bulkhead contiene el radio de explosión (limita cuánto puede ocupar shipping mientras aún se le llama). Juntos: el bulkhead aguanta desde el segundo cero y contiene el daño; el breaker, una vez confirmado el problema, apaga el tráfico inútil y le da a shipping espacio para recuperarse. Quitar el bulkhead deja descubierta la ventana inicial; quitar el breaker deja que se siga pagando el costo de llamar a un servicio muerto. La combinación no es redundante: cubre riesgos en momentos distintos. Eso es lo que armarás en el capstone —y saber el costo de cada uno es lo que te deja dimensionarlos sin desperdiciar—.
De la balanza al capstone
Ya tienes las dos caras del bulkhead. El beneficio (lecciones 2 a 6): contiene fallas, encoge el radio de explosión, protege el tráfico sano y el crítico. El costo (esta lección): más recursos reservados, menos elasticidad —medida: un pico sano de catalog pasa de 100% servido en el pool compartido a 50.2% en el bulkhead, con hilos ociosos que no puede tocar— y más complejidad de dimensionar y operar. Y el error opuesto al de la lección 2: sobre-particionar recrea la fragilidad por el lado del rechazo. Aislar es una balanza, no un reflejo: se justifica por un límite de falla real, con el margen y el número de compartimentos que ese límite amerita.
Lo que sigue es juntar todo lo del módulo con tus manos. La lección 8 es el proyecto: tomas el checkout de Mercado sobre un pool compartido, mides el contagio, aplicas bulkheads por dependencia, mides el aislamiento, dimensionas los compartimentos con criterio (pagando el costo justo, ni de más ni de menos), y entregas la tabla antes/después distinguiendo lo que el bulkhead resuelve —el contagio— de lo que deja para el breaker y la degradación —el shipping roto—. Es el ciclo "medir → aislar → medir" del módulo, completo.
Resumen y siguiente paso
En esta lección mediste y nombraste el costo del aislamiento, la otra cara de todo lo anterior. El costo central es la pérdida de elasticidad: un compartimento no le presta su capacidad ociosa a otro. Lo mediste con un pico sano de catalog (todas las dependencias OK): el pool compartido sirve 100% porque catalog toma prestados los hilos ociosos de payments y shipping; el bulkhead sirve solo 50.2% —rechaza 596 requests con 8 hilos ociosos inalcanzables—, porque la pared que aísla el riesgo también encierra la capacidad. El aislamiento convierte capacidad ociosa en capacidad inalcanzable.
Aprendiste las tres monedas del costo (más recursos reservados, menos elasticidad, más complejidad de dimensionar y operar); el error de sobre-particionar —demasiados compartimentos diminutos recrean la fragilidad por el lado del rechazo—; la regla para no caer en él (aísla a lo largo de límites de falla reales, nombrando la falla que cada pared contiene); y los diseños intermedios (colchón compartido, límites elásticos con techo, aislar solo lo que se cuelga) que recuperan algo de elasticidad sin perder toda la contención.
Antes de avanzar deberías poder: explicar por qué el pool compartido gana bajo un pico sano rotativo; nombrar las tres monedas del costo; y aplicar la prueba "¿qué falla contiene esta pared?" para decidir si un compartimento vale su precio.
Lo que sigue es el proyecto integrador. La lección 8 te pone a recorrer el ciclo completo del módulo sobre el checkout de Mercado —medir el contagio, aislar por dependencia, medir el aislamiento, dimensionar con criterio, y entregar la tabla antes/después—, cerrando el módulo con el bulkhead aplicado de punta a punta y su frontera clara con el breaker (que corta) y la degradación (que responde al usuario del compartimento inundado).
Recursos
- Michael T. Nygard, Release It!, 2ª ed. (Pragmatic Bookshelf, 2018) — el capítulo del Bulkhead discute el trato entre aislamiento y utilización de recursos, y advierte contra particionar más allá de lo que los límites de falla justifican. La fuente del equilibrio de esta lección. En inglés.
- Marc Brooker, "Some risks of coordinating only sometimes" y otros posts de la Amazon Builders' Library sobre aislamiento — aws.amazon.com/builders-library. Tratan el trato entre aislar cargas (contención) y compartir capacidad (elasticidad/utilización), el costo central de esta lección, a escala de AWS. Gratis y en inglés.
- Google SRE Book, "Handling Overload" — sre.google/sre-book/handling-overload. Cómo la reserva de capacidad por clase (aislamiento) interactúa con la utilización global, y por qué sobre-reservar desperdicia mientras sub-reservar rechaza tráfico sano; la tensión de dimensionamiento de esta lección. Gratis y en inglés.
- Documentación de resilience4j, "Bulkhead" (parámetros
maxConcurrentCallsymaxWaitDuration) — resilience4j.readme.io/docs/bulkhead. Muestra que cada bulkhead es un conjunto de parámetros que dimensionar; la complejidad operativa (tercera moneda del costo) hecha configuración concreta. En inglés.