La lección anterior puso cuotas a los tres entornos de Rutas Norte y dejó dos cabos sueltos muy concretos. El primero es práctico y molesta cada día: desde que rutas-norte-dev tiene cuota de cómputo, un simple kubectl run falla con must specify requests.cpu, y cualquier manifiesto de un tercero que no declare recursos es inaplicable. El segundo es más profundo: sabemos que cuando a un nodo le falta memoria alguien tiene que morir, pero no sabemos quién, y esa decisión no es aleatoria en absoluto. Esta lección cierra los dos: el LimitRange inyecta valores por defecto y pone topes por contenedor dentro del namespace, y las clases de calidad de servicio —Guaranteed, Burstable y BestEffort— determinan el orden exacto en que el kubelet desaloja y en que el kernel mata. Al final asignarás una clase razonada a cada componente de Rutas Norte y provocarás un desalojo real en tu minikube para verlo con tus propios ojos.
Contenido
- Qué resuelve un LimitRange
- Los campos:
default,defaultRequest,min,max,maxLimitRequestRatio - Demostración: un pod sin recursos sale con recursos
- Los tipos:
Container,PodyPersistentVolumeClaim - Interacción exacta entre LimitRange y ResourceQuota
- Las tres clases de QoS y la regla que las determina
- Consultar la clase de un pod
- Consecuencia 1:
oom_score_adjy el OOM killer del kernel - Consecuencia 2: el orden de desalojo del kubelet
- La clase de QoS de cada componente de Rutas Norte
- El LimitRange de los tres entornos
- Experimento: provocar un desalojo y ver quién cae primero
- Qué resuelve un LimitRange
El LimitRange es un objeto con namespace que hace dos cosas distintas sobre cada contenedor que se cree en él:
- Inyecta valores por defecto en los contenedores que no declaran
requestsolimits. - Valida topes: rechaza los contenedores que piden menos de un mínimo o más de un máximo.
La diferencia con la ResourceQuota es el ámbito, y conviene tenerla clarísima porque se confunden constantemente:
| ResourceQuota | LimitRange | |
|---|---|---|
| Ámbito | El namespace entero, agregado | Cada contenedor o pod individual |
| Pregunta que responde | "¿Cuánto puede consumir este entorno en total?" | "¿Cuánto puede pedir un solo contenedor?" |
| Modifica el objeto | No: solo acepta o rechaza | Sí: inyecta valores por defecto |
| Cuándo actúa | Al crear el objeto | Al crear el objeto |
| Objetos afectados | Pods, Services, PVCs, ConfigMaps... | Contenedores, pods y PVCs |
| Efecto típico | exceeded quota |
minimum memory usage per Container is 32Mi |
| Puede haber varios | Sí (se aplican todos) | Sí (se aplican todos) |
Una analogía con Rutas Norte: la ResourceQuota es el presupuesto anual del departamento (no se pueden gastar más de X euros entre todos). El LimitRange es la política de gastos por persona (nadie puede pedir un billete de más de Y euros, y si no se especifica clase, se asume turista).
Los tres problemas concretos que resuelve:
Problema 1: la cuota rompe todo lo cómodo. Ya lo vimos:
Error from server (Forbidden): pods "depurador" is forbidden: failed quota: cuota-dev:
must specify limits.cpu for: depurador; limits.memory for: depurador;
requests.cpu for: depurador; requests.memory for: depuradorProblema 2: un solo contenedor puede acaparar la cuota entera. Sin LimitRange, alguien puede desplegar un pod con requests.memory: 4Gi en rutas-norte-dev y agotar de golpe toda la cuota de memoria del entorno, dejando sin sitio a los cinco componentes.
Problema 3: valores absurdos. Un contenedor con requests.memory: 4Mi no arrancará nunca de forma estable, pero nada lo impide.
- Los campos:
default, defaultRequest, min, max, maxLimitRequestRatio
default, defaultRequest, min, max, maxLimitRequestRatioapiVersion: v1
kind: LimitRange
metadata:
name: limites-dev
namespace: rutas-norte-dev
spec:
limits:
- type: Container # se aplica a CADA contenedor
default: # LIMITS si el contenedor no los declara
cpu: 300m
memory: 256Mi
defaultRequest: # REQUESTS si el contenedor no las declara
cpu: 100m
memory: 128Mi
min: # nadie puede pedir MENOS que esto
cpu: 10m
memory: 32Mi
max: # nadie puede pedir MAS que esto
cpu: "2"
memory: 2Gi
maxLimitRequestRatio: # relacion maxima limit/request
cpu: "10"
memory: "4"Campo por campo:
| Campo | Qué hace | Si se incumple |
|---|---|---|
default |
Rellena los limits que falten |
— (solo rellena) |
defaultRequest |
Rellena las requests que falten |
— (solo rellena) |
min |
Mínimo que puede declararse | El pod se rechaza |
max |
Máximo que puede declararse | El pod se rechaza |
maxLimitRequestRatio |
Tope de la división limit / request |
El pod se rechaza |
Reglas de resolución que hay que conocer con precisión:
Regla 1: lo declarado siempre gana. El LimitRange nunca sobrescribe un valor que el contenedor declara. Solo rellena huecos.
Regla 2: si declaras limits pero no requests, la request toma el valor del limit, no el de defaultRequest. Este comportamiento pilla a mucha gente:
Con el LimitRange de arriba (defaultRequest.memory: 128Mi) uno esperaría requests: 128Mi. Pero el resultado es:
La regla de Kubernetes de "si solo hay limits, la request iguala al limit" tiene prioridad. Consecuencia práctica: reservas 1 GiB del presupuesto del namespace sin quererlo.
Regla 3: si declaras requests pero no limits, se aplica default. Aquí sí funciona como uno espera... salvo que default sea menor que tu request, en cuyo caso el pod se rechaza por incoherente.
Regla 4: maxLimitRequestRatio limita el sobrecompromiso individual. Con memory: "4", un contenedor con requests.memory: 128Mi no puede tener limits.memory mayor de 512Mi. Es la herramienta para impedir que alguien reserve poco y luego consuma mucho, que es justo el patrón que provoca desalojos.
Tabla de los seis escenarios posibles, con el LimitRange del ejemplo:
| Lo que declara el contenedor | Resultado tras el LimitRange |
|---|---|
| Nada | requests: 100m/128Mi, limits: 300m/256Mi |
Solo requests: cpu 200m |
requests: 200m/128Mi, limits: 300m/256Mi |
Solo limits: memory 1Gi |
requests: 100m/**1Gi**, limits: 300m/1Gi |
| Ambos completos | Sin cambios (si pasa min, max y ratio) |
requests.memory: 16Mi |
Rechazado: menor que min (32Mi) |
requests: 128Mi, limits: 1Gi |
Rechazado: ratio 8 > maxLimitRequestRatio 4 |
- Demostración: un pod sin recursos sale con recursos
Nada convence tanto como verlo. Partimos de rutas-norte-dev con su cuota y aplicamos el LimitRange:
kubectl apply -f k8s/entornos/dev/limitrange.yaml
kubectl describe limitrange limites-dev -n rutas-norte-devlimitrange/limites-dev created
Name: limites-dev
Namespace: rutas-norte-dev
Type Resource Min Max Default Request Default Limit Max Limit/Request Ratio
---- -------- --- --- --------------- ------------- -----------------------
Container cpu 10m 2 100m 300m 10
Container memory 32Mi 2Gi 128Mi 256Mi 4Ahora el mismo kubectl run que antes fallaba:
kubectl run depurador --image=busybox:1.36 -n rutas-norte-dev -- sleep 3600
kubectl get pod depurador -n rutas-norte-devFunciona. Y lo interesante es ver qué recursos tiene realmente:
{
"limits": {
"cpu": "300m",
"memory": "256Mi"
},
"requests": {
"cpu": "100m",
"memory": "128Mi"
}
}El manifiesto que enviamos no tenía resources y el objeto guardado en etcd sí los tiene. El LimitRange los ha inyectado.
¿Quién hace esa inyección? Un controlador de admisión (admission controller) llamado LimitRanger, que forma parte del apiserver. Recuerda el flujo del módulo 1: una petición pasa por autenticación, autorización y admisión antes de escribirse en etcd. La admisión tiene dos fases:
flowchart LR
A["kubectl apply"] --> B["Autenticacion<br/>quien eres"]
B --> C["Autorizacion RBAC<br/>que puedes hacer"]
C --> D["Admision MUTANTE<br/>LimitRanger inyecta<br/>default y defaultRequest"]
D --> E["Admision VALIDANTE<br/>LimitRanger comprueba min/max<br/>ResourceQuota comprueba el total"]
E --> F["etcd<br/>objeto guardado YA MODIFICADO"]
El orden es crucial y explica todo lo del apartado 5: el LimitRange muta primero, la ResourceQuota valida después. Cuando la cuota mira el pod, este ya tiene sus recursos inyectados.
Y una consecuencia que hay que tener presente: el objeto en el clúster ya no es igual al fichero YAML de tu repositorio. Es la primera vez en el curso que esto pasa, y conviene saberlo para no desconcertarse en un kubectl diff. También la deja anotada el propio pod:
{
"kubernetes.io/limit-ranger": "LimitRanger plugin set: cpu, memory request for container depurador;
cpu, memory limit for container depurador"
}Comprobemos también los topes. Un contenedor pidiendo demasiado:
kubectl run gigante --image=nginx:1.27.1-alpine -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"gigante","image":"nginx:1.27.1-alpine",
"resources":{"requests":{"memory":"3Gi"},"limits":{"memory":"3Gi"}}}]}}'Error from server (Forbidden): pods "gigante" is forbidden:
maximum memory usage per Container is 2Gi, but limit is 3GiY uno pidiendo demasiado poco:
kubectl run enano --image=busybox:1.36 -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"enano","image":"busybox:1.36",
"resources":{"requests":{"memory":"8Mi"},"limits":{"memory":"8Mi"}}}]}}'Error from server (Forbidden): pods "enano" is forbidden:
minimum memory usage per Container is 32Mi, but request is 8MiLos tres comportamientos verificados: inyecta, pone techo y pone suelo.
- Los tipos:
Container, Pod y PersistentVolumeClaim
Container, Pod y PersistentVolumeClaimUn LimitRange puede tener varias entradas en spec.limits, cada una con su type:
apiVersion: v1
kind: LimitRange
metadata:
name: limites-pro
namespace: rutas-norte-pro
spec:
limits:
# 1. Por CONTENEDOR: el unico que admite default y defaultRequest
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 200m
memory: 256Mi
min:
cpu: 50m
memory: 64Mi
max:
cpu: "4"
memory: 8Gi
maxLimitRequestRatio:
cpu: "4"
memory: "2"
# 2. Por POD: suma de TODOS sus contenedores. No admite defaults
- type: Pod
max:
cpu: "8"
memory: 16Gi
min:
cpu: 50m
memory: 64Mi
# 3. Por PVC: tamanyo de disco
- type: PersistentVolumeClaim
min:
storage: 1Gi
max:
storage: 200GiDiferencias entre los tres:
| Tipo | Ámbito | Admite default/defaultRequest |
Uso típico |
|---|---|---|---|
Container |
Cada contenedor por separado | Sí | El principal: valores por defecto y topes |
Pod |
La suma de los contenedores del pod | No | Impedir pods con 10 sidecars gigantes |
PersistentVolumeClaim |
Tamaño del disco solicitado | No | Controlar el coste de almacenamiento |
El tipo Pod es útil cuando llegue el módulo 6 con los sidecars e init containers: un pod puede tener tres o cuatro contenedores, cada uno dentro del max de Container, pero sumando entre todos una cantidad desmesurada. El límite de Pod corta eso.
El tipo PersistentVolumeClaim cobrará sentido en el módulo 5. Con max: storage: 200Gi nadie puede pedir un disco de 2 TiB por error de tecleo, algo que en la nube se traduce en una factura desagradable a fin de mes.
- Interacción exacta entre LimitRange y ResourceQuota
Este es el punto donde todo encaja. La secuencia, para un pod que llega sin resources a un namespace con ambos objetos:
sequenceDiagram
participant U as kubectl apply
participant A as apiserver
participant LR as LimitRanger (mutante)
participant LV as LimitRanger (validante)
participant Q as ResourceQuota (validante)
participant E as etcd
U->>A: Pod sin resources
A->>LR: fase mutante
LR->>LR: inyecta requests 100m/128Mi<br/>y limits 300m/256Mi
LR->>LV: fase validante
LV->>LV: comprueba min, max y ratio -> OK
LV->>Q: siguiente validador
Q->>Q: used + 100m <= hard? -> OK
Q->>E: pod guardado CON recursos
E-->>U: pod/depurador created
Las cuatro consecuencias de este orden:
Consecuencia 1: el LimitRange salva a la ResourceQuota de su propio rigor. Sin LimitRange, la cuota obliga a declarar recursos a mano en todo. Con LimitRange, el valor por defecto se inyecta y la cuota lo contabiliza sin que nadie escriba nada.
Consecuencia 2: los valores inyectados consumen cuota. Esto no es gratis y hay que dimensionarlo. Si defaultRequest.memory es 128Mi y alguien lanza 20 pods de depuración sin recursos, se comen 2,5 GiB del presupuesto del namespace. Por eso los valores por defecto deben ser modestos: son para pods que nadie ha calibrado.
Consecuencia 3: un pod puede pasar el LimitRange y fallar en la cuota. Son validaciones independientes y hay que cumplir las dos:
Error from server (Forbidden): error when creating "pod-grande.yaml": pods "analitica" is forbidden:
exceeded quota: cuota-dev, requested: requests.memory=1Gi,
used: requests.memory=3600Mi, limited: requests.memory=4GiEse pod cumplía el max del LimitRange (2Gi), pero no cabía en lo que quedaba de cuota.
Consecuencia 4: cambiar un LimitRange no afecta a los pods existentes. La admisión actúa solo en la creación. Si subes el defaultRequest, los pods que ya corren conservan lo que se les inyectó. Para aplicar los nuevos valores hay que recrearlos:
Y una regla de coherencia entre ambos objetos que evita un problema desagradable: el max del LimitRange nunca debe superar la cuota del namespace. Si max.memory es 8Gi pero la cuota de requests.memory es 4Gi, alguien puede escribir un manifiesto que pase el LimitRange y sea imposible de desplegar. Es mejor que el rechazo llegue con el mensaje claro del LimitRange ("maximum memory usage per Container is 2Gi") que con el de la cuota, más difícil de interpretar.
- Las tres clases de QoS y la regla que las determina
Cambiamos de tema y llegamos a la parte más interesante. Cuando se crea un pod, Kubernetes le asigna automáticamente una clase de calidad de servicio en status.qosClass. No se declara: se deduce de cómo estén puestos los recursos.
La regla exacta, en orden de evaluación:
flowchart TB
A["Pod creado"] --> B{"Todos los contenedores<br/>declaran requests Y limits<br/>de CPU Y memoria?"}
B -->|"No"| E{"Algun contenedor<br/>declara alguna request<br/>o algun limit?"}
B -->|"Si"| C{"Para cada contenedor:<br/>requests == limits<br/>en CPU Y memoria?"}
C -->|"Si"| D["Guaranteed"]
C -->|"No"| F["Burstable"]
E -->|"Si"| F
E -->|"No: nada en absoluto"| G["BestEffort"]
En palabras precisas:
Guaranteed — se asigna si y solo si, para todos los contenedores del pod (incluidos los init containers):
- Se declaran
limitsde CPU y de memoria. - Se declaran
requestsde CPU y de memoria (o se omiten, en cuyo caso igualan a loslimits). requestses exactamente igual alimitsen ambos recursos.
BestEffort — se asigna si ningún contenedor del pod declara ninguna request ni ningún limit, de ningún recurso.
Burstable — todo lo demás. Es el cajón por defecto: al menos un contenedor declara algo, pero no se cumple la condición estricta de Guaranteed.
Ejemplos que aclaran los casos frontera:
# --- Guaranteed TAMBIEN (solo limits: las requests se copian) ---
resources:
limits:
cpu: 500m
memory: 512Mi# --- Burstable: la memoria coincide pero la CPU no ---
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 500m
memory: 512Mi# --- Burstable: falta el limit de CPU ---
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
memory: 512MiTres trampas que conviene memorizar:
Trampa 1: basta un contenedor para arruinar la clase. Si un pod tiene la aplicación con requests == limits y un sidecar de registro sin recursos, el pod entero es Burstable. La clase es del pod, no del contenedor.
Trampa 2: con LimitRange activo, BestEffort es casi imposible. El LimitRanger inyecta valores por defecto, así que ningún pod llega a BestEffort. Es un efecto secundario importante y en general deseable.
Trampa 3: defaultRequest distinto de default impide Guaranteed. Si el LimitRange inyecta requests: 100m y limits: 300m, todos los pods sin recursos serán Burstable. Para que un pod sea Guaranteed hay que declararlo explícitamente.
- Consultar la clase de un pod
kubectl get pod postgres-reservas-5d8f6b9c4-k2mnp -n rutas-norte-pro \
-o jsonpath='{.status.qosClass}'; echoPara todos los pods del namespace de un vistazo:
kubectl get pods -n rutas-norte-pro \
-o custom-columns='NOMBRE:.metadata.name,QOS:.status.qosClass,\
CPU_REQ:.spec.containers[0].resources.requests.cpu,\
CPU_LIM:.spec.containers[0].resources.limits.cpu,\
MEM_REQ:.spec.containers[0].resources.requests.memory,\
MEM_LIM:.spec.containers[0].resources.limits.memory'NOMBRE QOS CPU_REQ CPU_LIM MEM_REQ MEM_LIM
api-reservas-7f4b8c9d6-2xkqp Burstable 300m 1 512Mi 768Mi
api-reservas-7f4b8c9d6-8mvtr Burstable 300m 1 512Mi 768Mi
postgres-reservas-5d8f6b9c4-k2mnp Guaranteed 1 1 2Gi 2Gi
redis-cache-6c9d8f7b5-vn4qx Burstable 50m 200m 256Mi 320Mi
tienda-web-5b7c9f4d8-h7pnk Burstable 50m 200m 64Mi 128Mi
worker-notificaciones-6f7d9c8b4-lm3pt Burstable 100m 800m 320Mi 512MiUn recuento rápido por clase:
Esos seis BestEffort merecen una mirada: en un clúster de producción son los primeros candidatos a morir, así que conviene saber quiénes son y si eso es intencionado.
kubectl get pods -A -o json \
| jq -r '.items[] | select(.status.qosClass=="BestEffort") | "\(.metadata.namespace)/\(.metadata.name)"'describe también la muestra:
- Consecuencia 1:
oom_score_adj y el OOM killer del kernel
oom_score_adj y el OOM killer del kernelAquí está la primera consecuencia real de la clase, y es de vida o muerte, literalmente.
Cuando el kernel de Linux se queda sin memoria, ejecuta el OOM killer, que elige una víctima. La elección se basa en una puntuación por proceso, oom_score, que combina la memoria que consume con un ajuste manual llamado oom_score_adj, un valor entre -1000 y 1000. Cuanto mayor sea, antes muere.
El kubelet establece ese ajuste según la clase de QoS:
| Clase de QoS | oom_score_adj |
Orden de sacrificio |
|---|---|---|
Guaranteed |
-997 | El último en morir |
Burstable |
Entre 2 y 999, según la request de memoria |
En medio |
BestEffort |
1000 | El primero en morir |
La fórmula para Burstable es la que da todo el matiz:
Léela despacio: cuanto mayor sea tu request de memoria, menor es tu puntuación y más tarde mueres. Un pod Burstable que pide mucha memoria está casi tan protegido como uno Guaranteed; uno que pide muy poca está casi tan expuesto como un BestEffort.
Comprobémoslo en un nodo con 8 GiB asignables. Para api-reservas con requests.memory: 512Mi:
kubectl exec -n rutas-norte-pro deploy/api-reservas -- cat /proc/1/oom_score_adj
kubectl exec -n rutas-norte-pro deploy/postgres-reservas -- cat /proc/1/oom_score_adjExactamente lo previsto. Y la conclusión de negocio es directa:
Cuando al nodo le falte memoria, el kernel matará
api-reservasmucho antes quepostgres-reservas. Y eso es justamente lo que queremos: perder un pod de la API significa perder unas peticiones que el cliente puede reintentar; perder la base de datos significa que toda la plataforma deja de vender billetes y, en el peor caso, que una transacción a medias corrompe datos.
Hay que distinguir dos situaciones distintas que se confunden a menudo:
| OOM del cgroup | OOM del nodo | |
|---|---|---|
| Causa | Un contenedor supera su propio limits.memory |
La suma de todo supera la memoria del nodo |
| Quién muere | Ese contenedor concreto | El proceso con mayor oom_score del nodo |
| Influye la clase de QoS | No: mueres por tu propio límite | Sí: es lo que decide |
| Se ve como | OOMKilled en ese pod |
OOMKilled en un pod que "no había hecho nada" |
| Se previene con | Un limits.memory bien calibrado |
No sobrecomprometer memoria + QoS bien asignada |
El segundo caso es el desconcertante: un pod muere y sus métricas muestran que estaba muy por debajo de su límite. La causa está en un vecino que se descontroló, y la clase de QoS es lo que determinó que la víctima fuera él y no otro.
- Consecuencia 2: el orden de desalojo del kubelet
Antes de que el kernel llegue a matar procesos, el kubelet tiene su propio mecanismo, más ordenado: el desalojo por presión de recursos (eviction).
El kubelet vigila los recursos del nodo cada 10 segundos. Cuando cruza un umbral, marca la condición correspondiente (MemoryPressure, DiskPressure, PIDPressure) y empieza a desalojar pods para recuperar recursos.
Los umbrales por defecto:
| Señal | Umbral por defecto | Condición que activa |
|---|---|---|
memory.available |
< 100Mi |
MemoryPressure |
nodefs.available |
< 10% |
DiskPressure |
nodefs.inodesFree |
< 5% |
DiskPressure |
imagefs.available |
< 15% |
DiskPressure |
Y el criterio de selección de las víctimas, en este orden estricto:
- Primero, si el pod excede sus
requests. Los que consumen más de lo que reservaron son candidatos antes que los que se mantienen dentro. - Segundo, la clase de QoS:
BestEffortprimero, luegoBurstable, yGuaranteeden último lugar. - Tercero, la PriorityClass, si está definida.
- Cuarto, cuánto excede el uso sobre la
request. Entre dos candidatos iguales, cae el que más se ha pasado.
Dicho de forma operativa:
El primer pod en caer es un
BestEffort. Si no hay ninguno, cae elBurstableque más se haya pasado de surequest. Un podGuaranteedque se mantiene dentro de sus recursos es prácticamente intocable.
Un pod desalojado se ve así:
kubectl get pods -n rutas-norte-dev
kubectl describe pod analitica-puntual -n rutas-norte-dev | head -14NAME READY STATUS RESTARTS AGE
analitica-puntual 0/1 Evicted 0 8m
Name: analitica-puntual
Namespace: rutas-norte-dev
Status: Failed
Reason: Evicted
Message: The node was low on resource: memory. Threshold quantity: 100Mi,
available: 84Mi. Container analitica was using 1204Mi,
request is 0, which exceeds its request of 0.Ese mensaje lo dice todo: request is 0 porque era BestEffort, y por tanto cualquier consumo excede su reserva. Fue el candidato perfecto.
Dos apuntes prácticos importantes:
Los pods desalojados no se borran solos. Quedan en Failed ocupando espacio en etcd. Conviene limpiarlos:
Un pod desalojado que pertenece a un Deployment se recrea. El ReplicaSet del módulo 2 ve que falta una réplica y crea otra, posiblemente en otro nodo. Un pod suelto desalojado, en cambio, no vuelve nunca. Otro motivo más para no desplegar pods sueltos.
- La clase de QoS de cada componente de Rutas Norte
Ahora la decisión de diseño, que es lo que da sentido a todo lo anterior. La pregunta es, para cada componente: ¿qué pasa si este pod muere de repente?
| Componente | Clase | Justificación de negocio |
|---|---|---|
postgres-reservas |
Guaranteed |
Guarda las reservas y los datos personales de los clientes. Si muere, la plataforma no vende. Una muerte violenta puede dejar transacciones a medias. Debe ser el último en caer y su rendimiento debe ser previsible |
api-reservas |
Burstable |
Tiene varias réplicas y es sin estado: si una muere, el Service reparte a las demás y el cliente reintenta. Necesita ráfaga de CPU en puentes, y requests == limits desperdiciaría capacidad el 95 % del tiempo |
tienda-web |
Burstable |
Sirve estáticos con consumo mínimo y muy variable. Tres réplicas, sin estado, arranque en segundos |
redis-cache |
Burstable |
Tiene estado pero es prescindible: si muere, la caché se pierde y api-reservas vuelve a consultar postgres-reservas. Más lento, pero correcto |
worker-notificaciones |
Burstable |
Asíncrono: si muere a media tanda, las notificaciones pendientes siguen en la base de datos y la siguiente ejecución las recoge |
informes-ocupacion |
Burstable con recursos bajos |
Tarea nocturna (módulo 6). Si el nodo tiene presión, que muera: se reintenta mañana |
| Análisis puntual ad hoc | BestEffort |
Consultas exploratorias de un analista. Es lo primero que debe caer, siempre |
Los manifiestos concretos. postgres-reservas como Guaranteed:
- name: postgres
image: postgres:16.4
resources:
# Guaranteed: requests == limits en CPU y memoria.
# Decision deliberada: esta base de datos guarda datos personales
# de clientes y es el ultimo pod que debe morir en el nodo.
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "1"
memory: 2Gikubectl get pod -l app=postgres-reservas -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echoapi-reservas como Burstable:
- name: api
image: registry.rutasnorte.example/api-reservas:2.5.0
resources:
# Burstable deliberado: 4 replicas sin estado detras de un Service.
# El limit de CPU triplica la request para absorber los picos de puentes.
requests:
cpu: 300m
memory: 512Mi
limits:
cpu: "1"
memory: 768MiEl análisis puntual como BestEffort:
apiVersion: v1
kind: Pod
metadata:
name: analisis-ocupacion-julio
namespace: rutas-norte-dev
labels:
app: analisis-puntual
app.kubernetes.io/part-of: rutas-norte
entorno: dev
annotations:
rutasnorte.example/motivo: "analisis exploratorio de ocupacion, RN-612"
rutasnorte.example/responsable: "[email protected]"
spec:
restartPolicy: Never
containers:
- name: analisis
image: postgres:16.4
command: ["sh", "-c", "psql -h postgres-reservas -c 'SELECT ...' > /tmp/salida.csv"]
resources: {} # vacio A PROPOSITO: BestEffort, el primero en caerOjo: con un LimitRange activo en rutas-norte-dev, ese resources: {} no producirá BestEffort, porque el LimitRanger inyectará los valores por defecto. Para conseguir un pod BestEffort de verdad hay que usar un namespace sin LimitRange, y por eso el experimento del apartado 12 usa uno aparte.
Un caso que merece comentario: redis-cache es Burstable aunque tenga estado. ¿No debería protegerse como postgres-reservas? No, y la diferencia es el valor del dato. redis-cache guarda la disponibilidad de plazas cacheada: si se pierde, se recalcula consultando la base de datos. El impacto es un pico de latencia y de carga sobre postgres-reservas, no una pérdida de información. Lo que determina la clase no es si el componente tiene estado, sino qué se pierde si muere.
- El LimitRange de los tres entornos
Cerramos la parte de configuración con los tres objetos, coherentes con las cuotas de la lección anterior.
rutas-norte-dev
apiVersion: v1
kind: LimitRange
metadata:
name: limites-dev
namespace: rutas-norte-dev
labels:
app.kubernetes.io/part-of: rutas-norte
entorno: dev
spec:
limits:
- type: Container
# Valores por defecto modestos: son para pods sin calibrar
default:
cpu: 300m
memory: 256Mi
defaultRequest:
cpu: 100m
memory: 128Mi
min:
cpu: 10m
memory: 32Mi
# max muy por debajo de la cuota (2 CPU / 4Gi): un solo pod no la agota
max:
cpu: "1"
memory: 1Gi
maxLimitRequestRatio:
cpu: "10" # generoso: en dev interesa iterar sin pelearse con el limite
memory: "4"rutas-norte-pre
apiVersion: v1
kind: LimitRange
metadata:
name: limites-pre
namespace: rutas-norte-pre
labels:
app.kubernetes.io/part-of: rutas-norte
entorno: pre
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 200m
memory: 256Mi
min:
cpu: 50m
memory: 64Mi
max:
cpu: "2"
memory: 4Gi
maxLimitRequestRatio:
cpu: "4" # mas estricto: pre debe parecerse a pro
memory: "2"
- type: Pod
max:
cpu: "4"
memory: 6Girutas-norte-pro
apiVersion: v1
kind: LimitRange
metadata:
name: limites-pro
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
limits:
- type: Container
# En pro los defaults son una RED DE SEGURIDAD, no una comodidad:
# todo componente de produccion debe declarar sus recursos calibrados.
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 200m
memory: 256Mi
min:
cpu: 50m
memory: 64Mi
max:
cpu: "4"
memory: 8Gi
maxLimitRequestRatio:
cpu: "4"
memory: "2" # impide reservar poco y consumir mucho: evita desalojos
- type: Pod
max:
cpu: "8"
memory: 16Gi
- type: PersistentVolumeClaim
min:
storage: 1Gi
max:
storage: 200Gi # nadie pide 2 TiB por un cero de masComparativa de las decisiones:
| Parámetro | dev |
pre |
pro |
Razonamiento |
|---|---|---|---|---|
defaultRequest.memory |
128Mi | 256Mi | 256Mi | En dev hay muchos pods pequeños de prueba |
max.memory (contenedor) |
1Gi | 4Gi | 8Gi | En pro postgres-reservas necesita margen para crecer |
maxLimitRequestRatio.memory |
4 | 2 | 2 | En pro el sobrecompromiso agresivo provoca desalojos |
Límite de Pod |
No | Sí | Sí | Se activa cuando aparecen los sidecars del módulo 6 |
| Límite de PVC | No | No | Sí | Control de coste del almacenamiento de producción |
kubectl apply -f k8s/entornos/dev/limitrange.yaml \
-f k8s/entornos/pre/limitrange.yaml \
-f k8s/entornos/pro/limitrange.yaml
kubectl get limitrange -Alimitrange/limites-dev created
limitrange/limites-pre created
limitrange/limites-pro created
NAMESPACE NAME CREATED AT
rutas-norte-dev limites-dev 2026-08-05T21:14:07Z
rutas-norte-pre limites-pre 2026-08-05T21:14:07Z
rutas-norte-pro limites-pro 2026-08-05T21:14:08Z
- Experimento: provocar un desalojo y ver quién cae primero
Toca comprobar la teoría. Vamos a crear un namespace sin LimitRange (para poder tener pods BestEffort de verdad), desplegar tres pods de las tres clases, y presionar la memoria del nodo hasta que el kubelet empiece a desalojar.
Aviso: hazlo en tu minikube de prácticas, nunca en un clúster compartido. Vamos a provocar deliberadamente presión de memoria en un nodo.
Paso 1: preparar el terreno.
kubectl create namespace laboratorio-qos
kubectl get node rutas-norte -o jsonpath='{.status.allocatable.memory}'; echoUnos 7,7 GiB asignables.
Paso 2: los tres pods.
# laboratorio-qos.yaml
apiVersion: v1
kind: Pod
metadata:
name: victima-besteffort
namespace: laboratorio-qos
labels: {rol: victima}
spec:
containers:
- name: carga
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "700M", "--vm-hang", "0"]
# resources ausente a proposito -> BestEffort
---
apiVersion: v1
kind: Pod
metadata:
name: victima-burstable
namespace: laboratorio-qos
labels: {rol: victima}
spec:
containers:
- name: carga
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "700M", "--vm-hang", "0"]
resources:
requests:
cpu: 100m
memory: 200Mi # pide POCO y consume MUCHO: candidato ideal
limits:
cpu: 500m
memory: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: protegido-guaranteed
namespace: laboratorio-qos
labels: {rol: protegido}
spec:
containers:
- name: carga
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "700M", "--vm-hang", "0"]
resources:
requests:
cpu: 200m
memory: 1Gi
limits:
cpu: 200m
memory: 1Gi # requests == limits -> Guaranteedkubectl apply -f laboratorio-qos.yaml
sleep 20
kubectl get pods -n laboratorio-qos -o custom-columns='NOMBRE:.metadata.name,QOS:.status.qosClass,ESTADO:.status.phase'pod/victima-besteffort created
pod/victima-burstable created
pod/protegido-guaranteed created
NOMBRE QOS ESTADO
protegido-guaranteed Guaranteed Running
victima-besteffort BestEffort Running
victima-burstable Burstable RunningLas tres clases confirmadas. Verifiquemos también sus oom_score_adj:
for P in victima-besteffort victima-burstable protegido-guaranteed; do
printf '%-22s ' "$P"
kubectl exec -n laboratorio-qos "$P" -- cat /proc/1/oom_score_adj
doneEl 975 de victima-burstable sale de la fórmula del apartado 8: 1000 - (1000 × 200Mi / 7751Mi) = 1000 - 25 = 975. Pide poquísimo, así que está casi tan expuesto como el BestEffort.
Paso 3: apretar hasta provocar la presión.
kubectl run presion --image=polinux/stress:1.0.4 -n laboratorio-qos --restart=Never -- \
stress --vm 1 --vm-bytes 4500M --vm-hang 0Paso 4: observar.
NAME READY STATUS RESTARTS AGE
presion 1/1 Running 0 12s
protegido-guaranteed 1/1 Running 0 3m
victima-besteffort 1/1 Running 0 3m
victima-burstable 1/1 Running 0 3m
victima-besteffort 0/1 Evicted 0 3m21s <-- PRIMERO
victima-burstable 0/1 Evicted 0 3m48s <-- SEGUNDO
protegido-guaranteed 1/1 Running 0 4m10s <-- SOBREVIVEEl orden es exactamente el previsto: primero el BestEffort, después el Burstable que más se había pasado de su request, y el Guaranteed sigue en pie.
Paso 5: leer los mensajes de desalojo.
Status: Failed
Reason: Evicted
Message: The node was low on resource: memory. Threshold quantity: 100Mi, available: 78Mi.
Container carga was using 712Mi, request is 0, which exceeds its request of 0.Status: Failed
Reason: Evicted
Message: The node was low on resource: memory. Threshold quantity: 100Mi, available: 92Mi.
Container carga was using 705Mi, request is 200Mi, which exceeds its request of 200Mi.Compara las dos últimas frases: el BestEffort excedía una reserva de 0 (todo lo que use excede), y el Burstable excedía su reserva de 200Mi en 505Mi. Ambos eran candidatos; el BestEffort primero por clase.
Paso 6: la condición del nodo y los eventos.
kubectl get node rutas-norte -o jsonpath='{.status.conditions[?(@.type=="MemoryPressure")].status}'; echo
kubectl get events -n laboratorio-qos --sort-by=.lastTimestamp | tail -5True
LAST SEEN TYPE REASON OBJECT MESSAGE
2m Warning Evicted pod/victima-besteffort The node was low on resource: memory
2m Normal Killing pod/victima-besteffort Stopping container carga
94s Warning Evicted pod/victima-burstable The node was low on resource: memory
94s Normal Killing pod/victima-burstable Stopping container cargaPaso 7: limpiar.
kubectl delete namespace laboratorio-qos
kubectl get node rutas-norte -o jsonpath='{.status.conditions[?(@.type=="MemoryPressure")].status}'; echoLo que este experimento demuestra, traducido a Rutas Norte: si un puente de agosto provoca presión de memoria en el nodo donde vive postgres-reservas, la base de datos no será la que caiga. Caerán antes los pods de análisis puntual, después las réplicas de api-reservas que se hayan pasado de su reserva —y el Service seguirá repartiendo entre las que queden—, y la plataforma seguirá vendiendo billetes. Esa cadena de supervivencia no es casualidad: la has diseñado tú al asignar las clases.
Errores Comunes y Consejos
| Error | Síntoma | Solución |
|---|---|---|
| Cuota sin LimitRange | Todo kubectl run falla |
Añade siempre un LimitRange junto a la cuota |
Esperar defaultRequest cuando solo hay limits |
Se reserva mucho más de lo previsto | La request copia el limit, no el defaultRequest |
max del LimitRange por encima de la cuota |
Manifiestos válidos pero indesplegables | Deja max claramente por debajo del techo del namespace |
| Cambiar el LimitRange esperando efecto retroactivo | Los pods viejos conservan sus valores | kubectl rollout restart |
Un sidecar sin recursos en un pod Guaranteed |
El pod entero es Burstable |
La clase es del pod: todos los contenedores deben cumplirla |
Creer que Guaranteed inmuniza contra OOMKilled |
El pod muere igual | Guaranteed protege del OOM del nodo, no de superar tu propio límite |
Pods BestEffort en producción |
Desalojos inesperados | Solo para trabajo verdaderamente descartable |
Burstable con request mínima y limit enorme |
Desalojado constantemente | Sube la request o baja el maxLimitRequestRatio |
Pods Evicted acumulados |
Ruido en kubectl get pods y en etcd |
kubectl delete pods -A --field-selector=status.phase=Failed |
| Pod suelto desalojado | Desaparece y no vuelve | Usa Deployments, no pods sueltos |
| Confundir cuota y LimitRange | Se pone el objeto equivocado | Cuota = total del namespace; LimitRange = por contenedor |
No revisar qosClass tras cambiar recursos |
Se pierde Guaranteed sin darse cuenta |
Comprobar con -o jsonpath='{.status.qosClass}' en el pipeline |
Consejos:
- Cuota y LimitRange van siempre juntos. Trátalos como una sola decisión al crear un namespace.
- Valores por defecto modestos,
maxgeneroso. El valor por defecto es para lo no calibrado; elmaxes una red de seguridad contra los ceros de más. Guaranteedsolo para lo verdaderamente crítico. Cuesta capacidad real: reservas el máximo todo el tiempo. En Rutas Norte, solopostgres-reservas.- Vigila los
Burstablecon relación alta. Un pod conrequestde 200Mi ylimitde 4Gi es un candidato permanente al desalojo.maxLimitRequestRatiolo previene desde el namespace. - Añade la clase de QoS a tus revisiones de manifiestos. Es una línea en la revisión de una pull request y evita descubrir en un incidente que la base de datos era
Burstable.
Ejercicios
Ejercicio 1: Los seis escenarios del LimitRange
Con el LimitRange limites-dev del apartado 11 aplicado en rutas-norte-dev:
- Crea seis pods, uno por cada fila de la tabla del apartado 2 (sin nada, solo
requestsde CPU, sololimitsde memoria de 1Gi, ambos completos,requests.memory: 16Mi, yrequests: 128Miconlimits: 1Gi). - Para los que se creen, muestra los
resourcesefectivos y su clase de QoS. - Para los que fallen, copia el mensaje de error exacto.
- Explica en particular por qué el tercero acaba con
requests.memory: 1Gien lugar de 128Mi, y qué consecuencia tiene sobre la cuota del namespace. - Calcula cuánta cuota consumirían entre todos si se hubieran creado los seis.
Ejercicio 2: Auditar y corregir las clases de QoS
- Lista todos los pods de los tres namespaces de Rutas Norte con su clase de QoS en una sola tabla.
- Comprueba si
postgres-reservasesGuaranteeden los tres entornos. Si no lo es en alguno, identifica por qué. - Modifica el manifiesto de
postgres-reservaspara que seaGuaranteedenrutas-norte-pro, verifica el cambio y comprueba suoom_score_adj. - Calcula a mano el
oom_score_adjesperado deapi-reservasconrequests.memory: 512Mien un nodo con 7751Mi asignables, y compáralo con el valor real. - Añade un sidecar sin recursos a
postgres-reservasy observa qué le pasa a la clase del pod. Explica el resultado y revierte el cambio.
Ejercicio 3: Reproducir el desalojo y razonar el diseño
- Reproduce el experimento del apartado 12 en tu minikube.
- Anota el orden exacto de los desalojos y los mensajes de
describe. - Modifica
victima-burstablepara que surequests.memorysea 1Gi (en lugar de 200Mi) manteniendo el mismo consumo, y repite el experimento. ¿Cambia el orden? Explica por qué usando la fórmula deloom_score_adj. - Explica qué habría pasado si
postgres-reservasde Rutas Norte hubiera estado en ese nodo comoBurstableconrequests.memory: 256Miy un consumo real de 1,8 GiB. - Propón dos medidas adicionales, más allá de la clase de QoS, para proteger
postgres-reservasde este escenario, e indica en qué lección del curso se estudia cada una.
Soluciones
Solución 1
# 1. Sin nada
kubectl run p1 --image=busybox:1.36 -n rutas-norte-dev -- sleep 3600
# 2. Solo requests de CPU
kubectl run p2 --image=busybox:1.36 -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"p2","image":"busybox:1.36",
"command":["sleep","3600"],"resources":{"requests":{"cpu":"200m"}}}]}}'
# 3. Solo limits de memoria
kubectl run p3 --image=busybox:1.36 -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"p3","image":"busybox:1.36",
"command":["sleep","3600"],"resources":{"limits":{"memory":"1Gi"}}}]}}'
# 4. Ambos completos e iguales
kubectl run p4 --image=busybox:1.36 -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"p4","image":"busybox:1.36",
"command":["sleep","3600"],"resources":{"requests":{"cpu":"200m","memory":"256Mi"},
"limits":{"cpu":"200m","memory":"256Mi"}}}]}}'
# 5. Por debajo del minimo
kubectl run p5 --image=busybox:1.36 -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"p5","image":"busybox:1.36",
"command":["sleep","3600"],"resources":{"requests":{"memory":"16Mi"}}}]}}'
# 6. Ratio excesivo
kubectl run p6 --image=busybox:1.36 -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"p6","image":"busybox:1.36",
"command":["sleep","3600"],"resources":{"requests":{"memory":"128Mi"},
"limits":{"memory":"1Gi"}}}]}}'pod/p1 created
pod/p2 created
pod/p3 created
pod/p4 created
Error from server (Forbidden): pods "p5" is forbidden:
minimum memory usage per Container is 32Mi, but request is 16Mi
Error from server (Forbidden): pods "p6" is forbidden:
memory max limit to request ratio per Container is 4, but provided ratio is 8.000000kubectl get pods -n rutas-norte-dev -o custom-columns='N:.metadata.name,QOS:.status.qosClass,\
RC:.spec.containers[0].resources.requests.cpu,RM:.spec.containers[0].resources.requests.memory,\
LC:.spec.containers[0].resources.limits.cpu,LM:.spec.containers[0].resources.limits.memory' \
| grep -E "^(N|p[0-9])"N QOS RC RM LC LM
p1 Burstable 100m 128Mi 300m 256Mi
p2 Burstable 200m 128Mi 300m 256Mi
p3 Burstable 100m 1Gi 300m 1Gi
p4 Guaranteed 200m 256Mi 200m 256MiEl caso p3 es el interesante: declaramos solo limits.memory: 1Gi y el resultado es requests.memory: 1Gi, no los 128Mi del defaultRequest. La razón es el orden de resolución: Kubernetes aplica primero su regla general de "si hay limit y no hay request, la request iguala al limit", y el defaultRequest del LimitRange solo rellena lo que sigue vacío después de eso.
La consecuencia sobre la cuota es seria: este pod, que probablemente consuma 4 MiB de memoria real, ha comprometido 1 GiB del presupuesto de 4 GiB del namespace. Con cuatro pods así, rutas-norte-dev se queda sin cuota de memoria sin que nadie esté usando nada. Es un error muy caro y muy invisible.
Consumo total si se hubieran creado los seis (solo cuentan los cuatro válidos):
| Pod | requests.cpu |
requests.memory |
|---|---|---|
p1 |
100m | 128Mi |
p2 |
200m | 128Mi |
p3 |
100m | 1024Mi |
p4 |
200m | 256Mi |
| Total | 600m de 2000m (30 %) | 1536Mi de 4096Mi (37,5 %) |
p3 solo es el 25 % de los pods y consume el 67 % de la memoria comprometida.
Solución 2
for NS in rutas-norte-dev rutas-norte-pre rutas-norte-pro; do
kubectl get pods -n $NS -o custom-columns='NS:.metadata.namespace,POD:.metadata.name,QOS:.status.qosClass' --no-headers
donerutas-norte-dev api-reservas-6d8f7c9b-4kx2p Burstable
rutas-norte-dev postgres-reservas-5d8f6b9c4-t8wmz Burstable
rutas-norte-dev redis-cache-6c9d8f7b5-p2njq Burstable
rutas-norte-dev tienda-web-5b7c9f4d8-h7pnk Burstable
rutas-norte-pre postgres-reservas-5d8f6b9c4-v4rbd Burstable
rutas-norte-pro api-reservas-7f4b8c9d6-2xkqp Burstable
rutas-norte-pro postgres-reservas-5d8f6b9c4-k2mnp Burstable
rutas-norte-pro redis-cache-6c9d8f7b5-vn4qx Burstablepostgres-reservas no es Guaranteed en ningún entorno. Motivo:
kubectl get pod -l app=postgres-reservas -n rutas-norte-pro \
-o jsonpath='{.items[0].spec.containers[0].resources}' | jq .La memoria coincide pero la CPU no (500m frente a 2). Basta un recurso desigual para perder Guaranteed. Es un error clásico: se iguala la memoria pensando en el OOM y se olvida la CPU.
kubectl patch deployment postgres-reservas -n rutas-norte-pro --type='json' -p='[
{"op":"replace","path":"/spec/template/spec/containers/0/resources/requests/cpu","value":"1"},
{"op":"replace","path":"/spec/template/spec/containers/0/resources/limits/cpu","value":"1"}
]'
kubectl rollout status deploy/postgres-reservas -n rutas-norte-pro
kubectl get pod -l app=postgres-reservas -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echo
kubectl exec -n rutas-norte-pro deploy/postgres-reservas -- cat /proc/1/oom_score_adjdeployment.apps/postgres-reservas patched
deployment "postgres-reservas" successfully rolled out
Guaranteed
-997El cálculo de api-reservas:
Coincide. La diferencia de 1931 puntos con postgres-reservas es enorme: en la práctica, el kernel sacrificará todas las réplicas de la API antes de tocar la base de datos.
El sidecar:
kubectl get pod -l app=postgres-reservas -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echoEl pod ha perdido Guaranteed por culpa de un sidecar sin recursos. En rutas-norte-pro el LimitRange le inyecta requests: 200m/256Mi y limits: 500m/512Mi, que son distintos entre sí, y eso basta. La corrección es dar al sidecar requests == limits también:
- name: exportador-metricas
image: prometheuscommunity/postgres-exporter:v0.15.0
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 50m
memory: 64MiCon eso el pod recupera Guaranteed. Lección: en un pod que debe ser Guaranteed, todos sus contenedores deben serlo, incluidos los sidecars y los init containers.
Solución 3
El orden observado es BestEffort → Burstable → el Guaranteed sobrevive, con los mensajes del apartado 12.
Con requests.memory: 1Gi en victima-burstable y el mismo consumo de 700Mi:
kubectl exec -n laboratorio-qos victima-burstable -- cat /proc/1/oom_score_adj
kubectl get pods -n laboratorio-qos -w868
NAME READY STATUS RESTARTS AGE
victima-besteffort 0/1 Evicted 0 2m14s
victima-burstable 1/1 Running 0 3m40s <-- ahora SOBREVIVE
protegido-guaranteed 1/1 Running 0 3m40sEl Burstable ya no cae. Dos motivos que actúan a la vez:
- Su
oom_score_adjbajó de 975 a 868, así que el kernel lo prioriza menos como víctima. - Y sobre todo, ya no excede su
request: consume 700Mi y reservó 1Gi. El primer criterio de selección del kubelet (apartado 9) es precisamente "pods que exceden susrequests", y este ya no está en ese grupo.
La moraleja operativa es contundente: declarar una request de memoria realista es la protección más barata que existe contra el desalojo. No cambia lo que consumes; cambia si eres candidato o no.
- Si
postgres-reservashubiera sidoBurstableconrequests.memory: 256Miy un consumo real de 1,8 GiB:
Un 967, casi tan expuesto como un BestEffort, y excediendo su request en más de 1,5 GiB, lo que lo pone en el primer grupo de candidatos. Habría sido de los primeros en caer. Para Rutas Norte eso significa: la base de datos se cae en el pico de venta de un puente, api-reservas empieza a devolver errores de conexión, tienda-web muestra errores a los clientes, y en el peor caso una transacción de reserva a medias deja plazas bloqueadas sin billete emitido. Un Recreate de PostgreSQL además implica recuperación del WAL al arrancar, con minutos de indisponibilidad.
Todo eso lo evita una línea: requests == limits.
- Dos medidas adicionales:
| Medida | Qué aporta | Dónde se estudia |
|---|---|---|
| PodDisruptionBudget | Impide que una operación voluntaria (drenar un nodo para mantenimiento) deje postgres-reservas sin ninguna réplica disponible |
09-05 |
| Taints, tolerations y afinidad de nodo | Reservar un nodo para la base de datos, sin vecinos ruidosos que puedan generar presión de memoria | 06-05 |
Y una tercera, la más importante a medio plazo: convertir postgres-reservas en un StatefulSet con volumen persistente y copias de seguridad verificadas, de modo que la supervivencia del dato no dependa de la supervivencia del pod. Es el trabajo de los módulos 5 y 6.
Conclusión
Has cerrado los dos cabos sueltos que dejó la lección anterior. El LimitRange convierte una cuota estricta en algo llevable: inyecta default y defaultRequest en los contenedores que no declaran nada, pone suelo y techo con min y max, y limita el sobrecompromiso individual con maxLimitRequestRatio. Sabes que actúa como controlador de admisión mutante antes de que la ResourceQuota valide, que por tanto los valores inyectados consumen cuota, que no es retroactivo, y que los tres tipos —Container, Pod y PersistentVolumeClaim— cubren desde el contenedor suelto hasta el coste de un disco. Y conoces la regla que más dinero cuesta cuando se ignora: si declaras solo limits, la request copia el limit, no el defaultRequest, y con ello reservas mucho más de lo que crees.
Dominas también las clases de calidad de servicio y la regla exacta que las determina: Guaranteed exige requests == limits de CPU y memoria en todos los contenedores del pod, BestEffort exige que ninguno declare nada, y Burstable es todo lo demás. Sabes consultarla con -o jsonpath='{.status.qosClass}' y, sobre todo, sabes qué consecuencias tiene: el oom_score_adj que el kubelet escribe en cada proceso (-997 para Guaranteed, hasta 1000 para BestEffort, y una fórmula proporcional a la request de memoria para Burstable) y el orden de desalojo del kubelet, que mira primero quién excede sus requests y después la clase. Has distinguido el OOM del cgroup —mueres por tu propio límite, sin importar la clase— del OOM del nodo, donde la clase lo decide todo.
Rutas Norte tiene ahora un diseño razonado: postgres-reservas es Guaranteed porque guarda las reservas y los datos personales de los clientes y su muerte para la venta; api-reservas, tienda-web, redis-cache y worker-notificaciones son Burstable porque tienen réplicas, son recuperables y necesitan ráfaga en los puentes; y el análisis exploratorio es BestEffort porque debe ser lo primero en caer. Los tres entornos tienen su LimitRange coherente con su cuota. Y lo has comprobado provocando un desalojo real en el minikube, viendo caer el BestEffort primero, el Burstable que se había pasado de su reserva después, y sobrevivir al Guaranteed; y descubriendo de paso que subir una request a un valor realista es la protección más barata que existe contra el desalojo.
Queda una última pieza del módulo, y es de otra naturaleza. Hemos dado a nuestros componentes su configuración, sus credenciales y sus recursos, pero no les hemos dado identidad. Todos los pods de Rutas Norte están usando ahora mismo la ServiceAccount default de su namespace, con un token montado que ninguno de ellos necesita, y ninguno tiene forma de decirle a la API de Kubernetes quién es. La lección siguiente, ServiceAccounts y Acceso a la API desde los Pods, lo resuelve: verás la diferencia entre usuarios y ServiceAccounts, por qué usar la default es mala idea, cómo han cambiado los tokens (de secretos eternos a tokens proyectados de vida limitada que el kubelet rota), qué hay exactamente en /var/run/secrets/kubernetes.io/serviceaccount/, por qué automountServiceAccountToken: false debe ser tu opción por defecto, y hablarás con la API desde dentro de un pod con curl para ver tanto una respuesta autorizada como un 403 Forbidden bien merecido.
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
