Equipo Easybits
7 min de lectura
infraestructura
Una tienda hospedada en nuestra flota tardaba diez segundos en pintar la imagen principal. La imagen pesa 163 KB.
Lo interesante no es el final —una línea de configuración— sino que durante un buen rato todo apuntaba a que no había nada roto.
Cuando algo va lento uno mira el servidor. Miramos:
| Qué medimos | Resultado |
|---|---|
La app, desde dentro de la caja (localhost) | 122 MB/s — el archivo en 1.3 ms |
| Internet, desde dentro de la caja | 21.5 MB/s |
| TTFB desde el cliente | 0.5 s, normal |
| CPU y RAM | de sobra |
| Lo que recibía el visitante | 16 KB/s |
La aplicación entregaba el archivo en 1.3 milisegundos. Al visitante le llegaba en diez segundos.
Esa es la trampa de este tipo de fallas: ninguna métrica de servidor se pone roja. El problema no vive en el servidor ni en el cliente, sino en el camino entre los dos, que es justo el tramo que nadie instrumenta.
Antes de dar con la buena pasamos por tres. Las contamos porque son las tentadoras, y porque el camino equivocado enseña más que el bonito.
"La imagen pesa mucho." Y sí pesaba: 2.5 MB de PNG, una fotografía guardada sin pérdida. La optimizamos a 163 KB en AVIF. Mejoró bastante… y el techo seguía ahí. Un problema real puede tapar a otro, y arreglar el primero te hace creer que ya está.
"La máquina es chica." Corría en el tier más pequeño, con CPU compartida. Sonaba razonable hasta que medimos desde dentro: 1.3 ms para servir el archivo, 21 MB/s de red. La caja estaba de vacaciones.
"El enlace interno entre fierros." Nuestra flota tiene el proxy en un fierro y algunas cajas en otro, conectados por Tailscale. Un enlace cayendo a relay explicaría perfectamente el síntoma. Era la hipótesis más convincente — y era falsa: pedimos el archivo desde dentro de la caja, a su propia URL pública, forzando el recorrido completo (internet → proxy → enlace interno → caja) y dio 1.47 MB/s. El tramo interno estaba sano.
Descartadas las tres, quedaba un solo sospechoso: el trecho entre el servidor y el visitante.
Esta es la parte que vale la pena robarse. Descarga el mismo archivo con N conexiones en paralelo y mira si el total escala:
Lo que nos salió:
Aquí está todo el diagnóstico. Si el total escala con las conexiones, no te falta ancho de banda. El tubo da para doce veces más; lo que no da es cada conexión por separado. Si en cambio el total se quedara clavado en 19 KB/s sin importar cuántas abras, entonces sí sería un límite de ancho de banda o un shaper.
Un ping completó el cuadro: 150 ms de ida y vuelta hasta el servidor (está en Francia, el visitante en México) y ~5% de pérdida de paquetes.
TCP tiene un algoritmo de control de congestión que decide cuántos datos manda sin esperar confirmación. Linux trae cubic de fábrica desde hace veinte años, y cubic pertenece a la familia loss-based: interpreta cada paquete perdido como señal de congestión y parte la ventana a la mitad.
Esa lógica es impecable dentro de un centro de datos, donde si se pierde un paquete es porque algo se saturó.
En un enlace intercontinental es un desastre. Ahí la pérdida es ruido del camino —fibra, saltos, peering—, no congestión. Y con 150 ms de ida y vuelta, cada repliegue tarda muchísimo en recuperarse. El resultado: la conexión vive permanentemente encogida, con unos 2 KB en vuelo, replegándose antes de haber levantado.
No estaba fallando nada. Cubic hacía exactamente lo que le pedimos, con una suposición que no aplicaba.
HTTP/2 multiplexa todos los recursos de una página sobre una sola conexión TCP. Es una virtud: elimina el costo de abrir conexiones.
Salvo cuando esa única conexión es la que se arrastra. Todo el sitio —HTML, CSS, JS, imágenes— viajaba por el único tubo estrangulado, y el navegador no podía compensar. Con HTTP/1.1, que abre seis conexiones, el sitio habría ido más rápido. Nuestra prueba de las 20 conexiones en paralelo era, sin querer, una simulación de eso.
BBR, de Google, ataca justo esto: no usa la pérdida como señal de congestión. Mide el ancho de banda real del cuello de botella y el RTT mínimo, y manda a esa tasa. Enlaces largos con pérdida son su caso de uso canónico.
Estaba en el kernel desde hacía años. Solo había que encenderlo:
Persistente en /etc/sysctl.d/99-bbr.conf y /etc/modules-load.d/bbr.conf.
Diez muestras de una conexión, contra el mismo archivo:
| antes (cubic) | después (BBR) | |
|---|---|---|
| Mediana | 41.8 KB/s | 117 KB/s |
| Peor caso | 21.8 KB/s | 58.7 KB/s |
| La imagen de 163 KB | ~10 s | 1.1–2.1 s |
Casi el triple, y el suelo —el peor momento, que es el que arruina la experiencia— también se triplicó.
Vale la pena decir por qué esto es tan barato de probar:
modprobe carga un módulo; sysctl escribe un parámetro. Después del cambio verificamos los uptimes: el proxy seguía con sus 19 días, las microVMs con los suyos. Ningún proceso se reinició.Y si corres microVMs como nosotros: las cajas ni se enteran, porque tienen su propio kernel invitado. Esto vive en el kernel del anfitrión, que es quien termina las conexiones con los visitantes.
Porque es un default, y los defaults solo se ven cuando duelen. BBR está en Linux desde 2016, pero ninguna distro lo activa: el módulo estaba ahí, esperando un modprobe que nadie tenía razón de escribir.
Hay algo estructural también. En un PaaS grande esto no te pasa: terminan el TCP en un punto de presencia a milisegundos de tu usuario y sus kernels ya vienen afinados. Correr tu propio fierro significa heredar los defaults de la distro, con todo lo bueno —y con esto.
Mide desde adentro y desde afuera al mismo tiempo. Lo que aisló el problema no fue una métrica sino la contradicción entre dos: la app volando en localhost y el visitante recibiendo migajas. Cada medición por separado parecía normal.
Un problema real puede tapar a otro. La imagen sí pesaba de más. Arreglarlo mejoró las cosas y casi nos hace cerrar el caso.
Sospecha de esto cuando algo se sienta lento solo desde lejos, el TTFB esté bien pero la transferencia mal, y el throughput escale con conexiones paralelas. Son treinta segundos de curl para descartarlo.
El cambio quedó en los dos fierros de la flota y en el bootstrap, para que un servidor nuevo no repita la historia. Beneficia a todas las cajas, no solo a la tienda que lo destapó.