Solución de problemas de contenedores Docker lentos: una guía de rendimiento paso a paso
Descubra por qué los contenedores Docker son lentos verificando CPU, memoria, E/S de disco, redes, límites, montajes y registro.
Solución de problemas de contenedores Docker lentos: una guía de rendimiento paso a paso
Cuando un contenedor Docker se siente lento, no empiece por reconstruir la imagen o cambiar indicadores de tiempo de ejecución aleatorios. Primero decida qué significa "lento". ¿El tiempo de respuesta de la API es alto? ¿Un trabajador se está quedando atrás? ¿El inicio es lento? ¿Las compilaciones son lentas? ¿El host está sobrecargado? Cada uno apunta a una solución diferente.
Un contenedor no es una separación mágica de la física. Todavía utiliza la CPU del host, la memoria del host, el almacenamiento del host, la red del host y el código de aplicación que envió. Docker agrega controles y espacios de nombres alrededor de esos recursos, pero no hace que una consulta lenta sea rápida ni que un disco saturado esté inactivo.
Comience con una vista rápida en vivo:
docker stats
Observe el contenedor mientras reproduce la ralentización. Una sola instantánea es menos útil que ver qué cambia bajo carga. Si la CPU salta y se mantiene alta, tiene una pista de CPU. Si la memoria sube hasta que el contenedor muere, siga la ruta de la memoria. Si BLOCK I/O se mueve mucho mientras las solicitudes se estancan, el almacenamiento merece atención. Si el contenedor parece tranquilo pero los usuarios aún ven latencia, mire la aplicación, las llamadas de red, la base de datos o los servicios ascendentes.
Primero, compare la salud del contenedor y del host
Un contenedor lento puede simplemente estar viviendo en un host lento. Verifique ambos niveles.
docker stats <contenedor>
top
free -h
df -h
En Linux, iostat -xz 1 es útil si está disponible. La alta utilización del disco o los largos tiempos de espera pueden explicar bases de datos lentas, instalaciones de paquetes y servicios con muchos registros. En Docker Desktop, también verifique la CPU y la memoria asignadas a la VM de Docker. Una Mac con mucha memoria aún puede privar a los contenedores si Docker Desktop está limitado demasiado bajo.
Si cada contenedor es lento, el host es el sospechoso. Si un contenedor es lento mientras que los vecinos están bien, concéntrese en esa carga de trabajo, sus límites, montajes y dependencias.
Cuellos de botella de CPU
En docker stats, la CPU puede superar el 100% porque Docker informa el uso entre núcleos. Un contenedor que usa el 200% está usando aproximadamente dos núcleos. La pregunta importante es si eso es esperado para la carga de trabajo.
Verifique los límites de tiempo de ejecución:
docker inspect <contenedor> --format 'NanoCPUs={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} Cpuset={{.HostConfig.CpusetCpus}}'
Si un servicio se inició con --cpus=0.5, puede estar limitado bajo tráfico normal. En Kubernetes o Compose, el mismo problema puede ocultarse en los límites de CPU. Un trabajador que procesaba trabajos rápidamente en una computadora portátil puede arrastrarse en CI porque solo obtiene media CPU.
Para la CPU a nivel de aplicación, perfile el proceso en lugar de adivinar. Para Node, use la creación de perfiles de CPU incorporada o herramientas tipo clinic. Para Python, muestree con py-spy donde esté permitido. Para Java, use JFR o async-profiler. Si no puede instalar herramientas dentro de una imagen de producción, ejecute la misma imagen en un entorno de prueba o use un patrón de contenedor de depuración.
Las causas comunes de CPU incluyen bucles de sondeo ajustados, serialización JSON costosa, retroceso de expresiones regulares, procesamiento de imágenes, compresión y demasiados hilos de trabajo compitiendo por muy pocos núcleos. Aumentar la CPU ayuda solo si la aplicación puede usarla y el host tiene capacidad.
Presión de memoria y muertes por OOM
Los problemas de memoria se manifiestan como un uso creciente de memoria, recolección de basura frecuente, actividad de intercambio en el host o salidas repentinas. Confirme el estado de OOM:
docker inspect <contenedor> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}}'
Si OOMKilled=true, el contenedor excedió su situación de memoria. Eso puede ser un límite explícito de --memory, un límite de VM de Docker Desktop o presión en todo el host.
Use docker stats mientras envía tráfico realista. Si la memoria crece sin aplanarse, sospeche una fuga, caché sin límites, acumulación de cola o una carga de trabajo que carga demasiados datos a la vez. Si la memoria salta durante el inicio y luego se estabiliza, el límite puede ser simplemente demasiado bajo para el tiempo de ejecución.
Los valores predeterminados del lenguaje importan. Java, Node y algunos servidores de aplicaciones pueden reservar o usar memoria de manera diferente dentro de los contenedores según la versión y la configuración. Establezca opciones explícitas de montón o memoria cuando necesite un comportamiento predecible. Por ejemplo, un servicio Java podría necesitar porcentajes de montón conscientes del contenedor; un servicio Node puede necesitar --max-old-space-size; una base de datos necesita configuraciones de caché que dejen espacio para el proceso y el sistema de archivos.
No establezca límites de memoria tan ajustados que la aplicación pase todo su tiempo recolectando basura. Un contenedor que nunca se bloquea pero se pausa constantemente sigue estando roto.
E/S de disco y montajes de enlace lentos
Los problemas de almacenamiento son fáciles de pasar por alto porque los gráficos de CPU y memoria se ven normales. En Docker, la lentitud del disco a menudo proviene de uno de cuatro lugares: E/S intensiva de la aplicación, registros excesivos, el controlador de almacenamiento o montajes de enlace en Docker Desktop.
Verifique la vista de Docker:
docker stats <contenedor>
docker logs --tail 20 <contenedor>
Si los registros son extremadamente ruidosos, el controlador de registro tiene trabajo que hacer. Los registros JSON-file pueden crecer rápidamente a menos que se configure la rotación. En un servicio ocupado, registrar cada cuerpo de solicitud o línea de depuración puede convertirse en un problema de rendimiento real.
Inspeccione la configuración de registro:
docker inspect <contenedor> --format '{{json .HostConfig.LogConfig}}'
Para configuraciones locales y de servidores pequeños, considere la rotación de registros en la configuración del demonio o en el archivo Compose. Para plataformas de producción, envíe los registros al sistema de registro de la plataforma y mantenga el volumen de registro de la aplicación intencional.
Los montajes de enlace merecen atención especial en macOS y Windows. Un árbol de origen montado desde el host en un contenedor Linux cruza una capa de virtualización. Eso es conveniente para el desarrollo, pero puede ser mucho más lento que un volumen con nombre para carpetas de dependencias, bases de datos o directorios con muchas escrituras.
Por ejemplo, un contenedor de desarrollo Node puede ser lento si node_modules reside en un montaje de enlace. Un mejor patrón es montar el código fuente mediante enlace pero mantener las dependencias en un volumen con nombre:
services:
app:
volumes:
- .:/app
- node_modules:/app/node_modules
volumes:
node_modules:
Para bases de datos, prefiera volúmenes con nombre sobre montajes de enlace a menos que tenga un flujo de trabajo de copia de seguridad o inspección específico que requiera rutas de host.
Latencia de red y lentitud de dependencias
Un contenedor puede ser "lento" porque está esperando otro servicio. El proceso local puede estar saludable mientras que DNS, una base de datos, Redis, una API o un proxy son lentos.
Pruebe desde dentro del contenedor:
docker exec -it <contenedor> sh
curl -w '
lookup:%{time_namelookup} connect:%{time_connect} start:%{time_starttransfer} total:%{time_total}
' -o /dev/null -s http://servicio:8080/health
Esa salida de curl -w separa la búsqueda DNS, la conexión TCP, el primer byte y el tiempo total. Si la búsqueda DNS es lenta, inspeccione /etc/resolv.conf y la configuración DNS del demonio Docker. Si la conexión es lenta o falla, verifique las redes, los firewalls y el enlace del servicio. Si el tiempo hasta el primer byte es lento, el servicio ascendente aceptó la conexión pero tardó en responder.
Para tráfico de contenedor a contenedor, use una red puente definida por el usuario para que los contenedores puedan resolverse entre sí por nombre:
docker network create appnet
docker run -d --name api --network appnet my-api
docker run --rm --network appnet curlimages/curl http://api:8080/health
No realice pruebas comparativas a través de puertos de host publicados cuando el tráfico real sea de contenedor a contenedor. Pruebe la ruta que utiliza la producción.
El rendimiento de inicio es un problema separado
El inicio lento a menudo proviene del tiempo de extracción de la imagen, la instalación de dependencias al iniciar el contenedor, las migraciones de base de datos o el calentamiento de la aplicación.
Un contenedor no debería instalar paquetes cada vez que se inicia. Si su punto de entrada ejecuta npm install, pip install, apt-get o descarga binarios en cada arranque, mueva ese trabajo a la compilación de la imagen a menos que haya una razón sólida para no hacerlo.
Verifique los registros de inicio con marcas de tiempo si su aplicación los proporciona. Si no, agregue marcas de tiempo simples alrededor de los pasos del punto de entrada mientras depura:
date; echo 'iniciando migraciones'
# comando de migración
date; echo 'iniciando servidor'
# comando del servidor
Para imágenes extraídas a través de una red, el tamaño de la imagen importa. Las compilaciones de múltiples etapas, .dockerignore y las bases de tiempo de ejecución más pequeñas mejoran el inicio en frío y la velocidad de implementación. Pero una vez que la imagen ya está presente y el contenedor se está ejecutando, el tamaño de la imagen generalmente importa menos que la CPU, la memoria, la E/S y el comportamiento de la aplicación.
El rendimiento de compilación no es rendimiento de tiempo de ejecución
Las compilaciones lentas de Docker son frustrantes, pero son una clase diferente de problema. Si los cambios de código fuerzan la instalación de dependencias en cada compilación, corrija el orden de las capas:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
No copie todo el repositorio antes de instalar las dependencias a menos que quiera que cada cambio de fuente invalide la capa de dependencias.
También mantenga pequeño el contexto de compilación:
.git
node_modules
coverage
dist
*.log
Los montajes de caché de BuildKit pueden ayudar con descargas repetidas de dependencias, pero primero asegúrese de que el Dockerfile esté ordenado correctamente. Un montaje de caché no puede salvar completamente un Dockerfile que invalida la caché demasiado pronto.
Los límites de recursos pueden proteger al host y dañar la aplicación
Los límites de CPU y memoria son útiles porque un contenedor no debería derribar un host. También pueden crear lentitud artificial si se copian de un ejemplo sin medir la carga de trabajo.
Inspeccione los límites:
docker inspect <contenedor> --format '{{json .HostConfig}}' | jq '{Memory, NanoCpus, CpuQuota, CpuPeriod, BlkioWeight}'
Si jq no está disponible, inspeccione el contenedor normalmente y busque HostConfig.
Para Compose, verifique la configuración real renderizada:
docker compose config
Esto detecta límites heredados de archivos de anulación o variables de entorno. Una sorpresa común es un archivo de anulación de desarrollo que establece límites bajos y accidentalmente se usa en un entorno de prueba.
Un flujo de diagnóstico práctico
Use este flujo cuando la queja sea simplemente "el contenedor es lento":
- Reproduzca el comportamiento lento y ejecute
docker statsdurante la reproducción. - Verifique la CPU, memoria, disco y límites de VM de Docker Desktop del host.
- Inspeccione los límites de CPU y memoria del contenedor.
- Lea los registros en busca de reintentos, tiempos de espera de conexión, migraciones, registro de depuración o pistas de OOM.
- Pruebe las dependencias desde dentro del contenedor con
curl,digo una imagen de depuración creada para ese propósito. - Verifique los montajes: mueva las rutas con muchas escrituras a volúmenes con nombre cuando sea apropiado.
- Perfile la aplicación si los gráficos de recursos apuntan de nuevo al código.
Las mejores soluciones tienden a ser específicas: aumentar un límite de memoria demasiado bajo, dejar de registrar cargas útiles grandes, mover datos de base de datos fuera de un montaje de enlace, corregir una ruta DNS lenta, reordenar las capas del Dockerfile o ajustar el tiempo de ejecución de la aplicación. El consejo genérico de "optimizar Docker" es menos útil que probar qué recurso es realmente lento.