La lliçó anterior va acabar amb dos problemes que cap de les tres capes d'escalat no resol. El primer: worker-notificacions pot tenir quaranta mil correus esperant a la cua amb 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 escalaria cap avall justament quan més consumidors calen. El segon: sabem amb mesos d'antelació que la venda del pont de maig obre a les 10:00 en punt, 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ç.
Els dos problemes tenen la mateixa arrel: la CPU és un senyal indirecte i tardà del que realment li importa al negoci. El que importa no és quant calcula un pod, sinó quanta feina pendent hi ha, quantes peticions arriben, i quan arribarà l'allau.
Aquesta lliçó ensenya a escalar per senyals de negoci. Veurem com l'HorizontalPodAutoscaler pot consumir mètriques personalitzades i externes a través de l'API d'agregació, què és KEDA i per què no substitueix l'HPA sinó que l'alimenta, l'objecte ScaledObject camp a camp, el catàleg d'escaladors disponibles, i la implementació completa a Rutas Norte: la cua de correus, les peticions per segon amb PromQL, l'escalat a zero, i el disparador cron que preescalfa la plataforma abans de l'obertura de la venda.
Contingut
- Per què la CPU és un mal senyal
- Mètriques personalitzades i externes a l'HPA
- L'API d'agregació i els adaptadors de mètriques
- Què és KEDA i quina és la seva arquitectura
- La idea clau: KEDA no substitueix l'HPA, l'alimenta
- L'objecte
ScaledObjectcamp a camp - El catàleg d'escaladors
TriggerAuthentication: credencials per als disparadorsScaledJob: un treball per missatge- Rutas Norte:
worker-notificacionsper longitud de cua - Rutas Norte:
api-reservesper peticions per segon - Rutas Norte: el disparador cron del pont de maig
- L'escalat a zero i les seves implicacions
- Depurar un
ScaledObjectque no escala - Errors comuns i consells
- Exercicis
- Conclusió
- Per què la CPU és un mal senyal
Comencem per entendre bé el problema, perquè la solució només té sentit quan el problema és clar.
El cas de worker-notificacions
worker-notificacions consumeix missatges d'una cua de correus de confirmació i els envia a través d'un servidor SMTP extern. El seu cicle per missatge:
1. Llegir un missatge de la cua ~2 ms de CPU
2. Consultar les dades de la reserva ~5 ms de CPU + 8 ms esperant PostgreSQL
3. Renderitzar la plantilla del correu ~12 ms de CPU
4. Connectar amb el servidor SMTP i enviar ~3 ms de CPU + 450 ms ESPERANT
5. Confirmar el missatge a la cua ~1 ms de CPU
Total: ~23 ms de CPU i ~458 ms d'espera.
Proporcio CPU/temps total: 23 / 481 = 4,8 %El 95 % del temps el procés està bloquejat esperant la xarxa. No consumeix CPU: espera.
Conseqüència directa: amb un requests.cpu de 300m i un objectiu d'HPA del 70 % (210m), un worker que processa missatges a tota màquina consumeix uns 60m. L'HPA calcularia:
Amb quaranta mil correus a la cua, l'HPA vol reduir a una rèplica. És exactament el comportament contrari al necessari, i no és un error de l'HPA: és que li estem fent la pregunta equivocada.
La pregunta correcta
La pregunta que importa no és «quanta CPU consumeixen els meus workers?» sinó «quanta feina pendent hi ha?». I aquesta pregunta té una resposta directa: la longitud de la cua.
Cua: 40.000 missatges.
Un worker processa ~2 missatges/segon (limitat pel SMTP).
Objectiu: buidar la cua en 20 minuts = 1.200 segons.
Missatges per segon necessaris: 40.000 / 1.200 = 33,3
Workers necessaris: 33,3 / 2 = 17 workers.Disset workers, no un. I aquest càlcul es pot expressar com una regla d'escalat trivial: «un worker per cada 500 missatges a la cua» (40.000 / 500 = 80... ajustem: un worker per cada 2.500 missatges donaria 16). El número exacte es calibra mesurant, però la forma de la regla és evident i no té res a veure amb la CPU.
Altres casos on la CPU menteix
| Càrrega | Per què la CPU menteix | Senyal correcte |
|---|---|---|
| Consumidors de cua | Feina dominada per espera de xarxa | Longitud de la cua |
| APIs amb moltes crides externes | El temps se'n va esperant tercers | Peticions per segon, connexions actives |
| Processament de fitxers | La CPU depèn de la mida, no del nombre | Fitxers pendents al magatzem |
| Serveis amb memòria cau molt efectiva | CPU baixa però latència alta si la memòria cau falla | Taxa d'encerts, latència p95 |
| Càrregues amb límit de taxa extern | La CPU és artificialment baixa | Peticions a la cua d'espera |
| WebSockets / connexions persistents | Moltes connexions inactives consumeixen memòria, no CPU | Nombre de connexions |
Un patró general: la CPU és bon senyal quan la feina del pod és calcular, i mal senyal quan la feina és coordinar, esperar o mantenir estat de connexió.
I el problema del temps
Fins i tot amb la mètrica correcta, tot escalat reactiu comparteix una limitació: actua després. La seqüència és sempre la mateixa: arriba la càrrega → es degrada el servei → es detecta → s'escala → es recupera. Entre el segon i el cinquè pas hi ha usuaris patint.
Per a esdeveniments predictibles —l'obertura de la venda a les 10:00, el tancament comptable del dia 1, la campanya de Black Friday a les 20:00— esperar que la mètrica pugi és absurd. És informació que ja tenim. El racional és escalar abans.
Aquesta capacitat s'anomena escalat programat, i és una de les coses que KEDA aporta de fàbrica.
- Mètriques personalitzades i externes a l'HPA
Abans de KEDA, vegem què pot fer l'HPA per si mateix. Recordem els tipus de mètrica de 09-01:
| Tipus | API que la serveix | Què mesura |
|---|---|---|
Resource / ContainerResource |
metrics.k8s.io |
CPU i memòria dels pods |
Pods |
custom.metrics.k8s.io |
Mètrica emesa pels pods del destí, promitjada |
Object |
custom.metrics.k8s.io |
Mètrica associada a un altre objecte de Kubernetes |
External |
external.metrics.k8s.io |
Alguna cosa de fora del clúster |
L'HPA sap consumir els quatre tipus. No cal cap component addicional perquè l'HPA entengui una mètrica externa: el seu codi ja ho contempla.
El que falta és qui serveix aquestes APIs. metrics.k8s.io la serveix metrics-server (07-02). Però custom.metrics.k8s.io i external.metrics.k8s.io no les serveix ningú per defecte. Si escrius un HPA amb type: External en un clúster net, obtindràs:
Conditions:
Type Status Reason Message
---- ------ ------ -------
ScalingActive False FailedGetExternalMetric unable to get external metric
rutas-norte-pro/longitud_cua_correus/nil:
no custom metrics API (external.metrics.k8s.io/v1beta1)
registeredAquest és el buit que omple un adaptador de mètriques.
La diferència entre Pods, Object i External
Val la pena tenir-la clara perquè determina com es calculen les rèpliques.
Pods: la mètrica l'emet cada pod del destí i l'HPA la promitja entre tots.
Fórmula: sostre(repliquesActuals × mitjanaEntrePods / objectiu). És la mateixa que la CPU, amb una altra magnitud.
Object: la mètrica pertany a un altre objecte del clúster (un Ingress, un Service). És un valor únic, no una mitjana.
- type: Object
object:
describedObject:
apiVersion: networking.k8s.io/v1
kind: Ingress
name: rutas-norte-public
metric:
name: peticions_per_segon
target:
type: Value
value: "5000"Amb type: Value l'HPA compara el valor brut amb l'objectiu. Amb type: AverageValue divideix el valor entre les rèpliques actuals abans de comparar.
External: la mètrica ve de fora del clúster. Una cua de RabbitMQ, un topic de Kafka, una mètrica de facturació.
- type: External
external:
metric:
name: longitud_cua_correus
selector:
matchLabels:
cua: notificacions-confirmacio
target:
type: AverageValue
averageValue: "2500"Per a cues, sempre AverageValue. El significat és «cada rèplica es fa càrrec de 2.500 missatges», i així el nombre de rèpliques creix linealment amb la cua:
| Missatges a la cua | Rèpliques desitjades |
|---|---|
| 1.000 | sostre(1000/2500) = 1 |
| 10.000 | sostre(10000/2500) = 4 |
| 40.000 | sostre(40000/2500) = 16 |
| 100.000 | sostre(100000/2500) = 40 (limitat per maxReplicas) |
Amb type: Value el comportament seria completament diferent i gairebé sempre indesitjat: compararia 40.000 amb 2.500 i multiplicaria les rèpliques actuals per 16 a cada cicle, cosa que produeix oscil·lacions violentes.
- L'API d'agregació i els adaptadors de mètriques
Entendre aquesta peça és el que converteix KEDA de màgia en enginyeria.
La capa d'agregació
Kubernetes permet estendre la seva API delegant certes rutes en serveis que corren dins del clúster. El mecanisme és l'objecte APIService:
NAME SERVICE AVAILABLE AGE
v1beta1.metrics.k8s.io kube-system/metrics-server True 41d
v1beta1.external.metrics.k8s.io keda/keda-operator-metrics-apiserver True 12dQuan algú (l'HPA, o tu amb kubectl) demana /apis/external.metrics.k8s.io/v1beta1/..., el servidor d'API no respon ell: reenvia la petició al Service registrat. Aquest servei retorna les dades en el format que l'API defineix, i el servidor d'API les passa al client com si fossin seves.
flowchart TD
HPA[Controlador HPA] -->|GET /apis/external.metrics.k8s.io/...| API[Servidor d'API<br/>kube-apiserver]
API -->|capa d'agregacio| ADAPT[Adaptador de metriques<br/>registrat com a APIService]
ADAPT -->|consulta nativa| SRC
subgraph SRC["Font real de dades"]
PROM[Prometheus]
RMQ[RabbitMQ]
KAFKA[Kafka]
PG[PostgreSQL]
end
ADAPT -->|retorna en format<br/>ExternalMetricValueList| API
API -->|resposta| HPA
style ADAPT fill:#cde,stroke:#369,stroke-width:2px
Només hi pot haver un servei registrat per cada API. És a dir, un únic proveïdor d'external.metrics.k8s.io a tot el clúster. Això és important: si ja tens l'adaptador de Prometheus registrat allà i després instal·les KEDA, un dels dos es quedarà sense registrar. Ho veurem als errors comuns.
L'adaptador de Prometheus
L'opció clàssica abans de KEDA és prometheus-adapter. Tradueix consultes PromQL en mètriques de l'API de Kubernetes mitjançant regles de configuració:
# Fragment de la configuracio de prometheus-adapter
rules:
- seriesQuery: 'peticions_http_total{namespace!="",pod!=""}'
resources:
overrides:
namespace: {resource: "namespace"}
pod: {resource: "pod"}
name:
matches: "^peticions_http_total$"
as: "peticions_per_segon"
metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'I un cop configurat, la mètrica està disponible:
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/pods/*/peticions_per_segon" \
| python3 -m json.toolFunciona, però té inconvenients que expliquen per què KEDA s'ha imposat:
Inconvenient de prometheus-adapter |
Com ho resol KEDA |
|---|---|
| Configuració en un ConfigMap global, difícil de raonar | Cada ScaledObject porta la seva consulta |
| Canviar una regla requereix reiniciar l'adaptador | Canvi en calent |
| Només parla amb Prometheus | Més de 70 fonts diferents |
| No permet escalar a zero | Escalat a zero natiu |
| Sense disparadors temporals | Escalador cron inclòs |
| Sense gestió de credencials | TriggerAuthentication |
- Què és KEDA i quina és la seva arquitectura
KEDA (Kubernetes Event-Driven Autoscaling) és un projecte graduat de la CNCF que afegeix al clúster la capacitat d'escalar càrregues en funció d'esdeveniments i mètriques externes de pràcticament qualsevol font.
Instal·lació
# Amb Helm (ho veurem a fons a 10-03)
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda --namespace keda --create-namespace
# Verificacio
kubectl get pods -n kedaNAME READY STATUS RESTARTS AGE
keda-admission-webhooks-6d8f7c9b4d-x2klm 1/1 Running 0 2m
keda-operator-7d9c5f8b6b-4mnpq 1/1 Running 0 2m
keda-operator-metrics-apiserver-5f7b8c9d6a-9wrtz 1/1 Running 0 2mI els CRDs que registra:
clustertriggerauthentications.keda.sh 2026-04-20T11:32:14Z
scaledjobs.keda.sh 2026-04-20T11:32:14Z
scaledobjects.keda.sh 2026-04-20T11:32:14Z
triggerauthentications.keda.sh 2026-04-20T11:32:14ZEls tres components
1. L'operador (keda-operator).
És el controlador principal. Vigila els objectes ScaledObject i ScaledJob i, quan en troba un:
- Crea i gestiona un HorizontalPodAutoscaler que apunta al mateix destí.
- Tradueix els
triggersdelScaledObjecten mètriquesExternalde l'HPA. - Gestiona l'escalat a zero activant i desactivant el destí (això l'HPA no ho sap fer per si sol).
- Actualitza l'estat del
ScaledObject.
2. El servidor de mètriques (keda-operator-metrics-apiserver).
Es registra com a proveïdor d'external.metrics.k8s.io a la capa d'agregació. Quan l'HPA pregunta pel valor d'una mètrica, aquest component:
- Identifica a quin
ScaledObjecti a quin disparador correspon. - Consulta la font real (Prometheus, RabbitMQ, Kafka...) fent servir l'escalador corresponent.
- Retorna el valor en el format que l'HPA espera.
3. El webhook d'admissió (keda-admission-webhooks).
Valida els ScaledObject abans d'acceptar-los: comprova que el destí existeix, que no hi ha dos ScaledObject sobre la mateixa càrrega, que el destí no té ja un HPA propi. Evita configuracions incoherents abans que causin problemes.
El diagrama complet
flowchart TD
SO[ScaledObject<br/>rutas-norte-pro]
OP[keda-operator]
HPA[HorizontalPodAutoscaler<br/>keda-hpa-worker-notificacions<br/>GENERAT per KEDA]
MS[keda-operator-metrics-apiserver]
API[Servidor d'API<br/>capa d'agregacio]
DEP[Deployment<br/>worker-notificacions]
subgraph FONTS["Fonts externes"]
RMQ[RabbitMQ<br/>cua de correus]
PROM[Prometheus]
CRON[Rellotge intern]
end
SO -->|1. observa| OP
OP -->|2. CREA I MANTE| HPA
OP -->|3. activa/desactiva<br/>escalat 0 a 1| DEP
HPA -->|4. pregunta el valor| API
API -->|5. delega| MS
MS -->|6. consulta| RMQ
MS -->|6. consulta| PROM
MS -->|6. consulta| CRON
MS -->|7. retorna valor| API
API -->|8. valor| HPA
HPA -->|9. PATCH /scale<br/>d'1 endavant| DEP
style OP fill:#cde,stroke:#369
style HPA fill:#ffd,stroke:#c90,stroke-width:2px
style MS fill:#cfc,stroke:#393
- La idea clau: KEDA no substitueix l'HPA, l'alimenta
Aquesta és la idea que més malentesos genera i la que cal fixar bé.
KEDA no és un autoescalador alternatiu. KEDA crea un HorizontalPodAutoscaler normal i corrent, i li dona de menjar mètriques.
Comprova-ho tu mateix. Després de crear un ScaledObject:
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
keda-hpa-worker-notificacions Deployment/worker-notificacions 1847/2500 (avg) 1 25 8Aquí està: un HPA amb el prefix keda-hpa-, gestionat per l'operador de KEDA. I el seu contingut:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: keda-hpa-worker-notificacions
namespace: rutas-norte-pro
labels:
app.kubernetes.io/managed-by: keda-operator
scaledobject.keda.sh/name: worker-notificacions
ownerReferences:
- apiVersion: keda.sh/v1alpha1
kind: ScaledObject
name: worker-notificacions
controller: true
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: worker-notificacions
minReplicas: 1
maxReplicas: 25
metrics:
- type: External
external:
metric:
name: s0-rabbitmq-notificacions-confirmacio
selector:
matchLabels:
scaledobject.keda.sh/name: worker-notificacions
target:
type: AverageValue
averageValue: "2500"És exactament l'HPA de 09-01, amb una mètrica External el nom de la qual l'ha generat KEDA (s0 = disparador 0). Tot el que vam aprendre sobre l'HPA continua aplicant: la fórmula, la banda de tolerància, el behavior, la regla que guanya la mètrica que demana més rèpliques.
Conseqüències pràctiques d'aquest disseny
| Conseqüència | Detall |
|---|---|
| No gestionis l'HPA a mà | És propietat del ScaledObject (ownerReferences). Si l'edites, KEDA ho reverteix; si l'esborres, el recrea |
| La càrrega no pot tenir un altre HPA | Un sol HPA per destí. Si ja en tens un de propi, cal esborrar-lo |
El behavior es configura des del ScaledObject |
A través d'advanced.horizontalPodAutoscalerConfig |
En esborrar el ScaledObject, desapareix l'HPA |
Per l'ownerReference. El Deployment es queda amb les rèpliques que tingués |
| Es depura com un HPA | kubectl describe hpa keda-hpa-... continua sent l'eina |
L'excepció: l'escalat a zero
Hi ha una cosa que l'HPA no sap fer i que KEDA gestiona directament: passar de 0 a 1 rèplica i d'1 a 0.
L'HPA no pot escalar a zero (llevat d'una feature gate opcional poc usada), i encara que pogués, tindria un problema irresoluble: amb zero pods no hi ha mètriques de pod, així que no sabria quan tornar a arrencar.
KEDA ho resol perquè consulta la font externa directament, no a través dels pods. Amb zero rèpliques de worker-notificacions, KEDA continua preguntant a RabbitMQ quants missatges hi ha. Quan la cua passa de 0 a 1 missatge, l'operador escala el Deployment de 0 a 1 i a partir d'aquí deixa que l'HPA prengui el comandament.
flowchart LR
Z["0 repliques<br/>servei apagat"] -->|"KEDA: hi ha activitat<br/>(operador)"| U["1 replica"]
U -->|"HPA: la metrica puja"| M["2 a 25 repliques"]
M -->|"HPA: la metrica baixa"| U
U -->|"KEDA: cooldownPeriod sense activitat<br/>(operador)"| Z
style Z fill:#eee,stroke:#999
style U fill:#cfc,stroke:#393
style M fill:#cde,stroke:#369
El tram 0↔1 el governa l'operador de KEDA; el tram 1↔N el governa l'HPA. Aquesta divisió explica molts comportaments: per exemple, que el cooldownPeriod només afecti la baixada a zero, i que la finestra d'estabilització del behavior governi la resta.
- L'objecte
ScaledObject camp a camp
ScaledObject camp a campapiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-notificacions
namespace: rutas-norte-pro
spec:
# A QUINA carrega s'aplica.
scaleTargetRef:
apiVersion: apps/v1 # Opcional, per defecte apps/v1
kind: Deployment # Opcional, per defecte Deployment
name: worker-notificacions
envSourceContainerName: worker # De quin contenidor llegir variables d'entorn
# CADA QUANT consulta KEDA la font externa.
pollingInterval: 15
# QUANT espera sense activitat abans de baixar a ZERO.
cooldownPeriod: 300
# Repliques quan NO hi ha activitat. 0 = escalat a zero.
minReplicaCount: 0
maxReplicaCount: 25
# Repliques en estat "inactiu" DIFERENT de zero.
idleReplicaCount: 0
# Que fer si la font de metriques NO RESPON.
fallback:
failureThreshold: 3
replicas: 6
# Configuracio avancada.
advanced:
restoreToOriginalReplicaCount: false
horizontalPodAutoscalerConfig:
name: hpa-worker-notificacions
behavior:
scaleUp: {}
scaleDown: {}
# QUE observa. Es una llista: n'hi pot haver diversos.
triggers:
- type: rabbitmq
metadata:
queueName: notificacions-confirmacio
mode: QueueLength
value: "2500"
authenticationRef:
name: rabbitmq-credencialsEls camps, un a un
scaleTargetRef — la càrrega destí. Admet Deployment, StatefulSet i qualsevol recurs amb subrecurs /scale, inclosos CRDs de tercers (Argo Rollouts, per exemple). El camp envSourceContainerName indica de quin contenidor llegir variables d'entorn quan un disparador les referencia.
pollingInterval (defecte: 30 s) — cada quant consulta KEDA la font externa. És un compromís:
| Valor | Avantatge | Inconvenient |
|---|---|---|
| 5-10 s | Reacció ràpida | Moltes consultes a la font; pot sobrecarregar-la |
| 15-30 s | Equilibrat. Recomanat | — |
| 60 s+ | Poca càrrega sobre la font | Reacció lenta |
Nota important: el pollingInterval només governa les consultes de KEDA per a l'escalat a zero. Un cop hi ha almenys una rèplica, l'HPA consulta les mètriques amb el seu propi cicle de 15 segons a través del servidor de mètriques de KEDA. Així que un pollingInterval de 60 s no fa que l'escalat d'1 a 25 sigui lent; només fa lenta l'activació des de zero.
cooldownPeriod (defecte: 300 s) — temps sense activitat abans de baixar a zero. Només aplica a la transició 1→0. La baixada de 25 a 1 la governa la finestra d'estabilització de l'HPA.
minReplicaCount (defecte: 0) — rèpliques mínimes. Amb 0, s'activa l'escalat a zero.
maxReplicaCount (defecte: 100) — sostre. Igual que el maxReplicas de l'HPA, i amb les mateixes advertències de capacitat de 09-03.
idleReplicaCount — un matís subtil però útil. Permet un estat «inactiu» amb més de zero rèpliques:
minReplicaCount: 3 # Quan hi ha activitat, minim 3
idleReplicaCount: 1 # Quan NO hi ha activitat, baixa a 1Significa: sense activitat, 1 rèplica (perquè la primera petició no esperi una arrencada en fred); amb activitat, mínim 3. Només és vàlid si idleReplicaCount < minReplicaCount. És un punt mitjà excel·lent entre l'escalat a zero i mantenir capacitat.
fallback — què fer si la font de mètriques falla:
fallback:
failureThreshold: 3 # Despres de 3 consultes fallides consecutives...
replicas: 6 # ...fixar 6 repliques i mantenir-lesAquest camp és fonamental en producció. Sense ell, si RabbitMQ deixa de respondre, l'HPA es queda amb la mètrica en <unknown> i no escala: el nombre de rèpliques es congela on estigués. Amb fallback, KEDA fixa un número segur i conegut.
El valor de replicas ha de ser suficient per al trànsit habitual: no el mínim ni el màxim, sinó un número amb el qual la plataforma funcioni mentre s'arregla la font de mètriques.
Limitació a conèixer: el fallback només funciona amb disparadors l'objectiu dels quals sigui AverageValue, i no aplica quan minReplicaCount és 0 amb la càrrega escalada a zero.
advanced.restoreToOriginalReplicaCount (defecte: false) — què passa en esborrar el ScaledObject. Amb false, el Deployment es queda amb les rèpliques que tingués en aquell moment. Amb true, torna al número que tenia abans que KEDA el gestionés. Posa'l a true si vols que esborrar el ScaledObject sigui reversible sense sorpreses.
advanced.horizontalPodAutoscalerConfig — la porta al behavior de 09-01:
advanced:
horizontalPodAutoscalerConfig:
name: hpa-worker-notificacions # Nom personalitzat de l'HPA generat
behavior:
scaleUp:
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 5
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300
selectPolicy: Min
policies:
- type: Percent
value: 25
periodSeconds: 60Tot l'après a 09-01 sobre el behavior continua sent vàlid, i es configura aquí. És la prova més clara que KEDA alimenta l'HPA en lloc de substituir-lo.
triggers — la llista de fonts. Amb diversos disparadors, guanya el que demana més rèpliques, exactament igual que amb diverses mètriques en un HPA. Aquesta regla és el que fa possible combinar «escala per cua» amb «escala per hora» de manera natural.
- El catàleg d'escaladors
KEDA porta més de setanta escaladors. Aquests són els que es fan servir de debò:
| Escalador | type |
Mètrica que observa | Cas d'ús típic |
|---|---|---|---|
| Prometheus | prometheus |
Resultat d'una consulta PromQL | El més versàtil. Qualsevol mètrica que ja tinguis |
| RabbitMQ | rabbitmq |
Longitud de cua o taxa de publicació | Consumidors de cua |
| Kafka | kafka |
Lag del grup de consumidors | Processament de fluxos |
| AWS SQS | aws-sqs-queue |
Missatges visibles + en vol | Cues a AWS |
| Azure Service Bus | azure-servicebus |
Missatges actius | Cues a Azure |
| Google Pub/Sub | gcp-pubsub |
Missatges sense confirmar | Cues a GCP |
| PostgreSQL | postgresql |
Resultat d'una consulta SQL | Treballs pendents en una taula |
| Redis | redis |
Longitud d'una llista o stream | Cues lleugeres |
| Cron | cron |
L'hora del dia | Escalat programat |
| CPU | cpu |
Utilització de CPU | Igual que l'HPA clàssic |
| Memòria | memory |
Utilització de memòria | Igual que l'HPA clàssic |
| MongoDB | mongodb |
Resultat d'una consulta | Documents pendents |
| Elasticsearch | elasticsearch |
Resultat d'una consulta | Documents per processar |
| Mètrica externa genèrica | external |
Un gRPC propi | Fonts a mida |
Un detall que sorprèn: KEDA també porta escaladors de CPU i memòria. Per què, si l'HPA ja ho fa? Perquè permet combinar CPU amb mètriques externes en un mateix ScaledObject, i perquè KEDA gestiona l'HPA per tu. És comú veure:
triggers:
- type: rabbitmq # Senyal principal: la cua
metadata: {...}
- type: cpu # Xarxa de seguretat: si la CPU es dispara, escalar igual
metricType: Utilization
metadata:
value: "80"Amb la regla de «guanya el que demana més», aquest segon disparador és una salvaguarda gratuïta: si la cua està buida però els pods estan al 90 % de CPU per alguna raó imprevista, s'escala igualment.
Exemples de configuració dels escaladors clau
Prometheus:
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitoratge.svc.cluster.local:9090
query: |
sum(rate(api_reserves_peticions_total{namespace="rutas-norte-pro"}[2m]))
threshold: "85"
# Quin valor fer servir si la consulta no retorna res (serie buida)
ignoreNullValues: "true"
unsafeSsl: "false"RabbitMQ:
- type: rabbitmq
metadata:
protocol: amqp
queueName: notificacions-confirmacio
mode: QueueLength # QueueLength o MessageRate
value: "2500"
# activationValue: per sota d'aixo, es considera INACTIU (per al 0)
activationValue: "10"
authenticationRef:
name: rabbitmq-credencialsCron:
- type: cron
metadata:
timezone: Europe/Madrid
start: "30 9 * * *" # A les 9:30
end: "0 14 * * *" # Fins a les 14:00
desiredReplicas: "25"PostgreSQL:
- type: postgresql
metadata:
query: "SELECT COUNT(*) FROM tasques_pendents WHERE estat = 'pendent'"
targetQueryValue: "50"
activationTargetQueryValue: "1"
authenticationRef:
name: postgres-credencialsactivationValue: la línia entre zero i u
Un concepte propi de KEDA que convé entendre. Cada escalador distingeix dos llindars:
value/threshold: l'objectiu per al càlcul de rèpliques (el fa servir l'HPA).activationValue: el llindar per sota del qual la càrrega es considera inactiva i pot baixar a zero.
metadata:
value: "2500" # 1 replica per cada 2500 missatges
activationValue: "10" # Amb menys de 10 missatges, es considera inactiuLa raó que siguin dos números diferents: sense activationValue, qualsevol valor més gran que zero activaria la càrrega. Amb una cua que sempre té 2 o 3 missatges residuals, l'escalat a zero mai passaria. L'activationValue defineix el soroll de fons acceptable.
TriggerAuthentication: credencials per als disparadors
TriggerAuthentication: credencials per als disparadorsEls disparadors necessiten parlar amb sistemes externs, i això requereix credencials. Ficar-les al ScaledObject en clar seria un desastre de seguretat, després de tot el mòdul 8.
KEDA ho resol amb TriggerAuthentication:
# 1. El Secret amb les credencials (modul 3 i 08-05)
apiVersion: v1
kind: Secret
metadata:
name: rabbitmq-credencials
namespace: rutas-norte-pro
type: Opaque
stringData:
# Cadena de connexio completa amb usuari i contrasenya.
host: "amqp://keda_lector:[email protected]:5672/"
---
# 2. El TriggerAuthentication que la referencia
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: rabbitmq-credencials
namespace: rutas-norte-pro
spec:
secretTargetRef:
- parameter: host # Nom del parametre que espera l'escalador
name: rabbitmq-credencials
key: host # Clau dins del SecretI al ScaledObject:
triggers:
- type: rabbitmq
metadata:
queueName: notificacions-confirmacio
mode: QueueLength
value: "2500"
authenticationRef:
name: rabbitmq-credencialsAltres fonts de credencials
TriggerAuthentication admet més que Secrets:
| Font | Camp | Ús |
|---|---|---|
| Secret | secretTargetRef |
El cas general |
| Variable d'entorn del pod destí | env |
Reutilitzar la config que ja té l'aplicació |
| Identitat del proveïdor de núvol | podIdentity |
AWS IRSA, Azure Workload Identity, GCP |
| HashiCorp Vault | hashiCorpVault |
Gestió centralitzada de secrets |
| Azure Key Vault | azureKeyVault |
Ídem a Azure |
Exemple amb identitat de núvol, que és la forma correcta en producció perquè elimina el secret de llarg termini:
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
name: aws-sqs-identitat
namespace: rutas-norte-pro
spec:
podIdentity:
provider: aws
roleArn: arn:aws:iam::000000000000:role/rutas-norte-keda-sqsConnecta directament amb el principi de mínim privilegi de 08-01: l'usuari que KEDA fa servir per llegir la cua només necessita permís de lectura de metadades, no de consumir missatges. A RabbitMQ, un usuari amb permís de read sobre la cua i res més.
ClusterTriggerAuthentication
La variant d'àmbit de clúster, per a credencials compartides entre namespaces:
apiVersion: keda.sh/v1alpha1
kind: ClusterTriggerAuthentication
metadata:
name: prometheus-lectura # Sense namespace
spec:
secretTargetRef:
- parameter: bearerToken
name: prometheus-token
key: tokenEs referencia amb kind: ClusterTriggerAuthentication a l'authenticationRef. Útil per a Prometheus, que és únic i el consulten tots els namespaces.
ScaledJob: un treball per missatge
ScaledJob: un treball per missatgeScaledObject escala un Deployment: pods de llarga vida que consumeixen en bucle. ScaledJob és diferent: crea un Job (06-03) per cada unitat de feina.
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: processador-factures
namespace: rutas-norte-pro
spec:
jobTargetRef:
parallelism: 1
completions: 1
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: processador
image: registry.rutasnorte.example/processador-factures:2.1.0
resources:
requests: {cpu: 500m, memory: 512Mi}
limits: {cpu: "2", memory: 2Gi}
pollingInterval: 30
maxReplicaCount: 20
successfulJobsHistoryLimit: 5
failedJobsHistoryLimit: 10
# Com calcular quants Jobs crear a partir del valor de la metrica.
scalingStrategy:
strategy: "default"
triggers:
- type: rabbitmq
metadata:
queueName: factures-pendents
mode: QueueLength
value: "1" # UN Job per missatge
authenticationRef:
name: rabbitmq-credencialsQuan fer servir cadascun
| Criteri | ScaledObject (Deployment) |
ScaledJob (Job per missatge) |
|---|---|---|
| Durada de la tasca | Curta (ms a segons) | Llarga (minuts a hores) |
| Cost d'arrencada | S'amortitza: el pod viu molt | Es paga per cada tasca |
| Aïllament entre tasques | Comparteixen procés | Total: un pod per tasca |
| Recursos per tasca | Iguals per a tots | Es poden ajustar per tipus de feina |
| Si una tasca falla | Pot afectar el pod sencer | Només falla aquell Job |
| Reintents | Els gestiona l'aplicació | backoffLimit de Kubernetes |
| Fuites de memòria | S'acumulen | Impossibles: el pod mor en acabar |
| Exemple a Rutas Norte | worker-notificacions (450 ms/correu) |
Generació d'un informe PDF de 20 min |
La regla: si la tasca dura menys que l'arrencada del pod, ScaledObject; si dura molt més, ScaledJob.
Per a worker-notificacions, cada correu triga 450 ms i arrencar un pod triga 15 segons. Un ScaledJob gastaria trenta vegades més temps arrencant que treballant. ScaledObject, sens dubte.
Però imaginem que Rutas Norte afegeix la generació de factures en PDF per a empreses: cada factura triga 12 minuts, consumeix 2 GiB de memòria de pic, i ocasionalment falla per un PDF corrupte. Allà ScaledJob brilla: aïllament total, reintents gestionats per Kubernetes, memòria alliberada en acabar.
- Rutas Norte:
worker-notificacions per longitud de cua
worker-notificacions per longitud de cuaAnem amb la implementació completa, el cas central de la lliçó.
Situació
- Fora de temporada, la cua de correus està pràcticament buida. S'envien uns 200 correus al dia, a ràfegues.
- Durant el pont de maig, la cua acumula 40.000 correus en dues hores.
- Cada worker processa ~2 correus/segon (limitat pel SMTP).
- Els correus de confirmació poden tolerar uns minuts de retard, però no hores.
El manifest
# k8s/entorns/pro/scaledobject-worker-notificacions.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-notificacions
namespace: rutas-norte-pro
labels:
app: worker-notificacions
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: worker-notificacions
# Consulta a RabbitMQ cada 15 s. Prou rapid perque la
# reactivacio des de zero sigui gairebe immediata, i prou lent per
# no matxucar l'API de gestio de RabbitMQ.
pollingInterval: 15
# 10 minuts sense activitat abans d'apagar del tot. Es generos a proposit:
# les reserves arriben a rafegues, i apagar i encendre cada dos minuts costaria
# mes (en arrencades en fred) que mantenir un pod.
cooldownPeriod: 600
# ESCALAT A ZERO. Fora de temporada no hi ha rao per mantenir workers
# encesos les 24 hores esperant un correu cada uns quants minuts.
minReplicaCount: 0
# Sostre: 25 workers x 2 correus/s = 50 correus/s = 3.000 correus/minut.
# Els 40.000 correus del pont es buiden en ~14 minuts. Suficient.
# A mes: 25 x 120m = 3 nuclis, que caben al pla de capacitat de 09-03.
maxReplicaCount: 25
# Si RabbitMQ deixa de respondre, no ens quedem congelats: fixem 6
# repliques, que es el cabal d'un dia normal amb marge. Millor un numero
# segur conegut que un numero congelat desconegut.
fallback:
failureThreshold: 3
replicas: 6
advanced:
# En esborrar el ScaledObject, tornar el Deployment a les seves repliques originals.
restoreToOriginalReplicaCount: true
horizontalPodAutoscalerConfig:
name: hpa-worker-notificacions
behavior:
scaleUp:
# Sense espera: si hi ha cua, hi ha feina pendent ARA.
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 5
periodSeconds: 30
scaleDown:
# 5 minuts de calma. Menys conservador que api-reserves perque
# arrencar un worker es barat (no te cache que escalfar) i
# perque no hi ha cap usuari esperant una resposta.
stabilizationWindowSeconds: 300
selectPolicy: Min
policies:
- type: Percent
value: 30
periodSeconds: 60
triggers:
# DISPARADOR PRINCIPAL: la longitud de la cua.
- type: rabbitmq
metadata:
protocol: amqp
queueName: notificacions-confirmacio
mode: QueueLength
# 1 worker per cada 2.500 missatges a la cua.
#
# Calcul: volem buidar 40.000 missatges en ~15 minuts.
# 40.000 / 900 s = 44,4 missatges/s necessaris
# 44,4 / 2 (per worker) = 22,2 workers
# 40.000 / 22,2 = 1.800 missatges per worker
# Arrodonim a 2.500 per no arribar al sostre amb marge just:
# 40.000 / 2.500 = 16 workers -> buida en ~21 minuts. Acceptable.
value: "2500"
# Per sota de 10 missatges, es considera inactiu i pot baixar a zero.
# No hi posem 0 perque sempre hi ha 1-2 missatges residuals en transit.
activationValue: "10"
authenticationRef:
name: rabbitmq-credencials
# DISPARADOR SECUNDARI: xarxa de seguretat per CPU.
# Si per alguna rao imprevista els workers se saturen de CPU (una
# plantilla de correu patologica, per exemple) escalem igual, encara que
# la cua sigui curta. Guanya el disparador que demana MES repliques.
- type: cpu
metricType: Utilization
metadata:
value: "80"Verificació
kubectl apply -f k8s/entorns/pro/scaledobject-worker-notificacions.yaml
kubectl get scaledobject -n rutas-norte-proNAME SCALETARGETKIND SCALETARGETNAME MIN MAX TRIGGERS AUTHENTICATION READY ACTIVE FALLBACK AGE
worker-notificacions apps/v1.Deployment worker-notificacions 0 25 rabbitmq rabbitmq-credencials True False False 34sColumnes clau:
| Columna | Significat |
|---|---|
READY |
KEDA ho ha configurat tot correctament |
ACTIVE |
Hi ha activitat ara mateix (per sobre de l'activationValue) |
FALLBACK |
Està fent servir el fallback perquè la font falla |
ACTIVE: False amb la cua buida significa que el Deployment està a zero rèpliques. Confirmem-ho:
La reactivació en viu
Obrim dos terminals. Al primer, observem:
Al segon, fiquem missatges a la cua:
# Simular 30.000 correus de confirmacio (obertura de la venda)
kubectl exec -n rutas-norte-pro rabbitmq-0 -- \
rabbitmqadmin publish routing_key=notificacions-confirmacio \
payload='{"reserva":"RN-2026-054321","destinatari":"[email protected]"}' \
--count=30000I al primer terminal:
NAME READY UP-TO-DATE AVAILABLE AGE
worker-notificacions 0/0 0 0 89d
worker-notificacions 0/1 0 0 89d <- KEDA activa: 0 -> 1
worker-notificacions 1/1 1 1 89d
worker-notificacions 1/6 1 1 89d <- L'HPA pren el comandament
worker-notificacions 6/6 6 6 89d
worker-notificacions 6/12 6 6 89d
worker-notificacions 12/12 12 12 89d
worker-notificacions 12/12 12 12 89d <- Estable: 30000/2500 = 12Es veu perfectament la divisió de responsabilitats: el salt 0→1 el fa l'operador de KEDA tan bon punt detecta activitat; a partir d'aquí és l'HPA aplicant la seva fórmula i el seu behavior.
I l'HPA generat confirma l'aritmètica:
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
hpa-worker-notificacions Deployment/worker-notificacions 2483/2500 (avg) 1 25 122483/2500 (avg): hi ha 12 rèpliques i 29.800 missatges; 29.800/12 = 2.483 missatges per rèplica, contra un objectiu de 2.500. El sistema és en equilibri.
Quan la cua es buida:
worker-notificacions 12/12 12 12 89d
worker-notificacions 12/8 12 12 89d <- L'HPA baixa (despres d'estabilitzacio)
worker-notificacions 8/3 8 8 89d
worker-notificacions 3/1 3 3 89d
worker-notificacions 1/1 1 1 89d <- Es queda en 1 (minim de l'HPA)
...passen 10 minuts de cooldownPeriod...
worker-notificacions 0/0 0 0 89d <- KEDA apaga: 1 -> 0
- Rutas Norte:
api-reserves per peticions per segon
api-reserves per peticions per segonAra substituïm l'HPA per CPU d'api-reserves per un basat en les mètriques Prometheus que vam instrumentar a 07-03.
Per què canviar
La CPU no és un senyal dolent per a api-reserves —és una API que sí que calcula— però és indirecte. Les peticions per segon són millors perquè:
- És la magnitud del negoci. «Cada rèplica atén 85 peticions per segon» és una frase que entén qualsevol a l'empresa.
- No depèn del
requests. Un canvi delrequests.cpu(recomanat pel VPA de 09-02) no altera el comportament de l'escalat. - És més ràpida. La CPU puja quan la feina ja està entrant; les peticions es compten en arribar.
- Elimina el conflicte amb el VPA. Com vam veure a 09-02, amb l'HPA per mètrica de negoci, el VPA pot governar CPU i memòria sense bucle.
Les mètriques instrumentades
De 07-03, api-reserves exposa a /metriques:
# HELP api_reserves_peticions_total Total de peticions HTTP ateses
# TYPE api_reserves_peticions_total counter
api_reserves_peticions_total{metode="GET",ruta="/rutes",codi="200"} 1847293
api_reserves_peticions_total{metode="POST",ruta="/reserves",codi="201"} 92841
# HELP api_reserves_duracio_segons Duracio de les peticions
# TYPE api_reserves_duracio_segons histogram
api_reserves_duracio_segons_bucket{ruta="/reserves",le="0.1"} 78201
api_reserves_duracio_segons_bucket{ruta="/reserves",le="0.5"} 91043
...
# HELP api_reserves_confirmades_total Reserves confirmades
# TYPE api_reserves_confirmades_total counter
api_reserves_confirmades_total 92841El manifest
# k8s/entorns/pro/scaledobject-api-reserves.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: api-reserves
namespace: rutas-norte-pro
labels:
app: api-reserves
app.kubernetes.io/part-of: rutas-norte
entorn: pro
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-reserves
pollingInterval: 15
cooldownPeriod: 300
# NO escalem a zero. api-reserves es l'API que sosté la venda: una
# arrencada en fred davant d'un usuari que vol comprar es inacceptable.
# 4 repliques es el terra de disponibilitat acordat (09-01, i 09-05).
minReplicaCount: 4
maxReplicaCount: 30
fallback:
failureThreshold: 3
# Si Prometheus cau, fixem 12 repliques: el triple del minim. Es un
# numero conservador que aguanta un dia normal amb folganca i ens dona temps
# a arreglar Prometheus sense risc de caiguda.
replicas: 12
advanced:
restoreToOriginalReplicaCount: true
horizontalPodAutoscalerConfig:
name: hpa-api-reserves
behavior:
# El MATEIX behavior asimetric de 09-01: pujar en zero segons,
# baixar despres de deu minuts de calma sostinguda.
scaleUp:
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 6
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 600
selectPolicy: Min
policies:
- type: Percent
value: 20
periodSeconds: 120
- type: Pods
value: 3
periodSeconds: 120
triggers:
# DISPARADOR PRINCIPAL: peticions per segon, mesurades a Prometheus.
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitoratge.svc.cluster.local:9090
# Nom amb el qual apareixera la metrica a l'HPA generat.
metricName: api_reserves_peticions_per_segon
query: |
sum(
rate(
api_reserves_peticions_total{
namespace="rutas-norte-pro",
app="api-reserves"
}[2m]
)
)
# 85 peticions per segon i replica.
#
# Calcul (de la prova de carrega de 09-06): una replica d'api-reserves
# amb 412m de CPU sosté ~120 rps abans que la latencia p95 superi
# els 300 ms. Deixem un 30 % de marge per absorbir el temps
# d'arrencada de les repliques noves: 120 x 0,7 = 84 -> 85 rps.
threshold: "85"
activationThreshold: "5"
ignoreNullValues: "true"
authenticationRef:
kind: ClusterTriggerAuthentication
name: prometheus-lectura
# XARXA DE SEGURETAT 1: CPU. Si per el que sigui les peticions son molt mes
# cares de l'habitual (una consulta patologica, un client abusiu), la
# CPU ho detectara encara que el nombre de peticions sigui normal.
- type: cpu
metricType: Utilization
metadata:
value: "70"La consulta PromQL, explicada
Esmicolada de dins cap enfora:
-
api_reserves_peticions_total{...}— selecciona el comptador de peticions, filtrat per namespace i aplicació. Retorna una sèrie per pod i per combinació d'etiquetes (mètode, ruta, codi). -
rate(...[2m])— converteix el comptador acumulat en peticions per segon, promitjant sobre els últims 2 minuts. És la funció correcta per a comptadors: gestiona els reinicis de pod (quan el comptador torna a zero) sense produir pics falsos.Per què 2 minuts? És un compromís. Amb
[1m]el senyal és més reactiu però molt sorollós (i amb unscrape_intervalde 30 s només hi hauria 2 mostres, cosa que fa elratepoc fiable). Amb[5m]el senyal és suau però arriba tard. Regla: la finestra delrateha de ser almenys 4 vegades l'interval de recol·lecció. Amb recol·lecció cada 30 s,[2m]és el mínim raonable. -
sum(...)— suma totes les sèries. Sense això tindríem una sèrie per pod i per ruta; KEDA necessita un únic número escalar.
És imprescindible que la consulta retorni un sol valor. Si retorna diverses sèries, KEDA registra un error. Prova sempre la consulta a la interfície de Prometheus abans de posar-la en un ScaledObject.
Comprovació de l'aritmètica
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
hpa-api-reserves Deployment/api-reserves 1247/85 (avg), 34%/70% 4 30 15Hi ha dues mètriques. La primera diu 1247/85 (avg)... i aquí hi ha un detall important que confon molta gent.
KEDA fa servir AverageValue per als disparadors de Prometheus. Això significa que l'HPA divideix el valor total entre les rèpliques:
Valor total de la consulta: 1.247 rps (totes les repliques juntes)
Repliques actuals: 15
Valor per replica: 1.247 / 15 = 83,1 rps
Objectiu: 85 rps
ratio = 83,1 / 85 = 0,978 -> dins de la banda de tolerancia. Estable.La visualització 1247/85 pot despistar perquè mostra el total contra l'objectiu per rèplica. El que compara internament és 83,1 contra 85.
I la segona mètrica, la CPU: 34%/70%. Demanaria baixar rèpliques, però guanya la que demana més, així que mana la de peticions per segon.
Migrar de l'HPA anterior
Important: no poden coexistir dos HPA sobre el mateix Deployment. L'HPA manual de 09-01 cal esborrar-lo abans.
# 1. Esborrar l'HPA manual
kubectl delete hpa api-reserves -n rutas-norte-pro
# 2. Crear el ScaledObject
kubectl apply -f k8s/entorns/pro/scaledobject-api-reserves.yaml
# 3. Verificar que KEDA ha creat el seu
kubectl get hpa -n rutas-norte-proSi et saltes el pas 1, el webhook d'admissió de KEDA rebutja el ScaledObject:
Error from server: admission webhook "vscaledobject.kb.io" denied the request:
the workload 'api-reserves' of type 'apps/v1.Deployment' is already managed by
the hpa 'api-reserves'És un missatge clar i una validació molt benvinguda: sense ella, tindries dos HPA barallant-se pel mateix replicas.
- Rutas Norte: el disparador cron del pont de maig
I ara la peça que resol el segon problema: escalar abans que arribi el trànsit.
El problema, amb cronòmetre
09:59:50 4 repliques d'api-reserves. Transit normal: 180 rps.
10:00:00 S'obre la venda. El transit salta a 2.400 rps en 40 segons.
10:00:15 Prometheus recol·lecta. El rate[2m] encara reflecteix la mitjana dels
ultims 2 minuts, que inclou 1m50s de calma: ~400 rps.
10:00:15 KEDA/HPA: 400/4 = 100 rps per replica. Ratio 1,18. Demana 5 repliques.
<- MOLT PER SOTA del necessari. El rate[2m] SUAVITZA el pic.
10:00:45 El rate ja reflecteix millor la realitat: ~1.500 rps. Demana 18 repliques.
10:01:15 Els primers pods nous estan Ready.
10:02:30 S'assoleixen les 28 repliques necessaries.
DEGRADACIO: dos minuts i mig al moment de mes valor de l'any.Fixa't en l'efecte pervers de la finestra de 2 minuts del rate: suavitza exactament el pic que volem detectar. És inevitable: sense aquesta finestra el senyal seria inutilitzablement sorollós. És una limitació intrínseca de qualsevol escalat reactiu basat en taxes.
La solució: el disparador cron
# k8s/entorns/pro/scaledobject-api-reserves.yaml (fragment afegit)
triggers:
- type: prometheus
# ... (el disparador principal de dalt, sense canvis)
- type: cpu
# ... (la xarxa de seguretat, sense canvis)
# DISPARADOR DE PREESCALFAMENT: el pont de maig.
#
# La venda obre a les 10:00 del 25 d'abril. A les 9:30 ja volem
# 25 repliques ARRENCADES I CALENTES: pool de connexions a PostgreSQL
# establert, cache de rutes poblada, JIT de Node.js escalfat.
#
# Recorda la regla de l'HPA: GUANYA EL DISPARADOR QUE DEMANA MES REPLIQUES.
# Entre les 9:30 i les 14:00, el terra son 25 repliques. Si el transit
# real en demana mes, el disparador de Prometheus puja fins a 30.
- type: cron
metadata:
timezone: Europe/Madrid
start: "30 9 25 4 *" # 25 d'abril a les 9:30
end: "0 14 25 4 *" # 25 d'abril a les 14:00
desiredReplicas: "25"Sintaxi de start i end: cron estàndard de cinc camps (minut hora dia mes dia-de-la-setmana). 30 9 25 4 * = a les 9:30 del dia 25 d'abril, qualsevol dia de la setmana.
El timezone és obligatori i crític. Sense ell, KEDA fa servir UTC, i en horari d'estiu espanyol això són dues hores de diferència: les teves 25 rèpliques apareixerien a les 11:30, una hora i mitja després de l'obertura. Fes servir sempre el nom IANA de la zona (Europe/Madrid), mai un desplaçament fix, perquè els canvis d'horari es gestionin sols.
Un patró més mantenible: cron setmanal
Dates fixes cal actualitzar-les cada any. Per a un patró recurrent:
# Preescalfament diari de la franja de mes venda.
# De dilluns a diumenge, de 9:30 a 13:00, terra de 10 repliques.
- type: cron
metadata:
timezone: Europe/Madrid
start: "30 9 * * *"
end: "0 13 * * *"
desiredReplicas: "10"
# Reforc de cap de setmana: divendres tarda i dissabte mati concentren
# les reserves d'escapada.
- type: cron
metadata:
timezone: Europe/Madrid
start: "0 16 * * 5" # Divendres a les 16:00
end: "0 22 * * 6" # Dissabte a les 22:00
desiredReplicas: "15"Amb diversos disparadors cron solapats, guanya el que demana més. Un divendres a les 17:00 estarien actius el diari (10) i el de cap de setmana (15): el terra és 15.
Escenari complet amb tots els disparadors
Vegem què passa el 25 d'abril amb els quatre disparadors actius:
| Hora | Cron pont | Cron diari | Prometheus | CPU | Rèpliques | Qui mana |
|---|---|---|---|---|---|---|
| 08:00 | — | — | 3 (180 rps) | 2 | 4 | minReplicaCount |
| 09:29 | — | — | 3 | 2 | 4 | minReplicaCount |
| 09:31 | 25 | 10 | 3 | 2 | 25 | Cron pont |
| 09:59 | 25 | 10 | 3 | 2 | 25 | Cron pont |
| 10:01 | 25 | 10 | 5 | 8 | 25 | Cron pont (el pic ja està cobert!) |
| 10:05 | 25 | 10 | 29 | 22 | 29 | Prometheus |
| 11:30 | 25 | 10 | 21 | 16 | 25 | Cron pont |
| 13:05 | 25 | — | 12 | 9 | 25 | Cron pont |
| 14:01 | — | — | 9 | 7 | 9 | Prometheus (amb estabilització de 10 min) |
A les 10:01, amb el trànsit ja disparat, el disparador de Prometheus només demana 5 rèpliques (pel suavitzat del rate) però ja en tenim 25 gràcies al cron. Zero degradació. El pic s'absorbeix sense que ningú se n'adoni.
I a les 10:05, quan el rate ja reflecteix la realitat i en demana 29, el sistema puja aquestes 4 rèpliques addicionals amb tota tranquil·litat.
Aquesta és la diferència entre reaccionar i anticipar-se. El cron no reemplaça l'escalat reactiu: li treu la responsabilitat del moment crític.
El mateix patró per al clúster
El cron escala pods, però recorda 09-03: els pods necessiten nodes. 25 rèpliques d'api-reserves requereixen nodes que triguen 3-6 minuts a arrencar.
Solució coherent: escalar també el coixí de sobreaprovisionament abans:
# k8s/entorns/pro/scaledobject-farciment-capacitat.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: farciment-capacitat
namespace: rutas-norte-pro
spec:
scaleTargetRef:
name: farciment-capacitat
minReplicaCount: 2
maxReplicaCount: 10
cooldownPeriod: 300
triggers:
# Ampliar el coixi UNA HORA ABANS que les repliques d'api-reserves.
# Aixi el Cluster Autoscaler te temps d'arrencar els nodes, i a les
# 9:30, quan el cron d'api-reserves demani 25 repliques, hi ha lloc.
- type: cron
metadata:
timezone: Europe/Madrid
start: "30 8 25 4 *" # 8:30, una hora abans
end: "0 15 25 4 *"
desiredReplicas: "10"La cadena temporal completa:
08:30 El cron amplia el farciment de 2 a 10 pods.
8 pods de farciment passen a Pending.
08:31 El Cluster Autoscaler detecta els pendents, demana 3 nodes.
08:35 Els nodes estan Ready. El farciment es col·loca.
CAPACITAT LLESTA I PAGADA, pero ocupada per pods que no fan res.
09:30 El cron d'api-reserves demana 25 repliques.
El planificador DESALLOTJA els pods de farciment (prioritat -10).
09:30 Les 25 repliques es col·loquen en SEGONS. No cal esperar nodes.
09:31 Els pods de farciment passen a Pending -> el CA arrenca mes nodes ->
el coixi es regenera per al que vingui.
09:32 25 repliques Ready i calentes.
10:00 OBERTURA. Zero degradacio.Aquí es veuen les quatre peces del mòdul treballant juntes: l'escalat programat (KEDA), el sobreaprovisionament amb preemption (09-03), l'autoescalat de nodes (09-03) i l'HPA reactiu (09-01) com a xarxa de seguretat.
- L'escalat a zero i les seves implicacions
L'escalat a zero és la funció més cridanera de KEDA, i la que més cal pensar abans de fer servir.
Què significa
Amb minReplicaCount: 0, quan no hi ha activitat durant el cooldownPeriod, KEDA porta el Deployment a zero rèpliques. No hi ha pods. No hi ha contenidors. El consum de CPU i memòria és literalment zero.
El cost: l'arrencada en fred
La contrapartida és inevitable. Des que apareix feina fins que es processa:
t=0 Arriba un missatge a la cua.
t=0-15 KEDA ho detecta al seu seguent cicle (pollingInterval).
t=15 KEDA escala el Deployment de 0 a 1.
t=16 El planificador col·loca el pod. Si NO hi ha lloc -> +3 min (09-03).
t=17 El kubelet descarrega la imatge. Si esta cachejada, ~2 s; si no, 20-60 s.
t=20 El contenidor arrenca. Node.js: ~15 s fins a estar llest.
t=35 Connexio a PostgreSQL i a RabbitMQ establerta.
t=36 Es processa el primer missatge.
RETARD TOTAL: entre 35 segons i 4 minuts.Quines càrregues ho toleren i quines no
| Càrrega | Escalar a zero? | Motiu |
|---|---|---|
worker-notificacions |
Sí | Ningú espera. Un correu 40 s més tard és igual |
informes-ocupacio |
No aplica | Ja és un CronJob: només existeix quan s'executa |
| Processador de treballs per lots | Sí | Ídem: sense usuari esperant |
| Entorns de desenvolupament i proves | Sí, molt recomanable | Estalvi enorme fora de l'horari laboral |
api-reserves |
No | Un usuari esperant 40 s per veure rutes se'n va a la competència |
botiga-web |
No | És la porta d'entrada |
postgres-reserves |
Mai | Base de dades amb estat |
redis-cache |
No | Arrencar buida descarrega tot sobre PostgreSQL |
La regla: si hi ha un ésser humà esperant una resposta, no escalis a zero.
El cas de rutas-norte-dev: l'estalvi més rendible
L'entorn de desenvolupament es fa servir de dilluns a divendres, de 8:00 a 19:00. Són 55 hores de 168 setmanals: el 67 % del temps està encès sense que ningú el faci servir.
# k8s/entorns/dev/scaledobject-apagat-nocturn.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: api-reserves-horari
namespace: rutas-norte-dev
labels:
app: api-reserves
entorn: dev
spec:
scaleTargetRef:
name: api-reserves
# Fora de l'horari laboral: ZERO. L'entorn s'apaga sol.
minReplicaCount: 0
maxReplicaCount: 3
cooldownPeriod: 300
triggers:
# De dilluns a divendres, de 7:45 a 19:30: 1 replica encesa.
# 7:45 perque estigui calenta quan arribi l'equip a les 8:00.
- type: cron
metadata:
timezone: Europe/Madrid
start: "45 7 * * 1-5"
end: "30 19 * * 1-5"
desiredReplicas: "1"Estalvi estimat: l'entorn de desenvolupament complet (5 components, ~4 nuclis i 8 GiB) apagat 113 hores setmanals. Aproximadament el 67 % del cost de l'entorn, que en un clúster gestionat pot ser un node sencer.
I hi ha un benefici addicional que no és econòmic: força l'equip que els serveis arrenquin ràpid i sense intervenció manual. Un entorn que s'apaga i s'encén cada dia detecta de seguida les dependències d'arrencada mal resoltes.
El patró idleReplicaCount: el millor dels dos mons
Per a càrregues que no toleren l'arrencada en fred però tampoc justifiquen mantenir capacitat plena:
spec:
# Quan HI HA activitat: minim 4 repliques.
minReplicaCount: 4
# Quan NO hi ha activitat: 1 replica encesa.
idleReplicaCount: 1Aquesta rèplica única manté la connexió a la base de dades, la memòria cau calenta i el procés llest. La primera petició s'atén en mil·lisegons, i l'HPA puja a 4 al següent cicle. És el compromís correcte per a la majoria de serveis amb usuaris, i molts equips el prefereixen al zero absolut.
L'activació des de zero: un detall important
Amb zero rèpliques, KEDA no pot fer servir mètriques de pod. Per això els disparadors de CPU i memòria no serveixen per activar des de zero: si no hi ha pods, no hi ha CPU per mesurar.
Conseqüència pràctica: un ScaledObject amb minReplicaCount: 0 necessita almenys un disparador que consulti una font externa (cua, Prometheus, cron, base de dades). Si només té disparadors de CPU i memòria, es quedarà a zero per sempre.
És un error freqüent i el missatge de KEDA no sempre ho deixa clar. Tingues-ho present.
- Depurar un
ScaledObject que no escala
ScaledObject que no escalaProcediment sistemàtic, del més general al més específic.
Pas 1: l'estat del ScaledObject
NAME SCALETARGETKIND SCALETARGETNAME MIN MAX READY ACTIVE FALLBACK AGE
worker-notificacions apps/v1.Deployment worker-notificacions 0 25 False False False 4mREADY: False és el primer problema. Significa que KEDA no l'ha pogut configurar.
Status:
Conditions:
Type: Ready
Status: False
Reason: ScalerFailed
Message: error creating scaler for trigger 0: error parsing rabbitmq metadata:
no host setting given
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning ScalerFailed 4m keda-operator error creating scaler for trigger 0El missatge assenyala el problema exacte: falta el host del disparador de RabbitMQ, és a dir, el TriggerAuthentication no està bé.
Taula de diagnòstic per condició
READY |
ACTIVE |
Símptoma | Causa probable |
|---|---|---|---|
False |
False |
No fa res | Error de configuració del disparador o del TriggerAuthentication |
True |
False |
Es queda en minReplicaCount |
La mètrica és per sota de l'activationThreshold |
True |
True |
Escala, però malament | El threshold està mal calibrat |
True |
True |
FALLBACK: True |
La font de mètriques no respon |
Unknown |
— | Sense estat | L'operador de KEDA no està funcionant |
Pas 2: l'HPA generat
Recorda: KEDA alimenta un HPA. Si l'HPA no veu la mètrica, no escala.
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True SucceededGetScale the HPA controller was able to get the target's current scale
ScalingActive False FailedGetExternalMetric unable to get external metric
rutas-norte-pro/s0-rabbitmq-notificacions-confirmacio/&LabelSelector{...}:
unable to fetch metrics from external metrics API:
rpc error: code = Unknown desc = error inspecting rabbitMQ:
Object Not FoundObject Not Found de RabbitMQ: la cua no existeix amb aquest nom. Un error tipogràfic a queueName, o la cua encara no s'ha creat.
Pas 3: consultar la mètrica directament
Pontegeja l'HPA i pregunta a l'API d'agregació:
kubectl get --raw "/apis/external.metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/s0-rabbitmq-notificacions-confirmacio?labelSelector=scaledobject.keda.sh%2Fname%3Dworker-notificacions" \
| python3 -m json.tool{
"kind": "ExternalMetricValueList",
"apiVersion": "external.metrics.k8s.io/v1beta1",
"items": [
{
"metricName": "s0-rabbitmq-notificacions-confirmacio",
"metricLabels": null,
"timestamp": "2026-05-01T10:14:32Z",
"value": "29800"
}
]
}Si això retorna un valor correcte, el problema és entre l'HPA i l'API, no a KEDA ni a la font. Si retorna error, el problema és a l'escalador o a la font.
Pas 4: els logs de l'operador
ERROR scale_handler error getting scale decision {"scaledObject.Namespace": "rutas-norte-pro",
"scaledObject.Name": "worker-notificacions",
"error": "dial tcp 10.96.42.17:5672: connect: connection refused"}connection refused: KEDA no pot connectar amb RabbitMQ. Causes: el Service no existeix, la NetworkPolicy el bloqueja (04-06), o RabbitMQ està caigut.
I del servidor de mètriques:
Pas 5: verificar la connectivitat de xarxa
Un cas molt freqüent després del mòdul 8: les NetworkPolicies bloquegen KEDA.
# Pot KEDA arribar a RabbitMQ?
kubectl run prova-connectivitat --rm -it --restart=Never \
-n keda --image=busybox:1.36 -- \
wget -qO- --timeout=5 http://rabbitmq.rutas-norte-pro.svc.cluster.local:15672/api/overviewSi falla i RabbitMQ funciona, revisa les NetworkPolicies del namespace rutas-norte-pro. KEDA viu al namespace keda i necessita permís explícit d'entrada:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permetre-keda-a-rabbitmq
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
app: rabbitmq
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: keda
ports:
- protocol: TCP
port: 5672
- protocol: TCP
port: 15672Aquest és l'error número u després d'implantar les NetworkPolicies de 04-06 i 08-04. Tot funcionava, s'afegeixen les polítiques de seguretat, i de sobte KEDA deixa d'escalar sense que ningú relacioni les dues coses.
Pas 6: provar la consulta PromQL a mà
Per a disparadors de Prometheus:
I a la interfície web, executa la consulta exacta del ScaledObject. Comprova:
- Retorna alguna dada? Si està buida, les etiquetes del selector no coincideixen.
- Retorna UN SOL valor? Si retorna diverses sèries, falta un
sum(). - El valor té sentit? Un
rateque dona 0,003 en lloc de 1.247 suggereix que falten unitats o que la finestra està malament.
Llista de comprovació ràpida
| Comprovació | Ordre |
|---|---|
| KEDA és viu? | kubectl get pods -n keda |
| L'API externa està registrada? | kubectl get apiservices v1beta1.external.metrics.k8s.io |
El ScaledObject està READY? |
kubectl get scaledobject -n rutas-norte-pro |
| L'HPA veu la mètrica? | kubectl describe hpa keda-hpa-... |
| La mètrica es pot consultar? | kubectl get --raw "/apis/external.metrics.k8s.io/..." |
| Hi ha errors a l'operador? | kubectl logs -n keda deployment/keda-operator |
| Hi ha connectivitat de xarxa? | Pod de prova des del namespace keda |
| La consulta PromQL és correcta? | Interfície de Prometheus |
| Hi ha un altre HPA en conflicte? | kubectl get hpa -n rutas-norte-pro |
| Hi ha lloc per als pods nous? | kubectl get pods --field-selector status.phase=Pending |
Aquesta última comprovació connecta amb 09-03 i mereix èmfasi: un ScaledObject pot estar funcionant perfectament i tot i així no veure més pods corrent, perquè els pods demanats estan en Pending. El ScaledObject diria ACTIVE: True i l'HPA mostraria 20 rèpliques, mentre només 8 estan realment servint. Mira sempre els pods, no només l'escalador.
Errors Comuns i Consells
Error 1: type: Value en lloc d'AverageValue per a una cua. Amb Value, l'HPA compara el valor brut amb l'objectiu i les rèpliques oscil·len violentament. Per a cues, sempre AverageValue: el significat és «cada rèplica es fa càrrec de N unitats».
Error 2: la consulta PromQL retorna diverses sèries. KEDA necessita un escalar. Sense sum() (o avg(), max(), segons el cas), el disparador falla. Prova sempre la consulta a Prometheus abans de posar-la en un manifest.
Error 3: escalar a zero un servei amb usuaris al davant. L'arrencada en fred són 35 segons com a mínim. Ningú espera 35 segons per una pàgina de bitllets. Fes servir idleReplicaCount: 1 si vols estalviar sense aquest cost.
Error 4: deixar un HPA manual sobre el mateix Deployment. El webhook de KEDA ho detecta i rebutja el ScaledObject amb un missatge clar. Esborra l'HPA antic primer.
Error 5: oblidar el timezone al disparador cron. KEDA fa servir UTC per defecte. En horari d'estiu espanyol, les teves 25 rèpliques apareixen dues hores tard. Fes servir sempre el nom IANA (Europe/Madrid), mai un desplaçament fix.
Error 6: NetworkPolicies que bloquegen KEDA. L'operador viu al namespace keda i necessita permís explícit per arribar a RabbitMQ, Prometheus o la base de dades. És l'error més freqüent després d'implantar les polítiques de 04-06 i 08-04, i el més difícil de relacionar amb la seva causa.
Error 7: no configurar el fallback. Si la font de mètriques cau, l'HPA es queda amb la mètrica en <unknown> i les rèpliques congelades on estiguessin. Amb fallback, fixes un número segur i conegut mentre arregles la font.
Error 8: minReplicaCount: 0 amb només disparadors de CPU o memòria. Amb zero pods no hi ha mètriques de pod, així que mai es reactivarà. L'escalat a zero necessita almenys un disparador de font externa.
Consell 1: comença amb minReplicaCount igual al teu número actual. Desplega el ScaledObject, observa una setmana quines mètriques reporta i què hauria fet, i només llavors obre el rang. Igual que el consell de l'HPA a 09-01: l'observació prèvia és gratis i evita ensurts.
Consell 2: calibra el threshold amb dades, no amb intuïció. El número ha de sortir d'una prova de càrrega (09-06): quantes peticions per segon sosté una rèplica abans que la latència p95 superi l'objectiu? Aquest número, menys un 30 % de marge per al temps d'arrencada, és el teu threshold.
Consell 3: combina disparadors. El senyal principal (cua, peticions) més una xarxa de seguretat de CPU més un cron de preescalfament. Amb la regla de «guanya el que demana més», cadascun cobreix un error dels altres i cap no molesta.
Consell 4: monitora el mateix KEDA. L'operador exposa mètriques Prometheus:
# Errors de l'escalador: si puja, KEDA no pot consultar la font
sum(rate(keda_scaler_errors_total[5m])) by (scaledObject, scaler) > 0
# Valor de la metrica que KEDA esta reportant
keda_scaler_metrics_value{scaledObject="worker-notificacions"}
# ScaledObjects en fallback: senyal que una font esta caiguda
keda_scaled_object_errors_total > 0El primer és l'alerta imprescindible: si KEDA no pot llegir la mètrica, el teu escalat no existeix, encara que tot sembli normal a kubectl get deployment.
Consell 5: a Grafana, superposa la mètrica de negoci i les rèpliques. Veure la longitud de la cua i el nombre de workers al mateix gràfic fa evident d'un cop d'ull si el threshold està ben calibrat: la cua ha de pujar, les rèpliques seguir-la, i la cua baixar. Si la cua puja i les rèpliques no, el threshold és massa alt.
Consell 6: documenta el càlcul del threshold al manifest. Un comentari que expliqui d'on surt el 2.500 converteix un número màgic en una decisió revisable. El següent que ho llegeixi sabrà si el pot tocar i amb quin criteri.
Consell 7: fes servir ClusterTriggerAuthentication per a Prometheus. És únic al clúster i el consulten tots els namespaces. Un sol objecte en lloc d'un per namespace.
Exercicis
Exercici 1: calcular el threshold d'una cua
Rutas Norte afegeix worker-factures, que genera les factures en PDF de les reserves i les puja a un magatzem d'objectes. Dades mesurades:
- Cada factura triga 8 segons a generar-se (2 s de CPU, 6 s esperant el magatzem).
- Un worker processa les factures d'una en una (no hi ha concurrència interna).
- Durant el pont de maig es generen 12.000 factures en les 4 hores següents al tancament de la venda.
- Objectiu de negoci: cap factura no ha de trigar més de 30 minuts des de la reserva.
- Cada worker consumeix
requests.cpu: 250mirequests.memory: 512Mi. - La ResourceQuota del namespace té 8 nuclis lliures en aquell moment.
Calcula: (a) les factures per segon que genera un worker; (b) els workers necessaris per complir l'objectiu de 30 minuts; (c) si caben a la quota; (d) el threshold (missatges per rèplica) que produeix aquest nombre de workers al pic; (e) escriu el ScaledObject complet, decidint a més si faries servir ScaledObject o ScaledJob i per què.
Exercici 2: diagnosticar un ScaledObject mut
worker-notificacions no escala. La cua de RabbitMQ té 18.000 missatges des de fa vint minuts i continua havent-hi 1 rèplica. Això és el que veus:
NAME SCALETARGETKIND MIN MAX READY ACTIVE FALLBACK AGE
worker-notificacions apps/v1.Deployment 0 25 True True True 3hConditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True ReadyForNewScale recommended size matches current size
ScalingActive True ValidMetricFound the HPA was able to successfully calculate a replica count
ScalingLimited False DesiredWithinRange the desired count is within the acceptable range
Metrics:
( current / target )
"s0-rabbitmq-notificacions-confirmacio" (target average value): 6 / 2500
Events: <none>ERROR scale_handler error getting metric for scaler {"scaledObject.Name": "worker-notificacions",
"scaler": "rabbitmqScaler",
"error": "Get \"https://rabbitmq.rutas-norte-pro.svc.cluster.local:15671/api/queues/%2F/notificacions-confirmacio\":
dial tcp 10.96.42.17:15671: i/o timeout"}Explica exactament què està passant, per què l'HPA diu que tot està bé, per què el valor de la mètrica és 6, què cal corregir i com evitar que torni a passar sense que ningú se n'assabenti.
Exercici 3: dissenyar l'estratègia d'escalat d'un component nou
Rutas Norte llança motor-preus, un servei que recalcula els preus dinàmics de totes les rutes en funció de l'ocupació. Perfil:
- És un servei HTTP intern que consulta
api-reserves. No és públic. - Rep peticions d'
api-reservescada vegada que algú busca rutes: entre 50 i 2.500 peticions per segon. - Cada petició triga 40 ms i consumeix bastant CPU (càlcul matemàtic pur).
- Una rèplica sosté ~180 peticions per segon abans que la latència p95 superi els 80 ms.
- A més, cada nit a les 3:00 recalcula la matriu de preus completa: és un procés de 45 minuts que consumeix 4 nuclis i que avui s'executa al mateix servei (cosa que degrada les respostes HTTP durant aquests 45 minuts).
- La latència d'aquest servei entra directament a la latència d'
api-reserves: si triga, la cerca de rutes triga. - Està instrumentat amb Prometheus: exposa
motor_preus_peticions_totalimotor_preus_duracio_segons.
Dissenya l'estratègia completa. Un ScaledObject o diversos? Quins disparadors? Escalat a zero? Què faries amb el procés nocturn? Escriu els manifests i justifica cada decisió.
Solucions
Solució 1
(a) Factures per segon d'un worker:
Cada factura: 8 segons, processades d'una en una.
Factures per segon per worker = 1 / 8 = 0,125 factures/s(b) Workers necessaris:
L'objectiu és que cap factura no esperi més de 30 minuts. El cas pitjor és la factura que entra l'última en el moment de més acumulació.
12.000 factures en 4 hores = 12.000 / 14.400 s = 0,833 factures/s d'entrada.
Si el sistema processes just a la velocitat d'entrada, la cua no creixeria
pero tampoc es buidaria, i una factura esperaria... depen de la cua
acumulada. Necessitem processar MES RAPID que l'entrada.
Enfocament per l'objectiu de 30 minuts:
En 30 minuts (1.800 s) entren 0,833 x 1.800 = 1.500 factures.
Perque cap no esperi mes de 30 min, cal processar 1.500 factures
en menys de 1.800 s: capacitat >= 1.500 / 1.800 = 0,833 factures/s.
Amb marge del 100 % (per absorbir rafegues, que l'entrada no es uniforme):
Capacitat objectiu: 1,67 factures/s
Workers = 1,67 / 0,125 = 13,3 -> 14 workersComprovació amb el pitjor cas realista: si les factures arriben concentrades (la meitat a la primera hora):
Primera hora: 6.000 factures = 1,67 factures/s d'entrada.
Amb 14 workers: 14 x 0,125 = 1,75 factures/s de capacitat.
Capacitat > entrada: la cua no creix de forma sostinguda. Correcte.
Acumulacio maxima estimada: ~500 factures.
Temps de buidatge d'aquestes 500: 500 / 1,75 = 286 s = 4,8 minuts.
MOLT per sota dels 30 minuts. De sobres.14 workers és la resposta, amb marge ampli.
(c) Caben a la quota?
14 workers x 250m = 3,5 nuclis
Quota lliure: 8 nuclis.
Hi caben. I hi ha marge per pujar el maxReplicaCount a 25:
25 x 250m = 6,25 nuclis < 8. Tambe hi cap.
Memoria: 14 x 512Mi = 7 GiB. Sense problema.Posem maxReplicaCount: 20 (14 amb un 40 % de marge), que consumeix 5 nuclis i deixa folgança.
(d) El threshold:
Al pic, quants missatges hi ha a la cua?
L'acumulacio maxima estimada es ~500 factures (calculat a dalt). Pero
si l'escalat triga a arrencar, l'acumulacio inicial es mes gran: en els
primers 2 minuts amb 1 sola replica, entren 1,67 x 120 = 200 factures
i se'n processen 0,125 x 120 = 15. Acumulacio: ~185.
Volem que amb ~500 missatges a la cua hi hagi 14 workers:
threshold = 500 / 14 = 35,7 -> arrodonim a 35
Verificacio amb altres valors de cua:
100 missatges -> sostre(100/35) = 3 workers
300 missatges -> sostre(300/35) = 9 workers
500 missatges -> sostre(500/35) = 15 workers (objectiu assolit)
700 missatges -> sostre(700/35) = 20 workers (sostre)threshold: "35"
(e) ScaledObject o ScaledJob?
Apliquem la regla de l'apartat 9:
Durada de la tasca: 8 segons.
Temps d'arrencada d'un pod: ~15 segons.
8 < 15: la tasca dura MENYS que l'arrencada.
-> ScaledObject. Un ScaledJob gastaria mes temps arrencant que treballant.Amb ScaledJob, generar 12.000 factures suposaria crear 12.000 pods, amb 15 segons d'arrencada cadascun: 50 hores de temps de còmput malgastat només en arrencar. Absurd.
Manifest complet:
# k8s/entorns/pro/scaledobject-worker-factures.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: worker-factures
namespace: rutas-norte-pro
labels:
app: worker-factures
app.kubernetes.io/part-of: rutas-norte
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: worker-factures
pollingInterval: 20
# 15 minuts sense activitat abans d'apagar. Generos perque les factures
# arriben a rafegues despres dels tancaments de venda, i apagar entre rafegues
# costaria mes en arrencades en fred que mantenir un pod.
cooldownPeriod: 900
# ESCALAT A ZERO. Ningu espera una factura en temps real: l'objectiu
# son 30 minuts, i l'arrencada en fred son 35 segons. Sobra marge.
# Fora de temporada es generen poques factures al dia.
minReplicaCount: 0
# 20 workers = 5 nuclis, dins dels 8 lliures de la quota.
# 20 x 0,125 = 2,5 factures/s = 9.000 factures/hora. Molt per sobre
# de les 3.000/hora del pic del pont.
maxReplicaCount: 20
fallback:
failureThreshold: 3
# Si el magatzem de la cua no respon, 5 workers: suficient per al
# cabal habitual mentre s'investiga.
replicas: 5
advanced:
restoreToOriginalReplicaCount: true
horizontalPodAutoscalerConfig:
name: hpa-worker-factures
behavior:
scaleUp:
# Sense espera: si hi ha cua, hi ha feina pendent.
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 5
periodSeconds: 30
scaleDown:
# 10 minuts: les factures arriben a rafegues despres de cada tancament
# de venda, i no volem destruir workers entre rafega i rafega.
stabilizationWindowSeconds: 600
selectPolicy: Min
policies:
- type: Percent
value: 30
periodSeconds: 120
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: factures-pendents
mode: QueueLength
# 1 worker per cada 35 factures a la cua.
#
# Calcul: un worker processa 1 factura cada 8 s = 0,125 factures/s.
# L'entrada al pic del pont es 0,833 factures/s (12.000 en 4 h).
# Amb marge del 100 % per a rafegues: capacitat objectiu 1,67 factures/s.
# Workers necessaris: 1,67 / 0,125 = 14.
# Acumulacio de cua esperada al pic: ~500 missatges.
# threshold = 500 / 14 = 35.
#
# Objectiu de negoci: cap factura no espera mes de 30 minuts.
# Amb 14 workers, el temps de buidatge de 500 factures es ~5 minuts.
value: "35"
# Per sota de 3 factures, es considera inactiu. Permet
# l'escalat a zero sense que 1-2 missatges residuals ho impedeixin.
activationValue: "3"
authenticationRef:
name: rabbitmq-credencials
# Xarxa de seguretat: si el magatzem d'objectes s'alenteix i les factures
# triguen 30 s en lloc de 8, la cua creixera i el disparador principal
# ja ho detectara. Pero si el problema es de CPU (un PDF patologic),
# aquest disparador ho cobreix.
- type: cpu
metricType: Utilization
metadata:
value: "80"Solució 2
Què està passant: KEDA no pot llegir la cua i està fent servir el fallback.
Les tres peces d'evidència encaixen així:
Peça 1: FALLBACK: True al ScaledObject. Aquest és el senyal principal i el que ho revela tot. Significa que KEDA ha superat el failureThreshold de consultes fallides i està retornant un valor artificial.
Peça 2: l'HPA diu que tot està bé. I té raó des del seu punt de vista: està rebent un valor de l'API externa (6), el compara amb el seu objectiu (2500), calcula sostre(1 × 6/2500) = 1 rèplica, i conclou que la mida actual coincideix amb la recomanada. L'HPA no sap que aquest 6 és un valor inventat.
Això és important i és la trampa de l'exercici: ScalingActive: True no significa que la mètrica sigui correcta, només que es va poder obtenir un número.
Peça 3: els logs revelen la causa arrel.
Get "https://rabbitmq.rutas-norte-pro.svc.cluster.local:15671/api/queues/..."
dial tcp 10.96.42.17:15671: i/o timeoutDues observacions sobre aquesta línia:
- Port 15671 i protocol HTTPS. El port de gestió de RabbitMQ és 15672 en HTTP i 15671 en HTTPS. KEDA està intentant HTTPS.
i/o timeout, noconnection refused. La diferència és diagnòstica:connection refusedsignifica que va arribar i alguna cosa va dir que no;i/o timeoutsignifica que el paquet es va perdre sense resposta, que és la signatura característica d'un tallafocs o una NetworkPolicy.
Per què el valor de la mètrica és 6:
El fallback està configurat amb replicas: 6. Quan KEDA entra en mode fallback, no retorna el nombre de rèpliques directament: retorna un valor de mètrica calculat perquè l'HPA arribi a aquest nombre de rèpliques.
KEDA vol que l'HPA calculi 6 repliques.
L'HPA calcula: sostre(repliquesActuals x valor / threshold)
Pero com que l'HPA fa servir AverageValue, la mecanica del fallback de KEDA es
reportar un valor tal que el resultat siguin les repliques del fallback.
A la sortida veiem "6 / 2500", que es el valor de fallback reportat.
L'HPA amb 1 replica calcula sostre(1 x 6/2500) = sostre(0,0024) = 1.I aquí hi ha la segona troballa de l'exercici: el fallback no està funcionant com s'esperava. L'HPA s'ha quedat en 1 rèplica, no en 6.
La raó és una limitació documentada de KEDA: el fallback no funciona correctament quan minReplicaCount és 0 i la càrrega estava escalada a zero o a prop. Amb minReplicaCount: 0 i el mecanisme d'activació implicat, el comportament del fallback no és l'esperat.
Què cal corregir:
Correcció 1 (immediata): restablir la connectivitat.
# 1. Existeix el Service i respon?
kubectl get svc rabbitmq -n rutas-norte-pro
kubectl get endpoints rabbitmq -n rutas-norte-pro
# 2. Hi ha una NetworkPolicy nova?
kubectl get networkpolicy -n rutas-norte-pro
kubectl describe networkpolicy -n rutas-norte-pro
# 3. Provar la connectivitat DES DEL namespace de KEDA
kubectl run prova --rm -it --restart=Never -n keda --image=busybox:1.36 -- \
wget -qO- --timeout=5 http://rabbitmq.rutas-norte-pro.svc.cluster.local:15672/api/overviewLa causa més probable, donat l'i/o timeout: una NetworkPolicy de denegació per defecte aplicada a rutas-norte-pro (mòdul 4 i 08-04) que no contempla el trànsit procedent del namespace keda.
# k8s/entorns/pro/networkpolicy-keda-rabbitmq.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permetre-keda-a-rabbitmq
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
app: rabbitmq
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: keda
ports:
- protocol: TCP
port: 15672 # API de gestio HTTP
- protocol: TCP
port: 5672 # AMQPCorrecció 2: revisar el port i el protocol del disparador.
# Si RabbitMQ no te TLS configurat al port de gestio, el
# TriggerAuthentication ha d'apuntar a http://...:15672, no https://...:15671
stringData:
host: "amqp://keda_lector:[email protected]:5672/"Correcció 3: fer robust el fallback.
Com que minReplicaCount: 0 compromet el fallback, l'opció segura per a una càrrega crítica en temporada alta és no escalar a zero:
spec:
# Durant la temporada alta, no escalem a zero. El fallback funciona
# de forma fiable i l'arrencada en fred no penalitza en el pitjor moment.
minReplicaCount: 2
idleReplicaCount: 1 # Fora d'activitat, 1 replica encesa
fallback:
failureThreshold: 3
replicas: 8Com evitar que torni a passar sense que ningú se n'assabenti:
Aquest és el punt més important de l'exercici. El sistema va fallar de forma silenciosa durant vint minuts. Tot semblava correcte a les comprovacions habituals.
Alertes necessàries a Alertmanager (07-04):
# 1. LA MES IMPORTANT: KEDA en mode fallback.
# Si aixo es dispara, l'escalat NO esta funcionant encara que tot sembli normal.
keda_scaled_object_errors_total > 0
# 2. Errors de l'escalador: KEDA no pot consultar la font.
sum(rate(keda_scaler_errors_total[5m])) by (scaledObject, scaler) > 0
# 3. Alerta de negoci: la cua creix sense que creixin els workers.
# Aquesta es la que hauria detectat el problema en 2 minuts.
(
rabbitmq_queue_messages{queue="notificacions-confirmacio"} > 5000
)
and
(
kube_deployment_status_replicas{deployment="worker-notificacions"} < 5
)
# 4. Antiguitat del missatge mes vell de la cua: l'indicador de negoci real.
rabbitmq_queue_head_message_timestamp_seconds > 600L'alerta 3 és especialment valuosa perquè no depèn que KEDA funcioni: mira el resultat (cua gran, pocs workers) en lloc del mecanisme. És el principi d'alertar sobre símptomes i no sobre causes del mòdul 7.
I una comprovació estructural: incloure KEDA a les proves de les NetworkPolicies. Cada vegada que s'afegeixi una política de denegació per defecte en un namespace, verificar explícitament que KEDA continua podent consultar les seves fonts. És fàcil d'oblidar perquè KEDA no és «trànsit de l'aplicació».
Solució 3
Anàlisi: són dues càrregues diferents ficades en un mateix servei.
La primera observació, i la més important de l'exercici, és d'arquitectura, no d'escalat:
| Càrrega | Perfil | Necessitat |
|---|---|---|
| API HTTP síncrona | 50-2.500 rps, 40 ms/petició, latència crítica | Moltes rèpliques petites, escalat ràpid |
| Recàlcul nocturn | 45 minuts, 4 nuclis, un cop al dia | Un pod gran, sense usuaris |
Ficar-les al mateix Deployment és un error de disseny, i l'enunciat ja ho assenyala: el procés nocturn «degrada les respostes HTTP durant aquests 45 minuts». Cap configuració de KEDA no arregla això.
Decisió 1: separar les dues càrregues.
El procés nocturn passa a ser un CronJob (06-03) amb els seus propis recursos, aïllat del servei HTTP. Això:
- Elimina la degradació de 45 minuts cada nit.
- Permet dimensionar cada càrrega per separat.
- Permet que el VPA en mode
Initial(09-02) ajusti el CronJob amb el temps. - Fa que un error del recàlcul no afecti el servei HTTP.
Decisió 2: escalat a zero per a l'API? NO.
motor-preus és al camí crític de cada cerca de rutes. La seva latència entra directament a la d'api-reserves. Una arrencada en fred de 35 segons amb un usuari esperant és inacceptable.
A més, hi ha un efecte de segon ordre important: si motor-preus és a zero i arriba una petició, api-reserves es queda esperant. Amb moltes peticions simultànies, api-reserves esgota el seu pool de connexions sortints i es degrada en cascada. L'escalat a zero d'un servei intern síncron pot tombar qui el crida.
Decisió 3: la mètrica. Peticions per segon, no CPU.
Encara que la feina de motor-preus és càlcul pur (on la CPU sí que correlaciona bé), les peticions per segon continuen sent millor senyal perquè:
- És la magnitud que coneixem: 180 rps per rèplica, mesurat.
- S'anticipa: es compten en arribar, abans de consumir CPU.
- No depèn del
requests, així que el VPA pot treballar lliurement.
Però afegim CPU com a xarxa de seguretat, perquè en un servei de càlcul pur una consulta patològica (una ruta amb moltes combinacions) podria consumir molta CPU amb poques peticions.
I afegim un tercer disparador que gairebé ningú considera: la latència. És la mètrica que realment li importa al negoci.
Els manifests:
# k8s/entorns/pro/scaledobject-motor-preus.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: motor-preus
namespace: rutas-norte-pro
labels:
app: motor-preus
app.kubernetes.io/part-of: rutas-norte
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: motor-preus
pollingInterval: 15
cooldownPeriod: 300
# NO escalem a zero. Es al cami critic de cada cerca de rutes:
# la seva latencia entra directament a la d'api-reserves. Una arrencada en fred
# de 35 s amb usuaris esperant es inacceptable, i a mes esgotaria el pool
# de connexions sortints d'api-reserves: degradacio en cascada.
#
# 3 repliques de terra: 3 x 180 = 540 rps, molt per sobre del minim de 50.
# El terra el marca la DISPONIBILITAT (09-05), no la capacitat.
minReplicaCount: 3
# Sostre: 2.500 rps / 180 rps per replica = 13,9 -> 14.
# Amb marge del 40 %: 20 repliques.
maxReplicaCount: 20
fallback:
failureThreshold: 3
# Si Prometheus cau, 8 repliques = 1.440 rps de capacitat. Cobreix el
# transit habitual amb folganca mentre s'arregla.
replicas: 8
advanced:
restoreToOriginalReplicaCount: true
horizontalPodAutoscalerConfig:
name: hpa-motor-preus
behavior:
scaleUp:
# Sense espera. Es un servei intern amb latencia critica: qualsevol
# retard es propaga a la cerca de rutes de l'usuari final.
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 5
periodSeconds: 30
scaleDown:
# 10 minuts. El transit de cerques es molt irregular (rafegues de
# usuaris comparant rutes) i no volem destruir capacitat entre
# rafega i rafega.
stabilizationWindowSeconds: 600
selectPolicy: Min
policies:
- type: Percent
value: 20
periodSeconds: 120
triggers:
# DISPARADOR PRINCIPAL: peticions per segon.
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitoratge.svc.cluster.local:9090
metricName: motor_preus_peticions_per_segon
query: |
sum(
rate(
motor_preus_peticions_total{
namespace="rutas-norte-pro",
app="motor-preus"
}[2m]
)
)
# 125 rps per replica.
#
# Calcul: una replica sosté 180 rps abans que la latencia p95
# superi els 80 ms. Apliquem un marge del 30 % per cobrir el temps
# d'arrencada de les repliques noves: 180 x 0,7 = 126 -> 125.
#
# Amb 2.500 rps: sostre(2500/125) = 20 repliques. Just el maxReplicaCount.
threshold: "125"
activationThreshold: "10"
ignoreNullValues: "true"
authenticationRef:
kind: ClusterTriggerAuthentication
name: prometheus-lectura
# DISPARADOR SECUNDARI: la LATENCIA, que es el que importa al negoci.
#
# Si el p95 supera els 60 ms (75 % de l'objectiu de 80 ms), escalem
# encara que les peticions per segon estiguin dins del normal. Aixo cobreix
# el cas de peticions anormalment CARES: rutes amb moltes combinacions,
# o una degradacio d'una dependencia.
#
# NOTA IMPORTANT: escalar per latencia es una arma de doble tall. Si la
# latencia puja per una causa que NO es resol amb mes repliques (per
# exemple, PostgreSQL saturat), escalar empitjora les coses (09-01, el
# coll d'ampolla real). Per aixo el llindar es conservador i el
# maxReplicaCount esta acotat.
- type: prometheus
metadata:
serverAddress: http://prometheus-operated.monitoratge.svc.cluster.local:9090
metricName: motor_preus_latencia_p95
query: |
histogram_quantile(0.95,
sum(rate(motor_preus_duracio_segons_bucket{
namespace="rutas-norte-pro"
}[2m])) by (le)
) * 1000
threshold: "60"
activationThreshold: "1"
ignoreNullValues: "true"
authenticationRef:
kind: ClusterTriggerAuthentication
name: prometheus-lectura
# XARXA DE SEGURETAT: CPU.
- type: cpu
metricType: Utilization
metadata:
value: "70"I el procés nocturn, separat:
# k8s/entorns/pro/cronjob-recalcul-preus.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: recalcul-preus
namespace: rutas-norte-pro
labels:
app: recalcul-preus
app.kubernetes.io/part-of: rutas-norte
spec:
schedule: "0 3 * * *"
timeZone: "Europe/Madrid" # CronJob suporta timeZone des de 1.27
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 2
# Si a les 2 hores no ha acabat, tallar. Evita que un proces penjat
# ancori un node (09-03) i se solapi amb el transit del mati.
activeDeadlineSeconds: 7200
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app: recalcul-preus
annotations:
# NO desallotjar durant l'execucio: perdriem 45 minuts de
# feina. El Cluster Autoscaler respectara aquesta anotacio (09-03).
cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
spec:
restartPolicy: OnFailure
containers:
- name: recalcul
image: registry.rutasnorte.example/motor-preus:4.2.0
command: ["node", "recalcular-matriu.js"]
resources:
requests:
cpu: "4"
memory: 4Gi
limits:
cpu: "4"
memory: 4Gi
# requests == limits -> QoS Guaranteed. S'executa a les 3:00
# amb el cluster buit: no li traiem capacitat a ningu i
# garantim que no el desallotgin per pressio de memoria.I el seu VPA, aplicant el de 09-02:
# k8s/entorns/pro/vpa-recalcul-preus.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: recalcul-preus
namespace: rutas-norte-pro
spec:
targetRef:
apiVersion: batch/v1
kind: CronJob
name: recalcul-preus
updatePolicy:
# Initial: cada execucio nova neix amb els recursos apresos. Mai
# desallotgem un Job a mig cami.
updateMode: "Initial"
resourcePolicy:
containerPolicies:
- containerName: recalcul
minAllowed:
cpu: "2"
memory: 2Gi
maxAllowed:
cpu: "8"
memory: 8Gi
controlledValues: RequestsAndLimitsResum de decisions i la seva justificació:
| Decisió | Elecció | Justificació |
|---|---|---|
Un o diversos ScaledObject? |
Un, més un CronJob separat | Són dues càrregues amb perfils oposats; barrejar-les degrada l'API |
| Escalat a zero? | No (minReplicaCount: 3) |
Camí crític síncron; l'arrencada en fred degradaria api-reserves en cascada |
| Mètrica principal | Peticions per segon | Magnitud mesurada i coneguda (180 rps/rèplica); no depèn del requests |
threshold |
125 rps | 180 mesurat × 0,7 de marge per a l'arrencada |
| Mètrica secundària | Latència p95 | És l'objectiu de negoci real; detecta peticions anormalment cares |
| Xarxa de seguretat | CPU al 70 % | Cobreix casos imprevistos; amb «guanya el que demana més», mai molesta |
maxReplicaCount |
20 | 2.500 rps / 125 = 20; acotat per no emmascarar problemes aliens |
fallback |
8 rèpliques | 1.440 rps de capacitat: cobreix el trànsit habitual mentre s'arregla Prometheus |
| Procés nocturn | CronJob amb QoS Guaranteed |
Aïllament total; recursos dedicats; s'executa amb el clúster buit |
| VPA del CronJob | Mode Initial |
Aprèn d'execucions anteriors sense desallotjar a mitges |
Advertència final que mereix constar: escalar per latència és útil però perillós. Si la latència puja perquè una dependència està saturada (PostgreSQL, per exemple), afegir rèpliques de motor-preus empitjora la situació: més rèpliques, més connexions, més pressió sobre la dependència. És exactament l'error de 09-01 d'escalar la capa equivocada. Per això el llindar és conservador (60 ms de 80) i el sostre està acotat en 20. I per això cal una alerta que distingeixi les dues situacions:
# Latencia alta AMB les repliques al maxim: el problema NO es resol escalant
(
histogram_quantile(0.95, sum(rate(motor_preus_duracio_segons_bucket[5m])) by (le)) * 1000 > 80
)
and
(
kube_deployment_status_replicas{deployment="motor-preus"} >= 20
)Conclusió
L'escalat per esdeveniments converteix un senyal indirecte i tardà —el consum de CPU— en el senyal que de debò descriu la feina pendent: la longitud d'una cua, les peticions per segon, o simplement l'hora del dia.
L'essencial:
- La CPU és bon senyal quan la feina del pod és calcular, i dolent quan és coordinar, esperar o mantenir connexions.
worker-notificacionspassa el 95 % del temps esperant el servidor SMTP: un HPA per CPU hauria reduït rèpliques amb quaranta mil correus a la cua. - L'HPA ja sap consumir mètriques personalitzades i externes (
Pods,Object,External). El que falta és qui serveixi aquestes APIs, i aquí entra un adaptador registrat a la capa d'agregació. - KEDA no substitueix l'HPA: en crea un i l'alimenta. Tot el de 09-01 continua vigent, inclòs el
behavior, que es configura des d'advanced.horizontalPodAutoscalerConfig. L'únic tram que governa KEDA directament és el 0↔1. - El
ScaledObjectdefineix el destí, l'interval de consulta, el rang de rèpliques, elfallbacki els disparadors. Amb diversos disparadors, guanya el que demana més rèpliques: combina el senyal principal, una xarxa de seguretat de CPU i un cron de preescalfament. TriggerAuthenticationmanté les credencials fora del manifest i permet fer servir identitats de núvol, coherent amb el mínim privilegi de 08-01.ScaledJobcrea un Job per unitat de feina. Fes-lo servir quan la tasca duri molt més que l'arrencada d'un pod; en cas contrari,ScaledObject.- El disparador cron és el que trenca la limitació de l'escalat reactiu. Per a un esdeveniment predictible com l'obertura de la venda, esperar que la mètrica pugi és absurd: el
rate[2m]a més suavitza precisament el pic que volem detectar. Escalar a les 9:30 elimina la degradació de les 10:00 per complet. - L'escalat a zero és potentíssim per a treballs per lots i entorns de desenvolupament, i inacceptable on hi ha un ésser humà esperant.
idleReplicaCountés el punt mitjà que serveix per a gairebé tota la resta. - La depuració segueix una cadena:
ScaledObject→ HPA generat → API d'agregació → logs de l'operador → connectivitat de xarxa → la consulta a la font. I les NetworkPolicies que bloquegen KEDA són l'error número u després d'implantar les polítiques del mòdul 8.
Rutas Norte té ara worker-notificacions escalant per la longitud real de la seva cua amb apagada completa fora de temporada, api-reserves escalant per peticions per segon amb les mètriques que vam instrumentar a 07-03, i un disparador cron que arrenca i escalfa 25 rèpliques mitja hora abans que s'obri la venda del pont de maig, amb el coixí de nodes ampliat una hora abans que això.
La plataforma puja quan cal i abans que calgui. Però tot el mòdul s'ha ocupat fins ara de créixer, i hi ha una pregunta que no hem fet: què passa quan alguna cosa es trenca o quan algú ha de tocar el clúster?
El proper dimarts toca actualitzar la versió de Kubernetes dels nodes. Algú executarà kubectl drain sobre un node per buidar-lo. Si en aquest node hi ha tres de les quatre rèpliques d'api-reserves —perquè el planificador les hi va posar i ningú li va dir que no ho fes—, aquest drenatge s'endurà per davant el 75 % de la capacitat de l'API en un instant. I si el node que cau no el drena ningú, sinó que s'apaga sol per una fallada de maquinari a la zona de disponibilitat B a les tres de la matinada, el resultat serà el mateix però sense previ avís.
A la propera lliçó, Alta Disponibilitat: PodDisruptionBudgets i Topologia, veurem la distinció entre les interrupcions que pots controlar i les que no, com el PodDisruptionBudget protegeix de les primeres (i com un PDB mal posat bloqueja el manteniment per sempre, com ja vam intuir a 09-03), com repartir les rèpliques entre nodes i zones amb topologySpreadConstraints, i el procediment complet de manteniment d'un node de principi a fi.
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
