Tanquem el mòdul 8 amb una plataforma segura i auditable, però amb una confessió incòmoda: els components de Rutas Norte continuen tenint un nombre de rèpliques fix, escrit a mà. api-reservesreplicas: 4 i botiga-webreplicas: 3 perquè algú, fa mesos, va mesurar un dimarts qualsevol i li va semblar suficient. Aquest número no sap res del pont de maig. L'any passat la plataforma va caure als onze minuts d'obrir la venda, i la resposta de l'equip va ser un kubectl scale a corre-cuita des d'un portàtil, a l'andana d'una estació, amb el tren sortint.

Aquesta lliçó elimina aquesta cursa. L'HorizontalPodAutoscaler (HPA) és l'objecte de Kubernetes que ajusta automàticament el nombre de rèpliques d'un Deployment en funció de mètriques observades. No és màgia: és un controlador més, amb el seu bucle de reconciliació —el mateix que vam estudiar al mòdul 1—, una fórmula aritmètica molt concreta i un grapat de paràmetres que cal entendre bé perquè el resultat sigui estabilitat i no un pèndol de pods arrencant i morint.

Veurem quines càrregues es poden escalar horitzontalment i quines no, l'API autoscaling/v2 completa, la fórmula exacta amb què es calculen les rèpliques desitjades, el bloc behavior que governa la velocitat de pujada i baixada, i els dos manifests definitius d'api-reserves i botiga-web. Acabarem amb un conflicte que mossega gairebé tots els equips la primera vegada: l'HPA i el camp replicas del YAML versionat barallant-se pel mateix número.

Contingut

  1. Escalar cap enfora davant d'escalar cap amunt
  2. Quines càrregues admeten escalat horitzontal i quines no
  3. Anatomia de l'HorizontalPodAutoscaler a autoscaling/v2
  4. Els quatre tipus de mètrica: Resource, Pods, Object i External
  5. La fórmula exacta del controlador, pas a pas
  6. La banda de tolerància i el ball de rèpliques
  7. Requisits ineludibles: requests i metrics-server
  8. Diagnòstic del temut <unknown>
  9. El bloc behavior: polítiques, períodes i finestra d'estabilització
  10. Diverses mètriques alhora: guanya la que demana més
  11. Els HPA de Rutas Norte, manifests complets
  12. Simulació d'una punta de càrrega i observació en viu
  13. El conflicte entre l'HPA i el camp replicas
  14. Què no escalar amb HPA i el coll d'ampolla real
  15. Errors comuns i consells
  16. Exercicis
  17. Conclusió

  1. Escalar cap enfora davant d'escalar cap amunt

Hi ha exactament dues maneres de donar més capacitat a un servei, i convé tenir els noms clars perquè la resta del mòdul s'hi recolza.

Escalar cap amunt (scale up, escalat vertical) és donar més recursos a cada instància: pujar el requests/limits de CPU de 200m a 800m, la memòria de 256Mi a 1Gi. El nombre de pods no canvia; cada pod és més gran. A Kubernetes això implica recrear el pod, perquè els recursos d'un contenidor formen part de la seva especificació immutable (amb el matís del redimensionament en calent que veurem a 09-02).

Escalar cap enfora (scale out, escalat horitzontal) és posar més instàncies de la mateixa mida: passar de 4 pods d'api-reserves a 12 pods idèntics. Cada pod continua sent igual de gran; n'hi ha més. A Kubernetes això és canviar un nombre enter: el replicas del Deployment.

Aspecte Escalat vertical (cap amunt) Escalat horitzontal (cap enfora)
Què canvia Mida de cada pod (requests/limits) Nombre de pods
Sostre El node més gran disponible Pràcticament il·limitat (amb nodes)
Requereix reiniciar el pod Sí (excepte redimensionament en calent) No, els pods existents no es toquen
Requisit de l'aplicació Que sàpiga aprofitar més CPU/RAM Que no tingui estat local ni afinitat de sessió
Tolerància a errors No millora: continua havent-hi un punt únic Millora: la càrrega es reparteix entre més instàncies
Cost de granularitat Salta a esglaons grans Fi: un pod cada vegada
Objecte de Kubernetes VPA (09-02) o edició manual HPA (aquesta lliçó), KEDA (09-04)
Velocitat de reacció Lenta (recreació del pod) Ràpida (segons, si la imatge està en memòria cau)

La regla pràctica: l'escalat vertical resol el problema d'una instància mal dimensionada; l'escalat horitzontal resol el problema del volum de trànsit. No són alternatives, són eixos diferents. Rutas Norte necessita tots dos: encertar la mida de cada pod (09-02) i multiplicar el nombre de pods quan arriba el pont de maig (aquesta lliçó).

Un detall important que molts passen per alt: l'escalat horitzontal també millora la disponibilitat, mentre que el vertical no. Dotze pods d'api-reserves repartits entre nodes sobreviuen a la caiguda d'un node; un únic pod gegantí, no. Aquesta idea la reprendrem a fons a 09-05.

  1. Quines càrregues admeten escalat horitzontal i quines no

L'escalat horitzontal només funciona si qualsevol rèplica pot atendre qualsevol petició. Això s'anomena ser sense estat (stateless), i és una propietat de l'aplicació, no de Kubernetes. Kubernetes et deixarà escalar qualsevol cosa; que el resultat sigui correcte depèn de com estigui escrita l'aplicació.

Repassem els components de Rutas Norte:

Component Escalable horitzontalment? Motiu
botiga-web (nginx) , sense reserves Serveix actius estàtics i fa de proxy. No guarda res entre peticions.
api-reserves (Node.js) API REST sense estat: la sessió va en un token signat, l'estat a PostgreSQL i redis-cache.
worker-notificacions , però la CPU no és el senyal correcte Consumeix d'una cua; afegir consumidors accelera el buidatge. La mètrica útil és la longitud de la cua (09-04).
redis-cache No amb HPA És una memòria cau amb estat particionat. Afegir rèpliques no reparteix la càrrega sense sharding explícit.
postgres-reserves Rotundament no Base de dades primària. Afegir pods no crea més bases de dades: crea rèpliques que es barallen pel mateix volum o queden òrfenes.
informes-ocupacio (CronJob) No aplica El seu paral·lelisme es controla amb parallelism al Job (06-03), no amb HPA.

El cas de postgres-reserves mereix un paràgraf. És un StatefulSet amb un volum persistent per rèplica. Si hi posessis un HPA i l'HPA decidís passar d'1 a 5 rèpliques, Kubernetes crearia cinc pods postgres-reserves-0 a postgres-reserves-4, cadascun amb el seu propi PVC buit, cadascun arrencant una base de dades independent i buida. El Service repartiria les peticions entre les cinc. El resultat seria una corrupció silenciosa de dades: unes reserves anirien a una base de dades i altres a una altra. Escalar una base de dades relacional és un problema d'arquitectura de dades (rèpliques de lectura, particionat, un operador que ho sàpiga fer — 06-07), no un problema de comptar pods.

Regla que convé memoritzar: si en afegir una rèplica el sistema no respon més peticions correctes per segon, l'HPA no és la teva eina.

  1. Anatomia de l'HorizontalPodAutoscaler a autoscaling/v2

L'HPA és un objecte de l'API com qualsevol altre. La seva versió estable i actual és autoscaling/v2; les versions v2beta1 i v2beta2 estan eliminades des de fa diverses versions i autoscaling/v1 només permet CPU (el servidor la continua servint per compatibilitat, però no la facis servir: perds behavior i totes les mètriques que no siguin CPU).

Aquest és l'esquelet complet, amb comentaris sobre cada camp:

apiVersion: autoscaling/v2          # Versio estable. MAI autoscaling/v1 en material nou.
kind: HorizontalPodAutoscaler
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  # A QUIN objecte li canvia el nombre de repliques.
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment              # Tambe val StatefulSet o qualsevol recurs amb subrecurs /scale
    name: api-reserves            # Ha d'existir al MATEIX namespace que l'HPA

  minReplicas: 4                  # Terra. L'HPA mai baixara d'aqui.
  maxReplicas: 30                 # Sostre. L'HPA mai pujara d'aqui, passi el que passi.

  # QUE mira per decidir. Es una LLISTA: n'hi pot haver diverses.
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 65  # Objectiu: 65 % del requests de CPU, de mitjana entre pods

  # COM de rapid puja i baixa. Opcional pero gairebe sempre necessari.
  behavior:
    scaleUp: {}
    scaleDown: {}

Tres punts de partida que convé fixar abans de continuar:

  • scaleTargetRef apunta a un objecte que exposa el subrecurs /scale. Deployments, ReplicaSets i StatefulSets l'exposen. Un DaemonSet no, perquè el seu nombre de pods el determina el nombre de nodes (06-02), i no tindria sentit.
  • minReplicas i maxReplicas són barreres dures. El maxReplicas és la teva xarxa de seguretat econòmica i operativa: si un error de l'aplicació dispara la CPU al 100 % sense que hi hagi trànsit real, l'HPA intentarà escalar sense fi; el sostre ho impedeix. Posa'l sempre, i posa'l pensant en què passaria si s'assolís.
  • minReplicas pot ser 0 des de Kubernetes 1.30 si la feature gate HPAScaleToZero està activa, però això requereix una mètrica externa o d'objecte i no està habilitat per defecte a la majoria de clústers. L'escalat a zero de debò el farem amb KEDA a 09-04.

El bucle del controlador

L'HPA el gestiona l'horizontal-pod-autoscaler que viu dins del kube-controller-manager. El seu cicle per defecte és de 15 segons (--horizontal-pod-autoscaler-sync-period). A cada cicle:

flowchart TD
    A[Cada 15 s] --> B[Llegeix l'HPA i l'objecte desti]
    B --> C[Consulta les metriques<br/>metrics.k8s.io o APIs d'agregacio]
    C --> D{Metriques disponibles?}
    D -- No --> E[TARGETS = unknown<br/>no escala, emet esdeveniment]
    D -- Si --> F[Aplica la formula<br/>repliques desitjades]
    F --> G{Dins de la<br/>banda de tolerancia?}
    G -- Si --> H[No fa res]
    G -- No --> I[Aplica behavior:<br/>estabilitzacio i politiques]
    I --> J[Acota entre min i maxReplicas]
    J --> K[PATCH al subrecurs /scale]
    K --> L[El Deployment ajusta el seu ReplicaSet]

Fixa't en el final del flux: l'HPA no crea pods. Modifica el camp replicas del Deployment mitjançant el subrecurs /scale, i a partir d'aquí el controlador de Deployments i el de ReplicaSets fan la seva feina habitual (mòdul 2). És reconciliació encadenada, exactament el patró que vam veure a 01-02.

  1. Els quatre tipus de mètrica: Resource, Pods, Object i External

El bloc metrics accepta quatre tipus. Dos els faràs servir avui; els altres dos es defineixen aquí i s'exploten a 09-04.

Tipus D'on surten les dades Què mesura Àmbit Es fa servir a
Resource metrics-server (metrics.k8s.io) CPU o memòria dels pods del destí Per pod, promitjat 09-01
ContainerResource metrics-server CPU/memòria d'un contenidor concret del pod Per contenidor 09-01
Pods API de mètriques personalitzades (custom.metrics.k8s.io) Qualsevol mètrica emesa pels pods del destí Per pod, promitjat 09-04
Object API de mètriques personalitzades Una mètrica associada a un altre objecte de Kubernetes (un Ingress, un Service) Valor únic 09-04
External API de mètriques externes (external.metrics.k8s.io) Alguna cosa de fora del clúster: longitud d'una cua, missatges en un topic Valor únic o dividit entre pods 09-04

Resource amb Utilization

És la forma més comuna. Expressa l'objectiu com a percentatge del requests del contenidor:

metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 65

Això significa: «mantén el consum mitjà de CPU dels pods d'api-reserves a l'entorn del 65 % del que cadascun té reservat a requests.cpu». Si requests.cpu és 500m, l'objectiu són 325m per pod.

És fonamental entendre que Utilization es calcula sobre requests, no sobre limits ni sobre la CPU del node. Un pod pot mostrar una utilització del 180 % si consumeix 900m amb un requests de 500m; això és perfectament possible perquè el requests és una reserva mínima, no un topall (mòdul 3).

Resource amb AverageValue

Expressa l'objectiu en unitats absolutes, ignorant el requests:

metrics:
  - type: Resource
    resource:
      name: memory
      target:
        type: AverageValue
        averageValue: 700Mi

«Mantén el consum mitjà de memòria per pod en 700Mi.»

Quan cal fer servir cadascun?

Utilization AverageValue
S'expressa en Percentatge del requests Unitats (m de CPU, Mi de memòria)
Requereix requests declarat Sí, obligatori No
Es trenca si canvies el requests Sí: l'objectiu real es mou sol No
Llegibilitat per a l'equip Alta («al 65 % del seu») Mitjana
Recomanat per a CPU, el cas general Memòria, o quan el requests canvia sovint (VPA)

Un avís sobre la memòria com a mètrica d'HPA: gairebé mai és bona idea. Molts entorns d'execució (la JVM, el recol·lector d'escombraries de Node.js, PostgreSQL) no retornen la memòria al sistema encara que ja no la necessitin. El consum puja i es queda amunt. Un HPA per memòria escalaria cap enfora i mai tornaria a baixar, perquè la memòria dels pods vells mai baixa de l'objectiu. Fes servir CPU, o millor encara, una mètrica de negoci (09-04).

ContainerResource

Variant de Resource que mira un contenidor concret en lloc de sumar tots els del pod. És molt útil a Rutas Norte, on diversos pods porten sidecars:

metrics:
  - type: ContainerResource
    containerResource:
      name: cpu
      container: api                # Nomes el contenidor "api", no el sidecar de metriques
      target:
        type: Utilization
        averageUtilization: 65

Sense això, un sidecar que consumeix 50m constants distorsiona el percentatge del pod sencer, i en pods petits la distorsió és gran. Si el teu destí té sidecars, prefereix ContainerResource.

Pods, Object i External (definició)

Els definim ara perquè en reconeguis la sintaxi, però el seu ús real arriba a 09-04.

# Pods: una metrica que emeten els mateixos pods del desti, promitjada entre ells.
- type: Pods
  pods:
    metric:
      name: peticions_per_segon
    target:
      type: AverageValue
      averageValue: "80"          # 80 peticions per segon i pod

# Object: una metrica associada a UN ALTRE objecte del cluster.
- type: Object
  object:
    describedObject:
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      name: rutas-norte-public
    metric:
      name: peticions_per_segon
    target:
      type: Value
      value: "2000"               # Valor TOTAL de l'objecte, no per pod

# External: alguna cosa de fora del cluster.
- type: External
  external:
    metric:
      name: longitud_cua_correus
      selector:
        matchLabels:
          cua: notificacions-confirmacio
    target:
      type: AverageValue
      averageValue: "500"         # 500 missatges pendents per pod

Diferència clau entre Value i AverageValue a Object i External: amb Value l'HPA compara el valor brut amb l'objectiu; amb AverageValue divideix el valor entre el nombre de rèpliques abans de comparar. Per a una cua sempre vols AverageValue («cada worker es fa càrrec de 500 missatges»), perquè és el que fa que el nombre de rèpliques creixi amb la cua.

Els tipus Pods, Object i External no funcionen de fàbrica: necessiten un adaptador de mètriques registrat a la capa d'agregació de l'API. Sense ell, l'HPA reportarà <unknown> eternament. Això és exactament el que resol KEDA a 09-04.

  1. La fórmula exacta del controlador, pas a pas

Aquí hi ha el cor de la lliçó. Tot el comportament de l'HPA surt d'una única expressió:

repliquesDesitjades = sostre( repliquesActuals × ( valorActual / valorObjectiu ) )

On sostre() és l'arrodoniment cap amunt a l'enter següent. Apliquem-la a api-reserves amb números reals.

Situació de partida

api-reserves està desplegat amb aquests recursos (els vam fixar al mòdul 3):

resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: "1"
    memory: 1Gi

I el seu HPA té averageUtilization: 65, minReplicas: 4, maxReplicas: 30.

Objectiu absolut per pod: 65 % de 500m = 325m.

Cas 1: dimarts a la tarda, tot tranquil

Hi ha 4 rèpliques. kubectl top pods mostra:

NAME                            CPU(cores)   MEMORY(bytes)
api-reserves-7c9d4f8b6d-2mk8p   140m         310Mi
api-reserves-7c9d4f8b6d-5xqzn   155m         298Mi
api-reserves-7c9d4f8b6d-9jw4t   132m         305Mi
api-reserves-7c9d4f8b6d-hb7rc   149m         301Mi

Mitjana: (140 + 155 + 132 + 149) / 4 = 144m.

repliquesDesitjades = sostre( 4 × (144 / 325) )
                    = sostre( 4 × 0,443 )
                    = sostre( 1,772 )
                    = 2

La fórmula demana 2 rèpliques. Però minReplicas és 4, així que es queda en 4. Correcte: no volem baixar de 4 en producció per disponibilitat.

Cas 2: obertura de la venda del pont de maig

Continua havent-hi 4 rèpliques, però el trànsit es dispara:

NAME                            CPU(cores)   MEMORY(bytes)
api-reserves-7c9d4f8b6d-2mk8p   910m         680Mi
api-reserves-7c9d4f8b6d-5xqzn   940m         702Mi
api-reserves-7c9d4f8b6d-9jw4t   895m         671Mi
api-reserves-7c9d4f8b6d-hb7rc   925m         694Mi

Mitjana: (910 + 940 + 895 + 925) / 4 = 917,5m. Compte: estan fregant el seu limit de 1000m, és a dir, estan patint throttling (mòdul 3).

repliquesDesitjades = sostre( 4 × (917,5 / 325) )
                    = sostre( 4 × 2,823 )
                    = sostre( 11,29 )
                    = 12

L'HPA vol 12 rèpliques. Està per sota de maxReplicas: 30, així que s'aplica (subjecte al behavior, que veurem a l'apartat 9).

Observa l'elegància de la fórmula: multiplica el nombre actual pel factor d'excés. Si vas al triple de l'objectiu amb 4 pods, demana 12 pods. És una regla de tres que assumeix implícitament que la càrrega es reparteix per igual entre les rèpliques, cosa que és certa si el Service fa un balanceig raonable i les peticions són homogènies.

Cas 3: passa la punta

Ara hi ha 12 rèpliques i la mitjana baixa a 120m:

repliquesDesitjades = sostre( 12 × (120 / 325) )
                    = sostre( 12 × 0,369 )
                    = sostre( 4,43 )
                    = 5

Demana 5 rèpliques. Baixarà de 12 a 5, però no de cop: la finestra d'estabilització i les polítiques de scaleDown controlen el ritme (apartat 9).

Detalls fins de la fórmula que gairebé ningú coneix

Aquests matisos expliquen comportaments que d'altra manera semblen erràtics:

  1. Pods sense mètriques. Si un pod encara no té mètriques (acaba d'arrencar), l'HPA l'exclou de la mitjana però el compta de manera conservadora: en escalar cap amunt assumeix consum 0 per a aquest pod (per no sobreestimar), i en escalar cap avall assumeix consum igual a l'objectiu (per no infraestimar). El resultat és que l'HPA és prudent en tots dos sentits mentre hi ha pods arrencant.

  2. Pods no llestos. Els pods que no han passat la seva sonda readinessProbe s'exclouen del càlcul en escalar cap amunt. Això evita el clàssic bucle infernal: pods nous que encara no reben trànsit, amb CPU alta per l'arrencada, farien creure a l'HPA que cal escalar encara més.

  3. --horizontal-pod-autoscaler-initial-readiness-delay (30 s per defecte): durant els primers 30 segons de vida d'un pod, les seves mètriques s'ignoren completament. La justificació és clara: l'arrencada de Node.js consumeix molta CPU compilant i carregant mòduls, i no volem que aquest pic contamini la decisió.

  4. --horizontal-pod-autoscaler-cpu-initialization-period (5 min per defecte): un marge addicional per a mètriques de CPU de pods acabats d'estar llestos.

La conseqüència pràctica dels punts 3 i 4 és que l'HPA té una latència d'arrencada d'almenys mig minut per decisió, i per això el sobreaprovisionament de 09-03 i el preescalfament per cron de 09-04 tenen sentit.

  1. La banda de tolerància i el ball de rèpliques

Si l'HPA apliqués la fórmula literalment cada 15 segons, el nombre de rèpliques oscil·laria sense parar. Amb 4 pods a 320m i un objectiu de 325m la fórmula dona sostre(4 × 0,984) = 4; amb 330m dona sostre(4 × 1,015) = 5. Una fluctuació de 10m provocaria un pod amunt i avall indefinidament. A això se'n diu ball o thrashing, i és car: cada arrencada consumeix CPU, escalfa memòries cau des de zero i embruta les mètriques.

Kubernetes ho evita amb una banda de tolerància: si el quocient valorActual / valorObjectiu està entre 0,9 i 1,1 (és a dir, dins d'un ±10 %), l'HPA no fa res.

ratio = valorActual / valorObjectiu

|ratio - 1| <= 0,10   →   no s'escala
|ratio - 1|  > 0,10   →   s'aplica la formula

Amb el nostre objectiu de 325m, la zona morta és:

Consum mitjà ratio Actua l'HPA?
280m 0,86 Sí, escala cap avall
300m 0,92 No, dins de la banda
325m 1,00 No
355m 1,09 No, dins de la banda
380m 1,17 Sí, escala cap amunt

Aquest llindar és global del clúster (--horizontal-pod-autoscaler-tolerance), no configurable per HPA... fins fa poc. Des de Kubernetes 1.33 existeix la feature gate HPAConfigurableTolerance, que permet fixar la tolerància per mètrica dins del behavior:

behavior:
  scaleUp:
    tolerance: 0.05     # Mes sensible en pujar: reacciona amb un 5 % d'exces
  scaleDown:
    tolerance: 0.15     # Mes mandros en baixar

Com a feature gate opcional, no la donis per feta: comprova la versió i la configuració del teu clúster abans de fer-la servir en producció. Al clúster de minikube del curs treballarem amb la tolerància per defecte del 10 %.

  1. Requisits ineludibles: requests i metrics-server

Dos requisits, i cap no és negociable.

Requisit 1: metrics-server instal·lat

L'HPA amb mètriques de tipus Resource llegeix de l'API metrics.k8s.io, que serveix metrics-server. El vam instal·lar a 07-02 amb l'addon de minikube. Verifica-ho:

# Esta registrada l'API de metriques?
kubectl get apiservices v1beta1.metrics.k8s.io
NAME                     SERVICE                      AVAILABLE   AGE
v1beta1.metrics.k8s.io   kube-system/metrics-server   True        41d

La columna AVAILABLE ha de dir True. Si diu False (MissingEndpoints) o similar, l'HPA no funcionarà.

# Retorna dades?
kubectl top pods -n rutas-norte-pro -l app=api-reserves

Si kubectl top funciona, l'HPA tindrà dades. Si no, no hi ha HPA que valgui. És la primera comprovació sempre.

A minikube, si encara no el tens:

minikube -p rutas-norte addons enable metrics-server
# Triga entre 30 i 60 segons a comencar a servir dades.

Requisit 2: requests declarats al contenidor

Aquesta és la trampa més freqüent. L'objectiu Utilization es calcula com a percentatge del requests. Si un contenidor no declara requests.cpu, l'HPA no té denominador, i la seva mètrica queda com <unknown> per sempre. No hi ha cap missatge d'error espectacular; simplement no escala.

# Fragment del Deployment d'api-reserves. SENSE AIXO NO HI HA HPA.
spec:
  template:
    spec:
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:1.14.2
          resources:
            requests:
              cpu: 500m           # <-- L'HPA NECESSITA aquest valor
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi

I compte amb els sidecars: si el pod té un contenidor sense requests.cpu, la mètrica del pod sencer queda invàlida per a l'HPA quan fas servir type: Resource. A Rutas Norte, api-reserves porta un sidecar exportador de mètriques; si aquest sidecar no declara requests, l'HPA de tot el Deployment es queda cec. Dues solucions:

  1. Declarar requests també al sidecar (recomanat, i a més necessari per a la QoS del mòdul 3).
  2. Fer servir type: ContainerResource apuntant només al contenidor api.

Apliquem les dues a Rutas Norte: cinturó i tirants.

  1. Diagnòstic del temut <unknown>

Tard o d'hora veuràs això:

kubectl get hpa -n rutas-norte-pro
NAME           REFERENCE                 TARGETS              MINPODS  MAXPODS  REPLICAS  AGE
api-reserves   Deployment/api-reserves   <unknown>/65%        4        30       4         3m12s

<unknown> significa: «l'HPA no ha pogut obtenir el valor actual d'aquesta mètrica». No escala, i punt. El diagnòstic es fa sempre en el mateix ordre:

kubectl describe hpa api-reserves -n rutas-norte-pro

Fixa't en el bloc Conditions i en els esdeveniments del final. Les causes, ordenades per freqüència:

Símptoma a describe Causa Solució
FailedGetResourceMetric ... unable to get metrics for resource cpu: no metrics returned from resource metrics API metrics-server no hi és o no respon Instal·lar/reparar metrics-server (07-02)
missing request for cpu on container X Un contenidor del pod no declara requests.cpu Afegir requests a tots els contenidors, sidecars inclosos
ScalingActive=False, reason: FailedGetScale El scaleTargetRef apunta a un objecte que no existeix o està mal escrit Corregir name/kind/apiVersion
did not receive metrics for any ready pods Tots els pods porten menys de 30 s llestos, o cap no passa la readiness Esperar; revisar sondes (07-01)
<unknown> només en mètriques Pods/External No hi ha adaptador de mètriques personalitzades registrat Instal·lar l'adaptador o KEDA (09-04)

Exemple de sortida real amb l'error de requests mancant:

Conditions:
  Type           Status  Reason                   Message
  ----           ------  ------                   -------
  AbleToScale    True    SucceededGetScale        the HPA controller was able to get the target's current scale
  ScalingActive  False   FailedGetResourceMetric  the HPA was unable to compute the replica count:
                                                  failed to get cpu utilization: missing request for cpu
                                                  in container exportador-metriques of Pod api-reserves-7c9d4f8b6d-2mk8p
Events:
  Type     Reason                        Age                 From                       Message
  ----     ------                        ----                ----                       -------
  Warning  FailedGetResourceMetric       12s (x8 over 2m)    horizontal-pod-autoscaler  missing request for cpu
  Warning  FailedComputeMetricsReplicas  12s (x8 over 2m)    horizontal-pod-autoscaler  invalid metrics (1 invalid out of 1)

El missatge assenyala amb nom i cognoms el contenidor culpable: exportador-metriques. Afegir-li requests.cpu resol el problema en el següent cicle de 15 segons.

Un truc de diagnòstic ràpid: consulta directament l'API de mètriques per veure què està veient l'HPA.

kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/pods" \
  | python3 -m json.tool | head -40

Si aquesta crida retorna dades i l'HPA continua en <unknown>, el problema és de requests, no de metrics-server.

  1. El bloc behavior: polítiques, períodes i finestra d'estabilització

Fins ara hem vist quantes rèpliques vol l'HPA. El bloc behavior decideix a quina velocitat arriba a aquest número. És la diferència entre un autoescalat que funciona i un que fa por.

Estructura

behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    selectPolicy: Max
    policies:
      - type: Percent
        value: 100
        periodSeconds: 30
      - type: Pods
        value: 4
        periodSeconds: 30
  scaleDown:
    stabilizationWindowSeconds: 600
    selectPolicy: Min
    policies:
      - type: Percent
        value: 20
        periodSeconds: 120

Camp a camp:

policies — cada política limita quant pot canviar el nombre de rèpliques en una finestra de periodSeconds:

  • type: Percent amb value: 100 i periodSeconds: 30 significa «en qualsevol finestra de 30 segons pots com a molt duplicar el nombre de rèpliques» (augmentar un 100 % sobre la base).
  • type: Pods amb value: 4 i periodSeconds: 30 significa «en qualsevol finestra de 30 segons pots afegir com a molt 4 pods».

selectPolicy — quan hi ha diverses polítiques, decideix quina mana:

Valor Efecte Quan fer-lo servir
Max (defecte a scaleUp) Permet el canvi més agressiu de totes les polítiques Pujar ràpid
Min (defecte a scaleDown) Permet el canvi més conservador Baixar a poc a poc
Disabled Desactiva l'escalat en aquesta direcció Congelar baixades durant una campanya

Exemple de l'efecte de selectPolicy: Max a scaleUp amb les nostres polítiques i 4 rèpliques actuals:

  • Política de percentatge: 100 % de 4 = permet afegir 4 → 8 rèpliques.
  • Política de pods: permet afegir 4 → 8 rèpliques.
  • Max pren el més gran: 8 rèpliques.

Amb 20 rèpliques actuals:

  • Percentatge: 100 % de 20 = permet afegir 20 → 40 rèpliques.
  • Pods: permet afegir 4 → 24 rèpliques.
  • Max pren 40 (limitat després per maxReplicas: 30 → 30).

I amb selectPolicy: Min hauria triat 24. Això il·lustra per què Max en pujar té sentit: quan ja tens moltes rèpliques, el percentatge et dona marge per créixer ràpid.

stabilizationWindowSeconds — és el paràmetre més important i el pitjor entès. Funciona així: l'HPA guarda un històric de les recomanacions dels últims N segons i fa servir el valor més conservador d'aquesta finestra.

  • A scaleUp, «més conservador» significa el mínim de les recomanacions recents. Amb una finestra de 60 s, si en l'últim minut l'HPA va recomanar 12, 8 i 14 rèpliques, escalarà a 8. Efecte: ignora pics aïllats.
  • A scaleDown, «més conservador» significa el màxim de les recomanacions recents. Amb una finestra de 600 s, si en els últims 10 minuts va recomanar 12, 5 i 6, mantindrà 12. Efecte: no baixa fins que la calma se sosté deu minuts sencers.

Valors per defecte si no defineixes behavior:

scaleUp scaleDown
stabilizationWindowSeconds 0 300 (5 minuts)
policies 100 % cada 15 s i 4 pods cada 15 s 100 % cada 15 s
selectPolicy Max Min

És a dir, per defecte l'HPA pot duplicar-se cada 15 segons i pot anar-se'n a minReplicas de cop després de 5 minuts de calma. El primer sol estar bé; el segon és massa brusc per a Rutas Norte.

La configuració raonada de Rutas Norte: pujar ràpid, baixar a poc a poc

La nostra política és deliberadament asimètrica, i convé entendre per què:

Pujar ràpid. El cost d'escalar de més durant uns minuts és uns cèntims de còmput. El cost d'escalar de menys durant uns minuts és que la venda del pont de maig cau i Rutas Norte perd milers d'euros en bitllets no venuts, més la reputació. Els costos són radicalment asimètrics, i per tant la resposta ho ha de ser. stabilizationWindowSeconds: 0 a scaleUp: quan la càrrega puja, s'actua.

Baixar a poc a poc. El trànsit d'un web de bitllets és irregular per naturalesa: una punta a les 10:00, una vall a les 10:03, una altra punta a les 10:05. Si baixéssim ràpid, estaríem destruint pods que cal tornar a crear trenta segons després, amb el cost d'arrencada en fred (connexions a PostgreSQL, memòria cau de rutes buida) i una latència pitjor per a l'usuari. stabilizationWindowSeconds: 600 a scaleDown: no baixem fins que la calma dura deu minuts. I ni tan sols llavors baixem de cop: un 20 % cada dos minuts.

La formulació mental: l'autoescalat cap amunt és un mecanisme de disponibilitat; l'autoescalat cap avall és un mecanisme de cost. La disponibilitat és urgent; l'estalvi pot esperar deu minuts.

Congelar l'escalat cap avall durant una campanya

Un ús avançat i molt pràctic de selectPolicy: Disabled. Durant els tres dies del pont de maig, l'equip de Rutas Norte pot decidir que no vol cap reducció de rèpliques, ni tan sols lenta:

behavior:
  scaleDown:
    selectPolicy: Disabled       # Durant el pont: mai baixar. Nomes pujar.

S'aplica el divendres al matí i es reverteix el dilluns. És un interruptor d'emergència legítim i molt més segur que esborrar l'HPA.

  1. Diverses mètriques alhora: guanya la que demana més

El camp metrics és una llista. Quan n'hi ha diverses, l'HPA calcula la fórmula per a cadascuna per separat i després aplica una regla molt simple:

Es queda amb el nombre de rèpliques MÉS ALT de tots els càlculs.

És un OR de necessitat: n'hi ha prou que una sola mètrica demani més rèpliques perquè s'escali. Mai es promitgen ni es combinen.

Exemple amb dues mètriques a api-reserves (5 rèpliques actuals):

Mètrica Objectiu Valor actual Rèpliques que demana
CPU (Utilization) 65 % 42 % sostre(5 × 0,646) = 4
Peticions per segon (Pods) 80 rps/pod 190 rps/pod sostre(5 × 2,375) = 12
Decisió final 12

La CPU diu que sobren pods; les peticions per segon diuen que en falten molts. Guanya 12. Aquesta asimetria és intencionada i correcta: l'HPA prefereix equivocar-se per excés de capacitat que per defecte.

Conseqüència pràctica que cal tenir present: si una de les mètriques es queda en <unknown>, l'HPA no pot garantir que no calgui escalar per ella. El seu comportament en aquest cas és conservador: si la mètrica vàlida demana pujar, puja; si demana baixar, no baixa, perquè no sap si la mètrica trencada justificaria mantenir les rèpliques. Això explica el desconcert habitual de «el meu HPA té una mètrica en <unknown> i s'ha quedat clavat amunt».

  1. Els HPA de Rutas Norte, manifests complets

Escriurem els dos manifests definitius. Van a k8s/entorns/pro/, perquè els valors de minReplicas difereixen per entorn.

HPA d'api-reserves

# k8s/entorns/pro/hpa-api-reserves.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
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

  minReplicas: 4          # Terra de disponibilitat: 4 repliques repartides entre zones (09-05)
  maxReplicas: 30         # Sostre: 30 x 500m = 15 nuclis de requests. Cap al pla de capacitat.

  metrics:
    # Metrica principal: CPU del contenidor "api", excloent el sidecar de metriques.
    - type: ContainerResource
      containerResource:
        name: cpu
        container: api
        target:
          type: Utilization
          averageUtilization: 65
          # 65 % de 500m = 325m per pod. Deixa un 35 % de marge per absorbir
          # el temps que triguen a arrencar les repliques noves.

  behavior:
    scaleUp:
      # Reaccio immediata: zero espera. La disponibilitat mana.
      stabilizationWindowSeconds: 0
      selectPolicy: Max
      policies:
        # Duplicar el nombre de repliques cada 30 s...
        - type: Percent
          value: 100
          periodSeconds: 30
        # ...o afegir 6 pods cada 30 s, el que permeti mes.
        # Amb poques repliques mana la politica de pods; amb moltes, la de percentatge.
        - type: Pods
          value: 6
          periodSeconds: 30

    scaleDown:
      # Deu minuts de calma sostinguda abans de comencar a baixar.
      stabilizationWindowSeconds: 600
      selectPolicy: Min
      policies:
        # Com a molt, treure el 20 % de les repliques cada 2 minuts.
        - type: Percent
          value: 20
          periodSeconds: 120
        # I mai mes de 3 pods cada 2 minuts. Amb selectPolicy: Min mana la mes suau.
        - type: Pods
          value: 3
          periodSeconds: 120

Un càlcul que justifica el maxReplicas: 30: cada pod reserva 500m de CPU i 512Mi de memòria. Trenta pods són 15 nuclis i 15 GiB només d'api-reserves. Sumant botiga-web, worker-notificacions i les bases de dades, això defineix la mida del clúster que necessitarem a 09-03. El maxReplicas no és un número que es posa a l'atzar: és un compromís de capacitat.

Un altre càlcul, sobre la velocitat de pujada. Partint de 4 rèpliques amb Max entre «duplicar» i «+6 pods»:

Moment Rèpliques Política que mana
t = 0 s 4
t = 30 s 10 +6 pods (duplicar donaria 8)
t = 60 s 20 duplicar (+6 donaria 16)
t = 90 s 30 duplicar donaria 40, maxReplicas talla en 30

De 4 a 30 rèpliques en 90 segons. Sumant uns 20-30 segons d'arrencada de cada pod de Node.js, la plataforma passa de la seva capacitat de dimarts a la seva capacitat màxima en uns dos minuts. Això és el que cal comparar contra la velocitat a què arriba el trànsit.

HPA de botiga-web

botiga-web és nginx servint actius estàtics. El seu perfil és diferent: consumeix poquíssima CPU per petició, arrenca en un segon i el seu límit real és l'amplada de banda i el nombre de connexions, no el còmput.

# k8s/entorns/pro/hpa-botiga-web.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: botiga-web

  minReplicas: 3
  maxReplicas: 15

  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          # Llindar MES BAIX que el de l'API: nginx arrenca en ~1 s i consumeix poc,
          # aixi que ens podem permetre escalar abans sense cost apreciable.
          # A mes es la porta d'entrada: si se satura, no entra ningu.
          averageUtilization: 50

  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      selectPolicy: Max
      policies:
        - type: Percent
          value: 100
          periodSeconds: 15     # Mes agressiu encara: nginx arrenca en un segon
        - type: Pods
          value: 4
          periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 300   # 5 min: menys conservador que l'API,
                                        # perque nginx no te arrencada en fred cara
      selectPolicy: Min
      policies:
        - type: Percent
          value: 25
          periodSeconds: 60

Comparació raonada de tots dos HPA:

Paràmetre api-reserves botiga-web Motiu de la diferència
Objectiu de CPU 65 % 50 % nginx és barat d'escalar; l'API no
minReplicas 4 3 Disponibilitat mínima acordada per component
maxReplicas 30 15 L'API és el component car en CPU
periodSeconds en pujar 30 s 15 s nginx arrenca en 1 s; Node.js en 20-30 s
Estabilització en baixar 600 s 300 s L'API té arrencada en fred costosa (pool de connexions, memòria cau)
Tipus de mètrica ContainerResource Resource L'API té sidecar de mètriques; nginx no

Aplicació

kubectl apply -f k8s/entorns/pro/hpa-api-reserves.yaml
kubectl apply -f k8s/entorns/pro/hpa-botiga-web.yaml

kubectl get hpa -n rutas-norte-pro
NAME           REFERENCE                 TARGETS       MINPODS  MAXPODS  REPLICAS  AGE
api-reserves   Deployment/api-reserves   28%/65%       4        30       4         22s
botiga-web     Deployment/botiga-web     11%/50%       3        15       3         19s

La columna TARGETS mostra valorActual/objectiu. Si veus números en lloc de <unknown>, tot està ben connectat.

  1. Simulació d'una punta de càrrega i observació en viu

Provocarem una punta a l'entorn de desenvolupament i veurem l'HPA treballar. Farem servir el perfil de minikube del curs.

Preparar el terreny

minikube -p rutas-norte addons enable metrics-server
kubectl -n rutas-norte-dev apply -f k8s/entorns/dev/hpa-api-reserves.yaml

Per a la demostració, un HPA de desenvolupament amb llindars baixos perquè reaccioni ràpid:

# k8s/entorns/dev/hpa-api-reserves.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-reserves
  namespace: rutas-norte-dev
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-reserves
  minReplicas: 1
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 40     # Llindar baix: escala aviat, es veu millor la demo
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
        - type: Pods
          value: 3
          periodSeconds: 15
    scaleDown:
      stabilizationWindowSeconds: 120   # Curt per a la demo; en pro seria 600

Terminal 1: observar

kubectl get hpa api-reserves -n rutas-norte-dev --watch

Terminal 2: generar càrrega

Un pod efímer que martelleja l'endpoint de cerca de rutes en bucle:

kubectl -n rutas-norte-dev run generador-carrega \
  --image=busybox:1.36 \
  --restart=Never \
  --rm -it \
  -- /bin/sh -c \
  'while true; do wget -q -O- http://api-reserves/rutes?origen=BIL\&desti=SDR > /dev/null; done'

Explicació de l'ordre:

  • --rm -it crea el pod, s'hi connecta i l'esborra en sortir amb Ctrl+C.
  • http://api-reserves fa servir el DNS intern del clúster (04-03): resol al Service del mateix namespace.
  • El bucle infinit de wget genera peticions tan ràpid com pot.

Per a una punta més seriosa, diverses rèpliques del generador:

kubectl -n rutas-norte-dev create deployment generador-carrega \
  --image=busybox:1.36 --replicas=6 \
  -- /bin/sh -c 'while true; do wget -q -O- http://api-reserves/rutes > /dev/null; done'

El que es veu al terminal 1

NAME           REFERENCE                 TARGETS    MINPODS  MAXPODS  REPLICAS  AGE
api-reserves   Deployment/api-reserves   3%/40%     1        10       1         5m
api-reserves   Deployment/api-reserves   3%/40%     1        10       1         5m15s
api-reserves   Deployment/api-reserves   187%/40%   1        10       1         5m30s
api-reserves   Deployment/api-reserves   187%/40%   1        10       4         5m30s
api-reserves   Deployment/api-reserves   142%/40%   1        10       4         5m45s
api-reserves   Deployment/api-reserves   142%/40%   1        10       7         5m45s
api-reserves   Deployment/api-reserves   94%/40%    1        10       7         6m
api-reserves   Deployment/api-reserves   94%/40%    1        10       10        6m
api-reserves   Deployment/api-reserves   61%/40%    1        10       10        6m30s
api-reserves   Deployment/api-reserves   38%/40%    1        10       10        7m

Llegeix la seqüència amb la fórmula a la mà:

  1. 3%/40% amb 1 rèplica: sostre(1 × 0,075) = 1. Res a fer.
  2. 187%/40% amb 1 rèplica: sostre(1 × 4,675) = 5. Però la política Pods: 3 cada 15s limita a 4. Aquí tens el behavior en acció.
  3. 142%/40% amb 4 rèpliques: sostre(4 × 3,55) = 15. La política limita a 4+3 = 7.
  4. 94%/40% amb 7 rèpliques: sostre(7 × 2,35) = 17. Limitat a 10 per maxReplicas.
  5. 38%/40% amb 10 rèpliques: ratio 0,95, dins de la banda de tolerància. Estable. Objectiu assolit.

Ara atura el generador (Ctrl+C o esborrant el Deployment) i observa la baixada:

api-reserves   Deployment/api-reserves   2%/40%     1        10       10        9m
api-reserves   Deployment/api-reserves   2%/40%     1        10       10        10m
api-reserves   Deployment/api-reserves   2%/40%     1        10       1         11m

Fixa-t'hi: la CPU cau immediatament al 2 %, però les rèpliques es queden en 10 durant dos minuts sencers. Aquesta és la stabilizationWindowSeconds: 120. Complerta la finestra, baixa a 1 de cop (en aquesta demo no vam definir polítiques de scaleDown, així que s'aplica la de per defecte: 100 % cada 15 s).

Els esdeveniments de l'escalat

kubectl describe hpa api-reserves -n rutas-norte-dev
Events:
  Type    Reason             Age    From                       Message
  ----    ------             ----   ----                       -------
  Normal  SuccessfulRescale  5m30s  horizontal-pod-autoscaler  New size: 4;  reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  5m15s  horizontal-pod-autoscaler  New size: 7;  reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  5m     horizontal-pod-autoscaler  New size: 10; reason: cpu resource utilization (percentage of request) above target
  Normal  SuccessfulRescale  30s    horizontal-pod-autoscaler  New size: 1;  reason: All metrics below target

Aquests esdeveniments són or pur per a l'anàlisi posterior d'un incident: et diuen exactament quan va escalar, a quant i per què. Recorda que els esdeveniments caduquen (una hora per defecte); per a l'historial llarg cal el registre centralitzat del mòdul 7.

  1. El conflicte entre l'HPA i el camp replicas

Aquest és l'error que mossega tots els equips exactament una vegada, i convé que no sigui al pont de maig.

El problema

El Deployment d'api-reserves que anem escrivint des del mòdul 2 diu:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reserves
spec:
  replicas: 4        # <-- Aquest camp
  ...

I ara un HPA també governa aquest mateix camp. Tenim dues autoritats sobre el mateix número.

Mentre ningú apliqui el YAML, no passa res: l'HPA modifica el camp al servidor i l'objecte viu feliç amb 22 rèpliques. El problema apareix quan algú torna a aplicar el manifest.

Escenari real:

10:00  L'HPA escala api-reserves a 22 repliques. La venda va be.
10:14  Un company corregeix una etiqueta al Deployment i executa:
       kubectl apply -f k8s/base/deployment-api-reserves.yaml
10:14  El manifest diu replicas: 4. L'API ho accepta.
       22 pods -> 4 pods, INSTANTANIAMENT.
10:14  La plataforma cau.
10:14  L'HPA (seguent cicle, fins a 15 s despres) detecta CPU al 400 %
       i torna a escalar. Pero ja hi ha 15-90 segons de caiguda.

Quinze segons de caiguda total al pic de venda de l'any. Tot per un camp que no hi hauria de ser.

I és pitjor amb GitOps: si Argo CD (10-05) té el repositori com a font de veritat i detecta que el clúster té 22 rèpliques quan el Git diu 4, marcarà el recurs com a OutOfSync i, amb sincronització automàtica, ho «corregirà» a 4. Cada vegada. Tindràs un estira-i-arronsa permanent entre l'HPA i l'operador de GitOps, amb la plataforma oscil·lant entre 4 i 22 rèpliques indefinidament.

La solució: treure replicas del manifest

# k8s/base/deployment-api-reserves.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
spec:
  # SENSE camp "replicas": el governa l'HorizontalPodAutoscaler api-reserves.
  # Veure k8s/entorns/pro/hpa-api-reserves.yaml
  # Si el tornes a posar aqui, cada "kubectl apply" tirara la plataforma a terra
  # durant el pic de transit. No ho facis.
  selector:
    matchLabels:
      app: api-reserves
  template:
    metadata:
      labels:
        app: api-reserves
        app.kubernetes.io/part-of: rutas-norte
    spec:
      containers:
        - name: api
          image: registry.rutasnorte.example/api-reserves:1.14.2
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi

En ometre replicas, el valor per defecte de l'API és 1, però només en la creació inicial. En aplicacions posteriors, el camp simplement no es toca: kubectl apply compara amb l'anotació kubectl.kubernetes.io/last-applied-configuration, veu que replicas no hi era abans ni hi és ara, i el deixa com està. L'HPA continua manant.

Una conseqüència a tenir en compte: la primera vegada que creïs el Deployment sense replicas, arrencarà amb 1 pod fins que l'HPA el porti a minReplicas al següent cicle (menys de 15 segons). És acceptable en un desplegament nou; si et molesta, crea l'HPA primer.

Aquest comentari explícit al YAML no és decoratiu: és documentació al lloc on algú prendrà la decisió equivocada. Escriu-lo.

Alternatives i els seus matisos

Enfocament Com Quan fer-lo servir
Ometre replicas Esborrar-lo del YAML Recomanat. Simple, funciona amb apply i amb GitOps
Server-Side Apply amb propietat compartida kubectl apply --server-side; l'HPA és propietari del camp Bo, però requereix que tot el flux faci servir SSA
ignoreDifferences a Argo CD Configurar Argo CD perquè ignori /spec/replicas Necessari si per alguna raó no pots treure el camp
Kustomize sense patch de rèpliques No fer servir replicas: als overlays Complementa la primera opció (10-04)

Compte amb una trampa de Server-Side Apply: si apliques amb SSA i el camp replicas que és al teu manifest, en prendràs la propietat i l'HPA rebrà un conflicte. L'API t'ho dirà amb un error explícit de conflicte de propietaris, cosa que és millor que l'error silenciós de kubectl apply clàssic, però continua sent un error.

Verificar qui mana

kubectl get deployment api-reserves -n rutas-norte-pro \
  -o jsonpath='{.metadata.managedFields[*].manager}{"\n"}'
kubectl-client-side-apply kube-controller-manager

Si veus kube-controller-manager entre els gestors, l'HPA està escrivint el camp. És la confirmació que la cadena funciona.

  1. Què no escalar amb HPA i el coll d'ampolla real

Acabem amb la lliçó més valuosa de totes, i la que més diners estalvia.

L'error d'escalar la capa equivocada

Imagina aquesta seqüència al pont de maig:

  1. Arriba l'allau de trànsit.
  2. L'HPA d'api-reserves escala de 4 a 30 rèpliques en dos minuts. Perfecte.
  3. Les 30 rèpliques obren, cadascuna, el seu pool de 20 connexions a postgres-reserves. Total: 600 connexions.
  4. postgres-reservesmax_connections = 200.
  5. Les 400 connexions sobrants són rebutjades. Els pods d'api-reserves retornen error 500.
  6. La CPU d'api-reserves puja (els reintents i la gestió d'errors consumeixen), així que l'HPA vol escalar més. maxReplicas ho talla en 30.
  7. La plataforma està caiguda amb 30 rèpliques on abans queia amb 4. Ha costat més diners i no ha servit de res.

Aquest patró té nom: desplaçar el coll d'ampolla sense resoldre'l. L'HPA no ho detecta perquè l'HPA només sap de CPU: no sap que la base de dades està saturada.

flowchart LR
    A[Transit x10] --> B[botiga-web<br/>HPA: 3 a 15]
    B --> C[api-reserves<br/>HPA: 4 a 30]
    C --> D[postgres-reserves<br/>1 replica<br/>max_connections=200]
    D -.->|COLL D'AMPOLLA| E[Errors 500]
    C --> F[redis-cache<br/>1 replica]
    style D fill:#f88,stroke:#900,stroke-width:3px
    style E fill:#f88,stroke:#900

La capa que no escala determina la capacitat de tot el sistema. És la llei de l'anella més feble aplicada a l'arquitectura.

Què cal fer en canvi

Amb postgres-reserves com a límit, les palanques reals són:

Palanca Què fa On es tracta
Limitar el pool per rèplica Amb 30 rèpliques × 6 connexions = 180 < 200 09-06
Posar un pool compartit (PgBouncer) Multiplexa milers de connexions d'aplicació sobre poques de base de dades 09-06
Cachejar a redis-cache La consulta de disponibilitat no arriba a PostgreSQL 09-06
Rèpliques de lectura Les cerques van a la rèplica; només les reserves al primari 06-07 (operador)
Escalar verticalment PostgreSQL Més CPU/RAM i disc més ràpid 09-02 i 09-06

Cap d'elles no és un HPA. La lliçó: abans de posar un HPA, identifica el recurs escàs. Si no és la CPU dels pods que escalaràs, l'HPA no t'ajudarà.

Llista de comprovació abans de posar un HPA

Pregunta't:

  1. L'aplicació és realment sense estat? Pot qualsevol rèplica atendre qualsevol petició sense memòria local ni afinitat de sessió?
  2. La CPU és el recurs escàs? O ho és la base de dades, el disc, la xarxa, un servei extern o un bloqueig global?
  3. La càrrega es reparteix per igual? Si un endpoint concentra el 90 % del cost i un sol client el crida, afegir rèpliques no ajuda.
  4. Quant triga un pod nou a ser útil? Si triga tres minuts, l'HPA arribarà tard a les puntes ràpides. Caldrà treballar l'arrencada (09-06) o preescalfar (09-04).
  5. Hi ha lloc al clúster per a les rèpliques noves? Si no, l'HPA només generarà pods Pending (09-03).
  6. El servei de sota aguanta N vegades més càrrega? El pool de connexions, la quota de l'API externa, el límit de taxa.

Només si les sis respostes són satisfactòries, l'HPA farà el que esperes.

Errors Comuns i Consells

Error 1: deixar el camp replicas al manifest. Ja ho hem desenvolupat a l'apartat 13, però mereix repetir-se perquè és l'error número u i les seves conseqüències són immediates i visibles. Treu-lo i deixa-hi un comentari al seu lloc.

Error 2: escalar per memòria. Els entorns d'execució amb recol·lector d'escombraries no retornen memòria al sistema. L'HPA puja i no torna a baixar mai. Fes servir CPU, o una mètrica de negoci (09-04). Si de debò necessites memòria, fes servir AverageValue en lloc d'Utilization i tingues expectatives realistes.

Error 3: posar l'objectiu d'utilització massa alt. Un averageUtilization: 90 sembla eficient, però deixa només un 10 % de marge. Quan l'HPA detecta la pujada, els pods nous triguen 30 segons a estar llestos, i durant aquests 30 segons els pods existents estan al 130 % i patint throttling. L'objectiu ha de deixar espai per al temps d'arrencada. Entre 50 % i 70 % és el rang sa; com més lent arrenqui la teva aplicació, més baix l'objectiu.

Error 4: maxReplicas sense pla de capacitat. Un maxReplicas: 200 en un clúster de tres nodes no et dona 200 rèpliques: et dona un munt de pods Pending i una falsa sensació de seguretat. Calcula maxReplicas × requests i compara-ho amb la capacitat real (09-03).

Error 5: oblidar els requests als sidecars. Un sol contenidor sense requests.cpu deixa cec l'HPA de tot el Deployment. A Rutas Norte, l'exportador de mètriques d'api-reserves és el sospitós habitual.

Error 6: no comprovar que els pods nous són útils. Un pod que arrenca i passa la readiness però té el pool de connexions buit i la memòria cau freda atén pitjor que un de calent. Durant els primers segons, escalar pot empitjorar la latència mitjana. Ajusta les sondes (07-01) perquè un pod només es declari llest quan de debò ho estigui.

Consell 1: posa primer l'HPA en mode observació. Crea'l amb minReplicas igual a maxReplicas (per exemple, tots dos a 4). L'HPA no podrà escalar, però calcularà i publicarà les mètriques a kubectl get hpa. Deixa'l una setmana, mira què hauria fet, i només llavors obre el rang. És gratis i evita ensurts.

Consell 2: alerta quan l'HPA estigui enganxat al sostre. Si api-reserves porta mitja hora en 30 rèpliques, el maxReplicas està limitant la capacitat i ningú se n'assabenta. Una alerta a Alertmanager (07-04):

kube_horizontalpodautoscaler_status_current_replicas
  >= kube_horizontalpodautoscaler_spec_max_replicas

Consell 3: registra un panell amb rèpliques i mètrica juntes. A Grafana, superposar «rèpliques actuals» i «CPU mitjana» al mateix gràfic fa evident si l'HPA reacciona bé o va per darrere. És el diagnòstic visual més útil que existeix per a un HPA.

Consell 4: prova l'HPA abans de necessitar-lo. Un assaig de càrrega contra rutas-norte-pre una setmana abans del pont de maig costa una tarda i evita la caiguda de l'any. Ho farem formalment amb k6 a 09-06.

Consell 5: els esdeveniments de l'HPA caduquen. Una hora per defecte. Si vols l'històric de l'incident, necessites EFK (07-05) o les mètriques de kube-state-metrics a Prometheus.

Exercicis

Exercici 1: aplicar la fórmula

botiga-web està desplegat amb requests.cpu: 200m i un HPA amb averageUtilization: 50, minReplicas: 3, maxReplicas: 15. En aquest moment hi ha 6 rèpliques i kubectl top dona:

botiga-web-6f7d9c4b58-2wq4m   210m
botiga-web-6f7d9c4b58-4kjnx   198m
botiga-web-6f7d9c4b58-7hgpz   225m
botiga-web-6f7d9c4b58-9mzbc   187m
botiga-web-6f7d9c4b58-kx2vt   204m
botiga-web-6f7d9c4b58-tn8fr   216m

Calcula: (a) l'objectiu absolut per pod; (b) el consum mitjà; (c) el ratio i si està dins de la banda de tolerància; (d) les rèpliques desitjades; (e) què farà l'HPA amb aquest behavior:

behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    selectPolicy: Max
    policies:
      - type: Percent
        value: 100
        periodSeconds: 15
      - type: Pods
        value: 4
        periodSeconds: 15

Exercici 2: diagnosticar un HPA mut

Un company ha desplegat worker-notificacions amb aquest HPA i es queixa que mai escala:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: worker-notificacions
  namespace: rutas-norte-pro
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker-notificacions
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

El Deployment és:

spec:
  template:
    spec:
      containers:
        - name: worker
          image: registry.rutasnorte.example/worker-notificacions:2.3.0
          resources:
            limits:
              cpu: 500m
              memory: 512Mi
        - name: exportador-metriques
          image: registry.rutasnorte.example/exportador:1.2.0

kubectl get hpa mostra <unknown>/70%. Identifica tots els problemes, escriu les ordres de diagnòstic que executaries i les correccions. A més, explica per què fins i tot arreglant tot això aquest HPA continuarà sent una mala idea per a aquest component concret.

Exercici 3: dissenyar un behavior per a un escenari nou

Rutas Norte llança panell-conductors, una aplicació web interna que fan servir els 40 conductors de la flota. Perfil de càrrega:

  • De dilluns a divendres, de 5:30 a 7:00, tots els conductors entren per consultar la seva ruta del dia: la càrrega passa de zero al seu màxim en quinze minuts.
  • La resta del dia hi ha ús esporàdic, molt baix.
  • A la nit no hi ha ningú.
  • L'aplicació és Node.js i triga uns 25 segons a estar llesta.
  • És interna: una caiguda de dos minuts és molesta però no costa diners.
  • L'equip té pressió de costos: el clúster és compartit i hi ha ResourceQuota.

Escriu l'HPA complet (autoscaling/v2) raonant cada decisió: minReplicas, maxReplicas, mètrica, objectiu, i tot el behavior. Compara almenys tres decisions amb les de l'HPA d'api-reserves i explica per què difereixen.


Solucions

Solució 1

(a) Objectiu absolut per pod:

50 % de 200m = 100m per pod

(b) Consum mitjà:

(210 + 198 + 225 + 187 + 204 + 216) / 6 = 1240 / 6 = 206,67m

(c) Ratio i banda de tolerància:

ratio = 206,67 / 100 = 2,067
|2,067 - 1| = 1,067 > 0,10   ->  FORA de la banda. L'HPA actua.

(d) Rèpliques desitjades:

repliquesDesitjades = sostre( 6 × 2,067 ) = sostre( 12,4 ) = 13

13 rèpliques, per sota de maxReplicas: 15.

(e) Què fa el behavior:

Rèpliques actuals: 6.

  • Política Percent 100 % / 15 s: permet afegir el 100 % de 6 = 6 → fins a 12 rèpliques.
  • Política Pods 4 / 15 s: permet afegir 4 → fins a 10 rèpliques.
  • selectPolicy: Max pren el sostre més alt: 12.

L'HPA en vol 13 però només pot arribar a 12 en aquest pas. Escalarà a 12 rèpliques ara.

Al següent cicle (15 s després), amb 12 rèpliques i suposant que la càrrega total no canvia, el consum mitjà baixarà a uns 103m per pod. El nou ratio seria 1,03, dins de la banda de tolerància, així que es quedarà en 12 i no arribarà mai a 13. Aquest és un comportament habitual i correcte: la tolerància absorbeix l'últim salt.

Observació addicional: els pods estan al 103 % del seu requests (206m contra 200m). No estan patint throttling perquè el seu limit és més gran, però estan consumint més del reservat, cosa que significa que depenen de la CPU sobrant del node. És un senyal que el requests de botiga-web està infradimensionat i que el VPA de 09-02 hi tindria alguna cosa a dir.

Solució 2

Hi ha quatre problemes, un darrere l'altre.

Problema 1: el contenidor worker no declara requests.cpu.

Només té limits. Sense requests, l'HPA no té denominador per al percentatge.

Matís important que molts desconeixen: quan declares limits sense requests, Kubernetes omple automàticament requests amb el valor de limits. Així que aquest contenidor sí que acaba tenint requests.cpu: 500m. Com a efecte secundari, la QoS del pod seria Guaranteed si tots els contenidors estiguessin així (mòdul 3). Però...

Problema 2: el contenidor exportador-metriques no declara cap recurs.

Aquest sí que és fatal. Sense limits ni requests, no hi ha emplenament automàtic, i l'HPA no pot calcular la utilització del pod. Aquest és el que produeix el <unknown>. A més fa que la QoS del pod sigui Burstable en lloc de Guaranteed.

Problema 3: no se sap si metrics-server està disponible.

Cal verificar-ho abans que res.

Problema 4 (de disseny, i el més important): la CPU és el senyal equivocat per a aquest component.

Ordres de diagnòstic, en ordre:

# 1. Funciona metrics-server?
kubectl top pods -n rutas-norte-pro -l app=worker-notificacions

# 2. Que diu l'HPA exactament?
kubectl describe hpa worker-notificacions -n rutas-norte-pro

# 3. Quins recursos declara cada contenidor?
kubectl get deployment worker-notificacions -n rutas-norte-pro \
  -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{": "}{.resources}{"\n"}{end}'

La sortida del pas 3 delataria el problema:

worker: {"limits":{"cpu":"500m","memory":"512Mi"}}
exportador-metriques: {}

Correcció del Deployment:

spec:
  template:
    spec:
      containers:
        - name: worker
          image: registry.rutasnorte.example/worker-notificacions:2.3.0
          resources:
            requests:
              cpu: 300m          # Explicit, no depenem de l'emplenament automatic
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi
        - name: exportador-metriques
          image: registry.rutasnorte.example/exportador:1.2.0
          resources:
            requests:
              cpu: 20m           # <-- LA CORRECCIO CLAU
              memory: 32Mi
            limits:
              cpu: 50m
              memory: 64Mi

I, defensivament, canviar l'HPA a ContainerResource perquè el sidecar no distorsioni el percentatge:

  metrics:
    - type: ContainerResource
      containerResource:
        name: cpu
        container: worker
        target:
          type: Utilization
          averageUtilization: 70

Per què continua sent mala idea encara que funcioni:

worker-notificacions consumeix missatges d'una cua de correus. La seva feina per missatge és sobretot espera d'entrada/sortida: parlar amb el servidor SMTP, esperar la resposta de xarxa. Això consumeix molt poca CPU.

Al pont de maig hi pot haver quaranta mil correus esperant a la cua i el worker estar al 20 % de CPU, perquè està bloquejat esperant l'SMTP la major part del temps. L'HPA per CPU veuria 20 % contra un objectiu de 70 % i conclouria que sobren rèpliques: escalaria cap avall justament quan més falten consumidors.

La mètrica correcta és la longitud de la cua: «vull un worker per cada 500 missatges pendents». Això no ho pot llegir l'HPA de fàbrica; cal una mètrica externa, i aquesta és exactament la raó de ser de KEDA a 09-04.

Solució 3

# k8s/entorns/pro/hpa-panell-conductors.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: panell-conductors
  namespace: rutas-norte-pro
  labels:
    app: panell-conductors
    app.kubernetes.io/part-of: rutas-norte
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: panell-conductors

  # minReplicas: 1. No es un servei que generi ingressos i a la nit no hi ha
  # ningu. Una replica basta per a l'us esporadic de la resta del dia i perque
  # la primera peticio de les 5:30 no trobi el servei apagat.
  # No hi posem 2 perque hi ha pressio de costos i una caiguda de 2 min es assumible.
  minReplicas: 1

  # maxReplicas: 6. Son 40 conductors concurrents com a maxim. Amb 6 repliques
  # toquen a menys de 7 usuaris per pod: en sobra. Un sostre baix protegeix la
  # ResourceQuota del namespace compartit.
  maxReplicas: 6

  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          # 50 %, no 65-70 %. L'aplicacio triga 25 s a estar llesta, aixi que
          # necessitem marge per cobrir aquesta arrencada. A mes la pujada de les
          # 5:30 es abrupta: millor anar per davant.
          averageUtilization: 50

  behavior:
    scaleUp:
      # Zero espera. La finestra critica dura 90 minuts al dia i tota la carrega
      # arriba en els primers 15. Qualsevol retard es menja la finestra sencera.
      stabilizationWindowSeconds: 0
      selectPolicy: Max
      policies:
        # periodSeconds 30, no 15: l'aplicacio triga 25 s a estar llesta.
        # Amb 15 s escalariem un altre cop ABANS que els pods anteriors
        # comencessin a absorbir carrega, i sobreescalariem.
        - type: Percent
          value: 100
          periodSeconds: 30
        - type: Pods
          value: 2
          periodSeconds: 30
        # Amb maxReplicas 6, "+2 pods" cobreix be el rang 1->3->6.

    scaleDown:
      # 300 s (5 min), no 600. El pic dura 90 minuts i despres no torna fins
      # l'endema: no hi ha puntes intermitents que justifiquin esperar
      # deu minuts, i la pressio de costos demana alliberar recursos aviat.
      stabilizationWindowSeconds: 300
      selectPolicy: Min
      policies:
        # Baixada suau: un pod cada 3 minuts. De 6 a 1 en 15 minuts.
        # Suficient per no deixar tirat un resseguidor de les 7:05.
        - type: Pods
          value: 1
          periodSeconds: 180

Comparació raonada amb l'HPA d'api-reserves:

Decisió api-reserves panell-conductors Per què difereixen
minReplicas 4 1 L'API genera ingressos i necessita disponibilitat multizona; el panell és intern, amb pressió de costos i una caiguda tolerable
maxReplicas 30 6 El trànsit de l'API és públic i impredictible (x10 al pont); el panell té un sostre conegut de 40 usuaris
Objectiu de CPU 65 % 50 % Totes dues són Node.js, però el panell pateix una pujada més abrupta (de zero al màxim en 15 min) i necessita més marge per cobrir els 25 s d'arrencada
periodSeconds en pujar 30 s 30 s Igual: totes dues són Node.js amb arrencada de 20-30 s. Coincidència justificada per la mateixa causa tècnica
Estabilització en baixar 600 s 300 s El trànsit de l'API és intermitent durant hores; el del panell és un únic bloc diari que no torna
Política de baixada 20 % / 120 s 1 pod / 180 s Amb només 6 rèpliques, un percentatge del 20 % donaria fraccions inútils; en números petits es raona millor en pods absoluts

Consideració addicional que mereix nota: aquest perfil de càrrega —zero a la nit, allau a hora fixa— és el candidat perfecte per a un disparador cron que preescalfi el servei a les 5:15, en lloc d'esperar que la CPU pugi i reaccionar tard. Això ja no ho fa l'HPA de fàbrica: és KEDA, i ho veurem a 09-04. Un HPA reactiu sempre va per darrere d'una càrrega que puja en esglaó; només un senyal anticipat resol aquest cas de debò.

Conclusió

L'HorizontalPodAutoscaler converteix el nombre de rèpliques d'una dada fixa escrita a mà en una conseqüència observada del trànsit real. És la diferència entre dimensionar per a un dimarts qualsevol i sobreviure al pont de maig sense que ningú hagi d'obrir el portàtil a l'andana d'una estació.

L'essencial que t'endús d'aquesta lliçó:

  • Escalar cap enfora i escalar cap amunt són eixos diferents. L'horitzontal resol el volum de trànsit i a més millora la disponibilitat; el vertical resol el dimensionament de cada instància. Rutas Norte necessita tots dos.
  • Només s'escala horitzontalment el que és realment sense estat. api-reserves i botiga-web sí; postgres-reserves mai, i l'intent produiria corrupció de dades, no capacitat.
  • La fórmula és una regla de tres: sostre(repliquesActuals × valorActual / valorObjectiu), amb una banda de tolerància del ±10 % que evita el ball.
  • Sense requests a tots els contenidors i sense metrics-server no hi ha HPA. El <unknown> de la columna TARGETS gairebé sempre apunta a un sidecar oblidat.
  • El behavior és on es decideix si l'autoescalat funciona. L'asimetria de Rutas Norte —pujar en zero segons, baixar després de deu minuts de calma— reflecteix una asimetria real de costos: perdre vendes és molt més car que sobrar capacitat una estona.
  • Amb diverses mètriques, guanya la que demana més rèpliques. Un OR de necessitat, deliberadament conservador.
  • El camp replicas ha de desaparèixer del manifest versionat. Si no, un kubectl apply innocent tirarà la plataforma al pic de venda, i amb GitOps serà una guerra permanent (10-05).
  • Abans de posar un HPA, troba el recurs escàs. Escalar la capa web quan el límit és la base de dades multiplica el cost sense afegir un sol bitllet venut.

Rutas Norte ja té api-reserves i botiga-web responent al trànsit pel seu compte. Però continuem arrossegant un problema del qual hem parlat diverses vegades sense resoldre'l del tot: l'HPA es recolza en el requests de CPU com a referència de tot el seu càlcul, i aquest requests continua sent un número que vam posar a ull. A 07-02 el vam recalibrar comparant-lo amb el consum real, però ho vam fer a mà, una vegada, mirant kubectl top durant una tarda. Si el requests està malament, el percentatge de l'HPA menteix, i amb ell totes les seves decisions.

A la propera lliçó, Autoescalat Vertical de Pods, veurem el component que resol exactament aquest problema: el VPA observa el consum real durant dies, calcula percentils i et diu amb dades quins requests i quins limits haurien de tenir els teus contenidors. Descobrirem també per què el VPA i l'HPA no poden governar la mateixa mètrica sense barallar-se, i quin és el flux de treball que fan servir els equips seriosos: el VPA com a assessor, no com a pilot automàtic.

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