Día 50 - Resource Requests y Limits en Pods de Kubernetes
Problema / Desafío
El equipo de Nautilus está viendo problemas de performance en algunas apps por contención de recursos. La consigna es crear un pod con límites de recursos definidos:
- Nombre del pod:
httpd-pod - Nombre del contenedor:
httpd-container - Imagen:
httpd:latest - Requests: memoria
15Mi, CPU100m - Limits: memoria
20Mi, CPU100m
Conceptos clave
requests vs limits: dos números, dos consumidores
Cada contenedor en Kubernetes puede declarar dos valores por recurso (CPU y memoria):
| Campo | Quién lo usa | Qué significa |
|---|---|---|
requests |
El scheduler | Garantía mínima. Solo nodos con al menos esa cantidad disponible pueden recibir este pod |
limits |
El kubelet / kernel | Techo enforced en tiempo de ejecución. Si el contenedor lo intenta cruzar, hay consecuencias |
requestses planeación: define el "espacio reservado" del pod en el nodolimitses policing: define qué pasa si el contenedor se pasa de la raya
Sin declarar requests, el scheduler asume 0 y puede meter el pod en un nodo saturado. Sin declarar limits, el contenedor puede crecer sin freno y desestabilizar al vecino.
Qué pasa cuando se excede el límite (no es lo mismo CPU que memoria)
| Recurso | Comportamiento al cruzar el limit |
|---|---|
| CPU | Throttling. El proceso sigue vivo, solo se le da menos tiempo de CPU. Latencia ↑, no muere |
| Memoria | OOMKilled. El kernel mata el proceso (señal SIGKILL). El contenedor reinicia (si la policy lo permite) |
Esta asimetría tiene una razón física: la CPU es compresible (se pueden dar menos ciclos), pero la memoria es incompresible (no se puede "dar menos RAM" — o cabe o no cabe). Cuando un proceso pide más RAM y no hay, el único recurso es matarlo.
Unidades de CPU: cores y millicores
Kubernetes mide CPU en cores o millicores (un milésimo de core):
1o1000m= un core completo (un hyperthread / vCPU en un nodo cloud)500m= medio core (50% del tiempo de CPU de un core)100m= 0.1 core (10% del tiempo de un core)10m= 0.01 core — útil para sidecars o procesos casi idle
El m significa "milli". Como cualquier CPU moderna tiene varios cores, se puede perfectamente pedir 2, 4, etc. (un solo proceso multi-thread puede consumir varios cores).
Unidades de memoria: la trampa de Mi vs M
Esto atrapa a casi todo el mundo la primera vez. Kubernetes acepta dos sistemas:
| Sufijo | Sistema | Valor |
|---|---|---|
K, M, G, T |
Decimal (SI) | 1M = 1 × 1000² = 1,000,000 bytes |
Ki, Mi, Gi, Ti |
Binario (IEC) | 1Mi = 1 × 1024² = 1,048,576 bytes |
Diferencia práctica: 15Mi ≈ 15.73 MB, mientras que 15M = 15 MB exactos. Una diferencia de ~5%. En memoria ajustada, eso es lo que decide si tu pod sobrevive o se va a OOMKilled.
Convención en el ecosistema: usar siempre las binarias (
Ki,Mi,Gi). Coinciden con lo que reportanfree -h,top,kubectl top, etc.
QoS Classes: el sistema oculto de prioridades
Cuando un nodo se queda sin memoria, el kubelet tiene que evictar pods. ¿A cuál mata primero? Decide según la QoS Class que Kubernetes le asigna automáticamente al pod, derivada de cómo escribiste requests y limits:
Glosario — "evictar" / "evicted": Spanglish del inglés evict (desalojar). En Kubernetes describe algo muy específico: el sistema decide matar el pod porque el nodo está bajo presión de recursos (memoria, disco, PIDs) o porque alguien lo drena con
kubectl drainpara mantenimiento. No es lo mismo que un pod que se cae por su cuenta:
Situación Causa Resultado Pod se cae solo ( CrashLoopBackOff)El proceso adentro sale con error (segfault, exit ≠ 0) Mismo pod reinicia en el mismo nodo, Restart Count↑Pod OOMKilledEl contenedor cruzó su propio limits.memoryMismo pod reinicia en el mismo nodo, Restart Count↑Pod EvictedEl nodo entero está bajo presión, o fue drenado Pod queda en Failed/Evicted, no se reinicia en ese nodo — un controller (Deployment, RS) crea uno nuevo en otro ladoDistinción importante porque el debugging es distinto: si tu pod aparece
Evicted, el problema no es de tu código sino del nodo. Hay que mirarkubectl describe node <nodo>para ver eventos de presión (MemoryPressure,DiskPressure).
| QoS Class | Cómo se obtiene | Prioridad de eviction |
|---|---|---|
| Guaranteed | requests == limits para todos los recursos (CPU y memoria) |
Último en ser evictado (más seguro) |
| Burstable | Tiene requests definidos pero requests != limits en al menos un recurso |
Eviction intermedio |
| BestEffort | No declaró ningún requests ni limits |
Primero en ser evictado |
Esto es importante para entender el pod de este día: aunque CPU es 100m == 100m, la memoria es 15Mi != 20Mi. Eso es suficiente para que la QoS Class quede como Burstable, no Guaranteed. Para subirla a Guaranteed habría que igualar también memoria (requests.memory == limits.memory).
Workloads críticos en producción suelen apuntar a
Guaranteed: garantiza que el pod no será evictado bajo presión de memoria salvo en el peor escenario. Para batch jobs o cosas que pueden re-correr,Burstable(o inclusoBestEffort) ahorra recursos.
Pasos
- Escribir el manifest
pod.ymlcon la secciónresources - Aplicar con
kubectl apply -f - Inspeccionar con
kubectl describepara confirmar requests, limits y QoS Class - (Opcional) Ver consumo real con
kubectl top pod
Comandos / Código
Solución utilizada
apiVersion: v1
kind: Pod
metadata:
name: httpd-pod
spec:
containers:
- name: httpd-container
image: httpd:latest
resources:
requests:
memory: "15Mi"
cpu: "100m"
limits:
memory: "20Mi"
cpu: "100m"
Aplicar:
Verificar la asignación de recursos y el QoS Class
Después de aplicar, hay que confirmar tres cosas en el describe:
- Los
Requests:están en15Mi / 100m - Los
Limits:están en20Mi / 100m - La
QoS Class:esBurstable(noGuaranteed)
Name: httpd-pod
Namespace: default
Node: jump-host/10.244.244.165
Status: Running
IP: 10.22.0.9
Containers:
httpd-container:
Image: httpd:latest
State: Running
Ready: True
Restart Count: 0
Limits:
cpu: 100m
memory: 20Mi
Requests:
cpu: 100m
memory: 15Mi
QoS Class: Burstable
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 29s default-scheduler Successfully assigned default/httpd-pod to jump-host
Normal Pulled 26s kubelet Successfully pulled image "httpd:latest" in 3.197s
Normal Started 25s kubelet Started container httpd-container
Por qué quedó Burstable y no Guaranteed: la regla de Guaranteed exige requests == limits para todos los recursos. Acá CPU coincide (100m == 100m), pero memoria no (15Mi != 20Mi), así que basta un solo recurso desalineado para que Kubernetes degrade la QoS Class al nivel intermedio.
Para extraer solo el campo en un script o pipeline:
-o jsonpath lee directo del API server y devuelve el string sin parseo de texto. Más estable que hacer kubectl describe | grep "QoS Class" porque no depende del formato visual de describe, que puede cambiar entre versiones de kubectl.
Ver el consumo real (runtime)
Si el cluster tiene metrics-server instalado (lo tiene en el lab):
kubectl top muestra uso real medido en runtime. Útil para calibrar requests y limits: si tu pod usa 8Mi sostenidos, pedir 15Mi de request es razonable; si jamás pasa de 200m de CPU pico, pedir 1 core es desperdicio.
Distinción crítica:
describemuestra lo declarado en el manifest.topmuestra lo consumido en tiempo real. Para diagnosticar OOMKilled vs limits mal calibrados, hay que mirar ambos.
Troubleshooting
| Problema | Causa y solución |
|---|---|
Pod en OOMKilled poco después de arrancar |
El limits.memory es menor de lo que el proceso necesita ni siquiera para inicializar. Subir el límite o investigar memory footprint real |
Pod en estado Pending con event 0/N nodes are available: insufficient cpu |
Los requests exceden lo libre en cualquier nodo. Bajar requests o agregar capacidad al cluster |
| App responde lento pero el pod no muere | CPU throttling: el contenedor está pegado al limits.cpu. kubectl top pod mostrará uso clavado en el techo. Subir el límite de CPU |
kubectl top pod da error: Metrics API not available |
El cluster no tiene metrics-server instalado. Es un addon, no viene por default en kubeadm |
QoS Class salió Burstable y querías Guaranteed |
Revisar que todos los recursos tengan requests == limits (no solo uno). Bastante común: olvidar igualar memoria |
Puse memory: 1G y el contenedor se queja de menos memoria que esperaba |
1G (decimal) = 1,000,000,000 bytes. Si esperabas 1 GiB usar 1Gi (= 1,073,741,824 bytes, ~7% más) |