Llevamos desde el módulo 2 escribiendo bloques resources en todos los manifiestos de Rutas Norte —cpu: 250m, memory: 256Mi— sin haber explicado nunca qué significan exactamente ni de dónde salen esos números. La lección anterior incluso los expuso al contenedor con resourceFieldRef para que api-reservas se dimensionara a sí mismo. Ha llegado el momento de tomárselos en serio, porque son la pieza que decide en qué nodo cabe cada pod, cuánta CPU recibe cuando hay competencia y qué proceso muere cuando falta memoria. Y arrastramos además la última deuda del módulo 2: ningún namespace tiene cuota, así que hoy mismo un despliegue equivocado en rutas-norte-dev puede comerse la capacidad del clúster y dejar sin sitio a los pods de producción. En esta lección entenderás el modelo de recursos completo y pondrás una ResourceQuota a cada uno de los tres entornos.
Contenido
- El modelo de recursos:
requestsylimits - Unidades: milicores,
Mifrente aM - Quién usa las
requests: el planificador - Quién usa los
limits: el kubelet y los cgroups - CPU comprimible frente a memoria incomprimible
- Diagnosticar el estrangulamiento de CPU
- Diagnosticar un
OOMKilled ephemeral-storagey el desalojo por disco- Sobrecompromiso del clúster
- ResourceQuota: qué limita y cómo se lee
- El efecto que obliga a declarar recursos
- Las cuotas de los tres entornos de Rutas Norte
- Cómo elegir valores razonables
- El modelo de recursos:
requests y limits
requests y limitsCada contenedor de un pod puede declarar dos números por cada tipo de recurso:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0
resources:
requests: # lo que el contenedor NECESITA GARANTIZADO
cpu: 250m
memory: 256Mi
limits: # el TECHO que no puede superar
cpu: "1"
memory: 512MiLa diferencia conceptual, que es la base de todo:
requests |
limits |
|
|---|---|---|
| Significado | "Reserva esto para mí" | "No me dejes pasar de aquí" |
| Quién lo usa | El planificador, al elegir nodo | El kubelet, en tiempo de ejecución |
| Cuándo actúa | Una vez, al crear el pod | Continuamente, mientras el pod vive |
| Si el nodo no tiene | El pod queda en Pending |
— |
| Si se supera | No se puede superar: está garantizado | CPU: se estrangula. Memoria: OOMKilled |
| Afecta a la factura del clúster | Sí: es capacidad comprometida | Indirectamente |
| Si se omite | Se asume 0 (peligroso) | Se asume ilimitado (peligroso) |
Una analogía útil, y que encaja con Rutas Norte: piensa en un autobús de la flota. La request es el asiento reservado: está pagado y nadie más puede ocuparlo, lo uses o no. El limit es el equipaje máximo permitido: puedes llevar menos, pero si intentas pasar del máximo te lo impiden en la puerta.
Y las dos consecuencias que hay que fijar desde el principio:
requestses lo que "cuesta" en el clúster. Un pod conrequests: 4Gibloquea 4 GiB de capacidad del nodo aunque su proceso use 100 MiB. El planificador considera ese nodo con 4 GiB menos disponibles para todo lo demás.limitsno reserva nada. Un pod conlimits: 8Gino reserva 8 GiB; simplemente no podrá pasar de ahí si intenta usarlos.
Puedes declarar solo requests, solo limits, ambos o ninguno:
| Declaración | Efecto |
|---|---|
Ambos, con requests < limits |
Lo habitual: garantía mínima con capacidad de ráfaga |
| Ambos, iguales | Máxima previsibilidad y máxima prioridad (lo verás en 03-05) |
Solo limits |
Kubernetes copia el limit en la request. Cuidado: reservas más de lo que crees |
Solo requests |
Sin techo: el contenedor puede consumir todo el nodo |
| Ninguno | El peor caso: sin garantía y sin techo |
- Unidades: milicores,
Mi frente a M
Mi frente a MCPU
La unidad de CPU es el core (más exactamente, un hilo de ejecución: un vCPU en la nube, un hiperhilo en metal). Se puede expresar de dos formas:
| Escritura | Significado |
|---|---|
1 o 1000m |
Un core completo |
500m |
Medio core |
250m |
Un cuarto de core |
100m |
Una décima de core |
0.5 |
Equivalente a 500m, pero se desaconseja |
1m |
El mínimo admitido |
La m es de mili: 1000m = 1 core. La forma con m es la preferida porque evita los errores de coma flotante y porque 0.1 puede sufrir problemas de representación. Escribe siempre milicores.
La CPU es fraccionable y elástica: si pides 250m no te dan un cuarto de procesador físico, sino un cuarto del tiempo de CPU disponible en una ventana de tiempo. Y si el nodo está ocioso, puedes usar más (hasta tu limit).
Memoria
La memoria se mide en bytes y admite dos familias de sufijos que no son equivalentes:
| Sufijo | Base | Valor | Ejemplo |
|---|---|---|---|
Ki |
2 | 1.024 | 512Ki = 524.288 bytes |
Mi |
2 | 1.048.576 | 256Mi = 268.435.456 bytes |
Gi |
2 | 1.073.741.824 | 1Gi = 1.073.741.824 bytes |
Ti |
2 | 2⁴⁰ | |
k |
10 | 1.000 | 512k = 512.000 bytes |
M |
10 | 1.000.000 | 256M = 256.000.000 bytes |
G |
10 | 1.000.000.000 | 1G = 1.000.000.000 bytes |
256M es un 6,9 % menos memoria que 256Mi. Y 1G es 74 MiB menos que 1Gi. En un límite de memoria, ese 6,9 % es exactamente la diferencia entre un contenedor que va justo y uno que muere con OOMKilled bajo carga.
Regla para Rutas Norte y para cualquier proyecto serio: usa siempre Mi y Gi, nunca M ni G. Es lo que hacen todas las herramientas de Kubernetes y lo que espera cualquiera que lea tu manifiesto.
Y un error tipográfico que cuesta caro:
Kubernetes lo acepta sin rechistar: m es un sufijo válido (mili). El contenedor recibe un límite de medio byte y muere al instante. El síntoma es un CrashLoopBackOff desconcertante. m solo tiene sentido en CPU.
Si ves eso, ya sabes cuál es el problema.
- Quién usa las
requests: el planificador
requests: el planificadorEl kube-scheduler que estudiaste en el módulo 1 tiene un trabajo: elegir un nodo para cada pod nuevo. Y la decisión se basa exclusivamente en las requests, no en el uso real.
flowchart TB
A["Pod nuevo:<br/>requests cpu=250m mem=256Mi"] --> B["FILTRADO<br/>Que nodos tienen capacidad ASIGNABLE libre?"]
B --> C{"nodo-1<br/>libre: 200m CPU"}
B --> D{"nodo-2<br/>libre: 1500m CPU"}
B --> E{"nodo-3<br/>libre: 800m CPU"}
C -->|"NO cabe"| F["Descartado"]
D -->|"cabe"| G["PUNTUACION"]
E -->|"cabe"| G
G --> H["Elegido: el de mejor puntuacion"]
H --> I["El pod se ATA al nodo<br/>spec.nodeName"]
El cálculo que hace el planificador para cada nodo es:
Dos matices críticos:
Matiz 1: asignable no es la capacidad total del nodo. Kubernetes reserva una parte para el sistema operativo y para sus propios componentes (kubeReserved, systemReserved, evictionHard).
Capacity:
cpu: 4
ephemeral-storage: 61202244Ki
memory: 8039484Ki
pods: 110
Allocatable:
cpu: 4
ephemeral-storage: 56403448552
memory: 7937084Ki
pods: 110En este minikube la diferencia es pequeña, pero en un nodo gestionado de la nube puede ser el 10-15 % de la memoria. Planifica siempre sobre Allocatable, no sobre Capacity.
Matiz 2: se suman las requests, no el uso real. Esta es la fuente de la confusión más común. Un nodo puede estar al 8 % de uso real de CPU y aun así rechazar un pod nuevo porque la suma de requests de lo que ya hay asignado no deja hueco.
Allocated resources:
(Total limits may be over 100 percent, i.e., overcommitted.)
Resource Requests Limits
-------- -------- ------
cpu 2350m (58%) 5200m (130%)
memory 2816Mi (36%) 4608Mi (59%)
ephemeral-storage 0 (0%) 0 (0%)Léelo con atención: los Requests de CPU suman el 58 % del asignable, pero los Limits suman el 130 %. Eso es el sobrecompromiso del apartado 9, y ese aviso entre paréntesis lo advierte explícitamente.
Cuando no hay hueco en ningún nodo, el pod se queda en Pending:
kubectl get pods -n rutas-norte-dev
kubectl describe pod api-reservas-6d8f7c-abc -n rutas-norte-dev | tail -5NAME READY STATUS RESTARTS AGE
api-reservas-6d8f7c-abc 0/1 Pending 0 2m14s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 2m default-scheduler 0/3 nodes are available:
3 Insufficient memory. preemption: 0/3 nodes are available: 3 No preemption victims found.Insufficient memory con el pod en Pending significa siempre lo mismo: las requests no caben. Las soluciones son bajar las requests (si estaban infladas), añadir un nodo, o esperar a que el autoescalador de clúster lo haga por ti.
Un aviso sobre las requests infladas: es tentador pedir de más "por si acaso". Pero cada MiB de request que no usas es capacidad del clúster que nadie puede usar y que estás pagando. En clústeres reales es normal encontrar un 30-40 % de capacidad comprometida y sin usar solo por requests mal calibradas.
- Quién usa los
limits: el kubelet y los cgroups
limits: el kubelet y los cgroupsLos limits no los ve el planificador. Los aplica el kubelet en el nodo, y no por su cuenta: se lo pide al kernel de Linux a través de los cgroups (control groups), el mecanismo que permite limitar los recursos de un conjunto de procesos.
Podemos verlo desde dentro del contenedor. Con limits: cpu: "1" y memory: 512Mi en cgroups v2:
kubectl exec -n rutas-norte-dev deploy/api-reservas -- sh -c \
'cat /sys/fs/cgroup/memory.max; cat /sys/fs/cgroup/cpu.max; cat /sys/fs/cgroup/cpu.weight'Descifremos las tres líneas, porque explican el comportamiento entero:
memory.max = 536870912: son exactamente 512 MiB. Es un tope duro. Si el proceso intenta reservar el byte 536870913, el kernel dispara el OOM killer.cpu.max = 100000 100000: son cuota y periodo en microsegundos. Significa "100.000 µs de CPU cada 100.000 µs", es decir, un core completo. Conlimits: cpu: 500msería50000 100000.cpu.weight = 10: deriva de larequestde CPU (250m). Es el peso relativo con el que el planificador del kernel reparte el tiempo de CPU cuando hay competencia. Un contenedor con el doble derequestrecibe el doble de tiempo.
Esa última línea es importante y poco conocida: la request de CPU no es solo para el planificador de Kubernetes; también determina la prioridad relativa dentro del nodo. Dos pods en el mismo nodo compitiendo por CPU se reparten el tiempo en proporción a sus requests.
- CPU comprimible frente a memoria incomprimible
Esta es la distinción más importante de la lección y la que hay que entender de verdad.
| CPU | Memoria | |
|---|---|---|
| Tipo de recurso | Comprimible | Incomprimible |
| Se puede quitar en caliente | Sí: basta con darle menos tiempo | No: lo que está reservado, está reservado |
Al superar el limit |
Estrangulamiento (throttling) | OOMKilled: el proceso muere |
| Consecuencia | El proceso va más lento | El contenedor se reinicia y se pierde el trabajo en curso |
| Se ve en | Métricas de container_cpu_cfs_throttled_seconds |
kubectl describe pod, RESTARTS |
| Gravedad | Latencia alta, tiempos de espera | Pérdida de peticiones, corte de servicio |
CPU: estrangulamiento. Si tu contenedor tiene limits: cpu: 500m e intenta usar más, el kernel simplemente no le da más tiempo de CPU en ese periodo de 100 ms. El proceso no muere: se queda esperando. El efecto visible es latencia. Para api-reservas esto significa que una petición que tardaba 80 ms empieza a tardar 400 ms, y si el TIEMPO_ESPERA_MS de tienda-web está en 2000, con suficiente estrangulamiento se empiezan a ver errores de tiempo de espera en el navegador del cliente.
Memoria: muerte. No hay forma de "estrangular" la memoria. Si un proceso ha reservado 512 MiB y pide más, el kernel no puede quitarle nada: o le da memoria o mata a alguien. Con limits: memory: 512Mi, mata al proceso que se pasó, dentro del cgroup del contenedor. El contenedor muere con código de salida 137 y el kubelet lo reinicia según el restartPolicy.
Consecuencia práctica para el diseño:
Los
limitsde memoria hay que calibrarlos con cuidado, porque quedarse corto mata el proceso. Loslimitsde CPU son mucho menos peligrosos, pero también más discutibles.
Y aquí hay un debate real en la comunidad que conviene conocer: ¿poner o no limits de CPU?
| A favor de poner límite de CPU | En contra de poner límite de CPU |
|---|---|
| Evita que un proceso desbocado degrade a sus vecinos | Estrangula aunque el nodo esté ocioso: se desperdicia capacidad |
| Hace el rendimiento predecible entre entornos | Puede provocar estrangulamiento en ráfagas cortas de arranque |
| Permite detectar antes que la aplicación necesita más | Con hilos, el estrangulamiento afecta a todo el proceso, no solo al hilo culpable |
Necesario para la clase Guaranteed (03-05) |
Muchos equipos grandes los omiten deliberadamente |
Postura de Rutas Norte, que es la recomendable para un equipo que empieza:
requestsde CPU y memoria: siempre, en todos los contenedores. Sin negociación.limitsde memoria: siempre. Un pod sin límite de memoria puede tumbar el nodo entero y provocar desalojos en cadena.limitsde CPU: sí endevypre(para detectar pronto el consumo excesivo) y generosos enpro, entre 2 y 4 veces larequest, para permitir absorber los picos de puentes y vacaciones.
- Diagnosticar el estrangulamiento de CPU
El estrangulamiento es traicionero porque no aparece en ningún estado del pod. El pod está Running, READY 1/1, sin reinicios, y aun así responde mal.
La señal está en el propio cgroup:
usage_usec 184920331
user_usec 152014882
system_usec 32905449
nr_periods 918442
nr_throttled 214883
throttled_usec 41220984La lectura:
nr_periods: cuántos periodos de 100 ms han transcurrido.nr_throttled: en cuántos de ellos el contenedor agotó su cuota y fue estrangulado.throttled_usec: microsegundos totales que ha pasado esperando.
El indicador clave es el cociente:
Casi una cuarta parte de los periodos han sido estrangulados. Interpretación:
| Cociente | Diagnóstico |
|---|---|
| < 1 % | Normal. Ráfagas puntuales |
| 1-5 % | Vigilar. Aceptable en procesos por lotes |
| 5-25 % | Problema real de latencia. Subir el limit |
| > 25 % | Grave. El límite está claramente mal puesto |
Un comando para revisar todos los pods de un componente:
for P in $(kubectl get pods -n rutas-norte-pro -l app=api-reservas -o name); do
echo "--- $P"
kubectl exec -n rutas-norte-pro "$P" -- sh -c \
'awk "/nr_periods|nr_throttled/ {print \$1, \$2}" /sys/fs/cgroup/cpu.stat'
done--- pod/api-reservas-7f4b8c9d6-2xkqp
nr_periods 918442
nr_throttled 214883
--- pod/api-reservas-7f4b8c9d6-8mvtr
nr_periods 917210
nr_throttled 198445La forma correcta de vigilar esto de manera continua es con métricas: la serie container_cpu_cfs_throttled_periods_total de cAdvisor, que verás en Monitoreo con Prometheus. El comando de arriba sirve para un diagnóstico puntual.
La corrección: subir el limit de CPU. Si api-reservas tiene requests: 250m y limits: 500m con un 23 % de estrangulamiento, subir el límite a "1" suele resolverlo sin coste real, porque el limit no reserva capacidad.
- Diagnosticar un
OOMKilled
OOMKilledEl caso opuesto es escandaloso y fácil de identificar:
NAME READY STATUS RESTARTS AGE
api-reservas-7f4b8c9d6-2xkqp 1/1 Running 4 (2m ago) 41m
api-reservas-7f4b8c9d6-8mvtr 0/1 OOMKilled 3 (18s ago) 41mEl detalle está en describe:
Containers:
api:
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Wed, 05 Aug 2026 23:12:04 +0200
Finished: Wed, 05 Aug 2026 23:14:41 +0200
Restart Count: 3
Limits:
cpu: 1
memory: 512Mi
Requests:
cpu: 250m
memory: 256MiLas tres pistas inequívocas: Reason: OOMKilled, Exit Code: 137 (que es 128 + 9, la señal SIGKILL) y un Restart Count que crece.
Cuidado con el Last State: describe el contenedor anterior. Si el pod está Running ahora mismo pero tiene RESTARTS 4, el Last State te dice por qué murió la vez pasada. Es la primera cosa que hay que mirar ante un pod con reinicios.
Y una advertencia sobre kubectl logs: al reiniciarse el contenedor, los logs empiezan de cero. Para ver los del contenedor muerto, que son los que contienen la pista:
2026-08-05T23:14:38.221Z INFO [pod=api-reservas-7f4b8c9d6-8mvtr] consulta_disponibilidad
ruta=Bilbao-Santander fechas=2026-08-14..2026-08-17 resultados=8412
2026-08-05T23:14:40.887Z WARN [pod=api-reservas-7f4b8c9d6-8mvtr] heap 478MB / 512MBAhí está la causa: una consulta de disponibilidad de un puente devolvió 8.412 resultados y la aplicación los cargó todos en memoria.
Las cinco causas típicas de un OOMKilled y su tratamiento:
| Causa | Señal | Solución |
|---|---|---|
| Límite demasiado bajo | Muere siempre bajo carga normal | Subir limits.memory |
| Fuga de memoria | Muere periódicamente, cada vez más rápido | Arreglar el código; mientras tanto, subir el límite retrasa el problema, no lo resuelve |
| Pico puntual (consulta grande) | Muere solo con ciertas peticiones | Paginar la consulta, limitar resultados |
| Entorno de ejecución que no ve el límite | La JVM o Node reservan según la RAM del nodo | -XX:MaxRAMPercentage o --max-old-space-size con resourceFieldRef (03-03) |
| Contenedor equivocado | El que muere es el sidecar, no la app | Mirar el nombre del contenedor en describe |
La cuarta merece una nota, porque es la más frecuente y la más invisible: un proceso Java sin MaxRAMPercentage en un nodo de 32 GiB con un limit de 512 MiB dimensiona su montón según los 32 GiB del nodo y muere en cuanto empieza a trabajar. La lección anterior ya te dio la herramienta para arreglarlo.
ephemeral-storage y el desalojo por disco
ephemeral-storage y el desalojo por discoHay un tercer recurso que casi nadie declara y que provoca incidentes desconcertantes: el almacenamiento efímero, es decir, el disco del nodo que usa el contenedor para:
- La capa de escritura de su sistema de ficheros (todo lo que escriba fuera de un volumen).
- Los volúmenes
emptyDir. - Los logs del contenedor (lo que escribe a stdout/stderr, que el kubelet guarda en el disco del nodo).
Se declara igual que los demás:
resources:
requests:
cpu: 250m
memory: 256Mi
ephemeral-storage: 1Gi
limits:
cpu: "1"
memory: 512Mi
ephemeral-storage: 2GiQué pasa al superar el límite: el kubelet desaloja el pod, con Reason: Evicted:
kubectl get pods -n rutas-norte-pro | grep Evicted
kubectl describe pod worker-notificaciones-6f7d9-abcde -n rutas-norte-pro | grep -A3 "Status:"worker-notificaciones-6f7d9-abcde 0/1 Evicted 0 34m
Status: Failed
Reason: Evicted
Message: Pod ephemeral local storage usage exceeds the total limit of containers 2Gi.Un matiz relevante: a diferencia del OOMKilled, el desalojo por disco no es inmediato. El kubelet comprueba el uso cada 10 segundos aproximadamente, así que un proceso puede llenar el disco antes de que lo desalojen.
Y el problema mayor: si el disco del nodo se llena, el kubelet entra en presión y desaloja pods aunque tengan sus límites en orden. Es un fallo que afecta a todo el nodo. Las señales:
Conditions:
Type Status Reason Message
---- ------ ------ -------
MemoryPressure False KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure True KubeletHasDiskPressure kubelet has disk pressure
PIDPressure False KubeletHasSufficientPID kubelet has sufficient PID available
Ready True KubeletReady kubelet is posting ready statusDiskPressure: True explica desalojos aparentemente aleatorios. El orden en que el kubelet elige a las víctimas depende de la clase de QoS, que es el tema de la lección siguiente.
En Rutas Norte, el candidato natural a llenar el disco es worker-notificaciones: si genera un log por cada correo enviado y en un puente se envían decenas de miles, sin rotación de logs el disco del nodo se llena. La regla práctica: declara ephemeral-storage en cualquier componente que escriba logs voluminosos o use emptyDir, y vigila el DiskPressure de los nodos.
- Sobrecompromiso del clúster
Volvamos a esa línea de antes:
La suma de los límites de CPU es el 130 % del asignable del nodo. ¿Está el clúster mal configurado? No: eso es normal y hasta deseable.
flowchart TB
subgraph N["Nodo: 4 cores asignables"]
R["REQUESTS comprometidas: 2350m<br/>(garantizadas, el planificador no las sobrepasa)"]
L["LIMITS sumados: 5200m<br/>(el planificador NO los mira)"]
end
R --> S["Seguro: el planificador nunca<br/>compromete mas requests que capacidad"]
L --> P["Sobrecompromiso: solo es un problema<br/>si TODOS pican a la vez"]
La lógica del sobrecompromiso: los pods no consumen su máximo simultáneamente. worker-notificaciones trabaja a ráfagas, api-reservas tiene picos en horas concretas, tienda-web sirve estáticos con poco consumo. Permitir que la suma de límites supere la capacidad aprovecha esos huecos.
Dónde está el peligro, según el recurso:
| Recurso | Si todos piden a la vez |
|---|---|
| CPU | Todos se estrangulan proporcionalmente a su request. Latencia alta, sin caídas. Recuperable |
| Memoria | El nodo se queda sin memoria. El kubelet desaloja pods. Si va muy rápido, el OOM killer del kernel mata procesos. Caídas reales |
La conclusión operativa es asimétrica y hay que grabarla:
Sobrecomprometer CPU es aceptable y habitual. Sobrecomprometer memoria de forma agresiva es peligroso.
Para Rutas Norte, las relaciones recomendadas:
| Recurso | Relación limit / request |
Motivo |
|---|---|---|
| CPU | Entre 2 y 4 | Absorber picos de puentes sin desperdiciar capacidad |
| Memoria | Entre 1 y 1,5 | Cuanto más ajustado, menos riesgo de desalojos en cadena |
Memoria de postgres-reservas |
Exactamente 1 (iguales) | Una base de datos nunca debe ser candidata a desalojo |
Ese último caso, con requests == limits, tiene un nombre y consecuencias muy concretas sobre quién muere primero: es la clase Guaranteed, y es el tema de la lección siguiente.
Y una advertencia sobre memoria que sorprende a mucha gente: Kubernetes desactiva el swap por defecto (aunque desde 1.30 hay soporte beta configurable). Sin swap, el kernel no tiene amortiguador: cuando la memoria se acaba, se acaba. Es un motivo más para no sobrecomprometerla.
- ResourceQuota: qué limita y cómo se lee
Todo lo anterior es por contenedor. La ResourceQuota actúa a otro nivel: pone un techo al conjunto de un namespace. Es el objeto que salda la última deuda del módulo 2.
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-dev
namespace: rutas-norte-dev
spec:
hard:
# --- Computo ---
requests.cpu: "2" # suma de las requests de CPU de todos los pods
requests.memory: 4Gi
limits.cpu: "4" # suma de los limits
limits.memory: 8Gi
requests.ephemeral-storage: 10Gi
# --- Numero de objetos ---
pods: "20"
services: "10"
configmaps: "20"
secrets: "20"
persistentvolumeclaims: "5"
services.loadbalancers: "0" # prohibido crear LoadBalancers en dev
services.nodeports: "0"
count/deployments.apps: "10"
count/jobs.batch: "10"
count/cronjobs.batch: "5"
# --- Almacenamiento ---
requests.storage: 20Gi # suma de lo pedido por todos los PVCLas tres familias de límites:
| Familia | Ejemplos | Qué controla |
|---|---|---|
| Cómputo | requests.cpu, limits.memory |
Que un entorno no consuma toda la capacidad |
| Número de objetos | pods, services, secrets |
Que nadie sature etcd o el plano de control |
| Almacenamiento | requests.storage, persistentvolumeclaims |
El coste de los discos (módulo 5) |
Puntos que definen su comportamiento:
- Es un objeto con namespace y solo afecta a su namespace. No existen cuotas globales de clúster.
- Se aplica en el momento de la creación. Un pod que superaría la cuota se rechaza; los que ya existen no se tocan.
- Puede haber varias ResourceQuota en un namespace. Se aplican todas y hay que cumplirlas todas.
- Los pods terminados no cuentan. Los que están en
SucceededoFailedse excluyen del cómputo. services.loadbalancers: "0"es una herramienta de control de coste muy útil: cada LoadBalancer en la nube cuesta dinero.
La lectura de una cuota:
Name: cuota-dev
Namespace: rutas-norte-dev
Resource Used Hard
-------- ---- ----
configmaps 6 20
count/cronjobs.batch 0 5
count/deployments.apps 5 10
limits.cpu 2300m 4
limits.memory 3200Mi 8Gi
persistentvolumeclaims 1 5
pods 9 20
requests.cpu 1150m 2
requests.memory 1600Mi 4Gi
secrets 4 20
services 4 10
services.loadbalancers 0 0Dos columnas: Used (lo consumido ahora) y Hard (el techo). Este namespace está al 57 % de las requests de CPU y al 40 % de memoria. Es la vista que responde a "¿cuánto margen me queda en desarrollo?".
Cuando alguien intenta pasarse:
kubectl scale deploy/api-reservas --replicas=12 -n rutas-norte-dev
kubectl get events -n rutas-norte-dev --sort-by=.lastTimestamp | tail -3deployment.apps/api-reservas scaled
LAST SEEN TYPE REASON OBJECT MESSAGE
3s Warning FailedCreate replicaset/api-reservas-6d8f7c9b Error creating: pods
"api-reservas-6d8f7c9b-" is forbidden: exceeded quota: cuota-dev, requested: requests.cpu=250m,
used: requests.cpu=1900m, limited: requests.cpu=2Presta atención a un detalle muy importante para el diagnóstico: el comando kubectl scale ha tenido éxito. El Deployment ahora dice 12 réplicas. Lo que falla es la creación de los pods por parte del ReplicaSet, y ese fallo no aparece en kubectl get deploy más que como un READY 8/12 que dura para siempre.
kubectl get deploy api-reservas -n rutas-norte-dev
kubectl describe deploy api-reservas -n rutas-norte-dev | grep -A4 ConditionsNAME READY UP-TO-DATE AVAILABLE AGE
api-reservas 8/12 12 8 3h
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
ReplicaProgressing False ProgressDeadlineExceededAnte un Deployment atascado en READY x/y, mira siempre los eventos del ReplicaSet y la cuota del namespace. Es una de las causas más frecuentes y de las menos evidentes, y conecta directamente con el diagnóstico de despliegues atascados que viste en el módulo 2.
Ámbitos (scopes)
Una cuota puede aplicarse solo a un subconjunto de pods:
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-alta-prioridad
namespace: rutas-norte-pro
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
scopeSelector:
matchExpressions:
- operator: In
scopeName: PriorityClass
values: ["alta"]Ámbitos disponibles: Terminating y NotTerminating (según tengan activeDeadlineSeconds), BestEffort y NotBestEffort (según la clase de QoS de la lección siguiente), y PriorityClass. Sirven para dar más margen a lo crítico que a lo experimental dentro del mismo namespace.
- El efecto que obliga a declarar recursos
Aquí está el comportamiento más útil y a la vez más desconcertante de las ResourceQuota:
Si un namespace tiene una cuota que limita
requests.cpuolimits.memory, entonces TODO pod creado en él debe declarar ese recurso. Si no lo declara, se rechaza.
La lógica es evidente en cuanto se piensa: para contabilizar la suma de requests.cpu del namespace, Kubernetes necesita que todos los pods declaren requests.cpu. Un pod sin declararla haría el cómputo imposible.
Demostración. Con la cuota de dev activa, intentamos crear un pod sin recursos:
Error from server (Forbidden): pods "prueba-sin-recursos" is forbidden: failed quota: cuota-dev:
must specify limits.cpu for: prueba-sin-recursos; limits.memory for: prueba-sin-recursos;
requests.cpu for: prueba-sin-recursos; requests.memory for: prueba-sin-recursosEste es, de largo, el efecto secundario más valioso de las cuotas: convierte la disciplina de declarar recursos en algo obligatorio a nivel de plataforma, no en una recomendación que la gente olvida.
Pero tiene un coste inmediato: rompe todo lo cómodo. Un pod efímero para depurar, un kubectl run rápido, un Job puntual... todos fallan. Y aquí es donde aparece la pieza que falta, que es el tema de la lección siguiente: un LimitRange en el namespace inyecta valores por defecto en los contenedores que no declaran nada, con lo que el pod pasa la validación de la cuota sin que nadie escriba resources a mano.
flowchart LR
A["Pod sin resources"] --> B{"Hay LimitRange<br/>en el namespace?"}
B -->|"Si"| C["Se inyectan<br/>default y defaultRequest"]
B -->|"No"| D["El pod sigue<br/>sin resources"]
C --> E{"Hay ResourceQuota<br/>de computo?"}
D --> E
E -->|"Si, y el pod declara"| F["Se contabiliza. ACEPTADO"]
E -->|"Si, y no declara"| G["RECHAZADO<br/>must specify requests.cpu"]
E -->|"No"| F
ResourceQuota y LimitRange son inseparables. Poner una cuota sin un LimitRange es una decisión defendible (obliga a todo el mundo a pensar sus recursos), pero convierte el día a día en una molestia constante. La combinación de las dos es la configuración estándar de cualquier namespace serio.
- Las cuotas de los tres entornos de Rutas Norte
Ahora sí: aplicamos las cuotas y la deuda queda saldada. El criterio es que dev no pueda hacer daño y pro tenga margen para los picos de puentes y vacaciones.
rutas-norte-dev
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-dev
namespace: rutas-norte-dev
labels:
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
hard:
requests.cpu: "2" # 2 cores comprometidos como maximo
requests.memory: 4Gi
limits.cpu: "4"
limits.memory: 8Gi
pods: "25"
services: "10"
services.loadbalancers: "0" # nada de balanceadores de pago en dev
services.nodeports: "2"
persistentvolumeclaims: "4"
requests.storage: 10Gi
configmaps: "30"
secrets: "30"
count/deployments.apps: "15"
count/cronjobs.batch: "5"rutas-norte-pre
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-pre
namespace: rutas-norte-pre
labels:
app.kubernetes.io/part-of: rutas-norte
entorno: pre
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 12Gi
pods: "30"
services: "15"
services.loadbalancers: "1" # uno, para probar el Ingress real
persistentvolumeclaims: "6"
requests.storage: 50Gi
configmaps: "40"
secrets: "40"
count/deployments.apps: "20"rutas-norte-pro
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-pro
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
hard:
requests.cpu: "24" # margen para el autoescalado del modulo 9
requests.memory: 48Gi
limits.cpu: "48"
limits.memory: 64Gi
pods: "150"
services: "25"
services.loadbalancers: "3"
persistentvolumeclaims: "12"
requests.storage: 500Gi
configmaps: "60"
secrets: "60"
count/deployments.apps: "30"Comparativa y justificación:
| Recurso | dev |
pre |
pro |
Por qué |
|---|---|---|---|---|
requests.cpu |
2 | 4 | 24 | pro sirve tráfico real y debe crecer en puentes |
requests.memory |
4Gi | 8Gi | 48Gi | Ídem, más el caché y la base de datos |
pods |
25 | 30 | 150 | El HPA necesita techo alto en pro |
services.loadbalancers |
0 | 1 | 3 | Cada uno cuesta dinero al mes |
requests.storage |
10Gi | 50Gi | 500Gi | Los datos de reservas solo crecen en producción |
Relación limits/requests CPU |
2× | 2× | 2× | Sobrecompromiso moderado y uniforme |
Relación limits/requests memoria |
2× | 1,5× | 1,33× | Más conservador cuanto más crítico |
Aplicación y verificación:
kubectl apply -f k8s/entornos/dev/resourcequota.yaml
kubectl apply -f k8s/entornos/pre/resourcequota.yaml
kubectl apply -f k8s/entornos/pro/resourcequota.yaml
kubectl get resourcequota -Aresourcequota/cuota-dev created
resourcequota/cuota-pre created
resourcequota/cuota-pro created
NAMESPACE NAME AGE REQUEST LIMIT
rutas-norte-dev cuota-dev 8s pods: 9/25, requests.cpu: 1150m/2, ... limits.cpu: 2300m/4, ...
rutas-norte-pre cuota-pre 7s pods: 4/30, requests.cpu: 500m/4, ... limits.cpu: 1000m/8, ...
rutas-norte-pro cuota-pro 6s pods: 14/150, requests.cpu: 4200m/24, ... limits.cpu: 9600m/48, ...La deuda está saldada. Ahora, si alguien escala api-reservas a 50 réplicas en desarrollo por error, o si un bucle de reinicio crea pods sin parar, el daño queda contenido dentro de rutas-norte-dev: los pods se quedarán sin crear y producción no se entera. Recuerda del módulo 2 que el namespace no aísla la red ni el DNS; la cuota es una de las pocas cosas que el namespace sí aísla de verdad, y por eso es tan valiosa.
Una nota final sobre la suma total. Si sumas las requests.cpu de las tres cuotas salen 30 cores, y tu clúster puede tener menos. Eso es intencionado: la cuota es un techo por entorno, no una reserva. Nadie garantiza que puedas llegar al techo de los tres a la vez; lo que garantiza es que ninguno solo pueda pasar de su techo.
- Cómo elegir valores razonables
Los números anteriores no salen de la intuición. Este es el método.
Paso 1: medir, no adivinar. Necesitas metrics-server, que activamos como addon en el módulo 1:
NAME CPU(cores) MEMORY(bytes)
postgres-reservas-5d8f6b9c4-k2mnp 180m 1842Mi
api-reservas-7f4b8c9d6-2xkqp 310m 387Mi
api-reservas-7f4b8c9d6-8mvtr 285m 371Mi
redis-cache-6c9d8f7b5-vn4qx 22m 148Mi
worker-notificaciones-6f7d9c8b4-lm3pt 95m 212Mi
tienda-web-5b7c9f4d8-h7pnk 8m 14MiEl detalle de este comando y sus limitaciones (es una foto instantánea, no un histórico) es el tema de Servidor de Métricas. Para calibrar bien hace falta un histórico con percentiles, que es lo que da Prometheus.
Paso 2: aplicar las fórmulas.
| Recurso | Fórmula | Razonamiento |
|---|---|---|
requests.memory |
Percentil 95 del uso × 1,2 | La memoria no se estrangula: quédate corto y mueres |
limits.memory |
requests.memory × 1,3 a 1,5 |
Margen para picos, sin sobrecomprometer |
requests.cpu |
Percentil 50 (mediana) del uso | La CPU sí se comprime: la mediana basta para la garantía |
limits.cpu |
requests.cpu × 2 a 4 |
Absorber ráfagas sin estrangular |
Fíjate en la asimetría: memoria por el percentil 95, CPU por la mediana. Es la aplicación directa del apartado 5.
Paso 3: calcular para api-reservas en producción. Con un histórico de dos semanas, incluyendo un puente:
resources:
requests:
cpu: 300m # ~ mediana
memory: 534Mi # 445Mi x 1,2 -> redondeamos a 512Mi
limits:
cpu: "1" # ~ 3x la request, cubre el pico de 940m
memory: 768Mi # 512Mi x 1,5Paso 4: iterar. Los valores iniciales están mal por definición. Revísalos:
- Tras cada cambio significativo de la aplicación.
- Tras cada temporada alta (en Rutas Norte, después de cada puente y del verano).
- Cuando aparezca estrangulamiento por encima del 5 % o cualquier
OOMKilled. - Trimestralmente, para recuperar capacidad comprometida y sin usar.
Cuatro reglas que ahorran disgustos:
- Empieza generoso y baja. Un límite alto de más cuesta capacidad; uno bajo de más provoca caídas en producción. En la primera iteración, prefiere el desperdicio.
- Multiplica por réplicas antes de mirar la cuota.
requests.cpu: 300m× 8 réplicas = 2400m del presupuesto depro. - Los componentes con estado son distintos.
postgres-reservasdebe tenerrequests == limitsde memoria: es la claseGuaranteedde la lección siguiente, y una base de datos nunca debe ser la primera candidata a morir. - Documenta de dónde salen los números. Un comentario en el YAML con la fecha de la medición y el percentil usado convierte el manifiesto en algo revisable:
resources:
# Calibrado 2026-07-28 sobre 14 dias (incluye puente de julio)
# CPU: mediana 280m, p95 620m | Memoria: p95 445Mi, pico 502Mi
requests:
cpu: 300m
memory: 512Mi
limits:
cpu: "1"
memory: 768MiErrores Comunes y Consejos
| Error | Síntoma | Solución |
|---|---|---|
memory: 512m en minúscula |
CrashLoopBackOff inmediato |
m es mili. Usa 512Mi |
Confundir M con Mi |
OOMKilled inexplicable con el 6,9 % de memoria de menos |
Usa siempre Mi/Gi |
Pod en Pending con Insufficient memory |
No hay nodo con hueco para las requests |
Bajar requests, añadir nodo o autoescalar |
| Mirar el uso real esperando que el planificador lo use | "Si el nodo está al 10 %, ¿por qué no cabe?" | El planificador solo mira requests |
Solo limits sin requests |
Se reserva más de lo previsto | Kubernetes copia el limit en la request |
Ningún limit de memoria |
Un pod puede tumbar el nodo | Declara siempre limits.memory |
| Límite de CPU muy ajustado | Latencia alta sin reinicios ni errores visibles | Mirar cpu.stat: nr_throttled/nr_periods |
| JVM o Node sin conocer su límite | OOMKilled con un límite aparentemente suficiente |
MaxRAMPercentage o resourceFieldRef (03-03) |
No mirar --previous en los logs |
"No hay nada en los logs" tras un reinicio | kubectl logs --previous |
| Cuota sin LimitRange | kubectl run falla siempre |
Añade un LimitRange (03-05) |
Deployment atascado en READY 8/12 |
La cuota rechaza los pods nuevos, sin error visible en el Deployment | kubectl get events y describe quota |
| Cuota puesta sin avisar al equipo | Despliegues que fallan sin explicación | Comunícalo y deja margen inicial |
No declarar ephemeral-storage |
Pods Evicted y DiskPressure en el nodo |
Declararlo en lo que escriba logs o use emptyDir |
Consejos:
- Toda plantilla de manifiesto debe traer
resources. Ponlo en tu plantilla base para que no haya que recordarlo. - Revisa la cuota antes de escalar.
kubectl describe quota -n <ns>antes de unkubectl scale. - Vigila las
requestssin usar. Es el gasto invisible más grande de un clúster: capacidad comprometida que nadie aprovecha. - Añade una alerta de cuota al 80 %. Que te enteres antes de que un despliegue falle, no durante.
ephemeral-storageno es opcional en producción. Un nodo conDiskPressuredesaloja pods sanos.
Ejercicios
Ejercicio 1: Provocar y diagnosticar los dos fallos
- Crea en
rutas-norte-devun poddevorador-memoriaconlimits.memory: 128Mique ejecute un proceso que intente reservar 300 MiB. Usa la imagenpolinux/stressobusyboxcon un fichero en/dev/shm. - Observa su estado, identifica el
Reasony elExit Code, y explica qué significa137. - Crea un pod
devorador-cpuconlimits.cpu: 100mque ejecute un bucle infinito. - Comprueba con
kubectl topcuánta CPU consume realmente y lee/sys/fs/cgroup/cpu.statpara calcular el porcentaje de estrangulamiento. - Explica en una tabla por qué uno murió y el otro no, a pesar de que ambos superaron su límite.
Ejercicio 2: Poner cuota a los tres entornos
- Aplica las tres ResourceQuota del apartado 12.
- Muestra el consumo actual de cada namespace con
kubectl describe. - Intenta escalar
api-reservasenrutas-norte-deva un número de réplicas que supere la cuota. ¿Qué comando falla y cuál tiene éxito? Localiza el mensaje de error exacto. - Intenta crear un pod con
kubectl runsin declarar recursos enrutas-norte-dev. Copia el error y explícalo. - Calcula cuántas réplicas de
api-reservas(conrequests: cpu 250m, memory 256Mi) caben como máximo enrutas-norte-devsegún cada uno de los cuatro límites de cómputo, e indica cuál es el que manda.
Ejercicio 3: Calibrar worker-notificaciones
Dispones de estas mediciones de dos semanas en producción, incluyendo un puente:
CPU: mediana 95m, p95 340m, pico 780m
Memoria: mediana 212Mi, p95 268Mi, pico 295Mi
Disco: logs 400 MiB/dia, emptyDir de adjuntos hasta 600 MiB- Calcula
requestsylimitsde CPU y memoria aplicando las fórmulas del apartado 13, y justifica por qué la memoria usa el p95 y la CPU la mediana. - Decide un valor de
ephemeral-storagey razónalo. - Escribe el bloque
resourcescompleto, con el comentario de trazabilidad. - Con 3 réplicas en
rutas-norte-pro, calcula qué porcentaje de la cuota de producción consume este componente. - Si el pico de CPU es de 780m y pones
limits.cpu: 700m, ¿qué le pasará al componente durante el puente? ¿Es aceptable para un trabajador de correos? Compara con la respuesta si fueraapi-reservas.
Soluciones
Solución 1
apiVersion: v1
kind: Pod
metadata:
name: devorador-memoria
namespace: rutas-norte-dev
labels:
app: laboratorio
entorno: dev
spec:
restartPolicy: Never
containers:
- name: estres
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "300M", "--vm-hang", "1"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 100m
memory: 128Mikubectl apply -f devorador-memoria.yaml
sleep 15
kubectl get pod devorador-memoria -n rutas-norte-dev
kubectl describe pod devorador-memoria -n rutas-norte-dev | grep -A6 "Last State"pod/devorador-memoria created
NAME READY STATUS RESTARTS AGE
devorador-memoria 0/1 OOMKilled 0 15s
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Wed, 05 Aug 2026 23:41:02 +0200
Finished: Wed, 05 Aug 2026 23:41:04 +0200El 137 es 128 + 9. Por convención de Unix, un proceso terminado por una señal devuelve 128 + número de señal, y la señal 9 es SIGKILL. Es la firma inequívoca de una muerte violenta: el kernel no le dio al proceso ninguna oportunidad de limpiar. (Su pariente, el 143 = 128 + 15, es SIGTERM: terminación ordenada, la del módulo 2.)
El devorador de CPU:
apiVersion: v1
kind: Pod
metadata:
name: devorador-cpu
namespace: rutas-norte-dev
labels:
app: laboratorio
entorno: dev
spec:
containers:
- name: bucle
image: busybox:1.36
command: ["sh", "-c", "while true; do :; done"]
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mikubectl apply -f devorador-cpu.yaml
sleep 60
kubectl top pod devorador-cpu -n rutas-norte-dev
kubectl exec devorador-cpu -n rutas-norte-dev -- cat /sys/fs/cgroup/cpu.statpod/devorador-cpu created
NAME CPU(cores) MEMORY(bytes)
devorador-cpu 100m 1Mi
nr_periods 604
nr_throttled 601
throttled_usec 53420118El bucle infinito querría consumir un core entero (1000m), pero kubectl top muestra exactamente 100m: el límite se cumple al milicore. Y el estrangulamiento es de 601/604 = 99,5 %: prácticamente todos los periodos han sido recortados. Aun así, el pod está Running y sin reinicios.
devorador-memoria |
devorador-cpu |
|
|---|---|---|
| Superó su límite | Sí (300 MiB > 128 MiB) | Sí (querría 1000m > 100m) |
| Recurso | Incomprimible | Comprimible |
| Qué pudo hacer el kernel | Nada: la memoria pedida no se puede "dar más despacio" | Darle menos tiempo de CPU |
| Resultado | OOMKilled, código 137 |
Running, estrangulado al 99,5 % |
| Impacto en el negocio | Pérdida del trabajo en curso | Lentitud |
Solución 2
kubectl apply -f k8s/entornos/dev/resourcequota.yaml \
-f k8s/entornos/pre/resourcequota.yaml \
-f k8s/entornos/pro/resourcequota.yaml
kubectl describe quota -n rutas-norte-devName: cuota-dev
Namespace: rutas-norte-dev
Resource Used Hard
-------- ---- ----
count/deployments.apps 5 15
limits.cpu 2300m 4
limits.memory 3200Mi 8Gi
pods 9 25
requests.cpu 1150m 2
requests.memory 1600Mi 4Gi
services 4 10
services.loadbalancers 0 0kubectl scale deploy/api-reservas --replicas=12 -n rutas-norte-dev
kubectl get deploy api-reservas -n rutas-norte-dev
kubectl get events -n rutas-norte-dev --field-selector reason=FailedCreate | tail -2deployment.apps/api-reservas scaled
NAME READY UP-TO-DATE AVAILABLE AGE
api-reservas 3/12 12 3 3h
LAST SEEN TYPE REASON OBJECT MESSAGE
5s Warning FailedCreate replicaset/api-reservas-6d8f7c9b Error creating: pods
"api-reservas-6d8f7c9b-" is forbidden: exceeded quota: cuota-dev,
requested: requests.cpu=250m, used: requests.cpu=1900m, limited: requests.cpu=2kubectl scale tiene éxito y la creación de los pods falla. El Deployment queda con READY 3/12 indefinidamente. Esta asimetría es la clave del diagnóstico: el error nunca está en el objeto que tocaste, sino en los eventos del ReplicaSet.
Error from server (Forbidden): pods "prueba" is forbidden: failed quota: cuota-dev:
must specify limits.cpu for: prueba; limits.memory for: prueba;
requests.cpu for: prueba; requests.memory for: pruebaLa cuota controla requests.cpu, requests.memory, limits.cpu y limits.memory. Para poder contabilizar esas cuatro sumas necesita que todos los pods las declaren, así que rechaza el que no lo hace. La solución elegante, sin obligar a escribirlas a mano, es el LimitRange de la lección siguiente.
El cálculo de réplicas máximas, con requests: cpu 250m, memory 256Mi y limits: cpu 500m, memory 512Mi:
| Límite de la cuota | Techo | Consumo por réplica | Réplicas máximas |
|---|---|---|---|
requests.cpu |
2000m | 250m | 8 |
requests.memory |
4096Mi | 256Mi | 16 |
limits.cpu |
4000m | 500m | 8 |
limits.memory |
8192Mi | 512Mi | 16 |
pods |
25 | 1 | 25 |
Manda requests.cpu (y limits.cpu, empatados): 8 réplicas. El límite efectivo es siempre el más restrictivo, y aquí es la CPU. Y ojo: esas 8 réplicas serían el namespace entero, así que hay que descontar lo que ya consumen tienda-web, postgres-reservas, redis-cache y worker-notificaciones.
Solución 3
- Aplicando las fórmulas:
requests.memory = p95 x 1,2 = 268Mi x 1,2 = 321,6Mi -> redondeamos a 320Mi
limits.memory = 320Mi x 1,5 = 480Mi -> redondeamos a 512Mi (cubre el pico de 295Mi con holgura)
requests.cpu = mediana = 95m -> redondeamos a 100m
limits.cpu = 100m x 4 = 400m -> subimos a 800m para cubrir el pico de 780mLa asimetría entre memoria y CPU es exactamente la del apartado 5. La memoria usa el p95 porque quedarse corto no significa "ir despacio", significa OOMKilled: se pierde el correo de confirmación que se estaba enviando y el cliente no recibe su billete. La CPU usa la mediana porque quedarse corto solo significa que el trabajador tarda más en vaciar la cola; el limit generoso permite que en un puente acelere y recupere.
ephemeral-storage: 400 MiB/día de logs más hasta 600 MiB deemptyDirde adjuntos. Asumiendo rotación diaria de logs y un margen de seguridad:
requests.ephemeral-storage = 400Mi (logs) + 600Mi (emptyDir) = 1Gi
limits.ephemeral-storage = 1Gi x 2 = 2GiEl factor 2 cubre el caso de que la rotación de logs falle un día o de que un puente genere el doble de adjuntos. Sin este límite, un fallo de rotación llenaría el disco del nodo y provocaría DiskPressure, desalojando también a los pods sanos que comparten nodo, incluido postgres-reservas.
- El bloque completo:
- name: worker
image: registry.rutasnorte.example/worker-notificaciones:1.8.0
resources:
# Calibrado 2026-08-01 sobre 14 dias (incluye puente del 15 de agosto)
# CPU: mediana 95m, p95 340m, pico 780m -> request=mediana, limit>pico
# Memoria: mediana 212Mi, p95 268Mi, pico 295Mi -> request=p95 x1,2
# Disco: 400Mi/dia de logs + 600Mi de emptyDir de adjuntos
requests:
cpu: 100m
memory: 320Mi
ephemeral-storage: 1Gi
limits:
cpu: 800m
memory: 512Mi
ephemeral-storage: 2Gi- Con 3 réplicas en
rutas-norte-pro:
| Recurso | Consumo (3 réplicas) | Cuota de pro |
Porcentaje |
|---|---|---|---|
requests.cpu |
300m | 24000m | 1,25 % |
requests.memory |
960Mi | 49152Mi | 1,95 % |
limits.cpu |
2400m | 48000m | 5,0 % |
limits.memory |
1536Mi | 65536Mi | 2,3 % |
pods |
3 | 150 | 2,0 % |
Consumo muy modesto: hay margen de sobra para que api-reservas escale en los picos.
- Con
limits.cpu: 700my un pico de 780m, el trabajador se estrangula durante el puente. No muere: procesa más despacio. El efecto concreto es que la cola de notificaciones crece y los correos de confirmación se retrasan, quizá de 30 segundos a varios minutos.
¿Es aceptable? Para worker-notificaciones, sí, con matices. Es un proceso asíncrono: el cliente ya tiene su reserva confirmada en pantalla cuando el correo se envía. Un retraso de minutos es molesto pero no rompe el negocio, y la cola se vacía sola cuando pasa el pico. Habría que vigilar que el retraso no crezca sin límite: si la tasa de llegada supera a la de proceso, la cola no se recupera nunca y ahí sí hay incidente.
Para api-reservas la respuesta sería que no. Es síncrono: el cliente está esperando delante de la pantalla. Un estrangulamiento del 20 % convierte una respuesta de 80 ms en una de 400 ms, tienda-web empieza a agotar su TIEMPO_ESPERA_MS de 2000, y el cliente ve errores justo en el momento de máxima venta del año. En un componente de cara al usuario, el límite de CPU debe cubrir el pico con holgura.
Esa diferencia —entre lo que solo va más despacio y lo que el cliente sufre— es la que hay que tener en la cabeza al calibrar cada componente.
Conclusión
Ya no escribes resources a ojo. Entiendes el modelo completo: las requests son lo que el planificador reserva al elegir nodo y lo que determina la prioridad relativa de CPU dentro del nodo; los limits son el techo que el kubelet impone a través de cgroups en tiempo de ejecución. Sabes que el planificador solo mira requests, nunca el uso real, lo que explica por qué un nodo al 10 % de uso puede rechazar un pod. Y dominas las unidades, incluidas las dos trampas que cuestan incidentes reales: la diferencia del 6,9 % entre M y Mi, y el 512m en minúscula que fija un límite de medio byte.
Tienes clara la distinción fundamental: la CPU es comprimible y superarla solo produce estrangulamiento —diagnosticable con el cociente nr_throttled/nr_periods de cpu.stat, invisible en el estado del pod—, mientras que la memoria es incomprimible y superarla produce OOMKilled con código 137, con las cinco causas típicas y el reflejo de mirar kubectl logs --previous. Conoces el ephemeral-storage y el desalojo por disco que puede tumbar pods sanos de todo un nodo, y entiendes por qué sobrecomprometer CPU es normal y sobrecomprometer memoria es peligroso.
Y has saldado la última deuda del módulo 2: los tres entornos de Rutas Norte tienen ResourceQuota. rutas-norte-dev no puede pasar de 2 cores y 4 GiB comprometidos, ni crear un solo LoadBalancer; rutas-norte-pro tiene 24 cores y 48 GiB de margen para los picos de puentes y vacaciones. Sabes leer kubectl describe quota, reconocer el error de cuota superada, y diagnosticar el caso traicionero en que kubectl scale tiene éxito y el Deployment se queda para siempre en READY 8/12. Y tienes un método para elegir valores: medir con kubectl top, aplicar el p95 a la memoria y la mediana a la CPU, documentar la fecha y el percentil en el propio manifiesto, e iterar tras cada temporada alta.
Queda un cabo suelto que ha aparecido dos veces en esta lección. Poner cuota de cómputo obliga a todos los pods a declarar recursos, y eso rompe cualquier kubectl run rápido y cualquier manifiesto de un tercero. La pieza que falta es el LimitRange, que inyecta valores por defecto en los contenedores que no declaran nada. Y hay otro cabo aún más interesante: sabemos que cuando falta memoria en un nodo alguien muere, pero no quién. La respuesta no es aleatoria: depende de una clasificación que Kubernetes asigna a cada pod según cómo haya declarado sus recursos. La lección siguiente, LimitRanges y Clases de Calidad de Servicio (QoS), cubre las dos cosas: los campos default, min, max y maxLimitRequestRatio, su interacción exacta con la ResourceQuota, y las clases Guaranteed, Burstable y BestEffort con su efecto sobre el oom_score_adj del kernel y el orden de desalojo del kubelet. Al terminar sabrás por qué postgres-reservas debe ser Guaranteed y podrás provocar un desalojo en tu minikube para ver con tus propios ojos qué pod cae primero.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
