Requests y Limits en Kubernetes: Guía Completa para Producción

Requests y Limits en Kubernetes: Guía Completa para Producción

Tus pods mueren con OOMKill a las 3 de la madrugada. El p99 de latencia se dispara cada pocos minutos sin causa aparente. El scheduler coloca cargas de trabajo en nodos que no pueden sostenerlas. En la mayoría de los incidentes de producción con Kubernetes, los requests y limits mal configurados son o bien la causa directa o un factor que acelera el problema.

Esta no es una guía de «qué son los requests y los limits». Es una referencia técnica en profundidad para ingenieros que operan Kubernetes en producción y necesitan entender qué ocurre realmente dentro del kernel cuando se establecen estos valores — y cuáles son las consecuencias cuando se configuran mal.


Qué son realmente los Requests y los Limits

La documentación de Kubernetes explica requests y limits a nivel de API. Lo que no explica con suficiente detalle es el mecanismo de aplicación: cgroups.

Cuando el kubelet admite un pod en un nodo, crea una jerarquía de cgroups para ese pod bajo /sys/fs/cgroup/. Cada contenedor del pod recibe su propio cgroup. Los valores que defines en el spec del pod se traducen directamente en parámetros de cgroup:

CPU requestcpu.shares (cgroups v1) o cpu.weight (cgroups v2)
CPU limitcpu.cfs_quota_us y cpu.cfs_period_us
Memory requestmemory.soft_limit_in_bytes (orientativo, usado para scoring de evicción)
Memory limitmemory.limit_in_bytes (aplicación estricta, desencadena OOMKill)

El scheduler usa los requests para tomar decisiones de ubicación. No conoce la utilización real — conoce la capacidad comprometida. Un nodo con 4 cores donde los pods en ejecución tienen un total de CPU request de 3,5 cores tiene 0,5 cores de capacidad schedulable restante, aunque la utilización real de CPU sea del 15%.

Por eso puedes tener un clúster «totalmente utilizado» (según requests) donde los nodos están ociosos, y también puedes tener nodos al 95% de utilización de CPU que siguen aceptando nuevos pods porque sus requests son bajos.

El kubelet usa los limits para aplicar restricciones en tiempo de ejecución mediante esos parámetros de cgroup. El scheduler nunca ve los limits.


CPU vs Memoria: Por Qué Se Comportan de Forma Fundamentalmente Distinta

Esta es la diferencia más importante que hay que entender sobre la gestión de recursos en Kubernetes, y es algo que se malinterpreta sistemáticamente incluso entre ingenieros con experiencia.

La CPU es un Recurso Compresible

La CPU es un recurso compartido en el tiempo. Si tu contenedor intenta usar más CPU de la que su limit permite, el scheduler CFS de Linux simplemente lo throttlea — deja de recibir tiempo de CPU hasta el siguiente período de planificación. El proceso continúa. Solo espera.

Desde la perspectiva de la aplicación: las cosas van más lentas. La latencia aumenta. El throughput cae. Pero el proceso no muere.

La Memoria no es un Recurso Compresible

La memoria no se comparte en el tiempo. Si tu contenedor intenta asignar memoria por encima de su limit, no hay una «ruta de degradación gradual». El OOM killer de Linux selecciona un proceso en el cgroup y lo mata. El contenedor muere.

Desde la perspectiva de la aplicación: el proceso termina. Kubernetes reinicia el contenedor. Ves OOMKilled en kubectl describe pod.

PropiedadCPUMemoria
AplicaciónCFS throttlingOOM Kill
¿Sobrevive el proceso?Sí (rendimiento degradado)No (muerto y reiniciado)
¿Compresible?No
Visibilidad del schedulerSolo requestsSolo requests
Consecuencia por exceder el limitPicos de latenciaReinicio del contenedor
¿Recomendado poner limits?Situacional (ver más abajo)Siempre

Esta asimetría guía todas las recomendaciones del resto de esta guía.


Clases QoS: Prioridad de Evicción Bajo Presión

Kubernetes asigna a cada pod una clase Quality of Service (QoS) en función de los requests y limits establecidos en todos sus contenedores. Esta clase determina la prioridad de evicción cuando un nodo está bajo presión de memoria.

Guaranteed

Condición: Todos los contenedores tienen definidos requests y limits de CPU y memoria, y los requests son iguales a los limits tanto para CPU como para memoria.

resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

Los pods Guaranteed son los últimos en ser eviccionados. El kubelet agotará los pods BestEffort y Burstable antes de tocar estos. Reciben la asignación de recursos más predecible en el nodo.

Advertencia: Guaranteed no significa «siempre disponible». Significa «el último en ser matado». En un nodo con sobrecarga severa, incluso los pods Guaranteed pueden ser eviccionados.

Burstable

Condición: Al menos un contenedor tiene definido un request o limit de CPU o memoria, pero el pod no cumple los criterios de Guaranteed.

resources:
  requests:
    cpu: "250m"
    memory: "256Mi"
  limits:
    cpu: "1000m"
    memory: "1Gi"

Los pods Burstable se eviccionan después de BestEffort pero antes que Guaranteed. Pueden superar su request cuando la capacidad está disponible, pero no están protegidos cuando el nodo está bajo presión.

BestEffort

Condición: Ningún contenedor del pod tiene definido ningún request ni limit de CPU ni de memoria.

# Sin bloque resources en absoluto

Los pods BestEffort son los primeros en ser eviccionados, siempre. Reciben la capacidad que queda después de que las cargas de trabajo planificadas consuman su parte solicitada. En un nodo con carga, pueden quedar completamente sin recursos.

En producción: nunca ejecutes cargas de trabajo con estado ni servicios críticos de negocio como BestEffort. El scheduler de Kubernetes los colocará en cualquier sitio, y el kubelet los matará primero.


Patrones de Mala Configuración y sus Consecuencias

Patrón 1: Sin Requests ni Limits

Efecto: QoS BestEffort. Primeros en ser eviccionados bajo presión de memoria. El scheduler coloca los pods de forma arbitraria — no tiene datos para tomar decisiones de ubicación, por lo que recurre a LeastRequestedPriority, lo que en la práctica significa que estos pods pueden acabar en los mismos nodos que cargas de trabajo muy cargadas.

Consecuencia real: Tus jobs de background «ligeros» matan tus servidores de API a las 3 de la madrugada cuando un pico de memoria desencadena evicción y los pods BestEffort resultan estar en el mismo nodo que ellos.

Patrón 2: Requests Iguales a Limits (QoS Guaranteed)

Este es el patrón «seguro» común recomendado en documentación antigua de Kubernetes. No es incorrecto, pero tiene una trampa:

CPU limits = CPU requests significa que el CPU throttling está garantizado que se activará. Tu pod será throttleado en el momento en que intente superar el request — durante el arranque, durante GC, durante un pico de tráfico — aunque el nodo tenga CPU libre abundante.

Para aplicaciones sensibles a la latencia, esto significa picos de throttling predecibles exactamente en los momentos en que más CPU necesitas.

Memoria: Establecer memory request = memory limit es apropiado y recomendable. El comportamiento es correcto: el pod funciona dentro de un presupuesto de memoria controlado.

Patrón 3: Limits Muy Superiores a los Requests (Burstable con Ratio Alto)

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "4000m"
    memory: "4Gi"

Este es el extremo opuesto. El scheduler cree que este pod necesita 100m de CPU y 128Mi de memoria. Docenas de estos pueden planificarse en un solo nodo. Cuando todos hacen burst simultáneamente — que lo harán, durante un despliegue, un evento de tráfico o un ciclo de GC — el nodo queda sobrecargado, la presión de memoria desencadena cascadas de OOMKill, y el scheduler no tiene ni idea de que algo va mal porque la capacidad comprometida (según requests) parece correcta.

El ratio limit:request importa. Un ratio de memory limit:request de 10x o 20x en muchos pods es una receta para la inestabilidad del nodo. Un punto de partida razonable es 2x–4x para memoria, menos para CPU.

Patrón 4: CPU Limits Establecidos «Por Seguridad»

Esta es la mala configuración más sutil y la que tiene mayor impacto oculto en la latencia. La analizamos en profundidad en la siguiente sección.


El Problema del CPU Throttling: CFS Bandwidth y Latencia Oculta

Aquí es donde muchos despliegues de Kubernetes en producción tienen un problema de rendimiento silencioso que no es fácil de diagnosticar.

Cómo Funciona el CFS Bandwidth Throttling

El Linux Completely Fair Scheduler (CFS) aplica los CPU limits mediante control de ancho de banda. Los parámetros relevantes son:

  • cpu.cfs_period_us: el período de contabilización, por defecto 100ms
  • cpu.cfs_quota_us: cuántos microsegundos de tiempo de CPU puede usar el cgroup por período

Si estableces cpu: "500m" como limit, Kubernetes establece cpu.cfs_quota_us = 50000 (50ms por período de 100ms). Esto significa que el contenedor puede usar como máximo el 50% de un núcleo de CPU por ventana de 100ms.

El problema: la quota se aplica por período, no como media móvil. Si tu contenedor usa su asignación completa de 50ms en los primeros 60ms de un período, es throttleado durante los 40ms restantes — aunque el nodo tenga 7 CPUs ociosas. La CPU permanece inactiva. Tu contenedor espera.

Por Qué Esto Causa Picos de Latencia Incluso con Baja Utilización

Esto es contraintuitivo y el origen de muchos misterios en producción. Puedes tener un contenedor funcionando al 10% de utilización media de CPU que es throttleado regularmente, porque su uso instantáneo de CPU dentro de una sola ventana de 100ms supera su quota.

Las aplicaciones Java con garbage collection de la JVM son especialmente vulnerables. El GC provoca un burst de CPU de corta duración. Si ese burst supera la quota por período, la pausa de GC se alarga artificialmente por throttling — aunque el propio evento de GC hubiera sido breve.

Lo mismo ocurre con el procesamiento del event loop de Node.js, la importación de Python al arrancar, y cualquier aplicación con comportamiento de CPU en ráfagas (que son la mayoría).

La Evidencia de Cloudflare y Netflix

Cloudflare publicó resultados mostrando que el CPU throttling era responsable de incrementos significativos en la latencia de cola en sus cargas de trabajo en contenedores, y que eliminar los CPU limits redujo la latencia p99 sustancialmente para servicios que aparentemente tenían margen. Netflix ha documentado patrones similares en sus trabajos de capacity planning, señalando que la aplicación de quota por período no modela con precisión el comportamiento real de CPU de las aplicaciones.

La comunidad del kernel lleva años siendo consciente de esto. La solución — migrar a cgroups v2 con mejor integración del scheduler — ayuda pero no elimina el problema. Kubernetes 1.25+ con nodos cgroups v2 experimenta menos throttling bajo los mismos limits, pero el problema fundamental persiste: los CPU limits throttlean aplicaciones con ráfagas de forma impredecible.

La Recomendación: Considera No Establecer CPU Limits

Esto es controvertido pero está respaldado por la evidencia:

Para servicios sensibles a la latencia: no establezcas CPU limits. Establece CPU requests con precisión y confía en el scheduler para la ubicación.

El argumento:
– El CPU throttling es un modo de fallo suave que es difícil de observar y diagnosticar
– OOMKill es un modo de fallo duro que es visible y recuperable
– Los CPU requests dan al scheduler datos precisos de ubicación sin crear throttling
– Los nodos gestionan la sobresuscripción de CPU de forma elegante mediante time-sharing; no gestionan la sobresuscripción de memoria de forma elegante

Cuándo seguir estableciendo CPU limits:
– Clústeres multi-tenant donde el aislamiento de vecinos ruidosos (noisy neighbor) es crítico
– Cargas de trabajo batch donde la asignación predecible de CPU importa más que la latencia
– Cuando tu monitorización y alertas pueden detectar CPU starvation a nivel de nodo

Cuando no estableces CPU limits, debes establecer CPU requests con precisión. Un request de 100m para un servicio que normalmente usa 800m significa que el scheduler lo coloca en un nodo que realmente no puede sostenerlo. El resultado es starvation real de CPU, no throttling artificial — pero starvation de CPU igualmente.


Memoria: Establece Siempre los Limits

El contraste con la CPU es directo. La memoria es no compresible. Un contenedor que tiene una fuga de memoria o una asignación desbocada consumirá toda la memoria disponible del nodo si no tiene restricciones. Esto no degrada de forma elegante — activa el OOM killer, que puede matar procesos no relacionados en el nodo.

Establece siempre memory limits. Siempre.

La consecuencia — OOMKill — es visible, queda registrada, y Kubernetes la gestiona reiniciando el contenedor. Un código de salida OOMKilled es accionable: o tienes una fuga de memoria, o tu limit es demasiado bajo, o tu metodología de sizing es incorrecta. Los tres son diagnosticables.

La alternativa — sin memory limit — significa que un único pod con fuga puede desestabilizar un nodo entero y desencadenar cascadas de evicción que afectan a cargas de trabajo no relacionadas.

Establece los memory requests igual al uso en estado estable del p95 de tu aplicación. Establece los memory limits en 1,5x–2x el request para absorber picos de tráfico y presión de GC. Perfila tu aplicación bajo carga para establecer estas líneas base.


Vertical Pod Autoscaler (VPA)

El VPA es el componente de Kubernetes diseñado para resolver automáticamente el problema del sizing. Observa la utilización real de recursos y recomienda (o aplica) requests ajustados.

Cómo Funciona el VPA

El VPA tiene tres componentes:

  • Recommender: Observa métricas históricas y calcula requests recomendados basándose en la utilización observada. No modifica pods.
  • Updater: Evicta pods cuyos requests actuales difieren significativamente de las recomendaciones (cuando el modo VPA es Auto o Recreate).
  • Admission Controller: Muta los specs de pods en el momento de admisión para aplicar las recomendaciones del Recommender.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-server-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-server
  updatePolicy:
    updateMode: "Off"   # Solo recomendar — no evictar pods
  resourcePolicy:
    containerPolicies:
    - containerName: api-server
      minAllowed:
        cpu: 100m
        memory: 128Mi
      maxAllowed:
        cpu: 4000m
        memory: 4Gi
      controlledResources: ["cpu", "memory"]
      controlledValues: RequestsAndLimits

Modos del VPA

ModoComportamiento
OffCalcula solo recomendaciones. Sin mutaciones de pods.
InitialAplica recomendaciones solo a pods nuevos. No evicta pods en ejecución.
RecreateEvicta pods cuando las recomendaciones cambian significativamente.
AutoActualmente equivalente a Recreate. Puede cambiar en versiones futuras.

Cuándo Usar el VPA

Right-sizing durante el despliegue inicial: Ejecuta VPA en modo Off durante 1–2 semanas en un servicio nuevo. Revisa las recomendaciones antes de aplicarlas. Este es el caso de uso más valioso.

Servicios con patrones de carga impredecibles o estacionales: El VPA adapta los requests según el comportamiento observado. Combinado con HPA para el escalado horizontal, obtienes réplicas bien dimensionadas que escalan hacia afuera horizontalmente.

El VPA y el HPA no pueden gestionar la misma métrica simultáneamente. Si el HPA está escalando por utilización de CPU, no uses VPA con controlledValues: RequestsAndLimits para CPU — entrarán en conflicto. Usa controlledValues: RequestsOnly y deja que el HPA gestione el escalado.

Limitaciones del VPA:
– Requiere reinicios de pods para aplicar recomendaciones (el Updater evicta pods)
– No funciona bien con cargas de trabajo con estado en ventanas de disponibilidad estrictas
– El Recommender necesita historial suficiente (al menos unos días) para producir recomendaciones fiables
– No tiene en cuenta picos de tráfico que todavía no se han observado


LimitRange y ResourceQuota: Salvaguardas a Nivel de Namespace

Los requests y limits en pods individuales resuelven el problema por carga de trabajo. LimitRange y ResourceQuota resuelven el problema de gobernanza a nivel de namespace y clúster.

LimitRange

LimitRange establece requests y limits por defecto para los contenedores de un namespace, y aplica límites mínimos y máximos. Cualquier pod admitido en el namespace que no tenga requests/limits explícitos recibirá los valores por defecto.

apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: production
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    max:
      cpu: "4000m"
      memory: "8Gi"
    min:
      cpu: "50m"
      memory: "64Mi"
  - type: Pod
    max:
      cpu: "8000m"
      memory: "16Gi"
  - type: PersistentVolumeClaim
    max:
      storage: "50Gi"
    min:
      storage: "1Gi"

Comportamientos clave:
default se aplica como limit para contenedores que establecen un request pero no un limit
defaultRequest se aplica como request para contenedores que no establecen ningún request
max y min causan que la admisión falle si se violan
– LimitRange se aplica en el momento de la admisión — cambiarla no afecta a los pods en ejecución

Usa LimitRange para:
– Prevenir que se admitan pods BestEffort (estableciendo valores de defaultRequest)
– Aplicar estándares organizativos para las especificaciones mínimas de recursos
– Proteger el clúster de pods que solicitan recursos sin límite

ResourceQuota

ResourceQuota limita la cantidad total de recursos que pueden consumir todos los pods de un namespace. Esta es la herramienta de gobernanza multi-tenant.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: production-quota
  namespace: production
spec:
  hard:
    requests.cpu: "20"
    requests.memory: "40Gi"
    limits.cpu: "40"
    limits.memory: "80Gi"
    pods: "100"
    persistentvolumeclaims: "20"
    requests.storage: "500Gi"
    count/deployments.apps: "50"
    count/services: "50"
    count/secrets: "100"
    count/configmaps: "100"

Interacción crítica con LimitRange: Cuando ResourceQuota está activo en un namespace, todos los pods deben tener requests y limits establecidos o serán rechazados. Por eso los valores por defecto de LimitRange son importantes — aseguran que los pods sin recursos explícitos no sean rechazados por el sistema de quotas.

Usa ResourceQuota para:
– Aplicar presupuestos de recursos por equipo/aplicación en clústeres compartidos
– Prevenir que despliegues desbocados consuman toda la capacidad del clúster
– Implementar políticas de chargeback (seguimiento del consumo de recursos por namespace)


Metodología Práctica de Sizing

Paso 1: Instrumenta Antes de Establecer Valores

Despliega inicialmente con solo requests establecidos (sin CPU limits, memory limits establecidos conservadoramente altos) y monitoriza durante 2–4 semanas bajo carga realista.

Consultas PromQL útiles para el sizing:

# Uso de CPU p95 en los últimos 7 días
histogram_quantile(0.95,
  rate(container_cpu_usage_seconds_total{
    container="api-server",
    namespace="production"
  }[5m])
)

# Memory working set p99 en los últimos 7 días
quantile_over_time(0.99,
  container_memory_working_set_bytes{
    container="api-server",
    namespace="production"
  }[7d]
)

# Ratio de CPU throttling (alertar si >5%)
rate(container_cpu_cfs_throttled_seconds_total{container="api-server"}[5m])
/
rate(container_cpu_cfs_periods_total{container="api-server"}[5m])

Paso 2: Establece los CPU Requests desde Observaciones del p95

Establece el CPU request = uso de CPU p95 bajo carga de producción realista. Para servicios sensibles a la latencia: no establezcas CPU limits. Para jobs batch o en background: establece CPU limits en 2x–4x el request.

Paso 3: Establece Memory Requests y Limits

Establece el memory request = memory working set p95 durante al menos 7 días. Establece el memory limit = max(pico observado, 1,5 × request). Para Java/Python con procesamiento intensivo, usa 2x.

# Ejemplo de producción: microservicio Java
resources:
  requests:
    cpu: "500m"       # p95 observado: ~420m
    memory: "768Mi"   # p95 observado: ~680Mi
  limits:
    # Sin CPU limit — servicio sensible a la latencia
    memory: "1.5Gi"   # 2x request, cubre presión de GC

Paso 4: Usa las Recomendaciones del VPA para Validar

Ejecuta VPA en modo Off junto a tus valores establecidos manualmente. Después de 1–2 semanas, compara las recomendaciones del VPA con tu configuración actual.

Paso 5: Ajusta para Eventos del Ciclo de Vida de la Carga de Trabajo

Ten en cuenta: el calentamiento de la JVM al arrancar (pico de CPU de 3–10x el estado estable), la superposición durante despliegues rolling (margen en la quota del namespace) y los picos de tráfico conocidos (dimensiona para el pico, no para la media).


Configurar Recursos en Kubernetes: Sizing por Tipo de Carga de Trabajo

Una de las preguntas más frecuentes al configurar recursos en Kubernetes para producción es qué valores poner según el tipo de servicio. No existe una respuesta única. El perfil de CPU y memoria de un microservicio Go es fundamentalmente distinto al de un job de procesamiento batch en Python.

La siguiente tabla recoge las recomendaciones según el tipo de carga de trabajo más habitual en entornos productivos:

Tipo de Carga de TrabajoCPU RequestCPU LimitMemory RequestMemory LimitQoS Objetivo
API sensible a latencia (Go, Java, Node)p95 observadoNo establecerp95 observado1,5–2x requestBurstable
Jobs batch / backgroundp50 observado2–4x requestp95 observado1,5x requestBurstable
Sistema crítico (coredns, metrics-server)ConservadorIgual al requestConservadorIgual al requestGuaranteed
Stateful / bases de datos (in-cluster)p95 observadoNo establecerp99 observado1,25x requestBurstable
Cargas de trabajo dev/testBajo (100m)2x requestBajo (128Mi)2x requestBurstable
Contenedores sidecar (envoy, otel-collector)Perfilar individualmenteContextualPerfilar individualmente1,5x requestIgual al principal

La columna CPU Limit merece una explicación adicional. Para APIs y servicios stateful, la recomendación de no establecer CPU limit no es un descuido — es una decisión deliberada fundamentada en el comportamiento del CFS throttling descrito anteriormente. En clústeres multi-tenant con requisitos de aislamiento estrictos, esta recomendación debe evaluarse caso a caso.


QoS Kubernetes en Profundidad: Interacción con el kubelet

El comportamiento del QoS no se limita a la evicción. Tiene implicaciones directas sobre cómo el kubelet gestiona la presión de memoria en el nodo y cómo interacciona con los umbrales de evicción configurados en la kubelet config.

Umbrales de Evicción y QoS

El kubelet tiene dos tipos de umbral de evicción:

  • Soft eviction: El nodo ha estado por encima de un umbral durante un período (eviction-soft-grace-period). El kubelet puede evictar pods Burstable y BestEffort.
  • Hard eviction: El nodo supera un umbral crítico de forma inmediata. El kubelet evicta pods sin período de gracia.

Dentro de cada clase QoS, el kubelet prioriza la evicción según cuánto ha excedido el pod su request. Un pod Burstable que usa el 180% de su memory request será eviccionado antes que uno que usa el 110%.

OOM Score Adjustment

El kernel de Linux asigna a cada proceso un oom_score_adj. El kubelet manipula este valor según la clase QoS:

  • Guaranteed: -998 (muy bajo, el OOM killer los evita casi siempre)
  • Burstable: valor proporcional al exceso sobre el request
  • BestEffort: 1000 (el OOM killer los selecciona primero)

Este mecanismo funciona independientemente de los memory limits — es el comportamiento del kernel Linux actuando sobre los procesos del contenedor directamente.


Monitorización y Alertas para Recursos en Kubernetes

Configurar recursos en Kubernetes para producción no termina en el despliegue inicial. Necesitas visibilidad continua sobre el comportamiento real de los recursos.

# OOMKill rate
- alert: ContainerOOMKilled
  expr: increase(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}[5m]) > 0
  for: 0m
  labels:
    severity: warning

# CPU throttling >10%
- alert: CPUThrottlingHigh
  expr: |
    rate(container_cpu_cfs_throttled_seconds_total[5m])
    /
    rate(container_cpu_cfs_periods_total[5m])
    > 0.10
  for: 5m
  labels:
    severity: warning

# Memoria cerca del limit >85%
- alert: MemoryNearLimit
  expr: |
    container_memory_working_set_bytes
    /
    (container_spec_memory_limit_bytes > 0)
    > 0.85
  for: 5m
  labels:
    severity: warning

Además de estas alertas, es recomendable incluir en tus dashboards de Prometheus/Grafana:

  • Ratio de utilización de requests: container_cpu_usage_seconds_total / kube_pod_container_resource_requests{resource="cpu"} — si está consistentemente por encima de 0,8, el request está subdimensionado.
  • Headroom de memoria por nodo: cuánta memoria schedulable queda en cada nodo según los requests comprometidos, independientemente de la utilización real.
  • Distribución QoS del clúster: cuántos pods son BestEffort, Burstable y Guaranteed. Un porcentaje alto de BestEffort en namespaces de producción es una señal de alerta.

Preguntas Frecuentes sobre Requests y Limits en Kubernetes

Mi aplicación Java sigue muriendo con OOMKill aunque he establecido el limit al doble del uso medio. ¿Qué me estoy perdiendo?

El heap de la JVM (-Xmx) no es el único consumidor de memoria. Los buffers off-heap, el Metaspace, los stacks de threads y el overhead de la JVM añaden entre un 25% y un 40% adicional. Establece -Xmx al ~75% de tu memory limit del contenedor. Para un limit de 1Gi: -Xmx768m es un punto de partida seguro.

¿Debo establecer los mismos recursos en todos los entornos?

No. Dev/test puede usar valores más bajos. Pero el ratio entre request y limit debería ser similar, y el perfil de recursos debería ser lo suficientemente parecido como para detectar malas configuraciones antes de llegar a producción.

¿Puedo usar HPA y VPA juntos?

Sí, con cuidado. Usa HPA para el escalado de réplicas (CPU o métricas personalizadas) y VPA en modo Off o con controlledValues: RequestsOnly como guía de right-sizing. Nunca tengas a ambos gestionando la misma métrica simultáneamente.

Mi clúster usa cgroups v2. ¿El CPU throttling sigue siendo un problema?

Mejorado, pero no eliminado. cgroups v2 usa un scheduler basado en pesos que reduce los artefactos de throttling. Sin embargo, la aplicación de cpu.cfs_quota_us sigue existiendo cuando se establecen CPU limits. Para cargas de trabajo sensibles a la latencia, el argumento de no establecer CPU limits sigue siendo válido en cgroups v2.

¿Cuál es un ratio de sobrecompromiso (overcommit) realista para el clúster?

CPU: un overcommit de 5–10x (total de requests vs cores físicos) es habitual para cargas de trabajo mixtas con requests precisos. Memoria: un overcommit de 1,5–2x a nivel de clúster es manejable con ratios de request:limit de 1,5x. Por encima de 2x, los eventos de presión de memoria en los nodos se vuelven frecuentes.

LimitRange está configurado pero los pods siguen siendo admitidos sin recursos. ¿Por qué?

Los valores por defecto de LimitRange solo se aplican a contenedores sin ninguna especificación de recursos en absoluto. Si un contenedor especifica requests.cpu pero no limits.cpu, el default de LimitRange para CPU no rellena el limit que falta. Verifica también que el LimitRange esté en el namespace correcto: kubectl get limitrange -n <namespace>.

¿Qué le hace a un nodo un pod sin memory limit?

Puede consumir toda la memoria disponible del nodo sin restricciones. Esto activa el OOM killer de Linux a nivel de nodo, que puede matar procesos fuera del contenedor — incluyendo el propio kubelet en casos extremos. Los memory limits no son negociables en producción.

¿Cómo detectar CPU throttling en producción si no tengo alertas configuradas?

El primer indicador es una latencia p99 inusualmente alta en servicios que aparentemente tienen baja utilización media de CPU. Para confirmar, consulta directamente: kubectl exec -it <pod> -- cat /sys/fs/cgroup/cpu/cpu.stat (cgroups v1) o kubectl exec -it <pod> -- cat /sys/fs/cgroup/cpu.stat (cgroups v2). La métrica throttled_time muestra el tiempo acumulado de throttling.


Checklist de Configuración de Recursos para Producción

Antes de poner un servicio en producción, verifica:

  • [ ] Todos los contenedores tienen requests.memory y limits.memory definidos
  • [ ] El ratio limits.memory / requests.memory está entre 1,5x y 2x
  • [ ] Los requests.cpu están basados en datos de utilización real (p95), no en estimaciones
  • [ ] Se ha tomado una decisión explícita sobre limits.cpu (no establecido por defecto para APIs)
  • [ ] El namespace tiene LimitRange configurado con valores defaultRequest razonables
  • [ ] El namespace tiene ResourceQuota si es un entorno compartido
  • [ ] Hay alertas activas para OOMKill, CPU throttling y memoria cerca del limit
  • [ ] El VPA está configurado en modo Off para observar recomendaciones durante las primeras semanas
  • [ ] La clase QoS resultante es la esperada (kubectl get pod <name> -o yaml | grep qosClass)

Validado contra Kubernetes 1.28–1.32. El comportamiento de cgroups v2 se indica donde difiere de v1. Los ejemplos de VPA usan la API autoscaling.k8s.io/v1 (VPA 0.14+).