Kubernetes decide en qué nodo colocar cada pod según los recursos que solicita, y limita lo que puede consumir según los límites que le pongas. Unas requests mal calculadas provocan nodos saturados, pods en Pending o contenedores que mueren por falta de memoria. En este tutorial configurarás requests y limits en un Deployment, comprobarás su clase de calidad de servicio (QoS), fijarás valores por defecto y cuotas por namespace, y provocarás y diagnosticarás un OOMKilled para ver cómo se comporta el clúster.
Requisitos previos
- Un clúster de Kubernetes en funcionamiento (por ejemplo K3s o MicroK8s sobre un VPS de CubePath con Ubuntu 24.04) y
kubectlconfigurado con permisos de administrador. - metrics-server instalado, necesario para
kubectl top. K3s lo incluye; en MicroK8s se activa conmicrok8s enable metrics-server; en otros clústeres se instala con:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
Comprueba que responde antes de continuar:
kubectl top nodes
Paso 1: Entender requests, limits y sus unidades
Cada contenedor puede declarar dos valores por recurso:
| Campo | Para qué sirve | Qué pasa si se supera |
|---|---|---|
requests | El planificador reserva esa cantidad en el nodo al colocar el pod. | Nada: el contenedor puede usar más si el nodo tiene recursos libres. |
limits de CPU | Tope de CPU que puede consumir el contenedor. | El kernel lo ralentiza (throttling); no se reinicia. |
limits de memoria | Tope de memoria del contenedor. | El kernel mata el proceso (OOMKilled) y el kubelet lo reinicia. |
Las unidades son:
- CPU: en núcleos.
1es un núcleo (o una vCPU) y250mson 250 milinúcleos, es decir, un cuarto de núcleo. - Memoria: en bytes, con sufijos binarios
Ki,Mi,Gi(1 Gi = 1024 Mi). Evita el sufijomen memoria:512msignifica 0,512 bytes, no 512 MiB.
Para ver cuánto se ha reservado ya en un nodo, revisa la sección Allocated resources de su descripción:
kubectl describe node your_node_name | grep -A 8 "Allocated resources"
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 850m (42%) 1500m (75%)
memory 1180Mi (30%) 2410Mi (62%)
Un pod solo se programa en un nodo si la suma de sus requests cabe en lo que queda libre de la capacidad asignable del nodo, independientemente del uso real.
Paso 2: Definir requests y limits en un Deployment
Crea un namespace de pruebas para trabajar aislado:
kubectl create namespace recursos-demo
Crea el manifiesto de un Deployment con requests y limits:
nano web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: recursos-demo
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:stable
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
Aplícalo y comprueba que los pods arrancan:
kubectl apply -f web.yaml
kubectl -n recursos-demo get pods
NAME READY STATUS RESTARTS AGE
web-6d8f7c9b8d-4kx2p 1/1 Running 0 12s
web-6d8f7c9b8d-q7hzl 1/1 Running 0 12s
Paso 3: Comprobar la clase QoS de los pods
Kubernetes asigna a cada pod una clase de calidad de servicio según sus requests y limits:
| Clase | Condición |
|---|---|
Guaranteed | Todos los contenedores tienen requests y limits de CPU y memoria, y requests igual a limits. |
Burstable | Al menos un contenedor tiene request o limit de CPU o memoria, pero no cumple lo anterior. |
BestEffort | Ningún contenedor define requests ni limits. |
Consulta la clase de los pods del namespace:
kubectl -n recursos-demo get pods -o custom-columns=NOMBRE:.metadata.name,QOS:.status.qosClass
NOMBRE QOS
web-6d8f7c9b8d-4kx2p Burstable
web-6d8f7c9b8d-q7hzl Burstable
Son Burstable porque las requests son menores que los limits. La clase influye cuando un nodo se queda sin memoria:
- Si el nodo entra en presión de memoria, el kubelet desaloja (evicta) pods. Primero elige los que usan más memoria de la que solicitaron, después tiene en cuenta la prioridad del pod y finalmente cuánto superan su request. En la práctica, los pods
BestEffortson los primeros en salir y losGuaranteedque respetan su request, los últimos. - Si el kernel tiene que matar procesos por falta de memoria, ajusta su preferencia según la clase QoS, de modo que los contenedores
Guaranteedson los menos propensos a morir.
Usa Guaranteed para cargas críticas con consumo predecible, como bases de datos, y Burstable para la mayoría de aplicaciones web. Evita BestEffort en producción.
Paso 4: Medir el consumo real para dimensionar
Las requests deben reflejar el consumo habitual, no el máximo. Consulta el uso actual por contenedor:
kubectl -n recursos-demo top pods --containers
POD NAME CPU(cores) MEMORY(bytes)
web-6d8f7c9b8d-4kx2p nginx 0m 3Mi
web-6d8f7c9b8d-q7hzl nginx 0m 3Mi
kubectl top solo muestra el instante actual. Para dimensionar bien, observa el consumo durante al menos una semana con carga real (Prometheus, Grafana o el sistema de métricas que uses) y aplica un criterio sencillo:
- Request de CPU: alrededor del consumo habitual (percentil 50 a 75).
- Request de memoria: cerca del consumo máximo sostenido, porque la memoria no se puede recuperar sin matar el proceso.
- Limit de memoria: con margen sobre el pico observado (por ejemplo, un 25 % a un 50 % más).
- Limit de CPU: opcional. Un limit bajo provoca throttling y latencias altas aunque el nodo tenga CPU libre. Muchos equipos fijan solo la request de CPU y dejan el limit sin definir o con bastante margen.
Para cambiar los valores de un Deployment sin editar el YAML, usa kubectl set resources. Kubernetes recreará los pods con los nuevos valores:
kubectl -n recursos-demo set resources deployment web -c nginx --requests=cpu=50m,memory=64Mi --limits=memory=128Mi
Paso 5: Fijar valores por defecto con LimitRange
Un LimitRange aplica valores por defecto a los contenedores de un namespace que no los declaran, y rechaza los que se salen de un mínimo o máximo. Así evitas que alguien despliegue un pod BestEffort por olvido:
nano limitrange.yaml
apiVersion: v1
kind: LimitRange
metadata:
name: valores-por-defecto
namespace: recursos-demo
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
cpu: 500m
memory: 256Mi
min:
cpu: 10m
memory: 16Mi
max:
cpu: "2"
memory: 2Gi
defaultRequest se usa como request cuando el contenedor no la declara y default como limit. Aplícalo y crea un pod sin recursos para comprobar el efecto:
kubectl apply -f limitrange.yaml
kubectl -n recursos-demo run sin-recursos --image=nginx:stable
kubectl -n recursos-demo get pod sin-recursos -o jsonpath='{.spec.containers[0].resources}{"\n"}'
{"limits":{"cpu":"500m","memory":"256Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}
Si intentas crear un contenedor con un limit de memoria superior a 2Gi, la API lo rechaza con un error Forbidden que menciona el LimitRange. Los LimitRange solo actúan al crear pods; no modifican los que ya existen.
Paso 6: Limitar el total del namespace con ResourceQuota
Una ResourceQuota fija el consumo total que puede reservar un namespace, útil cuando varios equipos o proyectos comparten el clúster:
nano quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-recursos
namespace: recursos-demo
spec:
hard:
requests.cpu: "2"
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "20"
persistentvolumeclaims: "5"
kubectl apply -f quota.yaml
kubectl -n recursos-demo describe resourcequota cuota-recursos
Name: cuota-recursos
Namespace: recursos-demo
Resource Used Hard
-------- ---- ----
limits.cpu 1500m 4
limits.memory 512Mi 8Gi
persistentvolumeclaims 0 5
pods 3 20
requests.cpu 200m 2
requests.memory 256Mi 4Gi
Importantecuando una cuota limita
requestsolimitsde un recurso, todos los pods nuevos del namespace deben declarar ese valor. Sin un LimitRange que aporte valores por defecto, los pods sin recursos se rechazan con el errormust specify limits.cpuo similar.
Si un Deployment supera la cuota, sus pods no llegan a crearse. El motivo aparece en los eventos del ReplicaSet, no en el pod:
kubectl -n recursos-demo get events --field-selector reason=FailedCreate
Paso 7: Provocar y diagnosticar un OOMKilled
Para ver qué ocurre cuando un contenedor supera su límite de memoria, crea un pod que intenta reservar 250 MiB con un límite de 100 MiB. La imagen polinux/stress es la que usa la documentación oficial de Kubernetes para este ejemplo:
nano oom.yaml
apiVersion: v1
kind: Pod
metadata:
name: prueba-oom
namespace: recursos-demo
spec:
containers:
- name: stress
image: polinux/stress
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"]
resources:
requests:
memory: 50Mi
limits:
memory: 100Mi
kubectl apply -f oom.yaml
kubectl -n recursos-demo get pod prueba-oom -w
NAME READY STATUS RESTARTS AGE
prueba-oom 0/1 ContainerCreating 0 2s
prueba-oom 0/1 OOMKilled 0 5s
prueba-oom 0/1 OOMKilled 1 (3s ago) 9s
prueba-oom 0/1 CrashLoopBackOff 1 (12s ago) 20s
Pulsa Ctrl+C para salir. Confirma el motivo de la última terminación:
kubectl -n recursos-demo describe pod prueba-oom | grep -A 4 "Last State"
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
El código de salida 137 indica que el proceso recibió SIGKILL. Cuando lo veas en una aplicación real, las soluciones son, por orden: comprobar si hay una fuga de memoria, ajustar la configuración de memoria de la aplicación (por ejemplo, el tamaño del heap de la JVM o los workers de un servidor web) y, si el consumo es legítimo, subir el limit y la request de memoria.
Un pod en Pending es el problema contrario: pide más de lo que cabe. kubectl describe pod lo explica en los eventos con un mensaje como 0/3 nodes are available: 3 Insufficient memory. En ese caso reduce la request o añade capacidad al clúster.
Paso 8: Limpiar el entorno de pruebas
Borra el namespace, que elimina también el Deployment, los pods, el LimitRange y la cuota:
kubectl delete namespace recursos-demo
Conclusión
Has configurado requests y limits, comprobado cómo determinan la clase QoS, fijado valores por defecto con LimitRange, acotado un namespace con ResourceQuota y diagnosticado un contenedor OOMKilled. Como siguientes pasos, instala Prometheus y Grafana para observar el consumo histórico, prueba el Vertical Pod Autoscaler en modo de solo recomendación para obtener sugerencias de requests, o configura un HorizontalPodAutoscaler, que calcula el uso de CPU en porcentaje sobre la request declarada.
