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

  1. Per què la CPU és un mal senyal
  2. Mètriques personalitzades i externes a l'HPA
  3. L'API d'agregació i els adaptadors de mètriques
  4. Què és KEDA i quina és la seva arquitectura
  5. La idea clau: KEDA no substitueix l'HPA, l'alimenta
  6. L'objecte ScaledObject camp a camp
  7. El catàleg d'escaladors
  8. TriggerAuthentication: credencials per als disparadors
  9. ScaledJob: un treball per missatge
  10. Rutas Norte: worker-notificacions per longitud de cua
  11. Rutas Norte: api-reserves per peticions per segon
  12. Rutas Norte: el disparador cron del pont de maig
  13. L'escalat a zero i les seves implicacions
  14. Depurar un ScaledObject que no escala
  15. Errors comuns i consells
  16. Exercicis
  17. Conclusió

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

ratio = 60 / 210 = 0,286
repliquesDesitjades = sostre(2 x 0,286) = 1

L'HPA vol BAIXAR a 1 replica.

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.

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

Aquest é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.

- type: Pods
  pods:
    metric:
      name: peticions_per_segon
    target:
      type: AverageValue
      averageValue: "85"

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.

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

kubectl get apiservices | grep metrics
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        12d

Quan 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.tool

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

  1. 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 keda
NAME                                              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          2m

I els CRDs que registra:

kubectl get crd | grep keda
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:14Z

Els 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 triggers del ScaledObject en mètriques External de 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 ScaledObject i 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

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

kubectl get hpa -n rutas-norte-pro
NAME                                 REFERENCE                         TARGETS        MINPODS  MAXPODS  REPLICAS
keda-hpa-worker-notificacions        Deployment/worker-notificacions   1847/2500 (avg)  1        25       8

Aquí està: un HPA amb el prefix keda-hpa-, gestionat per l'operador de KEDA. I el seu contingut:

kubectl get hpa keda-hpa-worker-notificacions -n rutas-norte-pro -o yaml
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.

  1. L'objecte ScaledObject camp a camp

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

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

Significa: 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-les

Aquest 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: 60

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

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

Cron:

- 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-credencials

activationValue: 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 inactiu

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

  1. TriggerAuthentication: credencials per als disparadors

Els 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 Secret

I al ScaledObject:

triggers:
  - type: rabbitmq
    metadata:
      queueName: notificacions-confirmacio
      mode: QueueLength
      value: "2500"
    authenticationRef:
      name: rabbitmq-credencials

Altres 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-sqs

Connecta 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: token

Es referencia amb kind: ClusterTriggerAuthentication a l'authenticationRef. Útil per a Prometheus, que és únic i el consulten tots els namespaces.

  1. ScaledJob: un treball per missatge

ScaledObject 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-credencials

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

  1. Rutas Norte: worker-notificacions per longitud de cua

Anem 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-pro
NAME                   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     34s

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

kubectl get deployment worker-notificacions -n rutas-norte-pro
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
worker-notificacions   0/0     0            0           89d

La reactivació en viu

Obrim dos terminals. Al primer, observem:

kubectl get deployment worker-notificacions -n rutas-norte-pro --watch

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=30000

I 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 = 12

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

kubectl get hpa hpa-worker-notificacions -n rutas-norte-pro
NAME                       REFERENCE                         TARGETS            MINPODS  MAXPODS  REPLICAS
hpa-worker-notificacions   Deployment/worker-notificacions   2483/2500 (avg)    1        25       12

2483/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

  1. Rutas Norte: api-reserves per peticions per segon

Ara 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è:

  1. És la magnitud del negoci. «Cada rèplica atén 85 peticions per segon» és una frase que entén qualsevol a l'empresa.
  2. No depèn del requests. Un canvi del requests.cpu (recomanat pel VPA de 09-02) no altera el comportament de l'escalat.
  3. És més ràpida. La CPU puja quan la feina ja està entrant; les peticions es compten en arribar.
  4. 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 92841

El 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

sum(
  rate(
    api_reserves_peticions_total{
      namespace="rutas-norte-pro",
      app="api-reserves"
    }[2m]
  )
)

Esmicolada de dins cap enfora:

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

  2. 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 un scrape_interval de 30 s només hi hauria 2 mostres, cosa que fa el rate poc fiable). Amb [5m] el senyal és suau però arriba tard. Regla: la finestra del rate ha de ser almenys 4 vegades l'interval de recol·lecció. Amb recol·lecció cada 30 s, [2m] és el mínim raonable.

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

kubectl get hpa hpa-api-reserves -n rutas-norte-pro
NAME               REFERENCE                 TARGETS                   MINPODS  MAXPODS  REPLICAS
hpa-api-reserves   Deployment/api-reserves   1247/85 (avg), 34%/70%    4        30       15

Hi 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-pro

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

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

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

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

  1. Depurar un ScaledObject que no escala

Procediment sistemàtic, del més general al més específic.

Pas 1: l'estat del ScaledObject

kubectl get scaledobject -n rutas-norte-pro
NAME                   SCALETARGETKIND      SCALETARGETNAME        MIN  MAX  READY  ACTIVE  FALLBACK  AGE
worker-notificacions   apps/v1.Deployment   worker-notificacions   0    25   False  False   False     4m

READY: False és el primer problema. Significa que KEDA no l'ha pogut configurar.

kubectl describe scaledobject worker-notificacions -n rutas-norte-pro
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 0

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

kubectl describe hpa keda-hpa-worker-notificacions -n rutas-norte-pro
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 Found

Object 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

kubectl logs -n keda deployment/keda-operator --tail=100 | grep -i "worker-notificacions"
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:

kubectl logs -n keda deployment/keda-operator-metrics-apiserver --tail=50

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/overview

Si 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: 15672

Aquest é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:

kubectl port-forward -n monitoratge svc/prometheus-operated 9090:9090

I a la interfície web, executa la consulta exacta del ScaledObject. Comprova:

  1. Retorna alguna dada? Si està buida, les etiquetes del selector no coincideixen.
  2. Retorna UN SOL valor? Si retorna diverses sèries, falta un sum().
  3. El valor té sentit? Un rate que 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 > 0

El 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: 250m i requests.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:

kubectl get scaledobject -n rutas-norte-pro
NAME                   SCALETARGETKIND      MIN  MAX  READY  ACTIVE  FALLBACK  AGE
worker-notificacions   apps/v1.Deployment   0    25   True   True    True      3h
kubectl describe hpa keda-hpa-worker-notificacions -n rutas-norte-pro
Conditions:
  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>
kubectl logs -n keda deployment/keda-operator --tail=20 | grep worker
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-reserves cada 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_total i motor_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 workers

Comprovació 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 timeout

Dues observacions sobre aquesta línia:

  1. Port 15671 i protocol HTTPS. El port de gestió de RabbitMQ és 15672 en HTTP i 15671 en HTTPS. KEDA està intentant HTTPS.
  2. i/o timeout, no connection refused. La diferència és diagnòstica: connection refused significa que va arribar i alguna cosa va dir que no; i/o timeout significa 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/overview

La 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       # AMQP

Correcció 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: 8

Com 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 > 600

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

motor-preus            -> Deployment + ScaledObject (API HTTP)
recalcul-preus         -> CronJob (proces nocturn)

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

Resum 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-notificacions passa 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 ScaledObject defineix el destí, l'interval de consulta, el rang de rèpliques, el fallback i 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.
  • TriggerAuthentication manté les credencials fora del manifest i permet fer servir identitats de núvol, coherent amb el mínim privilegi de 08-01.
  • ScaledJob crea 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

Mòdul 2: Components Principals de Kubernetes

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

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

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

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

© Copyright 2026. Tots els drets reservats