Introducción

Los equipos que despliegan workloads de machine learning en Amazon EKS enfrentan un cuello de botella recurrente: descargar imágenes de contenedores de 20-30 GB puede tardar varios minutos, durante los cuales GPUs costosas permanecen ociosas. En un entorno de producción con autoescalado, este retraso genera colas de requests y degradación del servicio. El problema no está en la velocidad de la red ni en el registry —las instancias aceleradas de AWS tienen 100-400 Gbps de ancho de banda—, sino en cómo containerd aprovecha los recursos disponibles.

Qué ocurrió

Un equipo de Amazon EKS identificó que, en su plataforma de ML, los pods demoraban más de dos minutos solo en el pull de imágenes, superando el SLA de provisionamiento. Cada pod descargaba una imagen de ~30 GB (frameworks de DL, CUDA stack y pesos del modelo) desde un registry, sin cache local útil por las reconstrucciones frecuentes. El análisis del pipeline de pull reveló que containerd procesaba cada layer secuencialmente: descarga, verificación de hash, escritura a disco, descompresión, verificación de hash descomprimido y extracción. Con layers de hasta 9 GB, el más grande dominaba el tiempo total.

Impacto para DevOps / Infraestructura / Cloud / Seguridad

El impacto va más allá del tiempo de espera. En entornos con autoescalado, los nodes nuevos tardan minutos en estar listos, lo que:

  • Aumenta el costo: GPUs (como p3.2xlarge o p4d.4xlarge) facturan por minuto mientras esperan la imagen.
  • Degrada la experiencia de usuario: Requests se encolan hasta que el pod esté ready, incrementando latencia.
  • Limita la escalabilidad: El lag entre demanda y capacidad disponible puede generar throttling.

Para un cluster con 100 nodes que recicla 20% diario, cada minuto ahorrado en pulls equivale a ~53 horas menos de GPU inactiva por mes. Además, el approach tradicional de optimizar imágenes (multi-stage builds, base images minimalistas) tiene un techo: el stack de GPU (PyTorch, cuDNN, CUDA) ocupa 3-4 GB comprimidos, sin margen para reducción.

Detalles técnicos

Estructura de las imágenes de ML

Una imagen de contenedor no es un archivo único, sino un manifest JSON que referencia layers (archivos tar.gz comprimidos). En imágenes de ML:

  • Distribución desigual: Un layer puede representar >50% del total. Ejemplo real: 30 GB totales con layers de 12 GB, 8 GB, 5 GB y el resto <1 GB.
  • Contenido: Los layers grandes suelen contener binarios de CUDA, librerías de DL o pesos de modelos.

Pipeline tradicional de pull en containerd

Para cada layer, containerd ejecuta 6 etapas secutivamente:

  • Descarga (HTTP GET)
  • Verificación SHA-256 de los bytes descargados
  • Escritura del blob comprimido a disco
  • Descompresión (gzip)
  • Verificación SHA-256 del contenido descomprimido
  • Extracción al directorio del snapshotter
  • Cuellos de botella:

    • Descarga: Por defecto, containerd usa 3 conexiones HTTP paralelas para layers, pero cada layer usa una sola conexión.
    • Descompresión: gzip es single-threaded y puede triplicar el tamaño (ej: 9 GB → 27 GB). En una instance c5.2xlarge (2 vCPUs), descomprimir un layer de 9 GB tarda ~1 minuto.
    • Verificación de hashes: Calcular SHA-256 sobre GBs de datos consume CPU.
    • Extracción: Escribir miles de archivos pequeños al disco satura el IOPS.

    El layer más grande dictamina el tiempo total: mientras se procesa, los demás esperan.

    Optimizaciones implementadas

    El equipo de EKS modificó el pipeline en dos áreas:

    1. Download paralelo por layer

    • División de cada layer en bloques de ~5 MB (configurable).
    • Descarga simultánea de bloques usando múltiples conexiones HTTP.
    • Reensamblado y verificación del hash en memoria.

    2. Unpack paralelo

    • Descompresión y verificación de hashes en parallel usando múltiples threads (configurable).
    • Extracción a disco en parallel, con control de concurrency para evitar saturar el storage.

    Componentes clave:

    • containerd v1.7+: Incluye soporte para download paralelo (max-concurrent-downloads y max-download-attempts por layer).
    • SOCI snapshotter: Snapshotter optimizado para OCI, con unpack paralelo.
    • Parámetros de tuning:

    [plugins.cri.containerd]
    max_concurrent_downloads = 10 # Conexiones HTTP totales para downloads
    max_download_attempts = 5 # Retries por layer

    [plugins.cri.containerd.runc.options]
    snapshotter = «soci»

    [plugins.cri.soci]
    base_directory = «/var/lib/soci»
    max_concurrent_unpacks = 5 # Layers a descomprimir en parallel
    max_unpack_bytes_per_sec = 100000000 # Throttling de IO (100 MB/s)

    Qué deberían hacer los administradores y equipos técnicos

    1. Verificar la versión de containerd

    Los clusters EKS gestionados por AWS con eksctl o el add-on de managed node groups ya usan containerd v1.7+ y SOCI snapshotter por defecto desde EKS release 1.23. Para clusters auto-gestionados:

    # Verificar versión en un node
    containsd –version
    # Debería ser >= 1.7.0

    2. Habilitar SOCI snapshotter (si no está activo)

    Para clusters existentes con containerd <1.7 o usando el snapshotter por defecto (overlayfs):

  • Instalar el plugin SOCI:
  • sudo apt-get update
    sudo apt-get install soci-snapshotter

  • Configurar containerd para usarlo:
  • # /etc/containerd/config.toml
    [plugins.cri.containerd]
    snapshotter = «soci»

  • Reiniciar containerd:
  • sudo systemctl restart containerd

    3. Ajustar parámetros de parallelismo

    Editá /etc/containerd/config.toml y ajustá los valores según tu hardware:

    [plugins.cri.containerd]
    max_concurrent_downloads = 10 # Aumentar para instancias con alto BW
    max_download_attempts = 5

    [plugins.cri.soci]
    max_concurrent_unpacks = 5 # Ajustar según cores disponibles
    max_unpack_bytes_per_sec = 100000000 # Limitar IO para evitar saturar storage

    • max_concurrent_downloads: 1 por cada 100 Mbps de BW disponible (ej: 20 para 2 Gbps).
    • max_concurrent_unpacks: 1 por cada 2 cores (para dejar recursos para el container runtime).

    4. Monitorear el rendimiento

    Usá métricas de Prometheus (si tenés containerd con metrics enabled) para validar la mejora:

    # Tiempo total de pull por imagen
    container_containerd_pull_duration_seconds
    # Tiempo por etapa (download, unpack)
    container_containerd_pull_layer_download_duration_seconds
    container_containerd_pull_layer_unpack_duration_seconds

    5. Considerar cache de layers para workloads estáticos

    Si tenés images que no cambian frecuentemente:

    • Usá snapshotter pre-loaded: Crea un volumen EBS con la imagen ya descargada y montalo en los nodes.
    • Implementá un daemonset que precargue las imágenes al iniciar el node.

    Conclusión

    El problema de las descargas lentas de imágenes grandes no requiere cambios en las imágenes ni en el registry. Con containerd v1.7+ y SOCI snapshotter, el download y unpack paralelo aprovechan el BW, CPU y storage disponibles en instancias aceleradas. En pruebas internas de AWS, una imagen de 30 GB pasó de 4 minutos a 26 segundos con estas optimizaciones. Para clusters EKS nuevos, estas mejoras están habilitadas por defecto. Para los existentes, los cambios son simples y no requieren downtime.

    Fuentes

    • https://thenewstack.io/accelerating-eks-image-pulls/

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *