La lliçó anterior va acabar assenyalant el supòsit ocult de tot el que portem construït: hem donat per fet que, quan l'HorizontalPodAutoscaler demana trenta rèpliques d'api-reserves, hi ha lloc on posar-les. No n'hi ha. El clúster de Rutas Norte té quatre nodes, i trenta pods d'api-reserves més quinze de botiga-web més vint de worker-notificacions no hi caben ni de bon tros.

El resultat d'aquesta situació té un nom i un aspecte molt concret: pods en estat Pending, amb un esdeveniment FailedScheduling que diu Insufficient cpu. Ja el sabem llegir des de 06-05 i 07-06. El que no sabem encara és què fer-hi de manera automàtica.

Aquest és el tercer i últim nivell de l'escalat. L'HPA canvia el nombre de pods. El VPA canvia la mida de cada pod. L'autoescalat de clúster canvia el nombre de nodes. Sense ell, els altres dos tenen un sostre dur que no poden travessar.

Veurem el Cluster Autoscaler en detall: com decideix ampliar, la part delicada de reduir nodes (que és on són tots els problemes reals), els temps que triga de debò un node a estar disponible, el patró de sobreaprovisionament que compensa aquests temps, i Karpenter com a alternativa moderna. Acabarem amb el pla de capacitat concret de Rutas Norte per al pont de maig, amb els seus números i el seu cost.

Contingut

  1. El sostre dur: quan l'HPA demana i no hi ha lloc
  2. El símptoma exacte: Pending i FailedScheduling
  3. Què és el Cluster Autoscaler i on viu
  4. Com decideix ampliar: simulació i expanders
  5. Grups de nodes i la seva relació amb el proveïdor
  6. La reducció de nodes: la part delicada
  7. Els motius pels quals un node no es pot buidar
  8. Diagnòstic: el ConfigMap d'estat i els esdeveniments
  9. Els temps reals: quant triga un node de debò
  10. Sobreaprovisionament amb pods de farciment
  11. Karpenter: l'alternativa moderna
  12. Com encaixen els tres escaladors
  13. Què es pot practicar a minikube
  14. El pla de capacitat de Rutas Norte i el seu cost
  15. Errors comuns i consells
  16. Exercicis
  17. Conclusió

  1. El sostre dur: quan l'HPA demana i no hi ha lloc

Posem l'escenari amb números reals. El clúster de Rutas Norte en producció:

Quantitat
Nodes 4
CPU per node 4 nuclis
Memòria per node 16 GiB
CPU total bruta 16 nuclis
Reservat per al kubelet i el sistema ~0,5 nuclis i 1,5 GiB per node
CPU assignable total ~14 nuclis
Memòria assignable total ~58 GiB

I el consum en repòs, amb tots els components a minReplicas:

Component Rèpliques requests.cpu CPU total
api-reserves (api + sidecar) 4 520m 2,08
botiga-web 3 200m 0,60
worker-notificacions 2 320m 0,64
postgres-reserves 1 2000m 2,00
redis-cache 1 300m 0,30
DaemonSets (Fluentd, Falco, CNI) 4×3 ~150m 1,80
Ingress controller 2 200m 0,40
Total en repòs 7,82 nuclis

Queden uns 6,2 nuclis lliures. Ara arriba el pont de maig:

L'HPA d'api-reserves vol 30 repliques.
Actuals: 4. Noves: 26. Cadascuna: 520m.
CPU necessaria: 26 x 520m = 13,52 nuclis.

CPU disponible: 6,2 nuclis.
CPU que falta: 7,32 nuclis.

Repliques que hi caben: 6,2 / 0,52 = 11,9 -> 11 repliques noves.
Repliques que NO hi caben: 15.

Quinze pods d'api-reserves es quedaran en Pending. L'HPA haurà fet la seva feina: va demanar 30 rèpliques i el Deployment les va crear. El ReplicaSet les té registrades. Però el planificador no troba on posar-les, i allà es queden.

I això és el pitjor de l'assumpte: l'HPA no ho sap. A kubectl get hpa veuràs REPLICAS 30. A Grafana, el panell de rèpliques dirà 30. La plataforma tindrà 15 pods llestos i 15 fantasmes. I com que l'HPA calcula la mitjana de CPU només entre els pods que sí que existeixen i estan llestos, veurà que estan al 90 % i demanarà... res més, perquè ja és a maxReplicas.

El resultat és una plataforma que cau amb la il·lusió d'estar escalada.

  1. El símptoma exacte: Pending i FailedScheduling

Aprenguem a reconèixer-ho amb precisió, perquè és el disparador de tota la resta.

kubectl get pods -n rutas-norte-pro -l app=api-reserves
NAME                            READY   STATUS    RESTARTS   AGE
api-reserves-7c9d4f8b6d-2mk8p   2/2     Running   0          4h
api-reserves-7c9d4f8b6d-5xqzn   2/2     Running   0          4h
...
api-reserves-7c9d4f8b6d-qw8vz   0/2     Pending   0          92s
api-reserves-7c9d4f8b6d-rt4nk   0/2     Pending   0          92s
api-reserves-7c9d4f8b6d-sv7mx   0/2     Pending   0          91s

Pending amb 0/2 i sense reinicis: el pod existeix a l'API però cap node no l'ha acceptat. No hi ha contenidors corrent perquè no hi ha on.

kubectl describe pod api-reserves-7c9d4f8b6d-qw8vz -n rutas-norte-pro
Name:             api-reserves-7c9d4f8b6d-qw8vz
Namespace:        rutas-norte-pro
Priority:         0
Node:             <none>
Status:           Pending
...
Events:
  Type     Reason            Age    From               Message
  ----     ------            ----   ----               -------
  Warning  FailedScheduling  95s    default-scheduler  0/4 nodes are available:
           4 Insufficient cpu. preemption: 0/4 nodes are available:
           4 No preemption victims found for incoming pod.
  Normal   NotTriggerScaleUp 90s    cluster-autoscaler  pod didn't trigger scale-up:
           1 max node group size reached

Dues línies d'or:

Node: <none> — confirmació que no està assignat a cap node.

0/4 nodes are available: 4 Insufficient cpu — el planificador va avaluar els quatre nodes i els quatre van fallar per CPU insuficient. El missatge sempre té la forma X/Y nodes are available: <raons>, i les raons s'agrupen per causa. Les que veuràs a la pràctica:

Missatge Significat Ho resol el CA?
Insufficient cpu No hi ha CPU assignable suficient
Insufficient memory No hi ha memòria assignable suficient
node(s) had untolerated taint {...} Taints sense toleration (06-05) Depèn del grup de nodes
node(s) didn't match Pod's node affinity/selector Afinitat de node no satisfeta (06-05) Només si hi ha un grup que la compleixi
node(s) didn't match pod topology spread constraints Distribució topològica (09-05) De vegades
node(s) had volume node affinity conflict El PV és en una altra zona (mòdul 5) No
pod has unbound immediate PersistentVolumeClaims El PVC no s'ha aprovisionat No
Insufficient nvidia.com/gpu Recurs estès esgotat Sí, si hi ha grup amb GPU

La línia NotTriggerScaleUp és la del Cluster Autoscaler, i en aquest exemple diu max node group size reached: el CA està instal·lat, ha vist el pod pendent, i no hi pot fer res perquè el grup de nodes ja és a la seva mida màxima. Tornarem a aquest missatge a l'apartat 8, perquè és la principal eina de diagnòstic.

Una ordre molt útil per veure tots els pendents d'un cop d'ull:

kubectl get pods --all-namespaces --field-selector status.phase=Pending

I per veure l'ocupació real dels nodes:

kubectl describe nodes | grep -A 8 "Allocated resources"
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests      Limits
  --------           --------      ------
  cpu                3410m (97%)   6200m (177%)
  memory             9856Mi (67%)  14Gi (98%)

Fixa't que la columna que importa és Requests, no Limits. El planificador col·loca pods segons requests. Un node al 97 % de requests està ple a efectes de planificació, encara que el consum real sigui del 30 %. És la lliçó del mòdul 3, ara amb conseqüències de capacitat.

  1. Què és el Cluster Autoscaler i on viu

El Cluster Autoscaler (CA) és un component que ajusta el nombre de nodes del clúster. Com el VPA, viu al repositori kubernetes/autoscaler i no forma part del nucli de Kubernetes.

El seu funcionament en una frase: observa els pods que no es poden planificar i afegeix nodes; observa els nodes infrautilitzats i els retira.

On s'executa

Corre com un Deployment dins del mateix clúster, normalment a kube-system, amb un ServiceAccount que té permisos amplis (llegir pods i nodes, crear esdeveniments, desallotjar pods) i credencials del proveïdor de núvol per manipular els grups de nodes.

A Kubernetes gestionat (10-06) el proveïdor l'instal·la i el configura per tu: a EKS, AKS i GKE actives l'autoescalat amb un interruptor i el proveïdor s'encarrega de la resta. És l'escenari més habitual, i per això convé entendre'n el comportament encara que mai l'hagis d'instal·lar a mà.

El que necessita per funcionar

Requisit Per què
Una API per crear i destruir nodes El CA no arrenca màquines: demana al proveïdor que ho faci
Grups de nodes amb mida mínima i màxima És la unitat sobre la qual opera
Nodes homogenis dins de cada grup Simula fent servir un node de plantilla per grup
Que tots els pods declarin requests Sense requests no pot simular si un pod hi cabria

Aquest tercer requisit és més important del que sembla. El CA assumeix que tots els nodes d'un grup són idèntics. Quan simula si un pod cabria en un node nou del grup A, fa servir la plantilla del grup A. Si el grup té màquines heterogènies, la simulació menteix i el CA pren decisions equivocades. D'aquí la recomanació universal: un tipus de màquina per grup de nodes.

I el quart requisit connecta amb tot el mòdul 3: un pod sense requests és invisible per al planificador a efectes de capacitat, i també per al CA. Aquí, un altre cop, els requests són la base de tot.

  1. Com decideix ampliar: simulació i expanders

El bucle del CA s'executa cada 10 segons per defecte (--scan-interval).

El procés de decisió

  1. Llista els pods no planificables. Els que porten almenys --max-pod-provisioning-time en Pending amb FailedScheduling.
  2. Agrupa pods equivalents. Els pods del mateix controlador amb els mateixos requisits es tracten com un grup, per no simular vint vegades el mateix.
  3. Per a cada grup de nodes, simula. «Si afegeixo un node del grup A, quants d'aquests pods pendents hi cabrien?» La simulació fa servir el planificador real, amb tots els seus predicats: recursos, taints, afinitats, distribució topològica, ports d'amfitrió.
  4. Descarta els grups que no ajuden. Si afegir un node del grup B no permet planificar ni un sol pod (perquè té un taint que el pod no tolera, per exemple), aquest grup es descarta.
  5. Tria entre els grups viables fent servir l'expander.
  6. Calcula quants nodes calen i demana al proveïdor que ampliï el grup.

Un detall important del pas 6: el CA pot afegir diversos nodes alhora. Si hi ha 15 pods pendents de 520m cadascun i en un node de 4 nuclis en caben 6, demanarà 3 nodes de cop, no un cada vegada. Això està limitat per --max-nodes-total i pel màxim del grup.

Els expanders

Quan diversos grups de nodes servirien, l'expander decideix quin.

Expander Criteri Quan fer-lo servir
random Tria a l'atzar entre els viables Per defecte. Només vàlid si tots els grups són equivalents
most-pods El grup que permetria planificar més pods pendents Quan el prioritari és desencallar ràpid
least-waste El grup que deixaria menys CPU i memòria ocioses després de col·locar els pods El més recomanable en general. Optimitza l'ajust
price El grup més barat (requereix suport del proveïdor) Quan el cost mana i hi ha tipus de màquina variats
priority Segons una llista de prioritats en un ConfigMap Control explícit: provar primer spot, després sota demanda

Exemple concret de least-waste. Hi ha 3 pods pendents que necessiten 500m i 512Mi cadascun (total: 1,5 nuclis i 1,5 GiB), i dos grups disponibles:

Grup Tipus de node Assignable Després de col·locar els 3 pods Malbaratament
A 2 nuclis, 8 GiB 1,8 nuclis / 7 GiB 0,3 nuclis i 5,5 GiB lliures CPU: 17 %, RAM: 79 %
B 8 nuclis, 32 GiB 7,7 nuclis / 30 GiB 6,2 nuclis i 28,5 GiB lliures CPU: 81 %, RAM: 95 %

least-waste tria el grup A: s'ajusta millor i no paga per 6 nuclis ociosos. most-pods triaria el B (hi cabrien molts més pods futurs). price triaria l'A si és més barat.

Configuració de l'expander:

# Fragment del Deployment del cluster-autoscaler
spec:
  containers:
    - name: cluster-autoscaler
      image: registry.k8s.io/autoscaling/cluster-autoscaler:v1.30.0
      command:
        - ./cluster-autoscaler
        - --cloud-provider=aws
        - --namespace=kube-system
        - --expander=least-waste
        - --scan-interval=10s
        - --balance-similar-node-groups         # Reparteix entre grups equivalents (zones)
        - --skip-nodes-with-local-storage=false
        - --skip-nodes-with-system-pods=false
        - --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/rutas-norte

L'opció --balance-similar-node-groups mereix menció perquè resol un problema real d'alta disponibilitat: si tens un grup de nodes per zona de disponibilitat, aquest indicador fa que el CA reparteixi els nodes nous equilibradament entre zones en lloc d'amuntegar-los en una. Sense ell, pots acabar amb dotze nodes a la zona A i cap a la B, i una caiguda de zona et deixa sense plataforma. Connecta directament amb el que veurem a 09-05.

L'expander priority: provar primer les màquines barates

Un patró molt rendible al núvol. Les instàncies spot (interrompibles) costen una fracció del preu, però el proveïdor les pot reclamar amb dos minuts d'avís.

apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler-priority-expander
  namespace: kube-system
data:
  priorities: |-
    100:
      - rutas-norte-spot-.*        # Primera opcio: spot, molt mes barat
    50:
      - rutas-norte-ondemand-.*    # Reserva: sota demanda, sempre disponible

El CA intenta primer el grup de prioritat 100. Si el proveïdor no té capacitat spot disponible, cau al de prioritat 50. Per a worker-notificacions, que tolera interrupcions sense problema, és l'opció evident. Per a postgres-reserves, mai.

  1. Grups de nodes i la seva relació amb el proveïdor

El CA no crea màquines: manipula grups de nodes del proveïdor. Cada proveïdor té el seu nom per al mateix:

Proveïdor Nom del concepte
AWS Auto Scaling Group (ASG)
Azure Virtual Machine Scale Set (VMSS) / Agent Pool
Google Cloud Managed Instance Group (MIG) / Node Pool
On-premise / kubeadm Requereix un proveïdor personalitzat o Cluster API

Un grup de nodes té una mida mínima, una màxima i una actual. El CA només canvia l'actual, dins d'aquests límits. Tota la resta (el tipus de màquina, la imatge, els taints i etiquetes inicials) el defineix el grup.

Els grups de Rutas Norte:

rutas-norte-general-a   min=1  max=8   4 nuclis / 16 GiB   zona A
rutas-norte-general-b   min=1  max=8   4 nuclis / 16 GiB   zona B
rutas-norte-general-c   min=1  max=8   4 nuclis / 16 GiB   zona C
rutas-norte-dades       min=2  max=3   8 nuclis / 32 GiB   taint: rol=dades:NoSchedule

Tres decisions de disseny que convé entendre:

Un grup per zona. Necessari perquè --balance-similar-node-groups reparteixi entre zones i perquè l'afinitat de zona funcioni. Amb un únic grup multizona, el CA no pot garantir en quina zona apareixerà el node nou, i això trenca la distribució topològica de 09-05.

Un grup dedicat a dades amb taint. postgres-reserves i redis-cache tenen tolerations per a rol=dades (06-05) i afinitat de node cap a aquest grup. Així les bases de dades viuen en màquines grans amb disc ràpid i no comparteixen node amb l'allau de pods d'api-reserves. El taint impedeix que els pods de l'aplicació hi aterrin.

El grup de dades té max=3, molt baix. És deliberat: no volem que el CA arrenqui màquines grans i cares per error. postgres-reserves no escala horitzontalment (09-01), així que aquest grup gairebé mai necessita créixer.

Etiquetes i taints als nodes nous

Quan el CA simula si un pod cabria en un node del grup A, fa servir una plantilla. Si el node real, en arrencar, rep etiquetes o taints que la plantilla no coneixia, la simulació menteix.

Cas típic i molt frustrant: un pod amb nodeSelector: disc=ssd està Pending. El CA simula amb la plantilla del grup, que no té aquesta etiqueta, conclou que el pod no hi cabria ni amb un node nou, i no escala. Però els nodes reals del grup que reben aquesta etiqueta en arrencar, mitjançant un script d'arrencada.

La solució és declarar les etiquetes i taints a la mateixa definició del grup de nodes del proveïdor (a AWS, amb etiquetes k8s.io/cluster-autoscaler/node-template/label/... a l'ASG), perquè el CA les conegui abans d'arrencar el node.

  1. La reducció de nodes: la part delicada

Ampliar és fàcil: hi ha pods pendents, s'afegeixen nodes. Reduir és on són tots els problemes, perquè retirar un node implica desallotjar els pods que té i confiar que reapareixeran en un altre lloc sense trencar res.

L'algoritme

Cada 10 segons, per a cada node:

  1. És per sota del llindar d'utilització? Per defecte, --scale-down-utilization-threshold=0.5: la suma de requests dels seus pods és menor del 50 % de la seva capacitat assignable.
  2. Fa prou temps que hi és? --scale-down-unneeded-time=10m: deu minuts consecutius per sota del llindar.
  3. Es pot buidar? Simula si tots els seus pods cabrien en altres nodes existents. Si no hi caben, no es toca.
  4. Hi ha algun pod que impedeixi el desallotjament? La llista de l'apartat 7.
  5. Si tot passa: acordona el node, desallotja els seus pods respectant els PDB, espera, i demana al proveïdor que l'elimini.

Paràmetres principals:

Paràmetre Defecte Què controla
--scale-down-enabled true Interruptor general de la reducció
--scale-down-utilization-threshold 0.5 Llindar d'infrautilització
--scale-down-unneeded-time 10m Temps consecutiu abans d'actuar
--scale-down-delay-after-add 10m Espera després d'haver afegit un node
--scale-down-delay-after-delete 0s Espera després d'haver eliminat un node
--scale-down-delay-after-failure 3m Espera després d'un error de reducció
--max-graceful-termination-sec 600 Màxim esperant que un pod acabi
--max-empty-bulk-delete 10 Nodes buits eliminables alhora

--scale-down-delay-after-add és el paràmetre clau per evitar el ball de nodes. Sense ell, el CA podria afegir un node, veure que el clúster queda infrautilitzat i treure'l immediatament. Deu minuts de gràcia trenquen aquest cicle.

Sobre el llindar d'utilització, un càlcul important:

Llindar per defecte: 50 %.

Node de 4 nuclis assignables amb aquests pods:
  - 2 pods d'api-reserves:  2 x 520m = 1040m
  - 1 pod de botiga-web:              200m
  - DaemonSets (fluentd, falco, cni): 450m
  Total requests: 1690m sobre 3500m assignables = 48,3 %

48,3 % < 50 %  ->  candidat a reduccio.

El CA simula: hi caben aquests 4 pods als altres nodes?
Si si -> els desallotja i elimina el node.
Si no -> el deixa.

Fixa't que els DaemonSets compten per a la utilització però no impedeixen l'eliminació (s'ignoren al pas 4, perquè desapareixen amb el node). Amb molts DaemonSets, la utilització base d'un node buit ja és alta, i això fa que el CA redueixi menys del que esperaries. En clústers amb molts agents (logging, seguretat, malla de serveis, monitoratge), és habitual haver de baixar el llindar al 30-40 % perquè la reducció funcioni.

El cas especial dels nodes buits

Un node que només té DaemonSets es considera buit i s'elimina per un camí ràpid, sense la simulació completa. Per això, després d'una punta de trànsit, els nodes que queden totalment lliures desapareixen bastant ràpid, mentre que els que tenen un pod solt triguen molt més.

Això explica un fenomen que desconcerta molta gent: després del pont de maig, vuit nodes se'n van en vint minuts i dos es queden durant hores amb un sol pod cadascun. La solució de fons no és tocar el CA: és la distribució topològica i la consolidació (Karpenter, apartat 11).

  1. Els motius pels quals un node no es pot buidar

Aquesta és la llista que cal conèixer de memòria, perquè explica el 90 % dels casos de «tinc nodes buits que l'autoescalador no treu i estic pagant per ells».

# Motiu Detall Com resoldre-ho
1 Pods sense controlador Un pod creat a mà (sense Deployment, ReplicaSet, Job, StatefulSet) no es pot recrear en un altre lloc: si el desallotges, desapareix per sempre Fes servir sempre un controlador. Mai kubectl run sense --restart en producció
2 Pods amb emmagatzematge local emptyDir, hostPath o volums locals: la dada viu en aquest node i es perdria --skip-nodes-with-local-storage=false si assumeixes la pèrdua; o migrar a PV de xarxa
3 PodDisruptionBudget restrictiu El PDB no permet desallotjar aquest pod sense baixar del mínim (09-05) Revisar el PDB; assegurar rèpliques suficients
4 Pods del sistema a kube-system Components crítics sense PDB --skip-nodes-with-system-pods=false (amb compte), o donar-los PDB
5 Anotació safe-to-evict: "false" Marca explícita de «no em moguis» Treure l'anotació si ja no aplica
6 Pods que no caben en cap altre node La simulació falla: el pod és massa gran o té restriccions Afegir capacitat o relaxar restriccions
7 Node amb l'anotació scale-down-disabled: "true" Exclusió explícita del node Treure l'anotació

L'anotació safe-to-evict

És l'eina de control més directa:

apiVersion: v1
kind: Pod
metadata:
  annotations:
    # Aquest pod NO ha de ser desallotjat pel Cluster Autoscaler.
    # El node que l'allotgi mai sera retirat mentre hi sigui.
    cluster-autoscaler.kubernetes.io/safe-to-evict: "false"

Quan té sentit "false":

  • Un treball per lots llarg que perdria hores de còmput si es reinicia. informes-ocupacio és un candidat: si triga 40 minuts i el CA el mata al minut 35, la nit es perd.
  • Una migració de base de dades en curs.
  • Un pod amb estat local irreproduïble.

I el valor contrari, "true", serveix per desbloquejar situacions:

metadata:
  annotations:
    # Aquest pod SI que pot ser desallotjat encara que faci servir emptyDir.
    # Sabem que el contingut del seu emptyDir es una cache regenerable.
    cluster-autoscaler.kubernetes.io/safe-to-evict: "true"

És la manera neta de resoldre el motiu 2 sense canviar l'indicador global del CA. A Rutas Norte, botiga-web fa servir un emptyDir per a la memòria cau de nginx: marcar-lo com a safe-to-evict: "true" permet que el CA retiri els seus nodes sense problema, perquè aquesta memòria cau es regenera sola.

El PDB com a bloquejador (avançament de 09-05)

El cas més freqüent i el més perillós. Un PDB així:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: postgres-reserves
  namespace: rutas-norte-pro
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: postgres-reserves

Amb una sola rèplica de postgres-reserves i minAvailable: 1, aquest pod mai pot ser desallotjat: fer-ho deixaria 0 disponibles. El node que l'allotgi queda ancorat permanentment. El CA ho intentarà, fallarà, i ho registrarà als seus esdeveniments.

En aquest cas concret el bloqueig és desitjable (no volem que el CA mogui la base de dades pel seu compte), però el mateix patró aplicat per error a un servei qualsevol produeix nodes zombis que ningú sap per què no se'n van. Ho desenvolupem a fons a 09-05.

  1. Diagnòstic: el ConfigMap d'estat i els esdeveniments

El CA publica el seu estat intern en un ConfigMap. És el primer lloc on mirar.

kubectl get configmap cluster-autoscaler-status -n kube-system -o yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler-status
  namespace: kube-system
data:
  status: |
    Cluster-autoscaler status at 2026-05-01 10:14:32:
    Cluster-wide:
      Health:      Healthy (ready=7 unready=0 notStarted=1 registered=8 longNotStarted=0)
                   LastProbeTime: 2026-05-01 10:14:31
      ScaleUp:     InProgress (ready=7 registered=8)
                   LastProbeTime: 2026-05-01 10:14:31
      ScaleDown:   NoCandidates (candidates=0)
                   LastProbeTime: 2026-05-01 10:14:31

    NodeGroups:
      Name:        rutas-norte-general-a
      Health:      Healthy (ready=3 unready=0 notStarted=1 registered=4
                   cloudProviderTarget=4 (minSize=1, maxSize=8))
      ScaleUp:     InProgress (ready=3 registered=4)
      ScaleDown:   NoCandidates (candidates=0)

      Name:        rutas-norte-general-b
      Health:      Healthy (ready=2 unready=0 notStarted=0 registered=2
                   cloudProviderTarget=2 (minSize=1, maxSize=8))
      ScaleUp:     NoActivity
      ScaleDown:   NoCandidates (candidates=0)

      Name:        rutas-norte-dades
      Health:      Healthy (ready=2 unready=0 notStarted=0 registered=2
                   cloudProviderTarget=2 (minSize=2, maxSize=3))
      ScaleUp:     NoActivity
      ScaleDown:   NoCandidates (candidates=0)

Com llegir-ho:

Camp Significat
ready Nodes operatius i acceptant pods
unready Nodes registrats però no llestos (problema)
notStarted Nodes demanats que encara estan arrencant. Aquest és el que cal mirar durant una punta
registered Total coneguts pel clúster
cloudProviderTarget Quants n'ha demanat el CA al proveïdor
ScaleUp: InProgress Hi ha una ampliació en marxa
ScaleDown: NoCandidates Cap node no compleix els criteris de reducció

A la sortida de dalt: el grup A té 3 llestos i 1 arrencant. El CA ja ha demanat el node; només cal esperar. Aquesta informació és exactament el que necessites durant un incident per saber si el CA està treballant o encallat.

Estats de ScaleDown que veuràs:

Estat Significat
NoCandidates Cap node no és per sota del llindar
CandidatesPresent Hi ha candidats, esperant l'unneeded-time
InProgress Buidant un node ara mateix

Els esdeveniments: per què NO s'escala

Quan el CA decideix no fer res, ho diu en un esdeveniment sobre el pod pendent:

kubectl get events -n rutas-norte-pro --field-selector reason=NotTriggerScaleUp \
  --sort-by=.lastTimestamp
LAST SEEN   TYPE     REASON              OBJECT                              MESSAGE
23s         Normal   NotTriggerScaleUp   pod/api-reserves-7c9d4f8b6d-qw8vz   pod didn't trigger scale-up:
                                                                             1 max node group size reached,
                                                                             1 node(s) had untolerated taint {rol: dades}

Aquest missatge és un regal: et diu exactament per què cada grup de nodes va ser descartat. Aquí: un grup és al màxim i un altre té un taint que el pod no tolera.

Missatges habituals de NotTriggerScaleUp i la seva traducció:

Missatge Significat real Acció
max node group size reached El grup és al seu maxSize Pujar el màxim del grup
node(s) had untolerated taint {...} Els nodes del grup tenen un taint que el pod no tolera Afegir toleration o crear un altre grup
node(s) didn't match Pod's node affinity El nodeSelector/afinitat no coincideix amb la plantilla Revisar les etiquetes del grup
Insufficient cpu (al missatge del CA) Ni un node nou del grup tindria CPU suficient per a aquest pod El pod és massa gran. Reduir els seus requests o fer servir un grup més gran
in backoff after failed scale-up Un intent anterior va fallar (sense capacitat al proveïdor, quota esgotada) Mirar els logs del CA i la quota del proveïdor
pod has unbound immediate PersistentVolumeClaims És un problema d'emmagatzematge, no de capacitat Revisar la StorageClass (mòdul 5)

I per a la reducció bloquejada:

kubectl logs -n kube-system deployment/cluster-autoscaler | grep -i "scale.down\|unremovable"
I0501 10:22:14.882  scale_down.go: Node ip-10-0-3-47 is not suitable for removal:
    pod redis-cache-0 is not replicated and has local storage
I0501 10:22:14.883  scale_down.go: Node ip-10-0-2-19 is not suitable for removal:
    pdb-blocked: not enough pod disruption budget to move postgres-reserves-0
I0501 10:22:14.884  scale_down.go: Node ip-10-0-1-88 is not suitable for removal:
    cluster-autoscaler.kubernetes.io/safe-to-evict annotation set to false

Els logs del CA són la font definitiva quan el ConfigMap i els esdeveniments no basten. Conserva'ls a EFK (07-05): són imprescindibles per a l'anàlisi posterior d'un incident de capacitat.

  1. Els temps reals: quant triga un node de debò

Aquí hi ha la dada que canvia l'estratègia, i la que gairebé mai s'explica.

Desglossament del temps

gantt
    title Temps des del pod Pending fins al pod Running en un node nou
    dateFormat  s
    axisFormat  %S s

    section Deteccio
    El pod passa a Pending       :a1, 0, 5s
    El CA el detecta (scan)      :a2, after a1, 10s

    section Aprovisionament
    Peticio al proveidor         :b1, after a2, 5s
    Arrencada de la maquina      :b2, after b1, 45s
    Arrencada del sistema        :b3, after b2, 20s

    section Unio al cluster
    El kubelet arrenca i registra :c1, after b3, 15s
    CNI i DaemonSets llestos     :c2, after c1, 30s
    El node passa a Ready        :c3, after c2, 5s

    section Pod
    Planificacio                 :d1, after c3, 2s
    Descarrega de la imatge      :d2, after d1, 40s
    Arrencada del contenidor     :d3, after d2, 25s
    Readiness probe OK           :d4, after d3, 10s

Sumant: entre 3 i 4 minuts en el cas bo. En el cas dolent (imatge gran no cachejada, regió saturada, DaemonSets pesants) pot passar dels 6 minuts.

Desglossament típic en taula:

Fase Temps típic Què l'empitjora
Detecció del pod pendent 10-15 s --scan-interval alt
Petició al proveïdor 5-10 s Reintents per quota
Arrencada de la màquina 30-60 s Tipus de màquina, regió saturada
Arrencada del sistema operatiu 15-30 s Imatge no optimitzada
Registre del kubelet 10-20 s Configuració complexa, bootstrap lent
CNI + DaemonSets llestos 20-60 s Molts DaemonSets pesants
Descàrrega de la imatge del pod 20-120 s Imatge gran, registre llunyà
Arrencada del contenidor + readiness 20-40 s Aplicació lenta d'arrencar
Total 2-6 min

Per què això no salva una punta sobtada

Tornem al pont de maig. La venda obre a les 10:00. El perfil de trànsit real:

10:00:00   S'obre la venda. El transit passa de 200 rps a 2400 rps en 40 segons.
10:00:15   L'HPA detecta CPU al 280 %. Demana 12 repliques.
10:00:20   El Deployment crea 8 pods. 4 caben als nodes existents.
           4 queden en Pending.
10:00:30   El CA detecta els pendents i demana 2 nodes.
10:00:45   L'HPA torna a avaluar: segueix saturat. Demana 24 repliques.
           Mes pods Pending.
10:01:00   El CA demana 3 nodes mes.
10:03:30   Arriba el primer node. Es planifiquen 6 pods.
10:04:10   Els primers pods del node nou passen a Ready.
10:05:00   Arriben els altres nodes. La plataforma assoleix capacitat.

TEMPS TOTAL DE DEGRADACIO: gairebe CINC MINUTS.

Cinc minuts de latència degradada i errors en el moment de més valor comercial de l'any. Milers d'usuaris que abandonen la compra. El Cluster Autoscaler, per ben configurat que estigui, no pot resoldre una punta que puja en quaranta segons, perquè la física d'arrencar una màquina virtual no ho permet.

D'aquí surten dues estratègies, i calen totes dues:

  1. Sobreaprovisionament: tenir capacitat ja arrencada i esperant (apartat 10).
  2. Escalat anticipat: escalar abans del pic fent servir un disparador temporal, no reactiu. És el disparador cron de KEDA que veurem a 09-04.

  1. Sobreaprovisionament amb pods de farciment

El patró que resol el problema de l'apartat anterior, i que fa servir de manera brillant la PriorityClass i la preemption de 06-05.

La idea

Despleguem pods que no fan res —dormen— però reserven CPU i memòria, amb una prioritat negativa. Efectes:

  1. El CA compta aquests requests com a ocupació, així que manté nodes arrencats per allotjar-los.
  2. Quan arriba un pod real (prioritat 0 o superior) i no hi ha lloc, el planificador desallotja instantàniament un pod de farciment i col·loca el real al seu forat. La preemption triga segons, no minuts.
  3. Els pods de farciment desallotjats passen a Pending, cosa que dispara el CA per arrencar nodes nous... que ompliran el coixí per a la propera vegada.

És un coixí de capacitat autoregeneratiu. Compres capacitat ociosa a canvi de temps de resposta.

sequenceDiagram
    participant HPA
    participant Sched as Planificador
    participant Farciment as Pods de farciment<br/>(prioritat -10)
    participant CA as Cluster Autoscaler

    Note over Farciment: Estat normal: 3 pods de farciment<br/>ocupant 3 nuclis als nodes
    HPA->>Sched: Crear 10 pods d'api-reserves (prioritat 0)
    Sched->>Sched: No hi ha CPU lliure
    Sched->>Farciment: PREEMPTION: desallotjar 3 pods de farciment
    Note over Sched: En 2-5 segons hi ha forat
    Sched->>Sched: Planificar pods d'api-reserves
    Farciment->>CA: 3 pods de farciment en Pending
    CA->>CA: Demanar nodes nous (3-4 min)
    Note over Farciment: El coixi es regenera sol

Implementació completa

Pas 1: la PriorityClass negativa.

# k8s/base/priorityclass-farciment.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: farciment-capacitat
# CLAU: valor NEGATIU. Qualsevol pod normal (prioritat 0 per defecte) te
# mes prioritat que aquests, aixi que els desallotjara sense dubtar-ho.
value: -10
# No es la classe per defecte: nomes la fan servir els pods que la demanen explicitament.
globalDefault: false
# preemptionPolicy per defecte es PreemptLowerPriority, pero aquests pods no
# han de desallotjar NINGU: son els ultims de la fila.
preemptionPolicy: Never
description: >
  Pods de farciment que reserven capacitat per absorbir puntes de transit.
  Son desallotjats instantaniament per qualsevol pod real. Veure 09-03.

Dos detalls importants:

  • value: -10 garanteix que qualsevol pod sense priorityClassName (prioritat 0) els superi.
  • preemptionPolicy: Never fa que aquests pods, en quedar-se Pending, no intentin desallotjar-ne d'altres. Seria absurd que el coixí fes fora un pod real.

Pas 2: el Deployment de farciment.

# k8s/entorns/pro/deployment-farciment-capacitat.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: farciment-capacitat
  namespace: rutas-norte-pro
  labels:
    app: farciment-capacitat
    app.kubernetes.io/part-of: rutas-norte
spec:
  # 6 pods de farciment. Cadascun reserva 1 nucli i 1 GiB.
  # Coixi total: 6 nuclis i 6 GiB, equivalent a gairebe dos nodes.
  # Suficient per absorbir 11 repliques d'api-reserves a l'instant.
  replicas: 6
  selector:
    matchLabels:
      app: farciment-capacitat
  template:
    metadata:
      labels:
        app: farciment-capacitat
        app.kubernetes.io/part-of: rutas-norte
      annotations:
        # Que el CA pugui retirar sense problema els nodes que nomes tinguin farciment.
        cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
    spec:
      priorityClassName: farciment-capacitat

      # Terminacio immediata: no hi ha res a desar, no esperem ni un segon.
      # Aixo es el que fa que la preemption sigui gairebe instantania.
      terminationGracePeriodSeconds: 0

      # Repartir el coixi entre nodes: de res serveix tenir 6 nuclis lliures
      # tots al mateix node si els pods nous necessiten repartir-se.
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: farciment-capacitat

      containers:
        - name: pausa
          # Imatge minima oficial: un proces que nomes dorm. ~700 KB.
          image: registry.k8s.io/pause:3.9
          resources:
            requests:
              cpu: "1"
              memory: 1Gi
            limits:
              cpu: "1"
              memory: 1Gi
          # No consumeix RES a la practica: nomes reserva el forat davant del
          # planificador. La CPU real usada es 0m.

La imatge pause és l'elecció canònica: és la mateixa que Kubernetes fa servir internament per al contenidor d'infraestructura de cada pod, pesa menys d'un megabyte, ja és a la memòria cau de tots els nodes i la seva única feina és dormir.

Pas 3: comprovar que funciona.

# Estat inicial: 6 pods de farciment repartits
kubectl get pods -n rutas-norte-pro -l app=farciment-capacitat -o wide
NAME                                  READY   STATUS    NODE
farciment-capacitat-5d8f7c9b4-2xkjp   1/1     Running   ip-10-0-1-42
farciment-capacitat-5d8f7c9b4-4mnqz   1/1     Running   ip-10-0-1-88
farciment-capacitat-5d8f7c9b4-7wrtv   1/1     Running   ip-10-0-2-19
farciment-capacitat-5d8f7c9b4-9hbcx   1/1     Running   ip-10-0-2-73
farciment-capacitat-5d8f7c9b4-kp3ln   1/1     Running   ip-10-0-3-47
farciment-capacitat-5d8f7c9b4-tv6ws   1/1     Running   ip-10-0-3-91

Ara forcem una punta:

kubectl scale deployment api-reserves -n rutas-norte-pro --replicas=20
kubectl get pods -n rutas-norte-pro -l app=farciment-capacitat --watch
NAME                                  READY   STATUS        AGE
farciment-capacitat-5d8f7c9b4-2xkjp   1/1     Terminating   4h
farciment-capacitat-5d8f7c9b4-4mnqz   1/1     Terminating   4h
farciment-capacitat-5d8f7c9b4-7wrtv   0/1     Pending       3s
farciment-capacitat-5d8f7c9b4-9hbcx   0/1     Pending       3s

I als esdeveniments dels pods d'api-reserves:

Events:
  Type     Reason      Age   From               Message
  ----     ------      ----  ----               -------
  Normal   Preempted   4s    default-scheduler  Preempted by pod
                                                farciment-capacitat-5d8f7c9b4-2xkjp
                                                on node ip-10-0-1-42
  Normal   Scheduled   3s    default-scheduler  Successfully assigned
                                                rutas-norte-pro/api-reserves-... to ip-10-0-1-42

Tres segons des de la creació del pod fins a la seva planificació, en lloc de tres minuts i mig. Aquesta és la diferència entre absorbir l'obertura de la venda i caure.

El cost

Res no és gratis:

Coixi: 6 nuclis i 6 GiB reservats permanentment.
Equival a ~1,7 nodes de 4 nuclis.

Cost estimat d'un node (4 nuclis, 16 GiB, sota demanda): 95 EUR/mes.
Cost del coixi: 1,7 x 95 = 162 EUR/mes = 1.940 EUR/any.

Benefici: absorbir instantaniament 11 repliques d'api-reserves.

Val la pena? A Rutas Norte, la resposta és clara: cinc minuts de caiguda a l'obertura del pont de maig costen molt més de 1.940 euros en bitllets no venuts. Però és una decisió de negoci, no tècnica, i s'ha de prendre amb números damunt la taula.

Optimització evident: el coixí no ha de ser constant. Un CronJob (06-03) que escali el Deployment de farciment de 2 a 6 rèpliques els divendres a la tarda i el torni a 2 els dilluns redueix el cost anual a una fracció:

# k8s/entorns/pro/cronjob-ajustar-farciment.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: ampliar-farciment-cap-de-setmana
  namespace: rutas-norte-pro
spec:
  schedule: "0 16 * * 5"       # Divendres a les 16:00
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: operador-escalat
          restartPolicy: OnFailure
          containers:
            - name: kubectl
              image: bitnami/kubectl:1.30
              command:
                - kubectl
                - scale
                - deployment/farciment-capacitat
                - --replicas=6
                - -n
                - rutas-norte-pro

I la seva parella el dilluns a les 6:00 amb --replicas=2. El ServiceAccount operador-escalat necessita un Role amb permís sobre deployments/scale, exactament com vam veure a 08-01.

  1. Karpenter: l'alternativa moderna

El Cluster Autoscaler té una limitació de disseny: treballa amb grups de nodes predefinits. Algú ha de decidir per endavant quins tipus de màquina existeixen, crear un grup per a cadascun, i mantenir-los.

Karpenter inverteix el plantejament: mira els pods pendents, calcula quina màquina seria la millor per a ells, i l'arrenca directament, sense grups.

Com funciona

En lloc de grups de nodes, defineixes restriccions de quins tipus de màquina són acceptables:

# NodePool: l'espai de decisions que Karpenter pot explorar
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: rutas-norte-general
spec:
  template:
    metadata:
      labels:
        entorn: pro
        app.kubernetes.io/part-of: rutas-norte
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]     # Prefereix spot; cau a on-demand
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["c", "m", "r"]           # Families de comput, general i memoria
        - key: karpenter.k8s.aws/instance-size
          operator: NotIn
          values: ["nano", "micro", "small"]
        - key: topology.kubernetes.io/zone
          operator: In
          values: ["eu-west-1a", "eu-west-1b", "eu-west-1c"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: rutas-norte

  # CONSOLIDACIO: la funcio estrella. Karpenter reavalua continuament si els
  # pods actuals cabrien en menys nodes, o en nodes mes barats, i migra.
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
    budgets:
      - nodes: "20%"           # Com a molt, tocar el 20 % dels nodes alhora

  limits:
    cpu: "200"                 # Sostre global de seguretat
    memory: 800Gi

Amb aquesta definició, quan apareixen 15 pods d'api-reserves pendents, Karpenter calcula que la millor màquina per allotjar-los és, posem, una c6i.4xlarge spot a la zona B, i l'arrenca. Sense que ningú hagi creat mai un grup de nodes c6i.4xlarge.

La consolidació

És la diferència més pràctica en el dia a dia. Karpenter revisa contínuament si el conjunt de pods actual cabria en una configuració de nodes més barata, i si és així, migra els pods i substitueix els nodes.

L'escenari que resol: després del pont de maig, el CA deixa cinc nodes amb un pod cadascun, perquè cap no baixa del llindar d'utilització de manera que permeti buidar-lo. Karpenter veu que aquests cinc pods cabrien en un sol node, arrenca aquest node, migra els pods i elimina els cinc.

És exactament el problema que vam esmentar al final de l'apartat 6, resolt d'arrel.

Comparació

Aspecte Cluster Autoscaler Karpenter
Unitat de treball Grups de nodes predefinits Nodes individuals
Elecció del tipus de màquina Fixa per grup Calculada segons els pods pendents
Configuració prèvia Un grup per tipus/zona Un NodePool amb restriccions
Temps d'aprovisionament 3-6 min 1-2 min (parla directament amb l'API d'EC2)
Consolidació No (només reducció per utilització) Sí, contínua
Optimització de cost Limitada (expander price) Nativa: tria el tipus més barat que serveixi
Gestió d'spot Grups separats + expander priority Nativa, amb caiguda automàtica a sota demanda
Suport de proveïdors Molts (AWS, Azure, GCP, OpenStack, Cluster API...) AWS madur; Azure disponible; altres en desenvolupament
Maduresa Molt alta, anys de producció Alta a AWS, més recent
Complexitat operativa Mitjana (mantenir grups) Baixa després de la configuració inicial
Predictibilitat Alta: saps quines màquines sortiran Menor: pot triar tipus inesperats

Quan triar cadascun:

  • Cluster Autoscaler si ets fora d'AWS, si tens requisits regulatoris sobre quins tipus de màquina són admissibles, si el teu proveïdor ja te'l dona configurat i funciona, o si valores la predictibilitat per sobre de l'estalvi.
  • Karpenter si ets a AWS, si el cost és una prioritat real, si la teva càrrega és heterogènia (pods petits i pods grans convivint), i si et compensa la reducció de feina operativa.

Per a Rutas Norte, amb quatre nodes i una càrrega homogènia, el CA és més que suficient. Si la plataforma creixés a cinquanta nodes amb càrregues variades, Karpenter seria la decisió evident. Tornarem a aquest tipus de decisions a 10-06 i 11-06.

Una nota important que la comparativa no ha d'amagar: Karpenter tampoc arregla la punta de quaranta segons. Un o dos minuts continuen sent massa. El sobreaprovisionament continua fent falta.

  1. Com encaixen els tres escaladors

Ja tenim les tres peces. Vegem el flux complet en ordre temporal.

flowchart TD
    T0["t=0 s<br/>Puja el transit"] --> HPA["t=15 s<br/>HPA: la CPU mitjana supera l'objectiu<br/>Calcula repliques desitjades"]
    HPA --> DEP["t=16 s<br/>El Deployment crea pods nous"]
    DEP --> SCHED{"t=17 s<br/>El planificador<br/>troba lloc?"}

    SCHED -->|Si| RUN["t=20-50 s<br/>Pod Running i Ready<br/>FI: absorbit"]

    SCHED -->|No| PRE{"Hi ha pods de<br/>farciment per desallotjar?"}
    PRE -->|Si| PREEMPT["t=20 s<br/>PREEMPTION<br/>Pod real col·locat en 3 s"]
    PREEMPT --> RUN2["t=50 s<br/>Pod Running<br/>FI: absorbit amb coixi"]
    PREEMPT --> PEND2["Els pods de farciment<br/>passen a Pending"]

    PRE -->|No| PEND["Pod en Pending<br/>esdeveniment FailedScheduling"]
    PEND2 --> CA
    PEND --> CA["t=30 s<br/>El Cluster Autoscaler detecta<br/>els pods no planificables"]

    CA --> SIM["Simula: quin grup de nodes<br/>permetria planificar-los?<br/>Aplica l'expander"]
    SIM --> REQ["t=40 s<br/>Peticio al proveidor de nuvol"]
    REQ --> BOOT["t=40 s a 3 min<br/>Arrencada, unio al cluster,<br/>CNI i DaemonSets"]
    BOOT --> READY["t=3 min<br/>Node Ready"]
    READY --> SCHED2["t=3 min 5 s<br/>Es planifiquen els pods pendents"]
    SCHED2 --> PULL["t=3 a 4 min<br/>Descarrega d'imatge i arrencada"]
    PULL --> RUN3["t=4 min<br/>Pod Running<br/>FI: absorbit amb node nou"]

    style RUN fill:#cfc,stroke:#393
    style RUN2 fill:#cfc,stroke:#393
    style RUN3 fill:#ffd,stroke:#c90
    style PEND fill:#fdd,stroke:#c00

Les tres rutes i els seus temps:

Ruta Quan passa Temps fins a servir trànsit
Cap als nodes actuals Hi ha capacitat lliure 20-50 s
Preemption del coixí No hi cap, però hi ha pods de farciment 30-60 s
Node nou No hi cap i no hi ha coixí 3-6 min

La conclusió estratègica és directa: el coixí de sobreaprovisionament no és un luxe, és el que converteix un incident de cinc minuts en un de trenta segons.

I encara queda una ruta millor, que no apareix al diagrama perquè no és reactiva: escalar abans que arribi el trànsit. Si a les 9:30 ja tens 25 rèpliques i vuit nodes perquè un disparador cron ho va ordenar, a les 10:00 no hi ha cap incident. Això és 09-04.

Un resum dels tres nivells

Nivell Què canvia Disparador Latència Component
1 Nombre de pods Mètrica de CPU/negoci 15-60 s HPA (09-01) / KEDA (09-04)
2 Mida de cada pod Històric de consum Hores-dies VPA (09-02)
3 Nombre de nodes Pods no planificables 3-6 min Cluster Autoscaler / Karpenter

Els tres són necessaris i cap no substitueix un altre. L'HPA sense CA té un sostre dur. El CA sense HPA no es dispara mai. El VPA els fa a tots dos més precisos, perquè els requests correctes són la base tant del càlcul de l'HPA com de la simulació del CA.

  1. Què es pot practicar a minikube

Moment d'honestedat: el Cluster Autoscaler no es pot provar de debò a minikube. No hi ha proveïdor de núvol que arrenqui màquines. El perfil rutas-norte té els nodes que li vas demanar en crear-lo, i aquests són tots.

El que SÍ que es pot practicar

1. Produir el símptoma i aprendre a llegir-lo.

# Un cluster de 2 nodes petits
minikube start -p rutas-norte --nodes=2 --cpus=2 --memory=4096

# Desplegar alguna cosa que no hi capiga
kubectl -n rutas-norte-dev create deployment ocupador \
  --image=registry.k8s.io/pause:3.9 --replicas=10

kubectl -n rutas-norte-dev set resources deployment/ocupador \
  --requests=cpu=800m,memory=512Mi

kubectl get pods -n rutas-norte-dev
NAME                        READY   STATUS    RESTARTS   AGE
ocupador-6f8d7c9b4-2mkjp    1/1     Running   0          15s
ocupador-6f8d7c9b4-4nqzx    1/1     Running   0          15s
ocupador-6f8d7c9b4-7wtrv    0/1     Pending   0          15s
ocupador-6f8d7c9b4-9bhcx    0/1     Pending   0          15s
...
kubectl describe pod -n rutas-norte-dev -l app=ocupador | grep -A5 Events

Veuràs exactament el FailedScheduling amb Insufficient cpu. És el mateix missatge que en producció.

2. El patró de sobreaprovisionament amb preemption. Això funciona perfectament, perquè la preemption és una funció del planificador, no del proveïdor de núvol.

# 1. Crear la PriorityClass negativa
kubectl apply -f k8s/base/priorityclass-farciment.yaml

# 2. Desplegar el farciment (ajustat a la mida de minikube)
kubectl -n rutas-norte-dev create deployment farciment \
  --image=registry.k8s.io/pause:3.9 --replicas=2
kubectl -n rutas-norte-dev set resources deployment/farciment \
  --requests=cpu=500m,memory=256Mi
kubectl -n rutas-norte-dev patch deployment farciment \
  -p '{"spec":{"template":{"spec":{"priorityClassName":"farciment-capacitat","terminationGracePeriodSeconds":0}}}}'

# 3. Verificar que el farciment ocupa
kubectl get pods -n rutas-norte-dev -o wide

# 4. Crear un pod real que no cabria sense desallotjar el farciment
kubectl -n rutas-norte-dev run pod-important \
  --image=registry.k8s.io/pause:3.9 \
  --overrides='{"spec":{"containers":[{"name":"pod-important","image":"registry.k8s.io/pause:3.9","resources":{"requests":{"cpu":"900m"}}}]}}'

# 5. Observar la preemption en directe
kubectl get events -n rutas-norte-dev --sort-by=.lastTimestamp | tail -20
LAST SEEN   TYPE     REASON      OBJECT                        MESSAGE
3s          Normal   Preempted   pod/farciment-7d8c9f4b6-2xkjp Preempted by pod
                                                               rutas-norte-dev/pod-important on node rutas-norte-m02
2s          Normal   Scheduled   pod/pod-important             Successfully assigned to rutas-norte-m02
1s          Normal   Scheduled   pod/farciment-7d8c9f4b6-9wqzn FailedScheduling

Veure el Preempted amb els teus propis ulls i mesurar que passa en segons és la part més valuosa de l'exercici, i es practica sense núvol.

3. Afegir i treure nodes a mà, simulant el que faria el CA.

# Afegir un node al perfil (equivalent al que fa el CA en ampliar)
minikube -p rutas-norte node add

# Veure com els pods Pending es planifiquen sols
kubectl get pods -n rutas-norte-dev --watch

# Retirar un node (equivalent a la reduccio, inclos el drenatge)
kubectl cordon rutas-norte-m03
kubectl drain rutas-norte-m03 --ignore-daemonsets --delete-emptydir-data
minikube -p rutas-norte node delete rutas-norte-m03

Aquesta seqüència cordondrain → eliminar és exactament el que fa el CA internament. Practicar-la a mà ensenya més sobre la reducció de nodes que llegir qualsevol documentació, i és la base del procediment de manteniment que veurem a 09-05.

4. Practicar l'anotació safe-to-evict i veure com bloqueja un drenatge.

kubectl annotate pod pod-important -n rutas-norte-dev \
  cluster-autoscaler.kubernetes.io/safe-to-evict=false

# Nota: aquesta anotacio la respecta el CA, no kubectl drain. Per veure el bloqueig
# real d'un drenatge cal un PDB, que es el que veurem a 09-05.

El que NO es pot practicar

No practicable Alternativa
Ampliació automàtica de nodes minikube node add a mà
Expanders i elecció de grup Estudiar la documentació i els logs d'un clúster real
Reducció automàtica de nodes cordon + drain + node delete a mà
El ConfigMap cluster-autoscaler-status No existeix sense CA instal·lat
Karpenter Requereix AWS

Per practicar el CA de debò calen: un clúster gestionat (10-06) amb l'autoescalat activat, o kind amb el proveïdor de proves del CA, o un entorn de núvol personal. Recomanació pràctica: un compte de proves d'un proveïdor durant una tarda, amb un clúster petit i maxSize baix, costa uns pocs euros i ensenya moltíssim.

  1. El pla de capacitat de Rutas Norte i el seu cost

Tanquem amb els números concrets. Aquesta és la feina que cal fer abans d'un pont de maig, i que no es va fer l'any passat.

Pas 1: la demanda màxima

Dels maxReplicas de 09-01 i els requests recalibrats amb el VPA de 09-02:

Component maxReplicas CPU/pod Mem/pod CPU màx Mem màx
api-reserves (api) 30 412m 800Mi 12,36 23,4 GiB
api-reserves (exportador) 30 6m 21Mi 0,18 0,6 GiB
botiga-web 15 70m 64Mi 1,05 0,9 GiB
worker-notificacions 20 120m 410Mi 2,40 8,0 GiB
postgres-reserves 1 2000m 4Gi 2,00 4,0 GiB
redis-cache 1 300m 2Gi 0,30 2,0 GiB
Ingress controller 4 200m 256Mi 0,80 1,0 GiB
DaemonSets (per node) 450m 512Mi (var.) (var.)
Subtotal aplicació 19,09 39,9 GiB

Pas 2: traduir a nodes

Node general: 4 nuclis, 16 GiB.
Assignable despres de reserves del kubelet i del sistema: ~3,5 nuclis, ~14 GiB.
DaemonSets per node: 450m i 512Mi.
Capacitat util per node: 3,05 nuclis i 13,5 GiB.

Nodes per CPU:     19,09 / 3,05 = 6,26  ->  7 nodes
Nodes per memoria: 39,9  / 13,5 = 2,96  ->  3 nodes

Limita la CPU: calen 7 nodes generals al pic.

A mes: postgres-reserves i redis-cache viuen al grup "dades" amb taint.
  postgres: 2 nuclis, 4 GiB.  redis: 0,3 nuclis, 2 GiB.
  Total: 2,3 nuclis i 6 GiB.  Caben en 1 node de 8 nuclis, pero en mantenim
  2 per disponibilitat (un per zona).

Coixi de sobreaprovisionament: 6 nuclis = 2 nodes addicionals.

TOTAL AL PIC: 9 nodes generals + 2 nodes de dades = 11 nodes.

Pas 3: configurar els grups

rutas-norte-general-a   min=1  max=4    (era max=8, ajustem: 3 zones x 4 = 12 > 9)
rutas-norte-general-b   min=1  max=4
rutas-norte-general-c   min=1  max=4
rutas-norte-dades       min=2  max=3

Capacitat maxima total: 12 nodes generals + 3 de dades.
Marge sobre el calculat: 33 %.

El marge del 33 % no és caprici: cobreix errors d'estimació, un desplegament en curs durant el pic (que duplica temporalment els pods d'un component), i la pèrdua d'una zona sencera.

Pas 4: els costos

Preus ficticis però d'ordre de magnitud realista:

Concepte Quantitat Cost unitari/mes Total/mes
Nodes generals base (3, un per zona) 3 95 € 285 €
Nodes de dades (2) 2 190 € 380 €
Coixí de sobreaprovisionament permanent 2 95 € 190 €
Subtotal permanent 855 €
Nodes extra durant el pont (6 nodes × 3 dies) 6×3 dies 3,17 €/dia 57 €
Total el mes del pont 912 €

I la comparació que justifica tot el mòdul:

Estratègia Cost anual Capacitat al pic Risc
Fix dimensionat per al dimarts (situació de partida) 8.000 € Insuficient La plataforma cau. Caiguda de 2023
Fix dimensionat per al pic 20.500 € Suficient Cap, però es paguen 12.500 € de capacitat ociosa 362 dies l'any
Autoescalat + coixí permanent 10.260 € + 57 € Suficient Baix
Autoescalat + coixí només en temporada alta 8.550 € + 57 € Suficient Baix

L'última fila és l'objectiu: pràcticament el mateix cost que el dimensionament fix insuficient, amb la capacitat del dimensionament per al pic. Aquest és tot el valor del mòdul 9 resumit en una línia.

I un recordatori d'honestedat intel·lectual: el coixí reduït fora de temporada només funciona si el CronJob que l'amplia s'executa de debò. Posa-hi una alerta: si el divendres a les 16:05 el Deployment de farciment no té 6 rèpliques, algú se n'ha d'assabentar.

Errors Comuns i Consells

Error 1: creure que l'HPA només et pot portar fins on hi ha capacitat, i no adonar-se'n. El símptoma és silenciós: kubectl get hpa mostra 30 rèpliques i tot sembla bé. Només kubectl get pods revela que 15 estan Pending. Alerta obligatòria sobre pods pendents:

sum(kube_pod_status_phase{phase="Pending", namespace="rutas-norte-pro"}) > 0

Error 2: maxReplicas de l'HPA incoherent amb el maxSize dels grups de nodes. Un maxReplicas: 100 amb grups que topen en 8 nodes és una mentida: mai arribaràs a 100 rèpliques. Calcula sempre Σ(maxReplicas × requests) i compara-ho amb Σ(maxSize × capacitat assignable). Documenta aquest càlcul al costat dels manifests.

Error 3: pods sense requests en un clúster amb CA. Un pod sense requests és invisible per a la simulació del CA: l'autoescalador creu que un node té lloc de sobres quan en realitat està saturat de pods que consumeixen sense declarar res. El LimitRange del mòdul 3 és la defensa: força un defaultRequest a tot el que es creï.

Error 4: esperar que el CA salvi una punta sobtada. Tres a sis minuts. Si el teu trànsit puja en quaranta segons, el CA arriba tard. Sobreaprovisionament (apartat 10) i escalat anticipat (09-04). No hi ha tercera via.

Error 5: PDB impossibles que ancoren nodes per sempre. Un minAvailable igual al nombre de rèpliques fa que cap pod no es pugui desallotjar mai, i el node que els allotgi no es retirarà mai. Estaràs pagant per nodes infrautilitzats sense saber per què. Ho desenvolupem a 09-05, però el diagnòstic és als logs del CA: pdb-blocked.

Error 6: un sol grup de nodes per a tot. Sense separar les dades de l'aplicació, una allau de pods d'api-reserves pot aterrar al mateix node que postgres-reserves i competir per la seva CPU. Taints i grups separats (06-05).

Error 7: nodes heterogenis dins d'un mateix grup. El CA fa servir una plantilla per grup. Si el grup barreja màquines de 2 i de 8 nuclis, la simulació menteix i les decisions són erràtiques. Un tipus de màquina per grup, sempre.

Consell 1: --balance-similar-node-groups si tens un grup per zona. Sense ell, el CA pot amuntegar tots els nodes nous en una zona i deixar-te exposat a una caiguda de zona. És una línia de configuració amb impacte directe en la disponibilitat.

Consell 2: monitora cluster_autoscaler_unschedulable_pods_count. El CA exposa mètriques Prometheus. Les que importen:

# Pods que el CA no aconsegueix col·locar
cluster_autoscaler_unschedulable_pods_count > 0

# Nodes per grup, per veure l'ocupacio dels grups
cluster_autoscaler_nodes_count

# Errors d'ampliacio (sense capacitat al proveidor, quota esgotada)
rate(cluster_autoscaler_failed_scale_ups_total[15m]) > 0

Aquest tercer indicador és especialment valuós: un failed_scale_up significa que el proveïdor t'ha dit que no. Quota esgotada, tipus d'instància sense disponibilitat a la zona, límit del compte. Són problemes que només es descobreixen quan ja és tard, llevat que els vigilis.

Consell 3: assaja l'escalat abans del pic. Una setmana abans del pont, llança una prova de càrrega contra rutas-norte-pre (09-06) i cronometra quant triga el clúster a assolir la capacitat màxima. Aquest número, mesurat i no estimat, és el que et diu si el coixí és suficient.

Consell 4: puja el maxSize dels grups abans de la temporada alta i baixa'l després. És un canvi d'un minut que evita el max node group size reached en el pitjor moment. Posa-ho a la llista de comprovació prèvia a l'esdeveniment.

Consell 5: desa els logs del CA. Són l'única font que explica per què no es va escalar o per què un node no es va retirar. Sense ells, l'anàlisi posterior d'un incident de capacitat és endevinar. EFK (07-05).

Consell 6: revisa el llindar de reducció si tens molts DaemonSets. Amb Fluentd, Falco, el CNI, l'exportador de node i potser un proxy de malla de serveis, la utilització base d'un node buit pot rondar el 25 %. Amb el llindar per defecte del 50 %, la reducció gairebé mai es dispara. Baixar-lo al 35 % pot estalviar nodes sencers.

Exercicis

Exercici 1: calcular la capacitat i detectar l'error

L'equip de Rutas Norte ha configurat això per al pont de maig:

HPA:

Component minReplicas maxReplicas requests.cpu requests.memory
api-reserves 4 40 520m 820Mi
botiga-web 3 20 70m 64Mi
worker-notificacions 2 25 120m 410Mi

Components fixos: postgres-reserves (1 rèplica, 2 nuclis, 4Gi), redis-cache (1 rèplica, 300m, 2Gi), Ingress (4 rèpliques, 200m, 256Mi).

Grups de nodes:

rutas-norte-general-a   min=1  max=3   4 nuclis / 16 GiB
rutas-norte-general-b   min=1  max=3   4 nuclis / 16 GiB
rutas-norte-dades       min=1  max=2   8 nuclis / 32 GiB   taint rol=dades

DaemonSets: 450m i 512Mi per node. Reserva del sistema: 500m i 2Gi per node.

ResourceQuota del namespace: requests.cpu: 40, requests.memory: 80Gi.

Calcula: (a) la CPU i memòria màximes que pot demandar l'aplicació; (b) la capacitat assignable màxima real dels grups; (c) si la configuració aguanta el pic; (d) el percentatge de maxReplicas d'api-reserves que és assolible a la pràctica; (e) què corregiries i en quin ordre.

Exercici 2: diagnosticar per què no es retira un node

Després del pont de maig, el clúster continua amb 9 nodes encara que el trànsit va tornar a la normalitat fa 5 hores. La factura preocupa. Això és el que veus:

kubectl get configmap cluster-autoscaler-status -n kube-system -o yaml | grep -A4 "ScaleDown"
      ScaleDown:   NoCandidates (candidates=0)
kubectl describe nodes | grep -A6 "Allocated resources"
Node ip-10-0-1-42:  cpu 2890m (82%)  memory 7Gi (50%)
Node ip-10-0-1-88:  cpu 1210m (34%)  memory 3Gi (21%)
Node ip-10-0-2-19:  cpu 2650m (75%)  memory 6Gi (43%)
Node ip-10-0-2-73:  cpu  980m (28%)  memory 2Gi (14%)
Node ip-10-0-3-47:  cpu 1450m (41%)  memory 4Gi (28%)
Node ip-10-0-3-91:  cpu 2100m (60%)  memory 5Gi (36%)
Node ip-10-0-1-15:  cpu  760m (21%)  memory 2Gi (14%)
Node ip-10-0-2-88:  cpu 1890m (54%)  memory 5Gi (36%)
Node ip-10-0-3-22:  cpu  890m (25%)  memory 2Gi (14%)
kubectl logs -n kube-system deployment/cluster-autoscaler --tail=50 | grep -i "not suitable"
scale_down.go: Node ip-10-0-1-88 is not suitable for removal:
    pod rutas-norte-pro/informes-ocupacio-28912440-x7kqp has
    cluster-autoscaler.kubernetes.io/safe-to-evict annotation set to false
scale_down.go: Node ip-10-0-2-73 is not suitable for removal:
    pod rutas-norte-pro/redis-cache-0 is not replicated and has local storage
scale_down.go: Node ip-10-0-1-15 is not suitable for removal:
    pdb-blocked: not enough pod disruption budget to move api-reserves-7c9d4f8b6d-mn2vp
scale_down.go: Node ip-10-0-3-22 is not suitable for removal:
    pod rutas-norte-pro/generador-carrega-5f8d9c-2wxkp is not replicated

Analitza cada node bloquejat, digues si el bloqueig és legítim o un error, i proposa la correcció per a cada cas. Calcula quants nodes es podrien retirar després de les correccions i l'estalvi mensual (95 €/node/mes).

Exercici 3: dissenyar l'estratègia per a una campanya llampec

Rutas Norte llança una campanya de Black Friday diferent de tot l'anterior:

  • Durada: 2 hores exactes, de 20:00 a 22:00 d'un divendres.
  • Trànsit esperat: x25 respecte a un dia normal (molt més que el pont de maig).
  • El pic arriba en 20 segons: s'anuncia per ràdio i tothom entra alhora.
  • Els bitllets són limitats: si la plataforma no respon en els primers 10 minuts, la campanya ha fracassat.
  • Pressupost: fins a 400 € per a aquella nit.
  • Es coneix amb tres setmanes d'antelació.

Dissenya l'estratègia completa de capacitat. Quina combinació d'HPA, CA, sobreaprovisionament i escalat programat faries servir? Quants nodes, quant coixí, amb quina antelació? Quant costaria? Escriu els manifests clau i una llista de comprovació amb horaris concrets per a aquella nit.


Solucions

Solució 1

(a) Demanda màxima de l'aplicació:

Component Rèpliques màx CPU/pod CPU total Mem/pod Mem total
api-reserves 40 520m 20,80 820Mi 32,03 GiB
botiga-web 20 70m 1,40 64Mi 1,25 GiB
worker-notificacions 25 120m 3,00 410Mi 10,01 GiB
Ingress 4 200m 0,80 256Mi 1,00 GiB
Subtotal general 26,00 44,29 GiB
postgres-reserves 1 2000m 2,00 4Gi 4,00 GiB
redis-cache 1 300m 0,30 2Gi 2,00 GiB
Subtotal dades 2,30 6,00 GiB

(b) Capacitat assignable màxima real:

Node general (4 nuclis, 16 GiB):
  Reserva del sistema:  500m, 2 GiB
  DaemonSets:           450m, 512Mi
  Disponible per a pods: 4 - 0,5 - 0,45 = 3,05 nuclis
                         16 - 2 - 0,5 = 13,5 GiB

Grups generals: a(max=3) + b(max=3) = 6 nodes
  CPU:     6 x 3,05 = 18,30 nuclis
  Memoria: 6 x 13,5 = 81,00 GiB

Node de dades (8 nuclis, 32 GiB):
  Disponible: 8 - 0,5 - 0,45 = 7,05 nuclis
              32 - 2 - 0,5 = 29,5 GiB

Grup dades (max=2): 2 nodes
  CPU:     14,10 nuclis
  Memoria: 59,00 GiB

(c) Aguanta el pic? NO.

GENERAL:
  Necessari: 26,00 nuclis
  Disponible: 18,30 nuclis
  DEFICIT: 7,70 nuclis (30 % del necessari)

  Memoria: necessaria 44,29 GiB, disponible 81 GiB. En sobra.
  -> LA CPU ES EL LIMIT.

DADES:
  Necessari: 2,30 nuclis i 6 GiB
  Disponible: 14,10 nuclis i 59 GiB
  -> En sobra moltissim. El grup de dades esta MOLT sobredimensionat.

I hi ha un segon problema que molts passarien per alt: la ResourceQuota.

Demanda total de requests.cpu: 26,00 + 2,30 = 28,30
Quota: 40. Hi cap (71 % d'us).

Demanda total de requests.memory: 44,29 + 6,00 = 50,29 GiB
Quota: 80 GiB. Hi cap (63 % d'us).

La quota NO es el problema aqui, pero convé notar que durant un rolling
update d'api-reserves coexisteixen pods vells i nous, cosa que pot afegir
fins a un 25 % mes: 28,30 x 1,25 = 35,4 nuclis. Al 88 % de la quota. Just.

(d) Percentatge de maxReplicas d'api-reserves assolible:

CPU disponible al grup general: 18,30 nuclis.
Consum dels altres components generals (al seu maxim):
  botiga-web 1,40 + worker 3,00 + ingress 0,80 = 5,20 nuclis

CPU disponible per a api-reserves: 18,30 - 5,20 = 13,10 nuclis
Repliques assolibles: 13,10 / 0,52 = 25,19  ->  25 repliques

25 de 40 = 62,5 %

El maxReplicas: 40 és ficció: la realitat són 25 rèpliques. I pitjor: quan l'HPA demani les rèpliques 26 a 40, es quedaran en Pending i ningú se n'assabentarà llevat que hi hagi una alerta.

(e) Correccions, per ordre de prioritat:

Prioritat 1 (imprescindible): pujar el maxSize dels grups generals.

Necessari: 26,00 nuclis / 3,05 per node = 8,52  ->  9 nodes
Amb un tercer grup per zona (recomanable per a 09-05): 3 grups x max=4 = 12 nodes
  Capacitat: 12 x 3,05 = 36,6 nuclis. Marge del 41 % sobre el necessari.
rutas-norte-general-a   min=1  max=4
rutas-norte-general-b   min=1  max=4
rutas-norte-general-c   min=1  max=4     <- grup nou, tercera zona

Prioritat 2: reduir el maxSize del grup de dades.

El grup de dades necessita 2,30 nuclis i 6 GiB. Un sol node de 8 nuclis
sobra. Amb max=2 mantenim disponibilitat (un per zona) pero no hi ha rao
per permetre'n mes. Es queda en max=2, que ja es correcte.

PERO: el tipus de maquina esta sobredimensionat. Un node de 4 nuclis i 16 GiB
bastaria per a postgres (2 nuclis, 4 GiB) i redis (0,3, 2 GiB). Canviar el tipus
de 8/32 a 4/16 estalviaria ~95 EUR/mes per node, 190 EUR/mes en total.

Matis: postgres es beneficia de memoria per a shared_buffers i cache de pagines.
Abans de reduir, mesurar amb el VPA (09-02) si 16 GiB basten. Amb requests de
4 GiB i un node de 16, hi ha 10 GiB de cache de sistema disponible: probablement si.

Prioritat 3: revisar el maxReplicas: 40 d'api-reserves.

Amb 12 nodes generals hi caben 25 rèpliques... espera, recalculem amb la nova capacitat:

Capacitat general nova: 36,6 nuclis
Menys els altres components: 36,6 - 5,20 = 31,4 nuclis
Repliques d'api-reserves assolibles: 31,4 / 0,52 = 60

Ara si que hi caben les 40. El maxReplicas: 40 es assolible.

Prioritat 4: afegir el coixí de sobreaprovisionament. Sense ell, les rèpliques 5 a 40 trigaran entre 3 i 6 minuts a aparèixer. Amb 6 pods de farciment d'1 nucli, les primeres 11 rèpliques són instantànies.

Prioritat 5: l'alerta de pods pendents. Sense ella, tot aquest càlcul es pot equivocar i ningú ho sabrà fins que sigui tard.

Solució 2

Anàlisi node per node:

Node Utilització Estat Motiu del bloqueig Legítim?
ip-10-0-1-42 82 % Ocupat Per sobre del llindar Sí, no és candidat
ip-10-0-1-88 34 % Bloquejat safe-to-evict: false a informes-ocupacio Depèn
ip-10-0-2-19 75 % Ocupat Per sobre del llindar
ip-10-0-2-73 28 % Bloquejat redis-cache-0 sense rèplica i amb emmagatzematge local Sí, legítim
ip-10-0-3-47 41 % Candidat Hauria de retirar-se
ip-10-0-3-91 60 % Ocupat Per sobre del llindar
ip-10-0-1-15 21 % Bloquejat PDB d'api-reserves NO, és un error
ip-10-0-2-88 54 % Ocupat Per sobre del llindar
ip-10-0-3-22 25 % Bloquejat generador-carrega sense controlador NO, és brossa

Cas 1: ip-10-0-1-88 — informes-ocupacio amb safe-to-evict: false.

Legitimitat: condicional. Si el Job s'està executant ara mateix, el bloqueig és correcte: matar-lo perdria la feina. Però el pont va acabar fa 5 hores i informes-ocupacio és un CronJob nocturn.

# Segueix viu el Job?
kubectl get jobs -n rutas-norte-pro
kubectl get pods -n rutas-norte-pro -l app=informes-ocupacio

Si apareix com a Running des de fa hores, és un Job penjat: probablement va fallar i s'ha quedat encallat. Correcció:

# 1. Investigar per que segueix viu
kubectl logs -n rutas-norte-pro informes-ocupacio-28912440-x7kqp --tail=50

# 2. Si esta penjat, eliminar-lo
kubectl delete job informes-ocupacio-28912440 -n rutas-norte-pro

I prevenció estructural al CronJob:

spec:
  jobTemplate:
    spec:
      # Si a les 2 hores no ha acabat, es talla. Evita jobs zombis que
      # ancoren nodes indefinidament.
      activeDeadlineSeconds: 7200
      ttlSecondsAfterFinished: 3600

Node recuperable: SÍ, després de netejar el Job.

Cas 2: ip-10-0-2-73 — redis-cache-0.

Legitimitat: sí, plenament. redis-cache és un StatefulSet d'una rèplica amb volum. El CA no el pot moure amb seguretat, i per descomptat no ho ha de fer pel seu compte: reiniciar la memòria cau descarrega tota la càrrega sobre postgres-reserves.

Correcció: cap a curt termini. A mitjà termini, les opcions són:

  • Ancorar redis-cache al grup de nodes de dades amb afinitat i taint (06-05), perquè no ocupi un node general.
  • Acceptar-ho: un node dedicat a la memòria cau no és un malbaratament si està ben dimensionat.

La millor correcció és la primera: redis-cache no hauria d'estar en un node general. És una decisió d'arquitectura que ja hauríem d'haver pres.

Node recuperable: NO directament, però sí després de moure redis-cache al grup de dades.

Cas 3: ip-10-0-1-15 — PDB bloquejant api-reserves.

Legitimitat: no, és un error de configuració. api-reserves és un Deployment amb moltes rèpliques: moure un pod hauria de ser trivial. Que el PDB ho impedeixi significa que està malament.

kubectl get pdb -n rutas-norte-pro api-reserves -o yaml

Sospita probable:

spec:
  minAvailable: 30        # <-- Posat durant el pont, quan hi havia 30 repliques

Després del pont, l'HPA ha baixat a 4-6 rèpliques. Amb minAvailable: 30 i 6 rèpliques vives, cap pod no es pot desallotjar mai: ja s'està per sota del mínim.

Correcció:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  # PERCENTATGE, no numero absolut. S'adapta sol a l'escalat de l'HPA.
  # Amb 6 repliques permet desallotjar-ne 1; amb 30, en permet desallotjar 6.
  maxUnavailable: 20%
  selector:
    matchLabels:
      app: api-reserves

Aquesta és la lliçó central de 09-05, avançada: amb HPA, els PDB en número absolut són una trampa. El nombre de rèpliques canvia sol, i un mínim absolut que era raonable al pic es torna impossible a la vall.

Node recuperable: SÍ, immediatament després de corregir el PDB.

Cas 4: ip-10-0-3-22 — generador-carrega sense controlador.

Legitimitat: no, és brossa. És el pod de prova de càrrega de l'apartat 12 de 09-01, creat amb kubectl run sense controlador i oblidat. Fa dies que és allà ancorant un node sencer.

kubectl delete pod generador-carrega-5f8d9c-2wxkp -n rutas-norte-pro

Prevenció:

  • Fes servir sempre --rm amb kubectl run per a proves.
  • Una política de Kyverno (08-03) que rebutgi pods sense ownerReferences a rutas-norte-pro.
  • Una revisió periòdica de pods orfes:
kubectl get pods -n rutas-norte-pro -o json | \
  python3 -c "import sys,json; [print(p['metadata']['name']) for p in json.load(sys.stdin)['items'] if not p['metadata'].get('ownerReferences')]"

Node recuperable: SÍ, immediatament.

Resum i estalvi:

Node Acció Es retira?
ip-10-0-1-88 Eliminar el Job penjat
ip-10-0-2-73 Moure redis-cache al grup de dades (mitjà termini) Sí, a mitjà termini
ip-10-0-3-47 Cap: és candidat, només necessita temps
ip-10-0-1-15 Corregir el PDB a maxUnavailable: 20%
ip-10-0-3-22 Esborrar el pod orfe
Nodes retirables a curt termini: 4 (ip-10-0-1-88, -3-47, -1-15, -3-22)
Nodes retirables a mitja termini: 1 mes (ip-10-0-2-73)

Estalvi immediat:     4 x 95 EUR = 380 EUR/mes = 4.560 EUR/any
Estalvi a mitja termini: 5 x 95 EUR = 475 EUR/mes = 5.700 EUR/any

Comprovació després de les correccions:

# Esperar el scale-down-unneeded-time (10 min per defecte) i verificar
watch -n 30 'kubectl get configmap cluster-autoscaler-status -n kube-system \
  -o jsonpath="{.data.status}" | grep -A2 ScaleDown'

Hauria de passar de NoCandidates a CandidatesPresent i després a InProgress.

Lliçó de fons de l'exercici: els nodes zombis mai són culpa del Cluster Autoscaler. Sempre són conseqüència d'alguna cosa mal configurada a les càrregues: un PDB impossible, un Job penjat, un pod orfe, un component amb estat al lloc equivocat. El CA és el missatger; els logs són el missatge.

Solució 3

Anàlisi de l'escenari. Aquest cas és qualitativament diferent del pont de maig:

Factor Pont de maig Black Friday
Durada 3 dies 2 hores
Multiplicador x10 x25
Velocitat de pujada Minuts 20 segons
Antelació Coneguda (calendari) 3 setmanes
Tolerància a l'error Baixa Nul·la: 10 minuts i s'ha acabat

La conclusió clau: l'escalat reactiu NO serveix aquí. Ni l'HPA (15 s de detecció + 30 s d'arrencada) ni el CA (3-6 min) arriben a temps per a una pujada de 20 segons. Tota la capacitat ha d'estar ja arrencada i calenta abans de les 20:00.

Estratègia: preescalat total.

Pas 1: calcular la capacitat necessària.

Transit normal d'un divendres nit: ~250 rps.
Transit esperat: 250 x 25 = 6.250 rps.

De la prova de carrega (09-06) sabem: cada replica d'api-reserves sosté
~85 rps amb latencia p95 acceptable.

Repliques necessaries: 6.250 / 85 = 73,5  ->  amb marge del 20 %: 88 repliques.

CPU: 88 x 520m = 45,76 nuclis.
Memoria: 88 x 820Mi = 70,4 GiB.

botiga-web: serveix estatic, escala millor. 6.250 rps / 400 rps per replica = 16
  -> amb marge: 20 repliques. 20 x 70m = 1,4 nuclis.

worker-notificacions: la cua s'omple pero es pot buidar DESPRES de les
  22:00. NO cal escalar-lo durant la campanya. Es queda en 4 repliques i
  s'escala a 25 a partir de les 22:00, quan ja sobra capacitat.
  Decisio deliberada: prioritat absoluta a la venda.

Ingress: 8 repliques (el doble de l'habitual). 1,6 nuclis.

TOTAL al pic: 45,76 + 1,4 + 0,48 + 1,6 = 49,24 nuclis
              70,4 + 1,25 + 1,6 + 2 = 75,25 GiB

Pas 2: traduir a nodes.

Capacitat util per node general (4 nuclis): 3,05 nuclis, 13,5 GiB.

Per CPU:     49,24 / 3,05 = 16,1  ->  17 nodes
Per memoria: 75,25 / 13,5 = 5,6   ->  6 nodes

LIMITA LA CPU: 17 nodes generals.

Alternativa: nodes mes grans. Amb nodes de 8 nuclis / 32 GiB:
  Util per node: 8 - 0,5 - 0,45 = 7,05 nuclis
  Nodes necessaris: 49,24 / 7,05 = 6,98  ->  7 nodes

7 nodes grans en lloc de 17 de petits:
  - Menys nodes que arrencar = menys temps total d'aprovisionament
  - Menys DaemonSets replicats (17 x 450m = 7,65 nuclis contra 7 x 450m = 3,15)
  - Pitjor granularitat i pitjor tolerancia a errors

DECISIO: nodes de 8 nuclis per a aquella nit. L'estalvi de DaemonSets (4,5
nuclis) es decisiu, i com que tota la capacitat s'arrenca ABANS, la pitjor
granularitat no importa.

Pas 3: els manifests.

# k8s/entorns/pro/campanya/hpa-api-reserves-blackfriday.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  annotations:
    rutasnorte.example/campanya: "black-friday-2026"
    rutasnorte.example/revertir-abans-de: "2026-11-28T23:00:00Z"
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-reserves
  # minReplicas ELEVAT A 88. Aquesta es la clau de tota l'estrategia:
  # la capacitat esta ARRENCADA I CALENTA abans de les 20:00, no s'espera
  # que l'HPA reaccioni. Amb una pujada de 20 segons, qualsevol
  # mecanisme reactiu arriba tard.
  minReplicas: 88
  maxReplicas: 110          # Marge del 25 % per si l'estimacio es queda curta
  metrics:
    - type: ContainerResource
      containerResource:
        name: cpu
        container: api
        target:
          type: Utilization
          averageUtilization: 60
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      selectPolicy: Max
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15
        - type: Pods
          value: 15
          periodSeconds: 15
    scaleDown:
      # DESACTIVAT durant la campanya. Ni una sola replica menys entre les
      # 19:00 i les 22:30, passi el que passi amb les metriques. Una vall de
      # 30 segons no ha de destruir capacitat que va costar 20 minuts d'aixecar.
      selectPolicy: Disabled
# k8s/entorns/pro/campanya/deployment-farciment-blackfriday.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: farciment-capacitat
  namespace: rutas-norte-pro
spec:
  # Coixi ampliat a 10 pods d'1 nucli. Absorbeix instantaniament les
  # repliques 89 a 108 si l'estimacio es queda curta.
  replicas: 10
  selector:
    matchLabels:
      app: farciment-capacitat
  template:
    metadata:
      labels:
        app: farciment-capacitat
      annotations:
        cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
    spec:
      priorityClassName: farciment-capacitat
      terminationGracePeriodSeconds: 0
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: farciment-capacitat
      containers:
        - name: pausa
          image: registry.k8s.io/pause:3.9
          resources:
            requests: {cpu: "1", memory: 1Gi}
            limits: {cpu: "1", memory: 1Gi}

Grups de nodes per a aquella nit:

rutas-norte-bf-a   min=3  max=4   8 nuclis / 32 GiB   zona A
rutas-norte-bf-b   min=3  max=4   8 nuclis / 32 GiB   zona B
rutas-norte-bf-c   min=2  max=4   8 nuclis / 32 GiB   zona C

Total minim garantit: 8 nodes (ja arrencats, no depenen del CA)
Total maxim: 12 nodes

Fixa't en el detall important: el min dels grups es puja a 3, no es confia en el CA. Posant el mínim alt, el proveïdor arrenca les màquines i el CA no les pot retirar. La capacitat està garantida per construcció, no per reacció.

Pas 4: el cost.

Node de 8 nuclis / 32 GiB: ~0,26 EUR/hora sota demanda.

Finestra de capacitat ampliada: 18:00 a 23:00 = 5 hores.
Nodes extra sobre la base (base: 3 nodes de 4 nuclis):
  8 nodes de 8 nuclis x 5 hores x 0,26 = 10,40 EUR

Transit de sortida addicional estimat: ~15 EUR
Marge per a incidencies (nodes fins al maxim de 12): +4 nodes x 5 h x 0,26 = 5,20 EUR

TOTAL ESTIMAT: ~31 EUR

PRESSUPOST: 400 EUR.
Marge: 369 EUR (92 %).

El resultat sorprèn i és l'ensenyament de l'exercici: dimensionar generosament per a dues hores és baratíssim. El cost de la infraestructura al núvol és proporcional al temps, i dues hores de sobredimensionament costen menys que un cafè per node. Amb un pressupost de 400 €, no hi ha cap excusa per escatimar capacitat aquella nit.

Amb el marge sobrant es poden permetre millores:

  • Pujar a 12 nodes des del principi en lloc de 8: +5,20 €.
  • Duplicar les rèpliques de redis-cache en mode rèplica de lectura: +2 €.
  • Mantenir la capacitat fins a les 2:00 per buidar la cua de notificacions sense presses: +15 €.

Tot això hi cap folgadament.

Pas 5: la llista de comprovació de la nit.

Hora Acció Responsable Verificació
3 setmanes abans Prova de càrrega a rutas-norte-pre a 6.250 rps (09-06) Plataforma Mesurar rps per rèplica real
2 setmanes abans Verificar quotes del proveïdor: permet 12 nodes de 8 nuclis? Plataforma Tiquet d'ampliació si no
1 setmana abans Assaig general: aplicar els manifests a pre i mesurar el temps d'arrencada complet Plataforma Cronòmetre
1 setmana abans Revisar PDB: maxUnavailable en percentatge, no absolut Plataforma kubectl get pdb
3 dies abans Congelar desplegaments. Cap canvi de codi fins al dilluns Tot l'equip Bloqueig a CI
17:00 Pujar el min dels grups de nodes a 3/3/2 Plataforma kubectl get nodes = 8
17:30 Verificar que els 8 nodes estan Ready i amb les imatges precarregades Plataforma kubectl get nodes
18:00 Aplicar l'HPA de campanya (minReplicas: 88) Plataforma kubectl get hpa
18:00 Ampliar el farciment a 10 rèpliques Plataforma kubectl get pods -l app=farciment
18:30 Verificar 88 pods Running i Ready, no només creats Plataforma kubectl get pods --field-selector status.phase=Running | wc -l
18:45 Prova de fum: 100 reserves reals de prova end-to-end QA Totes OK
19:00 Verificar Grafana: latència p95 estable, zero errors, zero Pending Plataforma Panells
19:30 Preescalfar memòries cau: consultar les 50 rutes més venudes Plataforma redis-cli DBSIZE
19:45 Sala de guerra oberta. Tot l'equip connectat Tots
19:55 Última verificació. kubectl get pods --all-namespaces | grep -v Running Plataforma Buit
20:00 OBERTURA. Vigilar: latència p95, taxa d'error, pods Pending, connexions a PostgreSQL Tots Panells
20:00-22:00 Vigilància contínua. Regla: no tocar res llevat d'incident confirmat Tots
22:00 Tancament de la campanya
22:15 Escalar worker-notificacions a 25 rèpliques per buidar la cua Plataforma Longitud de cua
23:00 Restaurar l'HPA normal (minReplicas: 4) Plataforma kubectl apply del manifest base
23:30 Baixar el min dels grups de nodes. El CA retirarà la resta Plataforma kubectl get nodes
Dia següent Anàlisi: rps reals, latències, comparació amb l'estimació Tots Informe

Notes crítiques sobre el pla:

  1. La verificació de les 18:30 és la més important. 88 pods creats no és el mateix que 88 pods Ready. Si a les 18:30 n'hi ha 20 en Pending, hi ha dues hores per arreglar-ho. Si te n'assabentes a les 20:01, no hi ha res a fer.

  2. El scaleDown: Disabled és innegociable. Sense ell, una vall d'un minut entre les 20:15 i les 20:16 podria destruir 30 rèpliques que trigarien minuts a tornar.

  3. worker-notificacions no s'escala durant la campanya. És una decisió deliberada: els correus de confirmació poden esperar dues hores, la venda no. Tota la capacitat va a la venda. Aquesta és la mena de compromís explícit que distingeix un pla de capacitat d'una llista de desitjos.

  4. La regla de "no tocar res" durant la campanya. Gairebé tots els incidents greus en esdeveniments d'aquest tipus els causa algú intentant arreglar alguna cosa sota pressió. Si els panells estan verds, es mira i no es toca.

  5. El preescalfament de memòries cau a les 19:30 estalvia que els primers 5.000 usuaris paguin el cost de les consultes fredes a PostgreSQL. És mitja hora de feina amb un impacte enorme en els primers minuts.

Conclusió

L'autoescalat de clúster és el nivell que sosté els altres dos. Sense ell, l'HorizontalPodAutoscaler té un sostre dur i invisible: demana rèpliques, el Deployment les crea, i es queden en Pending mentre els panells mostren números que no es corresponen amb la realitat.

L'essencial:

  • El símptoma és sempre el mateix: pods en Pending amb FailedScheduling i Insufficient cpu. Reconèixer-lo i alertar-ne és el primer.
  • El Cluster Autoscaler observa pods no planificables, simula quin grup de nodes els allotjaria, i tria amb un expander. least-waste és el més raonable en general.
  • Ampliar és fàcil; reduir és on són els problemes. Llindar d'utilització, temps d'espera, i una llista de bloquejadors que cal conèixer: pods sense controlador, emmagatzematge local, PDB restrictius, pods del sistema, i l'anotació safe-to-evict: "false".
  • Diagnosticar és llegir tres llocs: el ConfigMap cluster-autoscaler-status, els esdeveniments NotTriggerScaleUp sobre els pods pendents, i els logs del CA amb els seus not suitable for removal.
  • Un node triga entre 3 i 6 minuts a passar de la petició a servir trànsit. Això no salva una punta que puja en quaranta segons, per ben configurat que estigui tot.
  • El sobreaprovisionament amb pods de prioritat negativa converteix tres minuts i mig en tres segons, a canvi de pagar capacitat ociosa. És l'ús més elegant de la PriorityClass i la preemption de 06-05, i s'autoregenera.
  • Karpenter tria la màquina en funció dels pods pendents en lloc de treballar amb grups fixos, i consolida contínuament. Millor a AWS i amb càrregues heterogènies; el CA continua sent l'opció sòlida i predictible en la resta de casos.
  • Els tres escaladors encaixen en cascada: l'HPA crea pods → si no hi caben, Pending → el CA afegeix nodes. I el coixí talla el camí llarg.
  • El pla de capacitat de Rutas Norte costa pràcticament el mateix que el dimensionament fix insuficient de l'any passat, però amb la capacitat del dimensionament per al pic. Aquest és tot el valor del mòdul.

Però continuem tenint un problema de fons que cap de les tres lliçons no ha resolt. Tot el que hem construït és reactiu: espera que la CPU pugi per actuar. I hi ha dos casos on això simplement no funciona.

El primer és worker-notificacions. Durant el pont de maig pot tenir quaranta mil correus esperant a la cua i la CPU al 20 %, perquè la seva feina és esperar respostes del servidor de correu, no calcular. Un HPA per CPU veuria aquest 20 % contra un objectiu del 70 % i conclouria que sobren rèpliques: escalaria cap avall justament quan més consumidors calen.

El segon és el mateix moment d'obertura de la venda. Sabem que passa a les 10:00 en punt. És informació que tenim amb mesos d'antelació. I tanmateix, tot el nostre sistema espera a les 10:00:15 per descobrir, per la CPU, alguna cosa que ja sabíem al març.

A la propera lliçó, Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA, resolem els dos. Veurem com l'HPA pot consumir mètriques de negoci a través de l'API d'agregació, què és KEDA i per què no substitueix l'HPA sinó que l'alimenta, com escalar worker-notificacions per la longitud real de la seva cua —amb escalat a zero inclòs—, com escalar api-reserves per peticions per segon fent servir les mètriques que vam instrumentar a 07-03, i com programar un disparador que preescalfi tota la plataforma mitja hora abans que s'obri la venda del pont de maig.

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