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
- El sostre dur: quan l'HPA demana i no hi ha lloc
- El símptoma exacte:
PendingiFailedScheduling - Què és el Cluster Autoscaler i on viu
- Com decideix ampliar: simulació i expanders
- Grups de nodes i la seva relació amb el proveïdor
- La reducció de nodes: la part delicada
- Els motius pels quals un node no es pot buidar
- Diagnòstic: el ConfigMap d'estat i els esdeveniments
- Els temps reals: quant triga un node de debò
- Sobreaprovisionament amb pods de farciment
- Karpenter: l'alternativa moderna
- Com encaixen els tres escaladors
- Què es pot practicar a minikube
- El pla de capacitat de Rutas Norte i el seu cost
- Errors comuns i consells
- Exercicis
- Conclusió
- 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.
- El símptoma exacte:
Pending i FailedScheduling
Pending i FailedSchedulingAprenguem a reconèixer-ho amb precisió, perquè és el disparador de tota la resta.
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 91sPending 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.
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 reachedDues 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 | Sí |
Insufficient memory |
No hi ha memòria assignable suficient | Sí |
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:
I per veure l'ocupació real dels nodes:
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.
- 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.
- Com decideix ampliar: simulació i expanders
El bucle del CA s'executa cada 10 segons per defecte (--scan-interval).
El procés de decisió
- Llista els pods no planificables. Els que porten almenys
--max-pod-provisioning-timeenPendingambFailedScheduling. - Agrupa pods equivalents. Els pods del mateix controlador amb els mateixos requisits es tracten com un grup, per no simular vint vegades el mateix.
- 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ó.
- 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.
- Tria entre els grups viables fent servir l'
expander. - 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-norteL'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 disponibleEl 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.
- 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:NoScheduleTres 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 sí 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.
- 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:
- És per sota del llindar d'utilització? Per defecte,
--scale-down-utilization-threshold=0.5: la suma derequestsdels seus pods és menor del 50 % de la seva capacitat assignable. - Fa prou temps que hi és?
--scale-down-unneeded-time=10m: deu minuts consecutius per sota del llindar. - Es pot buidar? Simula si tots els seus pods cabrien en altres nodes existents. Si no hi caben, no es toca.
- Hi ha algun pod que impedeixi el desallotjament? La llista de l'apartat 7.
- 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).
- 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-reservesAmb 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.
- 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.
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=.lastTimestampLAST 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:
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 falseEls 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.
- 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:
- Sobreaprovisionament: tenir capacitat ja arrencada i esperant (apartat 10).
- Escalat anticipat: escalar abans del pic fent servir un disparador temporal, no reactiu. És el disparador cron de KEDA que veurem a 09-04.
- 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:
- El CA compta aquests
requestscom a ocupació, així que manté nodes arrencats per allotjar-los. - 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.
- 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: -10garanteix que qualsevol pod sensepriorityClassName(prioritat 0) els superi.preemptionPolicy: Neverfa que aquests pods, en quedar-sePending, 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 wideNAME 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-91Ara 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 --watchNAME 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 3sI 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-42Tres 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-proI 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.
- 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: 800GiAmb 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.
- 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.
- 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-devNAME 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
...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 -20LAST 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 FailedSchedulingVeure 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-m03Aquesta seqüència cordon → drain → 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.
- 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:
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]) > 0Aquest 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=dadesDaemonSets: 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:
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%)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 replicatedAnalitza 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 zonaPrioritat 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 | Sí |
| 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 | Sí |
| 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 | Sí |
| 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-ocupacioSi 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-proI 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: 3600Node 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-cacheal 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.
Sospita probable:
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-reservesAquesta é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.
Prevenció:
- Fes servir sempre
--rmambkubectl runper a proves. - Una política de Kyverno (08-03) que rebutgi pods sense
ownerReferencesarutas-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 | Sí |
| 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 | Sí |
| ip-10-0-1-15 | Corregir el PDB a maxUnavailable: 20% |
Sí |
| ip-10-0-3-22 | Esborrar el pod orfe | Sí |
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/anyComprovació 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 GiBPas 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 nodesFixa'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-cacheen 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:
-
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 enPending, hi ha dues hores per arreglar-ho. Si te n'assabentes a les 20:01, no hi ha res a fer. -
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. -
worker-notificacionsno 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. -
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.
-
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
PendingambFailedSchedulingiInsufficient 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 esdevenimentsNotTriggerScaleUpsobre els pods pendents, i els logs del CA amb els seusnot 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
PriorityClassi 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
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
