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 kubectl configurado con permisos de administrador.
  • metrics-server instalado, necesario para kubectl top. K3s lo incluye; en MicroK8s se activa con microk8s 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:

CampoPara qué sirveQué pasa si se supera
requestsEl 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 CPUTope de CPU que puede consumir el contenedor.El kernel lo ralentiza (throttling); no se reinicia.
limits de memoriaTope de memoria del contenedor.El kernel mata el proceso (OOMKilled) y el kubelet lo reinicia.

Las unidades son:

  • CPU: en núcleos. 1 es un núcleo (o una vCPU) y 250m son 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 sufijo m en memoria: 512m significa 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:

ClaseCondición
GuaranteedTodos los contenedores tienen requests y limits de CPU y memoria, y requests igual a limits.
BurstableAl menos un contenedor tiene request o limit de CPU o memoria, pero no cumple lo anterior.
BestEffortNingú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 BestEffort son los primeros en salir y los Guaranteed que 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 Guaranteed son 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

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.