BookinglyTech News
Infraestructura

Cuando la red de Docker se satura: autopsia de unos 2.1 segundos de latencia

Un cliente de e-commerce tenía fugas de rendimiento bajo carga que no eran ni CPU ni base de datos. La solución no fue Kubernetes ni más código, sino ajustar sysctl y DNS.

3 min de lecturaDev.to0 vistas

Hay síntomas en producción que engañan al primer vistazo rápido. Un cliente de mercado online llegó a Binadit con un problema clásico de pánico: la latencia p95 del checkout saltaba de 280ms a más de 2.1 segundos cuando el tráfico superaba las 900 peticiones por segundo. Instintivamente, el equipo de infraestructura lanzó más contenedores de la aplicación. El resultado fue peor. La base de datos Postgres mantenía queries estables entre 8 y 14ms y las gráficas de CPU y memoria no mostraban cuellos de botella evidentes. El problema real estaba en la capa de red entre contenedores, un sitio donde las herramientas por defecto apenas ofrecen visibilidad.

La infraestructura era un monolito PHP containerizado hace un año, corriendo en un único host con Docker Compose. El stack incluía contenedores de app, Redis, una cola de trabajadores y un balanceador de carga gestionado. Todo funcionó bien durante seis meses hasta que las ventas flash empezaron a generar tickets de bloqueo.

La auditoría reveló tres problemas compuestos, ninguno fatal por separado pero letales en conjunto. Primero, la sobrecarga de la red bridge por defecto. Cada salto entre contenedores (app a Redis, app a Postgres, app a search) pasaba por la proxy de userland del bridge por defecto. Esto añadía unos 1.8ms de latencia por salto. Con 900 req/s y 3-4 llamadas internas por petición, ese retraso se acumula rápidamente. Segundo, el agotamiento de la tabla conntrack. El kernel tenía el valor por defecto de nf_conntrack_max en 65.536. Las conexiones efímeras a Redis y Postgres llenaban la tabla durante los picos, y el kernel empezaba a descartar paquetes en silencio. Como los logs de syslog no se enviaban a ningún monitor útil, nadie lo detectó hasta que los usuarios se quejaron.

Tercero, la resolución DNS. El DNS embebido de Docker (127.0.0.11) resolvía nombres de servicio en cada nueva conexión en lugar de que la aplicación cacheara los resultados. Bajo carga, las búsquedas se colaban unas detrás de otras.

El equipo descartó migrar a Kubernetes. Habría sido un error: K8s tiene sus propias variantes de estos problemas (elección de CNI, modo kube-proxy, caché de CoreDNS). Cambiar el orquestador sin corregir la causa raíz solo trasladaría el fallo. También descartaron reescribir la app para reducir llamadas internas; el patrón de llamadas era normal y la capa de red debía manejarlo eficientemente.

La corrección en cuatro capas

La solución comenzó por el ajuste del kernel para conntrack. Se elevaron los límites y se reutilizaron los tiempos de espera TCP mediante /etc/sysctl.d/99-docker-network.conf. Se añadió monitorización de la utilización de la tabla conntrack para evitar fallos silenciosos. Luego se movió el tráfico interno fuera del bridge por defecto a una red personalizada con MTU más grande, manteniendo el tráfico este-oeste fuera de la ruta NAT de Docker. El balanceador público se quedó en una red separada para no cambiar la superficie de ataque externa.

Se implementó caché de DNS local con dnsmasq como contenedor sidecar. Un TTL de 10 segundos absorbió el churn de conexiones durante los picos sin causar problemas de resolución obsoleta al reiniciar contenedores. Finalmente, se añadió PgBouncer para agrupar conexiones. Al reducir las conexiones efímeras, se disminuyeron las entradas en conntrack y las búsquedas DNS, haciendo que todo lo anterior fuera más ligero. Si tu app se ralentiza solo bajo carga y los gráficos de CPU/memoria están quietos, deja de escalar contenedores y mira conntrack, tu controlador bridge y el comportamiento DNS. Ahí suele esconderse el problema.