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 serveiGuaranteed, 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

  1. Què resol un LimitRange
  2. Els camps: default, defaultRequest, min, max, maxLimitRequestRatio
  3. Demostració: un pod sense recursos surt amb recursos
  4. Els tipus: Container, Pod i PersistentVolumeClaim
  5. Interacció exacta entre LimitRange i ResourceQuota
  6. Les tres classes de QoS i la regla que les determina
  7. Consultar la classe d'un pod
  8. Conseqüència 1: oom_score_adj i l'OOM killer del kernel
  9. Conseqüència 2: l'ordre de desallotjament del kubelet
  10. La classe de QoS de cada component de Rutas Norte
  11. El LimitRange dels tres entorns
  12. Experiment: provocar un desallotjament i veure qui cau primer

  1. Què resol un LimitRange

El LimitRange és un objecte amb namespace que fa dues coses diferents sobre cada contenidor que s'hi creï:

  1. Injecta valors per defecte als contenidors que no declaren requests o limits.
  2. 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 : 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:

kubectl run depurador --image=busybox:1.36 -n rutas-norte-dev -- sleep 3600
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: depurador

Problema 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.

  1. Els camps: default, defaultRequest, min, max, maxLimitRequestRatio

apiVersion: 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:

          resources:
            limits:
              memory: 1Gi            # no declaro requests

Amb el LimitRange de dalt (defaultRequest.memory: 128Mi) un esperaria requests: 128Mi. Però el resultat és:

          resources:
            limits:
              memory: 1Gi
            requests:
              memory: 1Gi            # copiat del limit, NO el defaultRequest

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

  1. 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-dev
limitrange/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          4

Ara 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-dev
pod/depurador created

NAME        READY   STATUS    RESTARTS   AGE
depurador   1/1     Running   0          6s

Funciona. I l'interessant és veure quins recursos té realment:

kubectl get pod depurador -n rutas-norte-dev -o jsonpath='{.spec.containers[0].resources}' | jq .
{
  "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:

kubectl get pod depurador -n rutas-norte-dev -o jsonpath='{.metadata.annotations}' | jq .
{
  "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 3Gi

I 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 8Mi

Els tres comportaments verificats: injecta, posa sostre i posa terra.

  1. Els tipus: Container, Pod i PersistentVolumeClaim

Un 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: 200Gi

Diferències entre els tres:

Tipus Àmbit Admet default/defaultRequest Ús típic
Container Cada contenidor per separat 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.

  1. 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:

kubectl apply -f pod-gran.yaml
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=4Gi

Aquell 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:

kubectl rollout restart deploy -n rutas-norte-dev

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.

  1. 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 limits de CPU i de memòria.
  • Es declaren requests de CPU i de memòria (o s'ometen, cas en què igualen els limits).
  • requests és exactament igual que limits en 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 ---
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: 500m
              memory: 512Mi
# --- 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: 512Mi
# --- Burstable: nomes requests ---
          resources:
            requests:
              memory: 512Mi
# --- BestEffort: res de res ---
          resources: {}

Tres 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.

  1. Consultar la classe d'un pod

kubectl get pod postgres-reserves-5d8f6b9c4-k2mnp -n rutas-norte-pro \
  -o jsonpath='{.status.qosClass}'; echo
Guaranteed

Per 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    512Mi

Un recompte ràpid per classe:

kubectl get pods -A -o json | jq -r '.items[] | .status.qosClass' | sort | uniq -c
     28 Burstable
      3 Guaranteed
      6 BestEffort

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)"'
kube-system/coredns-7db6d8ff4d-2vqxn
rutas-norte-dev/depurador
...

describe també la mostra:

kubectl describe pod postgres-reserves-5d8f6b9c4-k2mnp -n rutas-norte-pro | grep -i qos
QoS Class:  Guaranteed

  1. Conseqüència 1: oom_score_adj i l'OOM killer del kernel

Aquí 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:

oom_score_adj = 1000 - (1000 x requests.memory / memoria assignable del node)

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:

oom_score_adj = 1000 - (1000 x 512 / 8192) = 1000 - 62 = 938
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_adj
938
-997

Exactament el previst. I la conclusió de negoci és directa:

Quan al node li falti memòria, el kernel matarà api-reserves molt abans que postgres-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 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.

  1. 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:

  1. 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.
  2. Segon, la classe de QoS: BestEffort primer, després Burstable, i Guaranteed en últim lloc.
  3. Tercer, la PriorityClass, si està definida.
  4. 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 el Burstable que més s'hagi passat de la seva request. Un pod Guaranteed que 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 -14
NAME                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:

kubectl delete pods -A --field-selector=status.phase=Failed

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.

  1. 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: 2Gi
kubectl get pod -l app=postgres-reserves -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echo
Guaranteed

api-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: 768Mi

L'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 caure

Compte: 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.

  1. 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: 6Gi

rutas-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 mes

Comparativa 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'activa quan apareixen els sidecars del mòdul 6
Límit de PVC No No 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 -A
limitrange/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

  1. 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}'; echo
namespace/laboratori-qos created
7937084Ki

Uns 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 -> Guaranteed
kubectl 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    Running

Les 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
done
victima-besteffort     1000
victima-burstable      975
protegit-guaranteed    -997

El 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 0

Pas 4: observar.

kubectl get pods -n laboratori-qos -w
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     <-- SOBREVIU

L'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.

kubectl describe pod victima-besteffort -n laboratori-qos | head -12
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.
kubectl describe pod victima-burstable -n laboratori-qos | head -12
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 -5
True

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 carrega

Pas 7: netejar.

kubectl delete namespace laboratori-qos
kubectl get node rutas-norte -o jsonpath='{.status.conditions[?(@.type=="MemoryPressure")].status}'; echo
namespace "laboratori-qos" deleted
False

El 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:

  1. Quota i LimitRange van sempre junts. Tracta'ls com una sola decisió en crear un namespace.
  2. Valors per defecte modestos, max generós. El valor per defecte és per al que no s'ha calibrat; el max és una xarxa de seguretat contra els zeros de més.
  3. Guaranteed només per al que és veritablement crític. Costa capacitat real: reserves el màxim tot el temps. A Rutas Norte, només postgres-reserves.
  4. Vigila els Burstable amb relació alta. Un pod amb request de 200Mi i limit de 4Gi és un candidat permanent al desallotjament. maxLimitRequestRatio ho preveu des del namespace.
  5. 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:

  1. Crea sis pods, un per cada fila de la taula de l'apartat 2 (sense res, només requests de CPU, només limits de memòria d'1Gi, tots dos complets, requests.memory: 16Mi, i requests: 128Mi amb limits: 1Gi).
  2. Per als que es creïn, mostra els resources efectius i la seva classe de QoS.
  3. Per als que fallin, copia el missatge d'error exacte.
  4. Explica en particular per què el tercer acaba amb requests.memory: 1Gi en lloc de 128Mi, i quina conseqüència té sobre la quota del namespace.
  5. Calcula quanta quota consumirien entre tots si s'haguessin creat els sis.

Exercici 2: Auditar i corregir les classes de QoS

  1. Llista tots els pods dels tres namespaces de Rutas Norte amb la seva classe de QoS en una sola taula.
  2. Comprova si postgres-reserves és Guaranteed als tres entorns. Si no ho és en algun, identifica per què.
  3. Modifica el manifest de postgres-reserves perquè sigui Guaranteed a rutas-norte-pro, verifica el canvi i comprova el seu oom_score_adj.
  4. Calcula a mà l'oom_score_adj esperat d'api-reserves amb requests.memory: 512Mi en un node amb 7751Mi assignables, i compara'l amb el valor real.
  5. Afegeix un sidecar sense recursos a postgres-reserves i 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

  1. Reprodueix l'experiment de l'apartat 12 al teu minikube.
  2. Anota l'ordre exacte dels desallotjaments i els missatges de describe.
  3. Modifica victima-burstable perquè la seva requests.memory sigui 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.
  4. Explica què hauria passat si postgres-reserves de Rutas Norte hagués estat en aquell node com a Burstable amb requests.memory: 256Mi i un consum real d'1,8 GiB.
  5. Proposa dues mesures addicionals, més enllà de la classe de QoS, per protegir postgres-reserves d'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.000000
kubectl 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   256Mi

El 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
done
rutas-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           Burstable

postgres-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 .
{
  "limits": { "cpu": "2", "memory": "2Gi" },
  "requests": { "cpu": "500m", "memory": "2Gi" }
}

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_adj
deployment.apps/postgres-reserves patched
deployment "postgres-reserves" successfully rolled out
Guaranteed
-997

El càlcul d'api-reserves:

oom_score_adj = 1000 - (1000 x 512 / 7751) = 1000 - 66 = 934
kubectl exec -n rutas-norte-pro deploy/api-reserves -- cat /proc/1/oom_score_adj
934

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:

        - name: exportador-metriques
          image: prometheuscommunity/postgres-exporter:v0.15.0
          # sense resources
kubectl get pod -l app=postgres-reserves -n rutas-norte-pro -o jsonpath='{.items[0].status.qosClass}'; echo
Burstable

El 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: 64Mi

Amb 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 BestEffortBurstable → el Guaranteed sobreviu, amb els missatges de l'apartat 12.

Amb requests.memory: 1Gi a victima-burstable i el mateix consum de 700Mi:

oom_score_adj = 1000 - (1000 x 1024 / 7751) = 1000 - 132 = 868
kubectl exec -n laboratori-qos victima-burstable -- cat /proc/1/oom_score_adj
kubectl get pods -n laboratori-qos -w
868

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          3m40s

El Burstable ja no cau. Dos motius que actuen alhora:

  1. El seu oom_score_adj va baixar de 975 a 868, així que el kernel el prioritza menys com a víctima.
  2. 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 seves requests", 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.

  1. Si postgres-reserves hagués estat Burstable amb requests.memory: 256Mi i un consum real d'1,8 GiB:
oom_score_adj = 1000 - (1000 x 256 / 7751) = 967

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.

  1. 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

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats