La lliçó anterior va posar quotes als tres entorns de Rutas Norte i va deixar dos caps solts molt concrets. El primer és pràctic i molesta cada dia: des que rutas-norte-dev té quota de computació, un simple kubectl run falla amb must specify requests.cpu, i qualsevol manifest d'un tercer que no declari recursos és inaplicable. El segon és més profund: sabem que quan a un node li falta memòria algú ha de morir, però no sabem qui, i aquesta decisió no és gens aleatòria. Aquesta lliçó tanca els dos: el LimitRange injecta valors per defecte i posa topalls per contenidor dins del namespace, i les classes de qualitat de servei —Guaranteed, Burstable i BestEffort— determinen l'ordre exacte en què el kubelet desallotja i en què el kernel mata. Al final assignaràs una classe raonada a cada component de Rutas Norte i provocaràs un desallotjament real al teu minikube per veure-ho amb els teus propis ulls.
Contingut
- Què resol un LimitRange
- Els camps:
default,defaultRequest,min,max,maxLimitRequestRatio - Demostració: un pod sense recursos surt amb recursos
- Els tipus:
Container,PodiPersistentVolumeClaim - Interacció exacta entre LimitRange i ResourceQuota
- Les tres classes de QoS i la regla que les determina
- Consultar la classe d'un pod
- Conseqüència 1:
oom_score_adji l'OOM killer del kernel - Conseqüència 2: l'ordre de desallotjament del kubelet
- La classe de QoS de cada component de Rutas Norte
- El LimitRange dels tres entorns
- Experiment: provocar un desallotjament i veure qui cau primer
- Què resol un LimitRange
El LimitRange és un objecte amb namespace que fa dues coses diferents sobre cada contenidor que s'hi creï:
- Injecta valors per defecte als contenidors que no declaren
requestsolimits. - Valida topalls: rebutja els contenidors que demanen menys d'un mínim o més d'un màxim.
La diferència amb la ResourceQuota és l'àmbit, i convé tenir-la claríssima perquè es confonen constantment:
| ResourceQuota | LimitRange | |
|---|---|---|
| Àmbit | El namespace sencer, agregat | Cada contenidor o pod individual |
| Pregunta que respon | "Quant pot consumir aquest entorn en total?" | "Quant pot demanar un sol contenidor?" |
| Modifica l'objecte | No: només accepta o rebutja | Sí: injecta valors per defecte |
| Quan actua | En crear l'objecte | En crear l'objecte |
| Objectes afectats | Pods, Services, PVCs, ConfigMaps... | Contenidors, pods i PVCs |
| Efecte típic | exceeded quota |
minimum memory usage per Container is 32Mi |
| N'hi pot haver diversos | Sí (s'apliquen tots) | Sí (s'apliquen tots) |
Una analogia amb Rutas Norte: la ResourceQuota és el pressupost anual del departament (no es poden gastar més de X euros entre tots). El LimitRange és la política de despeses per persona (ningú no pot demanar un bitllet de més de Y euros, i si no s'especifica classe, s'assumeix turista).
Els tres problemes concrets que resol:
Problema 1: la quota trenca tot el que és còmode. Ja ho vam veure:
Error from server (Forbidden): pods "depurador" is forbidden: failed quota: quota-dev:
must specify limits.cpu for: depurador; limits.memory for: depurador;
requests.cpu for: depurador; requests.memory for: depuradorProblema 2: un sol contenidor pot acaparar la quota sencera. Sense LimitRange, algú pot desplegar un pod amb requests.memory: 4Gi a rutas-norte-dev i esgotar de cop tota la quota de memòria de l'entorn, deixant sense lloc els cinc components.
Problema 3: valors absurds. Un contenidor amb requests.memory: 4Mi no arrencarà mai de manera estable, però res no ho impedeix.
- Els camps:
default, defaultRequest, min, max, maxLimitRequestRatio
default, defaultRequest, min, max, maxLimitRequestRatioapiVersion: v1
kind: LimitRange
metadata:
name: limits-dev
namespace: rutas-norte-dev
spec:
limits:
- type: Container # s'aplica a CADA contenidor
default: # LIMITS si el contenidor no els declara
cpu: 300m
memory: 256Mi
defaultRequest: # REQUESTS si el contenidor no les declara
cpu: 100m
memory: 128Mi
min: # ningu no pot demanar MENYS que aixo
cpu: 10m
memory: 32Mi
max: # ningu no pot demanar MES que aixo
cpu: "2"
memory: 2Gi
maxLimitRequestRatio: # relacio maxima limit/request
cpu: "10"
memory: "4"Camp per camp:
| Camp | Què fa | Si s'incompleix |
|---|---|---|
default |
Omple els limits que faltin |
— (només omple) |
defaultRequest |
Omple les requests que faltin |
— (només omple) |
min |
Mínim que es pot declarar | El pod es rebutja |
max |
Màxim que es pot declarar | El pod es rebutja |
maxLimitRequestRatio |
Topall de la divisió limit / request |
El pod es rebutja |
Regles de resolució que cal conèixer amb precisió:
Regla 1: el declarat sempre guanya. El LimitRange no sobreescriu mai un valor que el contenidor declara. Només omple forats.
Regla 2: si declares limits però no requests, la request pren el valor del limit, no el de defaultRequest. Aquest comportament enxampa molta gent:
Amb el LimitRange de dalt (defaultRequest.memory: 128Mi) un esperaria requests: 128Mi. Però el resultat és:
La regla de Kubernetes de "si només hi ha limits, la request iguala el limit" té prioritat. Conseqüència pràctica: reserves 1 GiB del pressupost del namespace sense voler.
Regla 3: si declares requests però no limits, s'aplica default. Aquí sí que funciona com un espera... llevat que default sigui menor que la teva request, cas en què el pod es rebutja per incoherent.
Regla 4: maxLimitRequestRatio limita el sobrecompromís individual. Amb memory: "4", un contenidor amb requests.memory: 128Mi no pot tenir limits.memory més gran de 512Mi. És l'eina per impedir que algú reservi poc i després consumeixi molt, que és just el patró que provoca desallotjaments.
Taula dels sis escenaris possibles, amb el LimitRange de l'exemple:
| El que declara el contenidor | Resultat després del LimitRange |
|---|---|
| Res | requests: 100m/128Mi, limits: 300m/256Mi |
Només requests: cpu 200m |
requests: 200m/128Mi, limits: 300m/256Mi |
Només limits: memory 1Gi |
requests: 100m/**1Gi**, limits: 300m/1Gi |
| Tots dos complets | Sense canvis (si passa min, max i ratio) |
requests.memory: 16Mi |
Rebutjat: menor que min (32Mi) |
requests: 128Mi, limits: 1Gi |
Rebutjat: ràtio 8 > maxLimitRequestRatio 4 |
- Demostració: un pod sense recursos surt amb recursos
Res no convenç tant com veure-ho. Partim de rutas-norte-dev amb la seva quota i apliquem el LimitRange:
kubectl apply -f k8s/entorns/dev/limitrange.yaml
kubectl describe limitrange limits-dev -n rutas-norte-devlimitrange/limits-dev created
Name: limits-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 4Ara el mateix kubectl run que abans fallava:
kubectl run depurador --image=busybox:1.36 -n rutas-norte-dev -- sleep 3600
kubectl get pod depurador -n rutas-norte-devFunciona. I l'interessant és veure quins recursos té realment:
{
"limits": {
"cpu": "300m",
"memory": "256Mi"
},
"requests": {
"cpu": "100m",
"memory": "128Mi"
}
}El manifest que hem enviat no tenia resources i l'objecte desat a etcd sí que en té. El LimitRange els hi ha injectat.
Qui fa aquesta injecció? Un controlador d'admissió (admission controller) anomenat LimitRanger, que forma part de l'apiserver. Recorda el flux del mòdul 1: una petició passa per autenticació, autorització i admissió abans d'escriure's a etcd. L'admissió té dues fases:
flowchart LR
A["kubectl apply"] --> B["Autenticacio<br/>qui ets"]
B --> C["Autoritzacio RBAC<br/>que pots fer"]
C --> D["Admissio MUTANT<br/>LimitRanger injecta<br/>default i defaultRequest"]
D --> E["Admissio VALIDANT<br/>LimitRanger comprova min/max<br/>ResourceQuota comprova el total"]
E --> F["etcd<br/>objecte desat JA MODIFICAT"]
L'ordre és crucial i explica tot el de l'apartat 5: el LimitRange muta primer, la ResourceQuota valida després. Quan la quota mira el pod, aquest ja té els seus recursos injectats.
I una conseqüència que cal tenir present: l'objecte al clúster ja no és igual que el fitxer YAML del teu repositori. És la primera vegada al curs que això passa, i convé saber-ho per no desconcertar-se en un kubectl diff. També la deixa anotada el mateix pod:
{
"kubernetes.io/limit-ranger": "LimitRanger plugin set: cpu, memory request for container depurador;
cpu, memory limit for container depurador"
}Comprovem també els topalls. Un contenidor demanant massa:
kubectl run gegant --image=nginx:1.27.1-alpine -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"gegant","image":"nginx:1.27.1-alpine",
"resources":{"requests":{"memory":"3Gi"},"limits":{"memory":"3Gi"}}}]}}'Error from server (Forbidden): pods "gegant" is forbidden:
maximum memory usage per Container is 2Gi, but limit is 3GiI un demanant massa poc:
kubectl run nan --image=busybox:1.36 -n rutas-norte-dev \
--overrides='{"spec":{"containers":[{"name":"nan","image":"busybox:1.36",
"resources":{"requests":{"memory":"8Mi"},"limits":{"memory":"8Mi"}}}]}}'Error from server (Forbidden): pods "nan" is forbidden:
minimum memory usage per Container is 32Mi, but request is 8MiEls tres comportaments verificats: injecta, posa sostre i posa terra.
- Els tipus:
Container, Pod i PersistentVolumeClaim
Container, Pod i PersistentVolumeClaimUn LimitRange pot tenir diverses entrades a spec.limits, cadascuna amb el seu type:
apiVersion: v1
kind: LimitRange
metadata:
name: limits-pro
namespace: rutas-norte-pro
spec:
limits:
# 1. Per CONTENIDOR: l'unic que admet default i 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. Per POD: suma de TOTS els seus contenidors. No admet defaults
- type: Pod
max:
cpu: "8"
memory: 16Gi
min:
cpu: 50m
memory: 64Mi
# 3. Per PVC: mida de disc
- type: PersistentVolumeClaim
min:
storage: 1Gi
max:
storage: 200GiDiferències entre els tres:
| Tipus | Àmbit | Admet default/defaultRequest |
Ús típic |
|---|---|---|---|
Container |
Cada contenidor per separat | Sí | El principal: valors per defecte i topalls |
Pod |
La suma dels contenidors del pod | No | Impedir pods amb 10 sidecars gegants |
PersistentVolumeClaim |
Mida del disc sol·licitat | No | Controlar el cost d'emmagatzematge |
El tipus Pod és útil quan arribi el mòdul 6 amb els sidecars i init containers: un pod pot tenir tres o quatre contenidors, cadascun dins del max de Container, però sumant entre tots una quantitat desmesurada. El límit de Pod talla això.
El tipus PersistentVolumeClaim prendrà sentit al mòdul 5. Amb max: storage: 200Gi ningú no pot demanar un disc de 2 TiB per un error de teclat, cosa que al núvol es tradueix en una factura desagradable a final de mes.
- Interacció exacta entre LimitRange i ResourceQuota
Aquest és el punt on tot encaixa. La seqüència, per a un pod que arriba sense resources a un namespace amb tots dos objectes:
sequenceDiagram
participant U as kubectl apply
participant A as apiserver
participant LR as LimitRanger (mutant)
participant LV as LimitRanger (validant)
participant Q as ResourceQuota (validant)
participant E as etcd
U->>A: Pod sense resources
A->>LR: fase mutant
LR->>LR: injecta requests 100m/128Mi<br/>i limits 300m/256Mi
LR->>LV: fase validant
LV->>LV: comprova min, max i ratio -> OK
LV->>Q: validador seguent
Q->>Q: used + 100m <= hard? -> OK
Q->>E: pod desat AMB recursos
E-->>U: pod/depurador created
Les quatre conseqüències d'aquest ordre:
Conseqüència 1: el LimitRange salva la ResourceQuota del seu propi rigor. Sense LimitRange, la quota obliga a declarar recursos a mà a tot arreu. Amb LimitRange, el valor per defecte s'injecta i la quota el comptabilitza sense que ningú escrigui res.
Conseqüència 2: els valors injectats consumeixen quota. Això no és gratis i cal dimensionar-ho. Si defaultRequest.memory és 128Mi i algú llança 20 pods de depuració sense recursos, es mengen 2,5 GiB del pressupost del namespace. Per això els valors per defecte han de ser modestos: són per a pods que ningú no ha calibrat.
Conseqüència 3: un pod pot passar el LimitRange i fallar a la quota. Són validacions independents i cal complir-les totes dues:
Error from server (Forbidden): error when creating "pod-gran.yaml": pods "analitica" is forbidden:
exceeded quota: quota-dev, requested: requests.memory=1Gi,
used: requests.memory=3600Mi, limited: requests.memory=4GiAquell pod complia el max del LimitRange (2Gi), però no cabia en el que quedava de quota.
Conseqüència 4: canviar un LimitRange no afecta els pods existents. L'admissió actua només en la creació. Si puges el defaultRequest, els pods que ja corren conserven el que se'ls va injectar. Per aplicar els valors nous cal recrear-los:
I una regla de coherència entre tots dos objectes que evita un problema desagradable: el max del LimitRange no ha de superar mai la quota del namespace. Si max.memory és 8Gi però la quota de requests.memory és 4Gi, algú pot escriure un manifest que passi el LimitRange i sigui impossible de desplegar. És millor que el rebuig arribi amb el missatge clar del LimitRange ("maximum memory usage per Container is 2Gi") que amb el de la quota, més difícil d'interpretar.
- Les tres classes de QoS i la regla que les determina
Canviem de tema i arribem a la part més interessant. Quan es crea un pod, Kubernetes li assigna automàticament una classe de qualitat de servei a status.qosClass. No es declara: es dedueix de com estiguin posats els recursos.
La regla exacta, en ordre d'avaluació:
flowchart TB
A["Pod creat"] --> B{"Tots els contenidors<br/>declaren requests I limits<br/>de CPU I memoria?"}
B -->|"No"| E{"Algun contenidor<br/>declara alguna request<br/>o algun limit?"}
B -->|"Si"| C{"Per a cada contenidor:<br/>requests == limits<br/>en CPU I memoria?"}
C -->|"Si"| D["Guaranteed"]
C -->|"No"| F["Burstable"]
E -->|"Si"| F
E -->|"No: res de res"| G["BestEffort"]
En paraules precises:
Guaranteed — s'assigna si i només si, per a tots els contenidors del pod (inclosos els init containers):
- Es declaren
limitsde CPU i de memòria. - Es declaren
requestsde CPU i de memòria (o s'ometen, cas en què igualen elslimits). requestsés exactament igual quelimitsen tots dos recursos.
BestEffort — s'assigna si cap contenidor del pod no declara cap request ni cap limit, de cap recurs.
Burstable — tota la resta. És el calaix per defecte: almenys un contenidor declara alguna cosa, però no es compleix la condició estricta de Guaranteed.
Exemples que aclareixen els casos frontera:
# --- Guaranteed TAMBE (nomes limits: les requests es copien) ---
resources:
limits:
cpu: 500m
memory: 512Mi# --- Burstable: la memoria coincideix 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 paranys que convé memoritzar:
Parany 1: n'hi ha prou amb un contenidor per arruïnar la classe. Si un pod té l'aplicació amb requests == limits i un sidecar de registre sense recursos, el pod sencer és Burstable. La classe és del pod, no del contenidor.
Parany 2: amb LimitRange actiu, BestEffort és gairebé impossible. El LimitRanger injecta valors per defecte, així que cap pod no arriba a BestEffort. És un efecte secundari important i en general desitjable.
Parany 3: defaultRequest diferent de default impedeix Guaranteed. Si el LimitRange injecta requests: 100m i limits: 300m, tots els pods sense recursos seran Burstable. Perquè un pod sigui Guaranteed cal declarar-ho explícitament.
- Consultar la classe d'un pod
kubectl get pod postgres-reserves-5d8f6b9c4-k2mnp -n rutas-norte-pro \
-o jsonpath='{.status.qosClass}'; echoPer a tots els pods del namespace d'un cop d'ull:
kubectl get pods -n rutas-norte-pro \
-o custom-columns='NOM:.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'NOM QOS CPU_REQ CPU_LIM MEM_REQ MEM_LIM
api-reserves-7f4b8c9d6-2xkqp Burstable 300m 1 512Mi 768Mi
api-reserves-7f4b8c9d6-8mvtr Burstable 300m 1 512Mi 768Mi
postgres-reserves-5d8f6b9c4-k2mnp Guaranteed 1 1 2Gi 2Gi
redis-cache-6c9d8f7b5-vn4qx Burstable 50m 200m 256Mi 320Mi
botiga-web-5b7c9f4d8-h7pnk Burstable 50m 200m 64Mi 128Mi
worker-notificacions-6f7d9c8b4-lm3pt Burstable 100m 800m 320Mi 512MiUn recompte ràpid per classe:
Aquests sis BestEffort mereixen una mirada: en un clúster de producció són els primers candidats a morir, així que convé saber qui són i si això és intencionat.
kubectl get pods -A -o json \
| jq -r '.items[] | select(.status.qosClass=="BestEffort") | "\(.metadata.namespace)/\(.metadata.name)"'describe també la mostra:
- Conseqüència 1:
oom_score_adj i l'OOM killer del kernel
oom_score_adj i l'OOM killer del kernelAquí hi ha la primera conseqüència real de la classe, i és de vida o mort, literalment.
Quan el kernel de Linux es queda sense memòria, executa l'OOM killer, que tria una víctima. L'elecció es basa en una puntuació per procés, oom_score, que combina la memòria que consumeix amb un ajust manual anomenat oom_score_adj, un valor entre -1000 i 1000. Com més alt sigui, abans mor.
El kubelet estableix aquest ajust segons la classe de QoS:
| Classe de QoS | oom_score_adj |
Ordre de sacrifici |
|---|---|---|
Guaranteed |
-997 | L'últim a morir |
Burstable |
Entre 2 i 999, segons la request de memòria |
Al mig |
BestEffort |
1000 | El primer a morir |
La fórmula per a Burstable és la que dona tot el matís:
Llegeix-la a poc a poc: com més gran sigui la teva request de memòria, menor és la teva puntuació i més tard mors. Un pod Burstable que demana molta memòria està gairebé tan protegit com un Guaranteed; un que en demana molt poca està gairebé tan exposat com un BestEffort.
Comprovem-ho en un node amb 8 GiB assignables. Per a api-reserves amb requests.memory: 512Mi:
kubectl exec -n rutas-norte-pro deploy/api-reserves -- cat /proc/1/oom_score_adj
kubectl exec -n rutas-norte-pro deploy/postgres-reserves -- cat /proc/1/oom_score_adjExactament el previst. I la conclusió de negoci és directa:
Quan al node li falti memòria, el kernel matarà
api-reservesmolt abans quepostgres-reserves. I això és justament el que volem: perdre un pod de l'API significa perdre unes peticions que el client pot reintentar; perdre la base de dades significa que tota la plataforma deixa de vendre bitllets i, en el pitjor cas, que una transacció a mitges corromp dades.
Cal distingir dues situacions diferents que es confonen sovint:
| OOM del cgroup | OOM del node | |
|---|---|---|
| Causa | Un contenidor supera el seu propi limits.memory |
La suma de tot supera la memòria del node |
| Qui mor | Aquell contenidor concret | El procés amb més oom_score del node |
| Hi influeix la classe de QoS | No: mors pel teu propi límit | Sí: és el que ho decideix |
| Es veu com | OOMKilled en aquell pod |
OOMKilled en un pod que "no havia fet res" |
| Es preveu amb | Un limits.memory ben calibrat |
No sobrecomprometre memòria + QoS ben assignada |
El segon cas és el desconcertant: un pod mor i les seves mètriques mostren que estava molt per sota del seu límit. La causa és en un veí que es va descontrolar, i la classe de QoS és el que va determinar que la víctima fos ell i no un altre.
- Conseqüència 2: l'ordre de desallotjament del kubelet
Abans que el kernel arribi a matar processos, el kubelet té el seu propi mecanisme, més ordenat: el desallotjament per pressió de recursos (eviction).
El kubelet vigila els recursos del node cada 10 segons. Quan creua un llindar, marca la condició corresponent (MemoryPressure, DiskPressure, PIDPressure) i comença a desallotjar pods per recuperar recursos.
Els llindars per defecte:
| Senyal | Llindar per defecte | Condició que activa |
|---|---|---|
memory.available |
< 100Mi |
MemoryPressure |
nodefs.available |
< 10% |
DiskPressure |
nodefs.inodesFree |
< 5% |
DiskPressure |
imagefs.available |
< 15% |
DiskPressure |
I el criteri de selecció de les víctimes, en aquest ordre estricte:
- Primer, si el pod excedeix les seves
requests. Els que consumeixen més del que van reservar són candidats abans que els que es mantenen dins. - Segon, la classe de QoS:
BestEffortprimer, desprésBurstable, iGuaranteeden últim lloc. - Tercer, la PriorityClass, si està definida.
- Quart, quant excedeix l'ús sobre la
request. Entre dos candidats iguals, cau el que més s'ha passat.
Dit de manera operativa:
El primer pod a caure és un
BestEffort. Si no n'hi ha cap, cau elBurstableque més s'hagi passat de la sevarequest. Un podGuaranteedque es manté dins dels seus recursos és pràcticament intocable.
Un pod desallotjat es veu així:
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.Aquest missatge ho diu tot: request is 0 perquè era BestEffort, i per tant qualsevol consum excedeix la seva reserva. Va ser el candidat perfecte.
Dos apunts pràctics importants:
Els pods desallotjats no s'esborren sols. Queden en Failed ocupant espai a etcd. Convé netejar-los:
Un pod desallotjat que pertany a un Deployment es recrea. El ReplicaSet del mòdul 2 veu que falta una rèplica i en crea una altra, possiblement en un altre node. Un pod solt desallotjat, en canvi, no torna mai. Un motiu més per no desplegar pods solts.
- La classe de QoS de cada component de Rutas Norte
Ara la decisió de disseny, que és el que dona sentit a tot l'anterior. La pregunta és, per a cada component: què passa si aquest pod mor de sobte?
| Component | Classe | Justificació de negoci |
|---|---|---|
postgres-reserves |
Guaranteed |
Desa les reserves i les dades personals dels clients. Si mor, la plataforma no ven. Una mort violenta pot deixar transaccions a mitges. Ha de ser l'últim a caure i el seu rendiment ha de ser previsible |
api-reserves |
Burstable |
Té diverses rèpliques i és sense estat: si una mor, el Service reparteix a les altres i el client reintenta. Necessita ràfega de CPU en ponts, i requests == limits malbarataria capacitat el 95 % del temps |
botiga-web |
Burstable |
Serveix estàtics amb consum mínim i molt variable. Tres rèpliques, sense estat, arrencada en segons |
redis-cache |
Burstable |
Té estat però és prescindible: si mor, la memòria cau es perd i api-reserves torna a consultar postgres-reserves. Més lent, però correcte |
worker-notificacions |
Burstable |
Asíncron: si mor a mitja tanda, les notificacions pendents continuen a la base de dades i l'execució següent les recull |
informes-ocupacio |
Burstable amb recursos baixos |
Tasca nocturna (mòdul 6). Si el node té pressió, que mori: es reintenta demà |
| Anàlisi puntual ad hoc | BestEffort |
Consultes exploratòries d'un analista. És el primer que ha de caure, sempre |
Els manifests concrets. postgres-reserves com a Guaranteed:
- name: postgres
image: postgres:16.4
resources:
# Guaranteed: requests == limits en CPU i memoria.
# Decisio deliberada: aquesta base de dades desa dades personals
# de clients i es l'ultim pod que ha de morir al node.
requests:
cpu: "1"
memory: 2Gi
limits:
cpu: "1"
memory: 2Gikubectl get pod -l app=postgres-reserves -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echoapi-reserves com a Burstable:
- name: api
image: registry.rutasnorte.example/api-reserves:2.5.0
resources:
# Burstable deliberat: 4 repliques sense estat darrere d'un Service.
# El limit de CPU triplica la request per absorbir els pics de ponts.
requests:
cpu: 300m
memory: 512Mi
limits:
cpu: "1"
memory: 768MiL'anàlisi puntual com a BestEffort:
apiVersion: v1
kind: Pod
metadata:
name: analisi-ocupacio-juliol
namespace: rutas-norte-dev
labels:
app: analisi-puntual
app.kubernetes.io/part-of: rutas-norte
entorn: dev
annotations:
rutasnorte.example/motiu: "analisi exploratoria docupacio, RN-612"
rutasnorte.example/responsable: "[email protected]"
spec:
restartPolicy: Never
containers:
- name: analisi
image: postgres:16.4
command: ["sh", "-c", "psql -h postgres-reserves -c 'SELECT ...' > /tmp/sortida.csv"]
resources: {} # buit A PROPOSIT: BestEffort, el primer a caureCompte: amb un LimitRange actiu a rutas-norte-dev, aquest resources: {} no produirà BestEffort, perquè el LimitRanger injectarà els valors per defecte. Per aconseguir un pod BestEffort de debò cal fer servir un namespace sense LimitRange, i per això l'experiment de l'apartat 12 en fa servir un a part.
Un cas que mereix comentari: redis-cache és Burstable encara que tingui estat. No s'hauria de protegir com postgres-reserves? No, i la diferència és el valor de la dada. redis-cache desa la disponibilitat de places en memòria cau: si es perd, es recalcula consultant la base de dades. L'impacte és un pic de latència i de càrrega sobre postgres-reserves, no una pèrdua d'informació. El que determina la classe no és si el component té estat, sinó què es perd si mor.
- El LimitRange dels tres entorns
Tanquem la part de configuració amb els tres objectes, coherents amb les quotes de la lliçó anterior.
rutas-norte-dev
apiVersion: v1
kind: LimitRange
metadata:
name: limits-dev
namespace: rutas-norte-dev
labels:
app.kubernetes.io/part-of: rutas-norte
entorn: dev
spec:
limits:
- type: Container
# Valors per defecte modestos: son per a pods sense calibrar
default:
cpu: 300m
memory: 256Mi
defaultRequest:
cpu: 100m
memory: 128Mi
min:
cpu: 10m
memory: 32Mi
# max molt per sota de la quota (2 CPU / 4Gi): un sol pod no l'esgota
max:
cpu: "1"
memory: 1Gi
maxLimitRequestRatio:
cpu: "10" # generos: a dev interessa iterar sense barallar-se amb el limit
memory: "4"rutas-norte-pre
apiVersion: v1
kind: LimitRange
metadata:
name: limits-pre
namespace: rutas-norte-pre
labels:
app.kubernetes.io/part-of: rutas-norte
entorn: 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" # mes estricte: pre s'ha d'assemblar a pro
memory: "2"
- type: Pod
max:
cpu: "4"
memory: 6Girutas-norte-pro
apiVersion: v1
kind: LimitRange
metadata:
name: limits-pro
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
limits:
- type: Container
# A pro els defaults son una XARXA DE SEGURETAT, no una comoditat:
# tot component de produccio ha de declarar els seus recursos calibrats.
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" # impedeix reservar poc i consumir molt: evita desallotjaments
- type: Pod
max:
cpu: "8"
memory: 16Gi
- type: PersistentVolumeClaim
min:
storage: 1Gi
max:
storage: 200Gi # ningu no demana 2 TiB per un zero de mesComparativa de les decisions:
| Paràmetre | dev |
pre |
pro |
Raonament |
|---|---|---|---|---|
defaultRequest.memory |
128Mi | 256Mi | 256Mi | A dev hi ha molts pods petits de prova |
max.memory (contenidor) |
1Gi | 4Gi | 8Gi | A pro postgres-reserves necessita marge per créixer |
maxLimitRequestRatio.memory |
4 | 2 | 2 | A pro el sobrecompromís agressiu provoca desallotjaments |
Límit de Pod |
No | Sí | Sí | S'activa quan apareixen els sidecars del mòdul 6 |
| Límit de PVC | No | No | Sí | Control de cost de l'emmagatzematge de producció |
kubectl apply -f k8s/entorns/dev/limitrange.yaml \
-f k8s/entorns/pre/limitrange.yaml \
-f k8s/entorns/pro/limitrange.yaml
kubectl get limitrange -Alimitrange/limits-dev created
limitrange/limits-pre created
limitrange/limits-pro created
NAMESPACE NAME CREATED AT
rutas-norte-dev limits-dev 2026-08-05T21:14:07Z
rutas-norte-pre limits-pre 2026-08-05T21:14:07Z
rutas-norte-pro limits-pro 2026-08-05T21:14:08Z
- Experiment: provocar un desallotjament i veure qui cau primer
Toca comprovar la teoria. Crearem un namespace sense LimitRange (per poder tenir pods BestEffort de debò), desplegarem tres pods de les tres classes, i pressionarem la memòria del node fins que el kubelet comenci a desallotjar.
Avís: fes-ho al teu minikube de pràctiques, mai en un clúster compartit. Provocarem deliberadament pressió de memòria en un node.
Pas 1: preparar el terreny.
kubectl create namespace laboratori-qos
kubectl get node rutas-norte -o jsonpath='{.status.allocatable.memory}'; echoUns 7,7 GiB assignables.
Pas 2: els tres pods.
# laboratori-qos.yaml
apiVersion: v1
kind: Pod
metadata:
name: victima-besteffort
namespace: laboratori-qos
labels: {rol: victima}
spec:
containers:
- name: carrega
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "700M", "--vm-hang", "0"]
# resources absent a proposit -> BestEffort
---
apiVersion: v1
kind: Pod
metadata:
name: victima-burstable
namespace: laboratori-qos
labels: {rol: victima}
spec:
containers:
- name: carrega
image: polinux/stress:1.0.4
command: ["stress"]
args: ["--vm", "1", "--vm-bytes", "700M", "--vm-hang", "0"]
resources:
requests:
cpu: 100m
memory: 200Mi # demana POC i consumeix MOLT: candidat ideal
limits:
cpu: 500m
memory: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: protegit-guaranteed
namespace: laboratori-qos
labels: {rol: protegit}
spec:
containers:
- name: carrega
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 laboratori-qos.yaml
sleep 20
kubectl get pods -n laboratori-qos -o custom-columns='NOM:.metadata.name,QOS:.status.qosClass,ESTAT:.status.phase'pod/victima-besteffort created
pod/victima-burstable created
pod/protegit-guaranteed created
NOM QOS ESTAT
protegit-guaranteed Guaranteed Running
victima-besteffort BestEffort Running
victima-burstable Burstable RunningLes tres classes confirmades. Verifiquem també els seus oom_score_adj:
for P in victima-besteffort victima-burstable protegit-guaranteed; do
printf '%-22s ' "$P"
kubectl exec -n laboratori-qos "$P" -- cat /proc/1/oom_score_adj
doneEl 975 de victima-burstable surt de la fórmula de l'apartat 8: 1000 - (1000 × 200Mi / 7751Mi) = 1000 - 25 = 975. Demana poquíssim, així que està gairebé tan exposat com el BestEffort.
Pas 3: prémer fins a provocar la pressió.
kubectl run pressio --image=polinux/stress:1.0.4 -n laboratori-qos --restart=Never -- \
stress --vm 1 --vm-bytes 4500M --vm-hang 0Pas 4: observar.
NAME READY STATUS RESTARTS AGE
pressio 1/1 Running 0 12s
protegit-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 <-- PRIMER
victima-burstable 0/1 Evicted 0 3m48s <-- SEGON
protegit-guaranteed 1/1 Running 0 4m10s <-- SOBREVIUL'ordre és exactament el previst: primer el BestEffort, després el Burstable que més s'havia passat de la seva request, i el Guaranteed segueix dret.
Pas 5: llegir els missatges de desallotjament.
Status: Failed
Reason: Evicted
Message: The node was low on resource: memory. Threshold quantity: 100Mi, available: 78Mi.
Container carrega 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 carrega was using 705Mi, request is 200Mi, which exceeds its request of 200Mi.Compara les dues últimes frases: el BestEffort excedia una reserva de 0 (tot el que faci servir l'excedeix), i el Burstable excedia la seva reserva de 200Mi en 505Mi. Tots dos eren candidats; el BestEffort primer per classe.
Pas 6: la condició del node i els esdeveniments.
kubectl get node rutas-norte -o jsonpath='{.status.conditions[?(@.type=="MemoryPressure")].status}'; echo
kubectl get events -n laboratori-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 carrega
94s Warning Evicted pod/victima-burstable The node was low on resource: memory
94s Normal Killing pod/victima-burstable Stopping container carregaPas 7: netejar.
kubectl delete namespace laboratori-qos
kubectl get node rutas-norte -o jsonpath='{.status.conditions[?(@.type=="MemoryPressure")].status}'; echoEl que aquest experiment demostra, traduït a Rutas Norte: si un pont d'agost provoca pressió de memòria al node on viu postgres-reserves, la base de dades no serà la que caigui. Cauran abans els pods d'anàlisi puntual, després les rèpliques d'api-reserves que s'hagin passat de la seva reserva —i el Service continuarà repartint entre les que quedin—, i la plataforma continuarà venent bitllets. Aquesta cadena de supervivència no és casualitat: l'has dissenyada tu en assignar les classes.
Errors Comuns i Consells
| Error | Símptoma | Solució |
|---|---|---|
| Quota sense LimitRange | Tot kubectl run falla |
Afegeix sempre un LimitRange al costat de la quota |
Esperar defaultRequest quan només hi ha limits |
Es reserva molt més del previst | La request copia el limit, no el defaultRequest |
max del LimitRange per sobre de la quota |
Manifests vàlids però indesplegables | Deixa max clarament per sota del sostre del namespace |
| Canviar el LimitRange esperant efecte retroactiu | Els pods vells conserven els seus valors | kubectl rollout restart |
Un sidecar sense recursos en un pod Guaranteed |
El pod sencer és Burstable |
La classe és del pod: tots els contenidors l'han de complir |
Creure que Guaranteed immunitza contra OOMKilled |
El pod mor igualment | Guaranteed protegeix de l'OOM del node, no de superar el teu propi límit |
Pods BestEffort a producció |
Desallotjaments inesperats | Només per a feina veritablement descartable |
Burstable amb request mínima i limit enorme |
Desallotjat constantment | Puja la request o baixa el maxLimitRequestRatio |
Pods Evicted acumulats |
Soroll a kubectl get pods i a etcd |
kubectl delete pods -A --field-selector=status.phase=Failed |
| Pod solt desallotjat | Desapareix i no torna | Fes servir Deployments, no pods solts |
| Confondre quota i LimitRange | Es posa l'objecte equivocat | Quota = total del namespace; LimitRange = per contenidor |
No revisar qosClass després de canviar recursos |
Es perd Guaranteed sense adonar-se'n |
Comprovar amb -o jsonpath='{.status.qosClass}' a la canalització |
Consells:
- Quota i LimitRange van sempre junts. Tracta'ls com una sola decisió en crear un namespace.
- Valors per defecte modestos,
maxgenerós. El valor per defecte és per al que no s'ha calibrat; elmaxés una xarxa de seguretat contra els zeros de més. Guaranteednomés per al que és veritablement crític. Costa capacitat real: reserves el màxim tot el temps. A Rutas Norte, noméspostgres-reserves.- Vigila els
Burstableamb relació alta. Un pod ambrequestde 200Mi ilimitde 4Gi és un candidat permanent al desallotjament.maxLimitRequestRatioho preveu des del namespace. - Afegeix la classe de QoS a les teves revisions de manifests. És una línia a la revisió d'una pull request i evita descobrir en un incident que la base de dades era
Burstable.
Exercicis
Exercici 1: Els sis escenaris del LimitRange
Amb el LimitRange limits-dev de l'apartat 11 aplicat a rutas-norte-dev:
- Crea sis pods, un per cada fila de la taula de l'apartat 2 (sense res, només
requestsde CPU, noméslimitsde memòria d'1Gi, tots dos complets,requests.memory: 16Mi, irequests: 128Miamblimits: 1Gi). - Per als que es creïn, mostra els
resourcesefectius i la seva classe de QoS. - Per als que fallin, copia el missatge d'error exacte.
- Explica en particular per què el tercer acaba amb
requests.memory: 1Gien lloc de 128Mi, i quina conseqüència té sobre la quota del namespace. - Calcula quanta quota consumirien entre tots si s'haguessin creat els sis.
Exercici 2: Auditar i corregir les classes de QoS
- Llista tots els pods dels tres namespaces de Rutas Norte amb la seva classe de QoS en una sola taula.
- Comprova si
postgres-reservesésGuaranteedals tres entorns. Si no ho és en algun, identifica per què. - Modifica el manifest de
postgres-reservesperquè siguiGuaranteedarutas-norte-pro, verifica el canvi i comprova el seuoom_score_adj. - Calcula a mà l'
oom_score_adjesperat d'api-reservesambrequests.memory: 512Mien un node amb 7751Mi assignables, i compara'l amb el valor real. - Afegeix un sidecar sense recursos a
postgres-reservesi observa què li passa a la classe del pod. Explica el resultat i reverteix el canvi.
Exercici 3: Reproduir el desallotjament i raonar el disseny
- Reprodueix l'experiment de l'apartat 12 al teu minikube.
- Anota l'ordre exacte dels desallotjaments i els missatges de
describe. - Modifica
victima-burstableperquè la sevarequests.memorysigui 1Gi (en lloc de 200Mi) mantenint el mateix consum, i repeteix l'experiment. Canvia l'ordre? Explica per què fent servir la fórmula de l'oom_score_adj. - Explica què hauria passat si
postgres-reservesde Rutas Norte hagués estat en aquell node com aBurstableambrequests.memory: 256Mii un consum real d'1,8 GiB. - Proposa dues mesures addicionals, més enllà de la classe de QoS, per protegir
postgres-reservesd'aquest escenari, i indica en quina lliçó del curs s'estudia cadascuna.
Solucions
Solució 1
# 1. Sense res
kubectl run p1 --image=busybox:1.36 -n rutas-norte-dev -- sleep 3600
# 2. Nomes 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. Nomes 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. Tots dos complets i iguals
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. Per sota del minim
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 excessiva
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 cas p3 és l'interessant: hem declarat només limits.memory: 1Gi i el resultat és requests.memory: 1Gi, no els 128Mi del defaultRequest. La raó és l'ordre de resolució: Kubernetes aplica primer la seva regla general de "si hi ha limit i no hi ha request, la request iguala el limit", i el defaultRequest del LimitRange només omple el que continua buit després d'això.
La conseqüència sobre la quota és seriosa: aquest pod, que probablement consumeixi 4 MiB de memòria real, ha compromès 1 GiB del pressupost de 4 GiB del namespace. Amb quatre pods així, rutas-norte-dev es queda sense quota de memòria sense que ningú no estigui fent servir res. És un error molt car i molt invisible.
Consum total si s'haguessin creat els sis (només compten els quatre vàlids):
| 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 només és el 25 % dels pods i consumeix el 67 % de la memòria compromesa.
Solució 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-reserves-6d8f7c9b-4kx2p Burstable
rutas-norte-dev postgres-reserves-5d8f6b9c4-t8wmz Burstable
rutas-norte-dev redis-cache-6c9d8f7b5-p2njq Burstable
rutas-norte-dev botiga-web-5b7c9f4d8-h7pnk Burstable
rutas-norte-pre postgres-reserves-5d8f6b9c4-v4rbd Burstable
rutas-norte-pro api-reserves-7f4b8c9d6-2xkqp Burstable
rutas-norte-pro postgres-reserves-5d8f6b9c4-k2mnp Burstable
rutas-norte-pro redis-cache-6c9d8f7b5-vn4qx Burstablepostgres-reserves no és Guaranteed en cap entorn. Motiu:
kubectl get pod -l app=postgres-reserves -n rutas-norte-pro \
-o jsonpath='{.items[0].spec.containers[0].resources}' | jq .La memòria coincideix però la CPU no (500m davant de 2). N'hi ha prou amb un recurs desigual per perdre Guaranteed. És un error clàssic: s'iguala la memòria pensant en l'OOM i s'oblida la CPU.
kubectl patch deployment postgres-reserves -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-reserves -n rutas-norte-pro
kubectl get pod -l app=postgres-reserves -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echo
kubectl exec -n rutas-norte-pro deploy/postgres-reserves -- cat /proc/1/oom_score_adjdeployment.apps/postgres-reserves patched
deployment "postgres-reserves" successfully rolled out
Guaranteed
-997El càlcul d'api-reserves:
Coincideix. La diferència de 1931 punts amb postgres-reserves és enorme: a la pràctica, el kernel sacrificarà totes les rèpliques de l'API abans de tocar la base de dades.
El sidecar:
kubectl get pod -l app=postgres-reserves -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echoEl pod ha perdut Guaranteed per culpa d'un sidecar sense recursos. A rutas-norte-pro el LimitRange li injecta requests: 200m/256Mi i limits: 500m/512Mi, que són diferents entre si, i això ja n'hi ha prou. La correcció és donar al sidecar requests == limits també:
- name: exportador-metriques
image: prometheuscommunity/postgres-exporter:v0.15.0
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 50m
memory: 64MiAmb això el pod recupera Guaranteed. Lliçó: en un pod que ha de ser Guaranteed, tots els seus contenidors ho han de ser, inclosos els sidecars i els init containers.
Solució 3
L'ordre observat és BestEffort → Burstable → el Guaranteed sobreviu, amb els missatges de l'apartat 12.
Amb requests.memory: 1Gi a victima-burstable i el mateix consum de 700Mi:
kubectl exec -n laboratori-qos victima-burstable -- cat /proc/1/oom_score_adj
kubectl get pods -n laboratori-qos -w868
NAME READY STATUS RESTARTS AGE
victima-besteffort 0/1 Evicted 0 2m14s
victima-burstable 1/1 Running 0 3m40s <-- ara SOBREVIU
protegit-guaranteed 1/1 Running 0 3m40sEl Burstable ja no cau. Dos motius que actuen alhora:
- El seu
oom_score_adjva baixar de 975 a 868, així que el kernel el prioritza menys com a víctima. - I sobretot, ja no excedeix la seva
request: consumeix 700Mi i n'havia reservat 1Gi. El primer criteri de selecció del kubelet (apartat 9) és precisament "pods que excedeixen les sevesrequests", i aquest ja no és en aquell grup.
La moralitat operativa és contundent: declarar una request de memòria realista és la protecció més barata que existeix contra el desallotjament. No canvia el que consumeixes; canvia si ets candidat o no.
- Si
postgres-reserveshagués estatBurstableambrequests.memory: 256Mii un consum real d'1,8 GiB:
Un 967, gairebé tan exposat com un BestEffort, i excedint la seva request en més d'1,5 GiB, cosa que el posa al primer grup de candidats. Hauria estat dels primers a caure. Per a Rutas Norte això significa: la base de dades cau al pic de venda d'un pont, api-reserves comença a retornar errors de connexió, botiga-web mostra errors als clients, i en el pitjor cas una transacció de reserva a mitges deixa places bloquejades sense bitllet emès. Un Recreate de PostgreSQL a més implica recuperació del WAL en arrencar, amb minuts d'indisponibilitat.
Tot això ho evita una línia: requests == limits.
- Dues mesures addicionals:
| Mesura | Què aporta | On s'estudia |
|---|---|---|
| PodDisruptionBudget | Impedeix que una operació voluntària (drenar un node per manteniment) deixi postgres-reserves sense cap rèplica disponible |
09-05 |
| Taints, toleracions i afinitat de node | Reservar un node per a la base de dades, sense veïns sorollosos que puguin generar pressió de memòria | 06-05 |
I una tercera, la més important a mitjà termini: convertir postgres-reserves en un StatefulSet amb volum persistent i còpies de seguretat verificades, de manera que la supervivència de la dada no depengui de la supervivència del pod. És la feina dels mòduls 5 i 6.
Conclusió
Has tancat els dos caps solts que va deixar la lliçó anterior. El LimitRange converteix una quota estricta en una cosa portable: injecta default i defaultRequest als contenidors que no declaren res, posa terra i sostre amb min i max, i limita el sobrecompromís individual amb maxLimitRequestRatio. Saps que actua com a controlador d'admissió mutant abans que la ResourceQuota validi, que per tant els valors injectats consumeixen quota, que no és retroactiu, i que els tres tipus —Container, Pod i PersistentVolumeClaim— cobreixen des del contenidor solt fins al cost d'un disc. I coneixes la regla que més diners costa quan s'ignora: si declares només limits, la request copia el limit, no el defaultRequest, i amb això reserves molt més del que et penses.
Domines també les classes de qualitat de servei i la regla exacta que les determina: Guaranteed exigeix requests == limits de CPU i memòria a tots els contenidors del pod, BestEffort exigeix que cap no declari res, i Burstable és tota la resta. La saps consultar amb -o jsonpath='{.status.qosClass}' i, sobretot, saps quines conseqüències té: l'oom_score_adj que el kubelet escriu a cada procés (-997 per a Guaranteed, fins a 1000 per a BestEffort, i una fórmula proporcional a la request de memòria per a Burstable) i l'ordre de desallotjament del kubelet, que mira primer qui excedeix les seves requests i després la classe. Has distingit l'OOM del cgroup —mors pel teu propi límit, sense importar la classe— de l'OOM del node, on la classe ho decideix tot.
Rutas Norte té ara un disseny raonat: postgres-reserves és Guaranteed perquè desa les reserves i les dades personals dels clients i la seva mort atura la venda; api-reserves, botiga-web, redis-cache i worker-notificacions són Burstable perquè tenen rèpliques, són recuperables i necessiten ràfega als ponts; i l'anàlisi exploratòria és BestEffort perquè ha de ser el primer a caure. Els tres entorns tenen el seu LimitRange coherent amb la seva quota. I ho has comprovat provocant un desallotjament real al minikube, veient caure el BestEffort primer, el Burstable que s'havia passat de la seva reserva després, i sobreviure el Guaranteed; i descobrint de passada que pujar una request a un valor realista és la protecció més barata que existeix contra el desallotjament.
Queda una última peça del mòdul, i és d'una altra naturalesa. Hem donat als nostres components la seva configuració, les seves credencials i els seus recursos, però no els hem donat identitat. Tots els pods de Rutas Norte estan fent servir ara mateix la ServiceAccount default del seu namespace, amb un token muntat que cap d'ells no necessita, i cap no té manera de dir a l'API de Kubernetes qui és. La lliçó següent, ServiceAccounts i Accés a l'API des dels Pods, ho resol: veuràs la diferència entre usuaris i ServiceAccounts, per què fer servir la default és mala idea, com han canviat els tokens (de secrets eterns a tokens projectats de vida limitada que el kubelet rota), què hi ha exactament a /var/run/secrets/kubernetes.io/serviceaccount/, per què automountServiceAccountToken: false ha de ser la teva opció per defecte, i parlaràs amb l'API des de dins d'un pod amb curl per veure tant una resposta autoritzada com un 403 Forbidden ben merescut.
Curs de Kubernetes
Mòdul 1: Introducció a Kubernetes
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
