La lliçó anterior va acabar amb una incomoditat. L'HorizontalPodAutoscaler d'api-reserves calcula totes les seves decisions com un percentatge del requests.cpu, i aquest requests.cpu: 500m és un número que algú va posar a ull fa mesos. A 07-02 el vam recalibrar comparant-lo amb el consum real, però ho vam fer a mà, una tarda, mirant kubectl top i anotant en un full de càlcul. Si el requests està malament, el percentatge de l'HPA menteix i totes les seves decisions es recolzen en una referència falsa.

I el problema és més ampli que l'HPA. Els requests i limits governen la planificació (on cap un pod), la classe de QoS (qui mor primer quan falta memòria), l'OOMKilled, l'estrangulament de CPU, el consum de la ResourceQuota del namespace i, al núvol, la factura. Tot això descansa sobre números que a la majoria d'organitzacions es van escriure una vegada, copiant el manifest d'un altre servei, i ningú no ha tornat a mirar.

El VerticalPodAutoscaler (VPA) existeix per a això. No escala per càrrega: escala per coneixement. Observa el que els teus contenidors consumeixen realment durant dies, calcula percentils i et diu amb dades què haurien de demanar. Aquesta lliçó cobreix què és, com funciona per dins, com llegir les seves recomanacions, per què la majoria d'equips seriosos el fan servir com a assessor i no com a pilot automàtic, i per què no pot conviure amb l'HPA sobre la mateixa mètrica.

Contingut

  1. El problema real: ningú sap què demanar
  2. Què és el VPA i què no és
  3. No forma part del nucli: instal·lació
  4. Els tres components i com col·laboren
  5. L'objecte VerticalPodAutoscaler camp a camp
  6. Els modes d'actualització: Off, Initial, Recreate i Auto
  7. El redimensionament en calent i el seu estat actual
  8. resourcePolicy: acotar, excloure i controlar
  9. Llegir una recomanació: target, lowerBound, upperBound, uncappedTarget
  10. El mode Off com a assessor: el flux de treball real
  11. La incompatibilitat clàssica: VPA i HPA sobre la mateixa mètrica
  12. Aplicació a Rutas Norte, component a component
  13. Interacció amb LimitRange i ResourceQuota
  14. Els límits del VPA
  15. Errors comuns i consells
  16. Exercicis
  17. Conclusió

  1. El problema real: ningú sap què demanar

Posem números al problema. Aquests són els requests que Rutas Norte arrossega des del mòdul 3, juntament amb el consum real mesurat durant dues setmanes amb Prometheus:

Contenidor requests.cpu CPU real (p95) requests.memory Memòria real (p95) Diagnòstic
api-reserves / api 500m 340m 512Mi 690Mi CPU sobrada, memòria curta
api-reserves / exportador 20m 4m 32Mi 18Mi Sobredimensionat x5
botiga-web / nginx 200m 55m 128Mi 64Mi Sobredimensionat x3
worker-notificacions 300m 90m 256Mi 410Mi CPU sobrada, memòria curta
informes-ocupacio 500m 1750m 512Mi 1,4Gi Molt infradimensionat
postgres-reserves 2 1,2 4Gi 3,6Gi Raonable

Cada fila explica una història diferent i cadascuna costa diners o disponibilitat:

  • api-reserves amb memòria curta. Demana 512Mi i fa servir 690Mi al p95. Funciona perquè el limit és 1Gi i hi ha memòria lliure als nodes... fins que no n'hi ha. El dia que un node s'omple, el kubelet desallotja pods, i un pod Burstable que consumeix per sobre del seu requests és dels primers candidats (mòdul 3). És una bomba de rellotgeria.
  • botiga-web sobredimensionat tres vegades. Amb 15 rèpliques al pic són 3 nuclis reservats dels quals se'n fan servir 0,8. Aquests 2,2 nuclis estan bloquejats per al planificador: cap altre pod no els pot fer servir encara que estiguin ociosos. Es tradueix en nodes de més.
  • informes-ocupacio molt infradimensionat. Demana 500m i necessita 1750m. Conseqüència: estrangulament brutal. Un informe que hauria de trigar 8 minuts en triga 40, i algunes nits no acaba abans de la finestra de manteniment.
  • L'exportador de mètriques. 20m de requests per a un procés que fa servir 4m. Sembla poc, però multiplicat per tots els pods de la plataforma són nuclis sencers malgastats.

Ara la pregunta incòmoda: com es corregeix això? La resposta habitual —«mirem Grafana i ajustem»— té tres problemes. Requereix que algú se'n recordi. Requereix que aquest algú sàpiga quin percentil mirar. I cal repetir-ho cada vegada que l'aplicació canvia, cosa que a la pràctica significa mai.

El VPA automatitza exactament aquesta feina.

  1. Què és el VPA i què no és

El VerticalPodAutoscaler és un component que:

  1. Observa el consum històric de CPU i memòria dels contenidors d'un conjunt de pods.
  2. Calcula una recomanació de requests (i opcionalment limits) basada en percentils d'aquest històric, amb decaïment exponencial per donar més pes al que és recent.
  3. Opcionalment aplica aquesta recomanació, recreant els pods amb els nous valors.

I molt important, el que no és:

El VPA... ...però no
Ajusta la mida de cada pod Canvia el nombre de pods (això és l'HPA)
Respon al que l'aplicació consumeix Respon a la càrrega entrant o a la latència
Corregeix un dimensionament equivocat Absorbeix una punta de trànsit
Treballa en escala d'hores i dies Reacciona en segons

Aquesta distinció és la clau per entendre la relació amb l'HPA. L'HPA és un mecanisme de resposta a la càrrega, amb constant de temps de segons. El VPA és un mecanisme de correcció del dimensionament, amb constant de temps de dies. No competeixen en el mateix terreny... llevat que tots dos mirin la mateixa mètrica, que és el conflicte de l'apartat 11.

Una manera útil de recordar-ho: l'HPA respon la pregunta «quants?»; el VPA respon la pregunta «de quina mida?».

  1. No forma part del nucli: instal·lació

A diferència de l'HPA, que viu dins del kube-controller-manager i està disponible a qualsevol clúster, el VPA és un component extern que cal instal·lar. Viu al repositori kubernetes/autoscaler, al costat del Cluster Autoscaler que veurem a 09-03.

Això té conseqüències pràctiques que convé conèixer abans de començar:

  • Has de comprovar la compatibilitat de versions entre el VPA i el teu Kubernetes. La taula de compatibilitat és al repositori; fer servir una versió desajustada produeix errors foscos.
  • Instal·la tres Deployments i un grapat de CRDs, RBAC i un webhook d'admissió.
  • A Kubernetes gestionat (10-06), molts proveïdors l'ofereixen com a complement activable amb un interruptor (GKE el té integrat des de fa anys). Si ets al núvol, mira primer si el teu proveïdor te'l dona fet.

Instal·lació amb l'script oficial

# 1. Clonar el repositori de l'autoescalador a la branca que correspongui
git clone -b vpa-release-1.2 https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler

# 2. Executar l'instal·lador. Crea CRDs, RBAC, els tres deployments i el webhook.
./hack/vpa-up.sh

Requisit previ important: el VPA necessita metrics-server (el mateix de 07-02) per a l'updater i l'admission-controller, i el seu recomanador pot llegir a més de Prometheus si el configures. A minikube:

minikube -p rutas-norte addons enable metrics-server

Verificació

kubectl get pods -n kube-system -l app in (vpa-recommender,vpa-updater,vpa-admission-controller)
NAME                                        READY   STATUS    RESTARTS   AGE
vpa-admission-controller-6b8f9d7c4d-x2klm   1/1     Running   0          2m
vpa-recommender-7d9c5f8b6b-4mnpq            1/1     Running   0          2m
vpa-updater-5f7b8c9d6a-9wrtz                1/1     Running   0          2m

I els CRDs registrats:

kubectl get crd | grep autoscaling.k8s.io
verticalpodautoscalers.autoscaling.k8s.io               2026-04-12T09:14:22Z
verticalpodautoscalercheckpoints.autoscaling.k8s.io     2026-04-12T09:14:22Z

El segon CRD, VerticalPodAutoscalerCheckpoint, mereix una nota: allà el recomanador persisteix l'histograma de consum cada pocs minuts. Sense ell, un reinici del recomanador perdria tot l'històric acumulat i caldria tornar a començar el període d'observació. Quan algú et digui «he reiniciat el VPA i ara les recomanacions són estranyes», el checkpoint és el primer que cal mirar.

Per desinstal·lar:

./hack/vpa-down.sh

  1. Els tres components i com col·laboren

El VPA no és un procés, en són tres, amb responsabilitats ben separades. Entendre aquesta separació explica gairebé tot el seu comportament.

flowchart TD
    subgraph Fonts["Fonts de dades"]
        MS[metrics-server<br/>metrics.k8s.io]
        PR[Prometheus<br/>opcional, historic llarg]
    end

    subgraph VPA["Components del VPA"]
        REC[Recomanador<br/>vpa-recommender]
        UPD[Actualitzador<br/>vpa-updater]
        ADM[Controlador d'admissio<br/>vpa-admission-controller]
    end

    OBJ[(Objecte VPA<br/>status.recommendation)]
    CKP[(Checkpoint<br/>histograma persistit)]
    API[Servidor d'API]
    POD[Pods d'api-reserves]

    MS --> REC
    PR -.-> REC
    REC -->|escriu recomanacio| OBJ
    REC <-->|desa i restaura| CKP
    OBJ -->|llegeix| UPD
    OBJ -->|llegeix| ADM
    UPD -->|API de desallotjament<br/>nomes si updateMode ho permet| API
    API -->|recrea el pod| POD
    POD -->|peticio de creacio| ADM
    ADM -->|MUTA els recursos<br/>abans de persistir| API

    style REC fill:#cde,stroke:#369
    style UPD fill:#fda,stroke:#960
    style ADM fill:#cfc,stroke:#393

El recomanador (vpa-recommender)

És el cervell. La seva feina:

  • Llegeix el consum de CPU i memòria de tots els pods que coincideixen amb algun targetRef d'un objecte VPA.
  • Manté, per contenidor, un histograma amb decaïment exponencial: les mostres velles pesen menys que les recents. La semivida per defecte és de 24 hores, cosa que significa que una mostra de fa un dia val la meitat que una d'ara.
  • Calcula percentils sobre aquest histograma i escriu la recomanació a status.recommendation de l'objecte VPA.
  • Persisteix l'histograma en un VerticalPodAutoscalerCheckpoint per sobreviure a reinicis.

Els percentils que fa servir per defecte:

Recurs Percentil per a target Marge afegit
CPU p90 de l'histograma +15 % de seguretat
Memòria Màxim de les finestres de 24 h dels últims 8 dies +15 % de seguretat

L'asimetria és deliberada i molt assenyada. Quedar-se curt de CPU produeix lentitud; quedar-se curt de memòria produeix OOMKilled. Per això la memòria es calcula amb el pic i la CPU amb un percentil alt però no extrem. Un contenidor pot sobreviure perfectament a pics de CPU per sobre del seu requests (l'hi presta el node si n'hi ha); a un pic de memòria per sobre del seu limit, no.

Detall addicional: el recomanador observa els esdeveniments OOMKilled i, quan en detecta un, puja la recomanació de memòria de cop (aproximadament un 20 %) sense esperar que l'histograma s'ajusti. És una realimentació directa des de l'error.

El recomanador funciona sempre, sigui quin sigui l'updateMode. Fins i tot en mode Off està treballant i publicant recomanacions. Això és precisament el que fa útil el mode assessor.

L'actualitzador (vpa-updater)

És el braç executor. Cada minut:

  • Compara els recursos actuals de cada pod amb la recomanació vigent.
  • Si la diferència supera un llindar (per defecte, si el valor actual és fora del rang [lowerBound, upperBound]), decideix que el pod cal recrear-lo.
  • Desallotja el pod fent servir l'API de desallotjament (eviction), la mateixa que fa servir kubectl drain. Això és fonamental: l'actualitzador respecta els PodDisruptionBudgets (09-05). Si el teu PDB no permet el desallotjament, el VPA espera pacientment.
  • No modifica el pod: el mata. El Deployment el recrea, i el nou pod passa pel controlador d'admissió.

Dues salvaguardes que eviten desastres:

  • No desallotja un pod si no han passat almenys 12 hores des de la seva creació (evita cicles de recreació).
  • Mai desallotja tots els pods d'un mateix controlador alhora.

L'actualitzador només actua en els modes Recreate i Auto. A Off i Initial està inactiu.

El controlador d'admissió (vpa-admission-controller)

És un MutatingAdmissionWebhook registrat a l'API. Quan arriba una petició de creació de pod:

  • Comprova si aquest pod coincideix amb algun VPA actiu.
  • Si sí, reescriu els requests (i els limits, mantenint la proporció) del pod abans que es persisteixi a etcd.
  • El pod neix ja amb els valors recomanats. El planificador el col·loca amb aquests números.

Aquest és el component que fa que la recreació funcioni. Sense ell, l'actualitzador mataria un pod i el Deployment el recrearia amb els valors originals del manifest: un bucle infinit de desallotjaments inútils.

Una conseqüència important i sovint sorprenent: el VPA no modifica el Deployment. Si fas kubectl get deployment api-reserves -o yaml continuaràs veient requests.cpu: 500m. Els pods reals tindran altres valors. La mutació passa al nivell del pod, en admissió. Per veure els valors efectius:

kubectl get pod api-reserves-7c9d4f8b6d-2mk8p -n rutas-norte-pro \
  -o jsonpath='{.spec.containers[*].resources}' | python3 -m json.tool

Un risc derivat: si el webhook d'admissió està caigut o mal configurat, segons la seva failurePolicy pot bloquejar la creació de tots els pods del clúster o deixar que passin sense mutar. L'instal·lador oficial el posa a Ignore per defecte, cosa que és el segur.

  1. L'objecte VerticalPodAutoscaler camp a camp

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
spec:
  # A QUIN recurs s'aplica. Mateix format que el scaleTargetRef de l'HPA.
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-reserves

  # QUE fa amb la recomanacio.
  updatePolicy:
    updateMode: "Off"                 # Off | Initial | Recreate | Auto
    minReplicas: 2                    # No desallotja si quedarien menys de N repliques

  # LIMITS de la recomanacio, per contenidor.
  resourcePolicy:
    containerPolicies:
      - containerName: api
        minAllowed:
          cpu: 200m
          memory: 256Mi
        maxAllowed:
          cpu: "2"
          memory: 2Gi
        controlledResources: ["cpu", "memory"]
        controlledValues: RequestsAndLimits
      - containerName: exportador-metriques
        mode: "Off"                   # Aquest contenidor queda exclos
Camp Tipus Què fa
targetRef objecte Deployment, StatefulSet, DaemonSet, CronJob... qualsevol controlador de pods
updatePolicy.updateMode cadena Què fer amb la recomanació (apartat 6)
updatePolicy.minReplicas enter L'actualitzador no desallotja si el controlador té menys rèpliques que això. Per defecte 2
resourcePolicy.containerPolicies[].containerName cadena Nom del contenidor, o "*" per a tots
.minAllowed / .maxAllowed recursos Terra i sostre de la recomanació
.mode cadena Auto (defecte) o Off per excloure aquest contenidor
.controlledResources llista ["cpu"], ["memory"] o tots dos
.controlledValues cadena RequestsAndLimits (defecte) o RequestsOnly

Nota sobre targetRef: a diferència de l'HPA, no requereix que l'objecte exposi el subrecurs /scale. Per això el VPA sí que es pot aplicar a un DaemonSet, mentre que l'HPA no. Això és útil de debò: els agents de Fluentd i de Falco que vam desplegar als mòduls 7 i 8 són DaemonSets, i els seus requests són igual d'arbitraris que la resta.

  1. Els modes d'actualització: Off, Initial, Recreate i Auto

L'updateMode és la decisió més important de l'objecte.

Mode El recomanador Muta pods nous Desallotja pods vius Risc Ús típic
Off No No Cap Assessoria. El més usat en producció
Initial No Baix Aplicar al següent desplegament, sense reinicis extra
Recreate Mitjà-alt Càrregues que toleren reinicis
Auto Sí (avui = Recreate) Mitjà-alt Igual que Recreate avui

Off — l'assessor

updatePolicy:
  updateMode: "Off"

El recomanador observa i publica a status.recommendation. Res més. Cap pod no es toca, cap manifest no canvia. És informació pura.

Cost operatiu: zero. Risc: zero. Valor: alt. Per això és, amb diferència, el mode més usat en producció seriosa.

Initial — aplicar només en crear

updatePolicy:
  updateMode: "Initial"

El controlador d'admissió muta els pods quan neixen per una altra raó: un desplegament nou, un reinici per caiguda de node, un escalat de l'HPA. L'actualitzador mai desallotja res.

És un punt intermedi interessant: els valors s'apliquen de debò, però sense causar ni un sol reinici addicional. La contrapartida és la lentitud: si api-reserves es desplega cada dues setmanes, les recomanacions triguen dues setmanes a materialitzar-se. I crea un efecte desconcertant: dos pods del mateix Deployment, creats en moments diferents, poden tenir recursos diferents.

Recreate — aplicar desallotjant

updatePolicy:
  updateMode: "Recreate"
  minReplicas: 2

L'actualitzador desallotja activament els pods els recursos dels quals s'han desviat. És autoescalat vertical de debò, amb la conseqüència inevitable: cada ajust és un reinici del pod.

Per a worker-notificacions és acceptable (processa missatges d'una cua, un reinici perd com a molt el missatge en vol, que la cua reentrega). Per a postgres-reserves és una altra història, i ho discutim a l'apartat 12.

minReplicas: 2 és la protecció essencial: l'actualitzador no desallotjarà si el controlador té menys rèpliques que aquest número. Amb una sola rèplica, un desallotjament és una caiguda completa del servei.

Auto — avui és Recreate

updatePolicy:
  updateMode: "Auto"

Semànticament significa «fes servir el millor mecanisme disponible». Avui, a la pràctica, Auto es comporta exactament igual que Recreate a la majoria d'instal·lacions. La intenció declarada del projecte és que, quan el redimensionament en calent sigui estable, Auto el faci servir i deixi de recrear pods.

Consell operatiu: fes servir Recreate explícitament si això és el que vols. Així, quan el significat d'Auto canviï amb una actualització del VPA, el teu comportament no canviarà per sorpresa.

  1. El redimensionament en calent i el seu estat actual

Durant anys, la limitació fonamental de l'escalat vertical a Kubernetes va ser que els recursos d'un contenidor eren immutables: canviar-los exigia recrear el pod.

Això està canviant. La funcionalitat s'anomena In-Place Pod Vertical Scaling (redimensionament vertical de pods en calent), i permet modificar requests i limits d'un contenidor en execució sense recrear el pod, mitjançant el subrecurs /resize.

Com funciona

Cada contenidor pot declarar una política de reinici per recurs:

spec:
  containers:
    - name: api
      image: registry.rutasnorte.example/api-reserves:1.14.2
      resizePolicy:
        - resourceName: cpu
          restartPolicy: NotRequired      # La CPU s'ajusta en calent
        - resourceName: memory
          restartPolicy: RestartContainer # La memoria exigeix reiniciar el contenidor
      resources:
        requests:
          cpu: 500m
          memory: 512Mi
        limits:
          cpu: "1"
          memory: 1Gi

I el canvi s'aplica amb:

kubectl patch pod api-reserves-7c9d4f8b6d-2mk8p -n rutas-norte-pro \
  --subresource resize \
  --patch '{"spec":{"containers":[{"name":"api","resources":{"requests":{"cpu":"800m"}}}]}}'

L'estat del procés queda reflectit al pod:

kubectl get pod api-reserves-7c9d4f8b6d-2mk8p -n rutas-norte-pro -o yaml | grep -A5 resize
  status:
    resize: InProgress
    containerStatuses:
      - name: api
        allocatedResources:
          cpu: 500m
        resources:
          requests:
            cpu: 800m

Estat actual i advertència

Aquesta és la part important per prendre decisions:

  • La funcionalitat ha avançat a beta a Kubernetes 1.33 i continua madurant. A 1.30-1.32 és alpha i requereix activar la feature gate InPlacePodVerticalScaling al servidor d'API i als kubelets.
  • Reduir la memòria en calent no està resolt de manera general: baixar el limit de memòria per sota del que el procés ja té mapat pot provocar un OOMKilled immediat. Per això la política habitual per a memòria és RestartContainer.
  • La integració del VPA amb aquesta funcionalitat existeix però no és completa ni és el camí per defecte.
  • A Kubernetes gestionat (10-06), la feature gate pot no estar disponible: no controles el pla de control.

Advertència explícita: no construeixis l'estratègia de capacitat de producció sobre el redimensionament en calent encara. És la direcció correcta i en un parell de versions serà la manera natural de fer les coses, però avui:

  1. Pot no estar disponible al teu clúster.
  2. El seu comportament amb memòria té arestes.
  3. El VPA no l'explota plenament.

Coneix-la, prova-la a rutas-norte-dev, segueix-li la pista. A rutas-norte-pro, el flux de treball de l'apartat 10.

  1. resourcePolicy: acotar, excloure i controlar

Deixar que un recomanador escrigui números sense baranes és una mala idea. El resourcePolicy és on poses el criteri humà.

Acotar mínims i màxims

resourcePolicy:
  containerPolicies:
    - containerName: api
      minAllowed:
        cpu: 200m           # Mai per sota: l'arrencada de Node.js necessita CPU
        memory: 256Mi       # Mai per sota: el heap base ja ocupa aixo
      maxAllowed:
        cpu: "2"            # Mai per sobre: cabria en pocs nodes
        memory: 2Gi         # Mai per sobre: sospitaria d'una fuita de memoria

Per què cada barana:

minAllowed. El recomanador es basa en el consum observat. Si un servei passa la nit sense trànsit, el seu p90 de CPU és gairebé zero, i sense minAllowed la recomanació baixaria a 15m. Quan arriba el trànsit del matí, el pod s'ofega arrencant. El minAllowed codifica «això és el mínim per arrencar i respondre decentment, encara que les mètriques diguin una altra cosa».

maxAllowed. Dos motius. Primer, planificació: un pod de 8 nuclis només cap en nodes grans, i si els teus nodes són de 4 nuclis aquest pod es queda Pending per sempre. Segon, detecció d'anomalies: si la recomanació arriba al sostre, hi ha alguna cosa a investigar (una fuita de memòria, una consulta patològica). El maxAllowed converteix un problema silenciós en un senyal.

Regla pràctica per al maxAllowed: mai per sobre del que cap en un node real, amb marge per al sistema. En nodes de 4 nuclis i 16 GiB, un maxAllowed raonable ronda els 3 nuclis i 12Gi.

Excloure un contenidor

resourcePolicy:
  containerPolicies:
    - containerName: exportador-metriques
      mode: "Off"

mode: "Off" a nivell de contenidor el treu del VPA per complet: no es recomana ni es muta. Quan fer-ho:

  • Sidecars amb recursos ja ben coneguts i estables. Un exportador de Prometheus consumeix 5m i 20Mi avui, demà i sempre. No necessita un recomanador.
  • Sidecars de malla de serveis (el proxy d'Istio, 08-04). Solen tenir la seva pròpia gestió de recursos i el VPA s'hi trepitja.
  • Contenidors amb requests deliberadament alts per raons que les mètriques no veuen: reservar CPU per a pics de latència crítica, per exemple.

Compte amb un detall: si exclous tots els contenidors del pod, el VPA no farà res, cosa que és correcta, però l'objecte continua existint i confon. Val més esborrar-lo.

Comodí per a tots els contenidors

resourcePolicy:
  containerPolicies:
    - containerName: "*"
      minAllowed:
        cpu: 10m
        memory: 32Mi
      maxAllowed:
        cpu: "1"
        memory: 1Gi
    - containerName: api          # La regla especifica GUANYA sobre el comodi
      maxAllowed:
        cpu: "2"
        memory: 2Gi

controlledResources i controlledValues

- containerName: api
  controlledResources: ["memory"]     # Nomes memoria; la CPU la deixa en pau
  controlledValues: RequestsOnly      # Nomes requests; no toca els limits

controlledResources és la peça clau per conviure amb l'HPA: si l'HPA escala per CPU, el VPA s'ha de limitar a la memòria (apartat 11).

controlledValues decideix si el VPA toca també els limits:

Valor Comportament Conseqüència en QoS
RequestsAndLimits (defecte) Ajusta tots dos, mantenint la proporció original Preserva la QoS: si era Guaranteed, ho continua sent
RequestsOnly Només ajusta requests, deixa els limits com estan Pot canviar la QoS de Guaranteed a Burstable

El detall de la proporció mereix un exemple. Si el manifest té requests.cpu: 500m i limits.cpu: 1000m (proporció 1:2) i el VPA recomana requests.cpu: 800m, amb RequestsAndLimits posarà limits.cpu: 1600m per mantenir l'1:2. És un comportament assenyat però que sorprèn: el VPA et pot pujar els limits per sobre del que esperaves, i això interactua amb la ResourceQuota del namespace (apartat 13).

  1. Llegir una recomanació: target, lowerBound, upperBound, uncappedTarget

Aquí hi ha el valor real del VPA en mode Off. Vegem una recomanació completa.

kubectl describe vpa api-reserves -n rutas-norte-pro
Name:         api-reserves
Namespace:    rutas-norte-pro
API Version:  autoscaling.k8s.io/v1
Kind:         VerticalPodAutoscaler
Spec:
  Target Ref:
    API Version:  apps/v1
    Kind:         Deployment
    Name:         api-reserves
  Update Policy:
    Update Mode:  Off
Status:
  Conditions:
    Type:                  RecommendationProvided
    Status:                True
    Last Transition Time:  2026-05-11T08:22:14Z
  Recommendation:
    Container Recommendations:
      Container Name:  api
      Lower Bound:
        Cpu:     287m
        Memory:  573Mi
      Target:
        Cpu:     412m
        Memory:  792Mi
      Uncapped Target:
        Cpu:     412m
        Memory:  792Mi
      Upper Bound:
        Cpu:     1140m
        Memory:  1489Mi
      Container Name:  exportador-metriques
      Lower Bound:
        Cpu:     4m
        Memory:  14Mi
      Target:
        Cpu:     6m
        Memory:  21Mi
      Uncapped Target:
        Cpu:     6m
        Memory:  21Mi
      Upper Bound:
        Cpu:     18m
        Memory:  48Mi

Què significa cada camp

Camp Significat Què fer-ne
target El valor recomanat. L'estimació del recomanador del que el contenidor necessita Aquest és el que portes al manifest
lowerBound Per sota d'això, el contenidor està clarament mancat de recursos Si el teu valor actual és per sota, actua ja
upperBound Per sobre d'això, estàs malgastant de forma clara Si el teu valor actual és per sobre, estàs pagant de més
uncappedTarget El target abans d'aplicar minAllowed/maxAllowed Si difereix del target, les teves baranes estan retallant

Els límits lowerBound i upperBound no són percentils del consum: són l'interval de confiança del recomanador. Com menys històric tingui, més ample serà. Un VPA amb dues hores de dades donarà un upperBound enorme; amb dues setmanes, un interval estret. L'amplada de l'interval és un indicador directe de fins a quin punt et pots refiar de la recomanació.

I són també el criteri d'actuació de l'actualitzador: en modes Recreate/Auto, el pod es desallotja quan el seu valor actual surt del rang [lowerBound, upperBound]. Dins del rang, es deixa en pau. Aquesta banda és l'equivalent vertical de la banda de tolerància de l'HPA.

Interpretació per a api-reserves

Contrastem-ho amb el que tenim al manifest:

Recurs Actual lowerBound target upperBound Veredicte
CPU (api) 500m 287m 412m 1140m Dins del rang, però un 21 % de més. Ajust opcional
Memòria (api) 512Mi 573Mi 792Mi 1489Mi Per sota del lowerBound. Corregir ja
CPU (exportador) 20m 4m 6m 18m Per sobre de l'upperBound. Sobredimensionat
Memòria (exportador) 32Mi 14Mi 21Mi 48Mi Dins del rang, ajust menor

La fila crítica és la memòria d'api: 512Mi és per sota del lowerBound de 573Mi. El VPA està dient, amb dades de dues setmanes, que aquest contenidor té menys memòria reservada de la que necessita per funcionar amb seguretat. És exactament el risc que intuíem a l'apartat 1, ara quantificat.

I l'exportador confirma l'altra intuïció: 20m reservats per a alguna cosa que necessita 6m. Amb 30 rèpliques d'api-reserves al pic, són 420m de CPU reservats i mai usats: gairebé mig nucli bloquejat per al planificador.

uncappedTarget: el senyal que les teves baranes pitgen

Suposem que posem maxAllowed.memory: 512Mi al resourcePolicy. La recomanació quedaria:

      Target:
        Memory:  512Mi          <- Retallat per maxAllowed
      Uncapped Target:
        Memory:  792Mi          <- El que el recomanador volia de debo

Quan target i uncappedTarget difereixen, el teu maxAllowed (o minAllowed) està mossegant. I aquí cal pensar-hi dues vegades, perquè hi ha dues interpretacions oposades i totes dues són plausibles:

  1. La barana està mal posada. Va ser un número conservador de fa mesos i l'aplicació ha crescut legítimament. Puja-la.
  2. La barana està fent la seva feina. Hi ha una fuita de memòria i el consum creix sense parar. El maxAllowed és l'única cosa que impedeix que aquest pod es mengi un node sencer.

Distingir entre les dues requereix mirar la tendència a Grafana (07-04): si la memòria creix de manera monòtona i mai baixa, és fuita; si s'estabilitza en un altiplà més alt, és creixement legítim. Un uncappedTarget que s'allunya del target mes a mes és una fuita de memòria fins que es demostri el contrari.

Extreure la recomanació per línia d'ordres

Per automatitzar el flux de treball:

kubectl get vpa api-reserves -n rutas-norte-pro \
  -o jsonpath='{range .status.recommendation.containerRecommendations[*]}{.containerName}{"\t"}{.target.cpu}{"\t"}{.target.memory}{"\n"}{end}'
api	412m	792Mi
exportador-metriques	6m	21Mi

Sortida neta, apta per posar-la en un informe setmanal o en un comentari automàtic d'una petició de canvi.

  1. El mode Off com a assessor: el flux de treball real

Aquí hi ha la pràctica que fan servir els equips seriosos, i mereix explicar-se bé perquè va a contracorrent del que suggereix el nom «autoescalador».

La majoria d'organitzacions amb Kubernetes en producció fan servir el VPA exclusivament en mode Off. No com a pilot automàtic, sinó com a assessor la recomanació del qual revisa una persona abans d'arribar a producció.

Per què

Motiu Explicació
El manifest continua sent la veritat Amb Recreate, el YAML diu 500m i el pod té 412m. Ningú sap quins recursos té el sistema mirant el repositori
Cada ajust és un reinici En mode actiu, el VPA reinicia pods pel seu compte, en qualsevol moment. Un reinici no planificat al pont de maig és intolerable
Compatibilitat amb GitOps Amb Argo CD o Flux (10-05), el VPA actiu crea una divergència permanent entre Git i el clúster. El mateix problema del replicas de 09-01
Revisió humana Un canvi de recursos mereix passar per una revisió de codi. «La memòria d'api-reserves puja de 512Mi a 800Mi» és una decisió amb conseqüències de cost i capacitat
Auditoria El registre de canvis de recursos queda a l'historial de Git, no en logs d'un controlador

El flux de treball, pas a pas

flowchart TD
    A[1. Crear el VPA en mode Off] --> B[2. Esperar 2 setmanes<br/>incloent un cicle de negoci complet]
    B --> C[3. Extreure la recomanacio<br/>kubectl get vpa]
    C --> D{4. target dins de<br/>les baranes?}
    D -- No --> E[Investigar: fuita o<br/>creixement legitim?]
    E --> C
    D -- Si --> F[5. Comparar amb el manifest actual]
    F --> G{6. Diferencia > 20 %?}
    G -- No --> H[No tocar. Tornar-hi en un mes]
    G -- Si --> I[7. Peticio de canvi amb els nous valors<br/>i la recomanacio a la descripcio]
    I --> J[8. Revisio de l'equip]
    J --> K[9. Merge, desplegament en pre, validacio]
    K --> L[10. Desplegament en pro]
    L --> M[11. Verificar a Grafana:<br/>menys throttling? menys OOM?]
    M --> B

Pas 2, dues setmanes i per què. L'histograma del recomanador té semivida de 24 hores, així que amb dos o tres dies ja hi ha senyal. Però dues setmanes cobreixen dos cicles setmanals complets, inclosos caps de setmana. A Rutas Norte això importa molt: el trànsit de dissabte no s'assembla al de dimecres. I si hi ha un tancament mensual o un esdeveniment periòdic, cal cobrir-lo també.

Pas 6, el llindar del 20 %. Canviar els recursos d'un Deployment implica desplegar-lo. No val la pena fer-ho per un 5 %. El llindar del 20 % és una convenció raonable: per sota, el soroll supera el senyal.

Pas 7, la petició de canvi. Aquí hi ha el cor del mètode. El canvi queda documentat:

Titol: Ajustar recursos d'api-reserves segons recomanacio del VPA

Recomanacio del VPA (14 dies d'observacio, 2026-04-27 a 2026-05-11):

  Contenidor           Actual        Recomanat      Canvi
  api / cpu            500m          412m           -17,6 %
  api / memoria        512Mi         792Mi          +54,7 %
  exportador / cpu     20m           6m             -70,0 %
  exportador / memoria 32Mi          21Mi           -34,4 %

Justificacio:
- La memoria d'"api" estava PER SOTA del lowerBound (573Mi). Risc real
  de desallotjament sota pressio de memoria del node. Es la correccio important.
- La CPU d'"api" baixa un 17,6 %. Compte: aixo afecta l'HPA, que calcula sobre
  el requests. Veure nota mes avall.
- L'exportador estava sobredimensionat x3. Amb 30 repliques al pic son
  420m de CPU alliberats per al planificador.

Nota sobre l'HPA: en baixar requests.cpu de 500m a 412m, l'objectiu absolut
de l'HPA passa de 325m a 268m per pod. L'HPA escalara ABANS que ara amb la
mateixa carrega. Es l'efecte desitjat (els pods estaven sobredimensionats), pero
cal vigilar el nombre de repliques al pic durant la primera setmana.

Validat a rutas-norte-pre amb la prova de carrega de k6 (09-06): sense
throttling, sense OOM, latencia p95 igual o millor.

Aquesta nota sobre l'HPA és exactament el tipus de raonament que es perd quan deixes que un controlador canviï els números pel seu compte.

El VPA en mode Off per a tota la plataforma

Una pràctica molt rendible: un VPA en mode Off sobre tots els Deployments, StatefulSets i DaemonSets de la plataforma, encara que mai els apliquis automàticament. El cost és un objecte per controlador i una mica de memòria al recomanador. El benefici és tenir, en qualsevol moment, la resposta a «està ben dimensionat, això?».

# Generar un VPA en mode Off per a cada Deployment del namespace
for d in $(kubectl get deploy -n rutas-norte-pro -o name | cut -d/ -f2); do
  cat <<EOF | kubectl apply -f -
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: $d
  namespace: rutas-norte-pro
  labels:
    app.kubernetes.io/part-of: rutas-norte
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: $d
  updatePolicy:
    updateMode: "Off"
EOF
done

I un informe setmanal comparant recomanació amb manifest. Aquest informe, revisat cada dilluns en quinze minuts, és probablement l'activitat de més retorn per minut invertit en l'operació d'un clúster.

  1. La incompatibilitat clàssica: VPA i HPA sobre la mateixa mètrica

Aquesta és la trampa que cal conèixer sí o sí.

El conflicte

El VPA i l'HPA no poden governar el mateix recurs (CPU o memòria) sobre la mateixa càrrega de treball. Si ho intentes, es retroalimenten de manera destructiva.

Vegem per què, amb api-reserves i números concrets. Suposem HPA per CPU amb averageUtilization: 65 i VPA en mode Recreate controlant CPU.

Estat inicial: 4 repliques, requests.cpu 500m, objectiu HPA 325m/pod.
Carrega total: 1600m.

t=0    Consum: 400m/pod (1600 / 4). Ratio 400/325 = 1,23.
       HPA -> sostre(4 x 1,23) = 5 repliques.

t=1min 5 repliques, consum 320m/pod. HPA satisfet (ratio 0,98).
       Pero el VPA observa 320m de consum i recomana requests.cpu = 368m
       (320m x 1,15 de marge).

t=15m  El VPA aplica 368m. ARA l'objectiu de l'HPA es 65 % de 368m = 239m.
       El consum real segueix en 320m. Ratio = 320/239 = 1,34.
       HPA -> sostre(5 x 1,34) = 7 repliques.

t=16m  7 repliques, consum 229m/pod. HPA satisfet.
       El VPA observa 229m i recomana requests.cpu = 263m.

t=30m  El VPA aplica 263m. Objectiu de l'HPA = 171m. Consum 229m.
       Ratio 1,34. HPA -> 10 repliques.

t=31m  10 repliques, consum 160m/pod. El VPA recomana 184m...

El bucle és evident: l'HPA redueix el consum per pod afegint rèpliques; el VPA interpreta aquest consum menor com que els pods són grans de més i baixa el requests; en baixar el requests, l'objectiu absolut de l'HPA baixa, i l'HPA torna a escalar. La càrrega total no ha canviat ni un vat, però hem passat de 4 rèpliques de 500m a 10 rèpliques de 263m, i el procés continua.

Cada iteració implica recrear pods (VPA) i crear/destruir pods (HPA). És una espiral d'inestabilitat que a més fa molt difícil el diagnòstic, perquè cada component per separat sembla estar comportant-se bé.

flowchart LR
    A[Carrega constant] --> B[L'HPA afegeix repliques]
    B --> C[Baixa el consum PER POD]
    C --> D[El VPA baixa el requests]
    D --> E[Baixa l'objectiu absolut de l'HPA]
    E --> B
    style B fill:#fdd,stroke:#c00
    style D fill:#fdd,stroke:#c00

Les combinacions vàlides

L'HPA escala per El VPA controla Vàlid? Comentari
CPU CPU NO El bucle descrit a dalt
Memòria Memòria NO Mateix bucle
CPU Només memòria Combinació recomanada. Eixos independents
Memòria Només CPU Sí, tècnicament Però escalar per memòria ja és mala idea (09-01)
Mètrica personalitzada / externa CPU i memòria La millor combinació de totes
— (sense HPA) CPU i memòria El cas simple

La configuració recomanada per a api-reserves, si volguessis VPA actiu:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-reserves-memoria
  namespace: rutas-norte-pro
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-reserves
  updatePolicy:
    updateMode: "Recreate"
    minReplicas: 2
  resourcePolicy:
    containerPolicies:
      - containerName: api
        # CLAU: nomes memoria. La CPU la governa l'HPA de 09-01.
        controlledResources: ["memory"]
        minAllowed:
          memory: 512Mi
        maxAllowed:
          memory: 2Gi
      - containerName: exportador-metriques
        controlledResources: ["memory"]
        minAllowed:
          memory: 16Mi
        maxAllowed:
          memory: 128Mi

controlledResources: ["memory"] és la línia que fa que la convivència sigui possible. Sense ella, tens el bucle.

La combinació òptima: HPA per mètrica de negoci + VPA complet

La configuració més elegant apareix quan l'HPA escala per una mètrica que no és CPU ni memòria: peticions per segon, longitud d'una cua. Llavors:

  • L'HPA decideix quants pods, segons la demanda del negoci.
  • El VPA decideix de quina mida, segons el consum observat.
  • No hi ha realimentació: la mètrica de negoci no depèn del requests.

És exactament el que habilita KEDA a 09-04. Quan worker-notificacions escali per longitud de cua, un VPA complet sobre ell serà perfectament segur i molt útil.

El VPA en mode Off és sempre segur

I això convé subratllar-ho: el conflicte només existeix quan el VPA actua. En mode Off, el VPA no canvia res, així que pot conviure amb qualsevol HPA sobre qualsevol mètrica sense el més mínim risc. La seva recomanació de CPU serà una mica pessimista (mesurarà pods als quals l'HPA ja ha alleujat la càrrega), però continuarà sent informació vàlida.

Aquest és un altre argument a favor del mode Off: elimina tota una classe de problemes d'un cop.

  1. Aplicació a Rutas Norte, component a component

Decisions concretes i justificades per a cada peça de la plataforma.

api-reserves i botiga-web: VPA en mode Off

Tots dos tenen HPA per CPU des de 09-01. Posem VPA en mode Off, com a assessors.

# k8s/entorns/pro/vpa-api-reserves.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-reserves
  updatePolicy:
    # MODE OFF. api-reserves te un HPA per CPU (09-01). Un VPA actiu sobre
    # CPU provocaria el bucle de realimentacio. I encara que limitessim el VPA a
    # memoria, no volem reinicis no planificats al component que
    # sosté la venda. Aqui el VPA es un ASSESSOR: revisem la seva recomanacio
    # cada dilluns i la portem al manifest en una peticio de canvi.
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: api
        minAllowed:
          cpu: 200m
          memory: 256Mi
        maxAllowed:
          cpu: "2"
          memory: 2Gi
      - containerName: exportador-metriques
        minAllowed:
          cpu: 5m
          memory: 16Mi
        maxAllowed:
          cpu: 100m
          memory: 128Mi
# k8s/entorns/pro/vpa-botiga-web.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
  labels:
    app: botiga-web
    app.kubernetes.io/part-of: rutas-norte
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: botiga-web
  updatePolicy:
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: nginx
        minAllowed:
          cpu: 50m
          memory: 64Mi
        maxAllowed:
          cpu: 500m
          memory: 512Mi

Amb els mesuraments de l'apartat 1, botiga-web és candidat clar a baixar de 200m a uns 70m de CPU. Amb 15 rèpliques al pic, això allibera gairebé dos nuclis: menys nodes que arrencar (09-03) i menys factura.

worker-notificacions: VPA en mode Recreate

Aquí sí que l'activem, i és el millor candidat de la plataforma.

# k8s/entorns/pro/vpa-worker-notificacions.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: worker-notificacions
  namespace: rutas-norte-pro
  labels:
    app: worker-notificacions
    app.kubernetes.io/part-of: rutas-norte
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker-notificacions
  updatePolicy:
    # MODE RECREATE. Aquest component consumeix missatges d'una cua: un reinici
    # no perd feina (la cua reentrega el missatge en vol) i no hi ha cap
    # usuari esperant una resposta. Es la carrega que MILLOR tolera el VPA actiu.
    updateMode: "Recreate"
    # Mai desallotjar si quedessin menys de 2 repliques: la cua no pot quedar-se
    # sense consumidors ni un minut durant el pont de maig.
    minReplicas: 2
  resourcePolicy:
    containerPolicies:
      - containerName: worker
        minAllowed:
          cpu: 50m
          memory: 128Mi
        maxAllowed:
          cpu: "1"
          memory: 1Gi
        # CPU i memoria, totes dues: a 09-04 aquest component passara a escalar per
        # LONGITUD DE CUA amb KEDA, que es una metrica externa. No hi ha conflicte
        # amb la CPU, aixi que el VPA pot governar-la sense problema.
        controlledResources: ["cpu", "memory"]
      - containerName: exportador-metriques
        mode: "Off"

informes-ocupacio: VPA sobre el CronJob

El cas més rendible de tots. Recordem: demana 500m i necessita 1750m. S'estrangula, triga cinc vegades més del que hauria i algunes nits no acaba.

# k8s/entorns/pro/vpa-informes-ocupacio.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: informes-ocupacio
  namespace: rutas-norte-pro
  labels:
    app: informes-ocupacio
    app.kubernetes.io/part-of: rutas-norte
spec:
  targetRef:
    apiVersion: batch/v1
    kind: CronJob            # El VPA suporta CronJob directament
    name: informes-ocupacio
  updatePolicy:
    # MODE INITIAL, no Recreate. Un Job es efimer: no te sentit desallotjar-lo
    # a mitja execucio (perdriem la feina feta i comencaria de zero).
    # Amb Initial, cada execucio NOVA neix amb els recursos correctes.
    updateMode: "Initial"
  resourcePolicy:
    containerPolicies:
      - containerName: generador
        minAllowed:
          cpu: 200m
          memory: 256Mi
        maxAllowed:
          cpu: "4"
          memory: 4Gi        # Sostre generos: es un proces per lots que se'n
                             # beneficia de tot el que li donis, i s'executa de
                             # matinada quan el cluster esta buit
        controlledResources: ["cpu", "memory"]

Initial és l'elecció correcta per a treballs per lots, i val la pena entendre per què: un Job que es desallotja als deu minuts d'una execució de trenta perd tota la feina i comença de nou. Amb Initial, cada execució nocturna neix amb els recursos que el recomanador ha après de les execucions anteriors. El VPA aprèn de la nit de dilluns per a la nit de dimarts.

Efecte esperat: de 40 minuts a uns 9. I com que s'executa a les 3 de la matinada, amb el clúster buit, aquests recursos extra no li treuen res a ningú.

postgres-reserves: la decisió difícil

Aquí cal pensar. Les dades:

  • És un StatefulSet amb una sola rèplica primària.
  • Té QoS Guaranteed (requests = limits), decisió deliberada del mòdul 3 perquè sigui l'últim candidat al desallotjament.
  • Un reinici implica tallar totes les connexions, perdre la memòria cau de pàgines de PostgreSQL i uns segons d'indisponibilitat total de la plataforma.
  • Guarda dades personals de clients: qualsevol risc de corrupció és inacceptable.

Decisió: VPA en mode Off, amb controlledValues: RequestsAndLimits.

# k8s/entorns/pro/vpa-postgres-reserves.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: postgres-reserves
  namespace: rutas-norte-pro
  labels:
    app: postgres-reserves
    app.kubernetes.io/part-of: rutas-norte
spec:
  targetRef:
    apiVersion: apps/v1
    kind: StatefulSet
    name: postgres-reserves
  updatePolicy:
    # MODE OFF, sense discussio. Raons:
    #
    # 1. UNA SOLA REPLICA PRIMARIA. Un desallotjament es una caiguda total de la
    #    plataforma. Ni tan sols minReplicas ens protegeix: amb 1 replica
    #    l'updater no actuaria, pero no volem dependre d'aixo.
    #
    # 2. QoS GUARANTEED. Amb controlledValues: RequestsAndLimits el VPA
    #    mante la proporcio requests:limits, que aqui es 1:1, aixi que la
    #    QoS es preservaria. Pero es una propietat massa important per
    #    deixar-la en mans d'un controlador.
    #
    # 3. EL REINICI NO ES GRATIS. PostgreSQL perd la cache de pagines i les
    #    primeres consultes despres de l'arrencada van a disc. La latencia empitjora
    #    durant minuts.
    #
    # 4. EL DIMENSIONAMENT D'UNA BASE DE DADES NO ES DEDUEIX DEL CONSUM. El
    #    shared_buffers, el work_mem i l'effective_cache_size es configuren
    #    en harmonia amb la memoria del contenidor. Canviar la memoria sense
    #    tocar postgresql.conf no millora res, o empitjora.
    #
    # Tot i aixi el VPA es MOLT util aqui: ens avisara si ens estem quedant
    # curts abans que es manifesti com a incident.
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: postgres
        minAllowed:
          cpu: "1"
          memory: 2Gi
        maxAllowed:
          cpu: "4"
          memory: 8Gi
        # RequestsAndLimits mante la proporcio 1:1 i per tant la QoS
        # Guaranteed, per al dia que decidim aplicar la recomanacio a ma.
        controlledValues: RequestsAndLimits
      - containerName: exportador-postgres
        minAllowed:
          cpu: 10m
          memory: 32Mi
        maxAllowed:
          cpu: 100m
          memory: 128Mi

El punt 4 del comentari mereix èmfasi, perquè és l'argument de fons: el dimensionament d'una base de dades és una decisió de configuració, no d'observació. Pujar la memòria del contenidor sense ajustar shared_buffers en harmonia amb ella no millora el rendiment: només malgasta memòria. El VPA no sap res de postgresql.conf. Un operador de PostgreSQL (06-07) sí, i aquest és el camí correcte per escalar una base de dades.

Taula resum de les decisions

Component Mode controlledResources Justificació
api-reserves Off tots dos (assessor) HPA per CPU; sense reinicis no planificats al component crític
botiga-web Off tots dos (assessor) HPA per CPU; estalvi clar pendent d'aplicar a mà
worker-notificacions Recreate cpu, memory Tolera reinicis; escalarà per cua (09-04), sense conflicte
informes-ocupacio Initial cpu, memory Job efímer; no desallotjar a mitges; l'estalvi de temps és enorme
postgres-reserves Off tots dos (assessor) Una rèplica, QoS Guaranteed, reinici car, dimensionament lligat a la configuració
redis-cache Off tots dos (assessor) Memòria cau amb estat; un reinici la buida i descarrega tota la càrrega sobre PostgreSQL

  1. Interacció amb LimitRange i ResourceQuota

El VPA no viu sol. Al mòdul 3 vam posar baranes de namespace que interactuen amb ell de formes que convé anticipar.

LimitRange

Recordem el LimitRange de rutas-norte-pro:

apiVersion: v1
kind: LimitRange
metadata:
  name: limits-per-defecte
  namespace: rutas-norte-pro
spec:
  limits:
    - type: Container
      default:
        cpu: 500m
        memory: 512Mi
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      min:
        cpu: 50m
        memory: 64Mi
      max:
        cpu: "4"
        memory: 8Gi

Interaccions:

  1. El max del LimitRange guanya sobre el maxAllowed del VPA. Si el VPA recomana 6 nuclis i el LimitRange topa en 4, el pod mutat serà rebutjat en admissió. El VPA és conscient dels LimitRange i ajusta la seva recomanació, però si els canvies després de crear el VPA, pot haver-hi un desfasament.
  2. El min pot elevar una recomanació baixa. Si el VPA recomana 20m i el LimitRange exigeix un mínim de 50m, el pod acabarà amb 50m.
  3. Els default desapareixen de l'equació quan el VPA muta el pod, perquè el pod ja arriba amb valors explícits.

Regla pràctica: mantén el maxAllowed del VPA per sota del max del LimitRange. Així la retallada es veu a uncappedTarget (informació útil) en lloc de produir un rebuig en admissió (error confús).

ResourceQuota

La ResourceQuota és més traïdora, perquè l'error apareix tard i en un altre lloc.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-pro
  namespace: rutas-norte-pro
spec:
  hard:
    requests.cpu: "40"
    requests.memory: 80Gi
    limits.cpu: "80"
    limits.memory: 160Gi

El problema sorgeix de la interacció entre HPA i VPA sobre la quota:

Situacio:  api-reserves, requests.cpu 500m, HPA amb maxReplicas 30.
Consum maxim de quota: 30 x 500m = 15 nuclis. Cap en els 40 de la quota.

El VPA recomana pujar a 800m (l'aplicacio ha crescut).
S'aplica.

Consum maxim ARA: 30 x 800m = 24 nuclis.
Sumant botiga-web, worker-notificacions, postgres i redis: 43 nuclis.
LA QUOTA SON 40.

Resultat: durant el pont de maig, l'HPA intenta crear la replica numero 26
i el ReplicaSet rep:
  Error creating: pods "api-reserves-..." is forbidden:
  exceeded quota: quota-pro, requested: requests.cpu=800m,
  used: requests.cpu=39.4, limited: requests.cpu=40

L'HPA marca ScalingLimited. La plataforma es queda curta AL PIC.

L'error no apareix quan s'aplica el VPA: apareix setmanes després, en el pitjor moment possible, i el missatge d'error és als esdeveniments del ReplicaSet, no a l'HPA ni al VPA. És un error diferit i mal senyalitzat.

Comprovació obligatòria en aplicar qualsevol recomanació del VPA:

Σ (requests recomanat × maxReplicas de l'HPA)  ≤  ResourceQuota × 0,8

El factor 0,8 deixa marge per a pods efímers, treballs i desplegaments (que durant un rolling update consumeixen quota de la versió vella i la nova alhora).

Càlcul per a Rutas Norte amb la recomanació:

Component requests.cpu rec. maxReplicas CPU màxima
api-reserves (api) 412m 30 12,36
api-reserves (exportador) 6m 30 0,18
botiga-web 70m 15 1,05
worker-notificacions 120m 20 2,40
postgres-reserves 2000m 1 2,00
redis-cache 300m 1 0,30
Total 18,29

18,29 contra una quota de 40, amb marge del 80 % (32). Hi cap amb folgança. I fixa't que la recomanació del VPA redueix el consum màxim respecte a l'escenari actual (que seria 15 + 3 + ... ≈ 21), perquè botiga-web i l'exportador estaven sobredimensionats. Ajustar bé els requests no només estalvia diners: dona marge per escalar més.

Un avís sobre controlledValues: RequestsAndLimits

Com hem vist, el VPA manté la proporció requests:limits. Si la teva proporció és 1:2 i el VPA puja el requests un 50 %, el limit puja també un 50 %. I la ResourceQuota té una línia limits.cpu separada que també es pot esgotar. Vigila-les totes dues.

  1. Els límits del VPA

Acabem amb el que el VPA no pot fer. Conèixer les limitacions evita esperar-ne el que no dona.

Necessita història

Un VPA acabat de crear no sap res. Les seves primeres recomanacions, en les primeres hores, són poc fiables: l'upperBound serà enorme i el target reflectirà només el que ha vist. Regla: no prenguis decisions amb menys d'una setmana de dades, i prefereix-ne dues.

Corol·lari incòmode: per a un servei nou, el VPA no ajuda. Cal estimar a mà, desplegar, i deixar que el VPA corregeixi després.

Reacciona lent

Amb semivida de 24 hores, un canvi real de perfil triga dies a reflectir-se plenament. Això és deliberat i correcte —no volem que un pic de cinc minuts canviï els recursos de tota la plataforma—, però significa que el VPA no serveix per respondre a esdeveniments:

  • No serveix per al pont de maig. Quan el VPA s'adoni que cal més CPU, el pont haurà acabat.
  • No serveix per a un desplegament que canvia radicalment el perfil de consum. Allà cal estimar a mà i deixar que el VPA validi després.

El VPA és per al règim permanent, no per al transitori. Els transitoris els cobreix l'HPA (09-01) i KEDA (09-04).

Recrear un pod amb estat no és gratis

En modes Recreate i Auto, cada ajust és un reinici. El cost depèn del component:

Component Cost del reinici
botiga-web (nginx) Gairebé nul: un segon, sense estat
api-reserves Moderat: 25 s d'arrencada, pool de connexions nou, memòria cau freda
worker-notificacions Baix: la cua reentrega el missatge en vol
redis-cache Alt: es perd tota la memòria cau, i tota aquesta càrrega cau sobre PostgreSQL
postgres-reserves Molt alt: tall de totes les connexions, memòria cau de pàgines perduda

El VPA respecta els PDB (09-05), cosa que mitiga el risc, però no l'elimina: un PDB garanteix que no caigui tot alhora, no que el reinici sigui indolor.

No coneix el context de negoci

El VPA veu consum de CPU i memòria. No veu:

  • Que el pont de maig comença el divendres.
  • Que aquest pod atén el procés de pagament i no es pot reiniciar durant una transacció.
  • Que la memòria alta d'ahir a la nit va ser una execució de manteniment i no el règim normal.
  • Que shared_buffers de PostgreSQL ha de canviar en harmonia amb la memòria del contenidor.

Tot això ho posa una persona. I aquesta és, resumida en una frase, la justificació del mode Off.

Altres limitacions a tenir presents

  • No gestiona recursos que no siguin CPU i memòria. L'emmagatzematge efímer, les GPU i els recursos estesos queden fora.
  • Amb updateMode actiu, no s'hauria de fer servir juntament amb un desplegament continu agressiu: els desallotjaments del VPA i els rolling updates se solapen de manera poc predictible.
  • Un sol VPA per càrrega de treball. Dos objectes VPA amb targetRef al mateix Deployment produeixen comportament indefinit. Kubernetes no ho impedeix; tu sí que ho has de fer.

Errors Comuns i Consells

Error 1: posar el VPA en mode Auto sobre CPU quan ja hi ha un HPA per CPU. L'error catastròfic d'aquesta lliçó. El bucle de realimentació de l'apartat 11 triga hores a manifestar-se i és difícil de diagnosticar perquè cada component sembla funcionar bé per separat. Si tens HPA per CPU: controlledResources: ["memory"] o mode Off. Sense excepcions.

Error 2: aplicar la recomanació d'un VPA de dos dies. L'upperBound desmesurat delata que hi ha poca història. Espera dues setmanes. Si tens pressa, mira el lowerBound: si el teu valor actual és per sota, això sí que és un senyal fiable fins i tot amb poques dades, perquè significa que el contenidor ja s'ha quedat curt en observacions reals.

Error 3: no posar maxAllowed. Sense sostre, una fuita de memòria fa que la recomanació creixi sense fi. En mode actiu, acabaràs amb un pod demanant 30Gi que no cap en cap node i es queda Pending eternament. El maxAllowed converteix aquesta fuita en un senyal visible (uncappedTarget allunyant-se del target) en lloc d'una caiguda.

Error 4: creure que el VPA modifica el Deployment. No ho fa. El kubectl get deployment continuarà mostrant els valors del manifest. La mutació passa al pod, en admissió. Per veure la realitat cal mirar els pods, no el controlador. Aquesta confusió genera moltíssims tiquets de «el VPA no funciona».

Error 5: VPA actiu sobre una càrrega d'una sola rèplica. Un desallotjament és una caiguda total. El minReplicas: 2 per defecte protegeix, però si el baixes a 1 «perquè funcioni», estàs demanant talls de servei. Si tens una sola rèplica, o en poses dues, o fas servir mode Off.

Error 6: oblidar la ResourceQuota. L'apartat 13. Pujar els requests sense recalcular requests × maxReplicas contra la quota produeix un error diferit que es manifesta al pic de trànsit, setmanes després, amb un missatge d'error en un lloc inesperat.

Consell 1: comença pels CronJobs. Són el cas més rendible i el de menor risc. Ningú espera una resposta, el mode Initial és segur, i l'estalvi de temps d'execució sol ser espectacular. informes-ocupacio passant de 40 a 9 minuts és una victòria visible que compra credibilitat per a la resta.

Consell 2: exporta les recomanacions a Prometheus. El VPA exposa mètriques, i kube-state-metrics publica kube_verticalpodautoscaler_status_recommendation_containerrecommendations_target. Amb això pots construir a Grafana (07-04) un panell de «recomanat contra configurat» per a tota la plataforma, i una alerta quan la desviació superi el 30 %:

abs(
  kube_verticalpodautoscaler_status_recommendation_containerrecommendations_target{resource="memory"}
  -
  kube_pod_container_resource_requests{resource="memory"}
)
/ kube_pod_container_resource_requests{resource="memory"} > 0.3

Consell 3: un VPA en mode Off sobre absolutament tot. Cost gairebé nul, informació permanent. Inclou-lo a la plantilla base de tot component nou.

Consell 4: fes servir el lowerBound com a alerta de risc. Un contenidor els requests del qual són per sota del lowerBound està en perill real de desallotjament o d'OOMKilled. És el senyal més accionable que dona el VPA, i mereix una alerta.

Consell 5: revisa l'uncappedTarget mensualment. Si creix mes a mes mentre el target es queda enganxat al maxAllowed, tens una fuita de memòria. El VPA la detecta abans que es converteixi en incident.

Consell 6: documenta al manifest d'on surten els números. Un comentari com # requests.memory: 800Mi -- recomanacio VPA 2026-05-11, p95 real 690Mi converteix un número màgic en una decisió traçable. El següent que ho llegeixi sabrà si el pot tocar.

Exercicis

Exercici 1: interpretar una recomanació

Aquest és el describe del VPA de redis-cache després de tres setmanes en mode Off:

Status:
  Recommendation:
    Container Recommendations:
      Container Name:  redis
      Lower Bound:
        Cpu:     180m
        Memory:  1420Mi
      Target:
        Cpu:     245m
        Memory:  1980Mi
      Uncapped Target:
        Cpu:     245m
        Memory:  3410Mi
      Upper Bound:
        Cpu:     390m
        Memory:  2048Mi

El manifest actual diu:

resources:
  requests:
    cpu: 500m
    memory: 1Gi
  limits:
    cpu: 500m
    memory: 2Gi

I el resourcePolicy del VPA:

resourcePolicy:
  containerPolicies:
    - containerName: redis
      minAllowed:
        cpu: 100m
        memory: 512Mi
      maxAllowed:
        cpu: "1"
        memory: 2Gi

Respon: (a) què passa amb la CPU; (b) què passa amb la memòria i per què target i uncappedTarget difereixen; (c) què significa que upperBound sigui exactament 2048Mi; (d) quina QoS té el pod ara i què implica; (e) què faries, amb quina prioritat, i què comprovaries abans.

Exercici 2: diagnosticar un conflicte

botiga-web porta tres dies amb un comportament estrany. L'equip reporta:

  • El nombre de rèpliques oscil·la entre 3 i 12 diverses vegades al dia sense que el trànsit ho justifiqui.
  • Els pods es reinicien amb freqüència, amb motiu Evicted.
  • Els requests.cpu dels pods no coincideixen amb el manifest i canvien sols.

La configuració és:

---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: botiga-web
  minReplicas: 3
  maxReplicas: 15
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: botiga-web
  updatePolicy:
    updateMode: "Auto"

Explica exactament què està passant, amb la seqüència temporal, i dona dues solucions alternatives amb els seus manifests i les seves contrapartides.

Exercici 3: dissenyar l'estratègia per a un component nou

Rutas Norte afegeix exportador-comptable, un component que genera els fitxers de facturació per a la gestoria. Perfil:

  • És un CronJob que s'executa el dia 1 de cada mes a les 2:00.
  • Cada execució triga entre 20 i 90 minuts, segons el volum de reserves del mes.
  • Consumeix molta memòria: carrega en memòria totes les reserves del mes per agregar-les.
  • Si falla, cal rellançar-lo a mà i l'equip d'administració es queda sense els fitxers aquell dia.
  • Ha estat OOMKilled dos dels últims quatre mesos.
  • Actualment: requests.memory: 1Gi, limits.memory: 2Gi, requests.cpu: 500m, limits.cpu: "1".
  • El namespace té una ResourceQuota de 40 nuclis i 80Gi.

Dissenya l'estratègia completa: VPA sí o no? Quin mode? Quin resourcePolicy? Quant temps d'observació necessites i per què és un cas especial? Què faries a més del VPA? Escriu el manifest i justifica cada decisió.


Solucions

Solució 1

(a) La CPU està sobredimensionada.

El manifest demana 500m; l'upperBound és 390m. El valor actual és per sobre de l'upperBound, cosa que significa que el recomanador està segur que sobra CPU. El target és 245m: menys de la meitat del que està configurat. Com que redis-cache és una sola rèplica, l'estalvi absolut és modest (255m), però allibera capacitat de planificació en un node.

(b) La memòria: target 1980Mi, uncappedTarget 3410Mi.

Aquesta és l'observació crítica de l'exercici. El maxAllowed.memory és 2Gi = 2048Mi, i l'uncappedTarget de 3410Mi és molt per sobre. El recomanador vol 3410Mi i les baranes ho estan retallant a 1980Mi.

A més, el requests actual (1Gi = 1024Mi) és per sota del lowerBound (1420Mi). Doble senyal d'alarma: falta memòria reservada I el recomanador en vol molta més de la que li deixem.

Fuita de memòria o creixement legítim? A Redis hi ha una tercera possibilitat molt específica, i és la més probable: la memòria cau ha crescut fins a omplir l'espai disponible. Redis, sense una política d'expulsió configurada, continua acceptant claus fins a esgotar la memòria. L'uncappedTarget creixent no indica un error del programari: indica que maxmemory no està configurat a Redis i la memòria cau creix sense control.

Comprovació:

kubectl exec -n rutas-norte-pro redis-cache-0 -- redis-cli CONFIG GET maxmemory
kubectl exec -n rutas-norte-pro redis-cache-0 -- redis-cli CONFIG GET maxmemory-policy
kubectl exec -n rutas-norte-pro redis-cache-0 -- redis-cli INFO memory

Si maxmemory és 0, aquest és el problema de fons, i cap ajust de requests no el resol: només retarda el següent OOMKilled.

(c) upperBound exactament 2048Mi.

No és casualitat: és el maxAllowed. Els límits de la recomanació també es retallen per les baranes. Quan veus un upperBound que coincideix exactament amb el teu maxAllowed, el recomanador t'està dient que el seu interval de confiança està truncat, i que la recomanació real és fora del que li permets.

(d) QoS del pod.

CPU:     requests 500m == limits 500m
Memoria: requests 1Gi  != limits 2Gi

Com que no tots els recursos tenen requests igual a limits, la QoS és Burstable, no Guaranteed (mòdul 3).

Implicació seriosa: sota pressió de memòria al node, un pod Burstable que consumeix per sobre del seu requests és candidat prioritari al desallotjament. I redis-cache està consumint 1980Mi amb un requests de 1024Mi: gairebé el doble del reservat. És dels primers que cauran quan el node s'ompli, i la seva caiguda descarrega tota la càrrega sobre postgres-reserves justament quan el node ja és sota pressió. Un efecte dòmino de manual.

(e) Pla d'acció, per prioritat.

Prioritat 1 (avui): configurar maxmemory a Redis. És la correcció d'arrel.

# ConfigMap de redis-cache
maxmemory 1500mb
maxmemory-policy allkeys-lru   # Expulsa les claus menys usades en arribar al topall

Amb això, Redis deixa de créixer sense control i comença a comportar-se com una memòria cau de debò. L'uncappedTarget s'hauria d'estabilitzar les setmanes següents.

Prioritat 2 (aquesta setmana): pujar el requests.memory a 2Gi i igualar-lo al limit.

resources:
  requests:
    cpu: 300m         # Baixat de 500m: el target es 245m, amb marge
    memory: 2Gi       # Pujat d'1Gi: estava per sota del lowerBound
  limits:
    cpu: 300m         # Igual que requests
    memory: 2Gi       # Igual que requests -> QoS Guaranteed

Això aconsegueix tres coses: cobreix el lowerBound, promou el pod a QoS Guaranteed (deixa de ser candidat prioritari al desallotjament, coherent amb el seu paper d'amortidor de PostgreSQL), i allibera 200m de CPU.

Prioritat 3 (d'aquí a un mes): revisar de nou. Amb maxmemory posat, comprovar que l'uncappedTarget s'ha estabilitzat per sota de 2Gi. Si continua creixent, hi ha alguna cosa més.

Comprovacions abans d'aplicar:

# 1. Cap a la ResourceQuota? (redis-cache es 1 replica, impacte petit)
kubectl describe resourcequota quota-pro -n rutas-norte-pro

# 2. Hi ha un node amb 2Gi lliures de debo?
kubectl describe nodes | grep -A5 "Allocated resources"

# 3. Quant triga a recuperar-se la cache despres del reinici?
#    (el canvi de requests obliga a recrear el pod)
#    Programar-ho en horari de baix transit, MAI a prop del pont de maig.

Aquest últim punt és el que s'oblida sempre: canviar els recursos de redis-cache implica reiniciar-lo, i reiniciar-lo buida la memòria cau. Durant els minuts següents, tota la càrrega de consultes de disponibilitat va directa a postgres-reserves. Cal fer-ho un dimarts a les 4 de la matinada, no un divendres a les 10.

Solució 2

Què està passant: el bucle de realimentació HPA-VPA de l'apartat 11, en estat pur.

L'HPA escala botiga-web per CPU. El VPA està en mode Auto sense controlledResources, cosa que significa que controla CPU i memòria. Tots dos governen la CPU.

Seqüència temporal:

t=0     3 repliques, requests.cpu 200m, objectiu HPA = 50 % de 200m = 100m.
        Carrega total: 480m -> 160m per pod. Ratio 1,6.
        HPA -> sostre(3 x 1,6) = 5 repliques.

t=2min  5 repliques, 96m per pod. HPA satisfet (ratio 0,96, en tolerancia).

t=20min El VPA porta una estona observant ~96m de consum. Recomana
        requests.cpu = 110m (96 x 1,15). L'actualitzador DESALLOTJA els pods
        d'un en un.  <-- REINICIS "Evicted" que reporta l'equip

t=25min Pods recreats amb requests.cpu 110m.
        Objectiu de l'HPA ARA = 50 % de 110m = 55m.
        El consum real segueix en 96m. Ratio = 96/55 = 1,75.
        HPA -> sostre(5 x 1,75) = 9 repliques.  <-- OSCIL·LACIO que reporta l'equip

t=27min 9 repliques, 53m per pod. HPA satisfet.
        El VPA observa 53m, recomana 61m...

t=45min El VPA aplica 61m. Objectiu HPA = 30m. Consum 53m. Ratio 1,77.
        HPA -> 16 repliques, tallat per maxReplicas a 15.

t=50min 15 repliques, 32m per pod. El VPA recomana 37m...
        I el cicle continua, ara limitat pel sostre.

        Quan la carrega baixa de nit, el proces s'inverteix: el VPA puja
        el requests (perque els pods estan al limit d'una recomanacio
        minuscula), l'objectiu de l'HPA puja, l'HPA baixa repliques, i tornem
        a comencar. D'aqui l'oscil·lacio 3 <-> 12 "diverses vegades al dia".

Els tres símptomes queden explicats: l'oscil·lació de rèpliques (HPA reaccionant a objectius mòbils), els Evicted (l'actualitzador del VPA desallotjant), i els requests que no coincideixen amb el manifest i canvien sols (el controlador d'admissió mutant els pods).

Ordres que ho confirmarien:

# Els esdeveniments de l'HPA mostren reescalats frequents sense causa de transit
kubectl describe hpa botiga-web -n rutas-norte-pro

# Els esdeveniments del namespace mostren EvictedByVPA
kubectl get events -n rutas-norte-pro --field-selector reason=EvictedByVPA \
  --sort-by=.lastTimestamp

# Els requests reals dels pods difereixen del manifest
kubectl get pods -n rutas-norte-pro -l app=botiga-web \
  -o custom-columns=NOM:.metadata.name,CPU:.spec.containers[0].resources.requests.cpu

# La recomanacio del VPA ha anat baixant
kubectl describe vpa botiga-web -n rutas-norte-pro

Solució A (recomanada): VPA en mode Off.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: botiga-web
  updatePolicy:
    # OFF. L'HPA governa la CPU. El VPA queda com a assessor: revisem la seva
    # recomanacio cada dilluns i l'apliquem a ma en una peticio de canvi.
    updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: nginx
        minAllowed:
          cpu: 50m
          memory: 64Mi
        maxAllowed:
          cpu: 500m
          memory: 512Mi

Avantatges: elimina el conflicte per complet, sense reinicis sorpresa, el manifest continua sent la veritat, compatible amb GitOps. Contrapartida: algú ha d'aplicar la recomanació a mà.

Solució B: VPA actiu només sobre memòria.

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: botiga-web
  namespace: rutas-norte-pro
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: botiga-web
  updatePolicy:
    updateMode: "Recreate"
    minReplicas: 3
  resourcePolicy:
    containerPolicies:
      - containerName: nginx
        # LA LINIA CLAU: nomes memoria. La CPU es territori de l'HPA.
        controlledResources: ["memory"]
        minAllowed:
          memory: 64Mi
        maxAllowed:
          memory: 512Mi

Avantatges: la memòria s'ajusta sola, sense intervenció. Contrapartides: continua havent-hi reinicis no planificats; el requests.cpu queda sense ajustar (caldria fer-ho a mà igualment); durant el pont de maig caldria passar a Off per evitar reinicis en el pitjor moment.

Quina triar. Per a botiga-web, la A. L'estalvi de memòria de nginx és petit (parlem de desenes de Mi per pod) i no compensa el risc de reinicis no planificats a la porta d'entrada de la plataforma. La solució B tindria sentit en una càrrega amb consum de memòria variable i gran, i que tolerés reinicis: worker-notificacions és millor candidat.

Acció immediata mentre es decideix:

kubectl patch vpa botiga-web -n rutas-norte-pro --type merge \
  -p '{"spec":{"updatePolicy":{"updateMode":"Off"}}}'

Això atura l'oscil·lació al següent cicle de l'actualitzador, sense esborrar res.

Solució 3

VPA sí o no? Sí, rotundament. És un cas de llibre: un treball per lots amb consum variable que ja ha estat OOMKilled dues vegades. El cost d'un reinici és zero (el Job és efímer) i el cost de l'error actual és alt (intervenció manual, gestoria sense fitxers).

Quin mode? Initial. Igual que informes-ocupacio:

  • Recreate no té sentit: desallotjar un Job als 40 minuts d'una execució de 90 perd tota la feina i comença de zero. Seria pitjor que l'OOMKilled.
  • Off desaprofita l'ocasió: aquí sí que volem aplicació automàtica, perquè el consum varia cada mes amb el volum de reserves i ningú revisarà la recomanació mensualment.
  • Initial aplica la recomanació a cada execució nova. Perfecte.

El cas especial de l'observació: la freqüència mensual.

Aquest és el punt interessant de l'exercici. L'histograma del recomanador té semivida de 24 hores. Una execució mensual significa que, quan arriba la següent, la mostra anterior té trenta semivides d'antiguitat: el seu pes és 2⁻³⁰, és a dir, zero.

El VPA, tal com és, és pràcticament inútil per a un treball mensual. Cada execució comença gairebé des de zero.

Tres formes de resoldre-ho, de menys a més recomanable:

  1. Ajustar la semivida del recomanador. El vpa-recommender accepta --cpu-histogram-decay-half-life i --memory-histogram-decay-half-life. Pujar-los a 720h (30 dies) faria que la mostra del mes anterior pesés la meitat. Contrapartida: és un paràmetre global del recomanador, afecta tots els VPA del clúster i tornaria lentíssimes les recomanacions d'api-reserves. Només viable amb una segona instància del recomanador dedicada, cosa que és complexitat seriosa.

  2. Executar el treball amb més freqüència a pre. Programar-lo setmanalment a rutas-norte-pre amb dades representatives, deixar que el VPA aprengui allà, i portar la recomanació a producció a mà. Funciona, però requereix dades de prova realistes.

  3. VPA en Initial + un minAllowed generós + mesurament explícit. L'opció pragmàtica i la que recomano.

A més del VPA, tres mesures que importen més:

# k8s/entorns/pro/cronjob-exportador-comptable.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: exportador-comptable
  namespace: rutas-norte-pro
  labels:
    app: exportador-comptable
    app.kubernetes.io/part-of: rutas-norte
spec:
  schedule: "0 2 1 * *"          # Dia 1 de cada mes a les 2:00
  concurrencyPolicy: Forbid
  # MESURA 1: reintents automatics. Si falla, que ho intenti sol abans de
  # despertar ningu. Amb 3 intents i backoff, un OOM puntual es recupera.
  jobTemplate:
    spec:
      backoffLimit: 3
      # MESURA 2: termini maxim. Si a les 4 hores no ha acabat, alguna cosa va malament
      # i volem saber-ho abans que se solapi amb la carrega del mati.
      activeDeadlineSeconds: 14400
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: exportador
              image: registry.rutasnorte.example/exportador-comptable:3.1.0
              resources:
                requests:
                  cpu: 500m
                  # MESURA 3, LA IMPORTANT: pujar la memoria JA, sense esperar
                  # el VPA. Dos OOMKilled en quatre mesos amb limit de 2Gi
                  # signifiquen que 2Gi no basta. Pugem a 4Gi d'entrada.
                  memory: 4Gi
                limits:
                  cpu: "2"
                  # requests == limits en memoria -> QoS Guaranteed per al
                  # contenidor. S'executa de matinada, el cluster esta buit,
                  # no li traiem res a ningu i garantim que no el
                  # desallotgin per pressio de memoria del node.
                  memory: 4Gi

I el VPA:

# k8s/entorns/pro/vpa-exportador-comptable.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: exportador-comptable
  namespace: rutas-norte-pro
  labels:
    app: exportador-comptable
    app.kubernetes.io/part-of: rutas-norte
spec:
  targetRef:
    apiVersion: batch/v1
    kind: CronJob
    name: exportador-comptable
  updatePolicy:
    # INITIAL: cada execucio nova neix amb els recursos apresos. Mai
    # desallotgem un Job a mig cami: perdriem 40 minuts de feina.
    updateMode: "Initial"
  resourcePolicy:
    containerPolicies:
      - containerName: exportador
        minAllowed:
          cpu: 500m
          # minAllowed GENEROS i deliberat. Amb execucio mensual, el
          # histograma del recomanador (semivida 24 h) esta practicament
          # buit quan arriba la seguent execucio. Sense aquest terra, el VPA
          # recomanaria valors minusculs basats en gairebe cap dada i
          # tindriem un OOMKilled garantit. El terra es la nostra assegurança.
          memory: 4Gi
        maxAllowed:
          cpu: "4"
          # Sostre alt: el volum de reserves creix i desembre (amb el
          # tancament anual) pot ser molt mes gran que un mes normal. 8Gi es el
          # que cap comodament en un node de 16Gi amb el cluster buit de
          # matinada. Si l'uncappedTarget arriba aqui, cal revisar
          # l'algoritme de l'exportador: probablement carrega en memoria el que
          # hauria de processar per lots.
          memory: 8Gi
        controlledResources: ["cpu", "memory"]
        # RequestsAndLimits mante la proporcio 1:1 de memoria i per tant
        # la QoS del contenidor.
        controlledValues: RequestsAndLimits

Comprovació de la ResourceQuota:

Quota del namespace: 40 nuclis, 80Gi.
Pitjor cas de l'exportador: 4 nuclis i 8Gi (si el VPA arriba al maxAllowed).

Com que s'executa a les 2:00 del dia 1, la resta de la plataforma esta en
minReplicas: api-reserves 4, botiga-web 3, worker 2.
Consum nocturn estimat: ~8 nuclis, ~14Gi.
Amb l'exportador al maxim: 12 nuclis, 22Gi. Molt per sota de la quota.

CONCLUSIO: hi cap amb folganca. I es una rao mes per executar-lo de nit.

Pla de seguiment:

Quan Què fer
Immediat Aplicar els 4Gi i els reintents. No esperar el VPA
Després de la 1a execució kubectl describe vpa per veure el primer target, i mesurar el pic real amb kubectl top pods durant l'execució
Després de la 3a execució Comparar els tres target. Si són consistents, ajustar el minAllowed a un valor més fi
Al cap de 6 mesos Revisar la tendència. Si l'uncappedTarget creix linealment amb el volum de reserves, l'exportador escala malament i cal processar per lots en lloc de carregar-ho tot en memòria

La lliçó de fons de l'exercici: quan la freqüència d'execució és molt menor que la semivida de l'histograma, el VPA perd gairebé tota la seva utilitat automàtica. En aquests casos, la barana (minAllowed) importa més que la recomanació, i el mesurament manual de les primeres execucions continua sent insubstituïble. El VPA no eximeix de pensar: accelera el pensar bé.

Conclusió

El VerticalPodAutoscaler no respon a la càrrega: respon a la ignorància. Converteix els requests i limits de números que algú va copiar d'un altre manifest en valors derivats del consum real observat durant dies.

L'essencial:

  • El VPA respon a «de quina mida?»; l'HPA a «quants?». Eixos diferents, constants de temps diferents: dies contra segons.
  • No és al nucli. Cal instal·lar-lo des de kubernetes/autoscaler, comprovar compatibilitat de versions, i al núvol mirar primer si el proveïdor l'ofereix.
  • Tres components: el recomanador calcula (percentils amb decaïment exponencial, p90 per a CPU, pic per a memòria), l'actualitzador desallotja respectant els PDB, i el controlador d'admissió muta els pods en néixer. El Deployment no canvia mai.
  • Off és el mode dels equips seriosos. El manifest continua sent la veritat, no hi ha reinicis sorpresa, funciona amb GitOps i cada canvi de recursos passa per una revisió humana. Initial per a treballs per lots; Recreate només per a càrregues que toleren reinicis.
  • target, lowerBound, upperBound, uncappedTarget. El target és el que portes al manifest; estar per sota del lowerBound és una alarma; un uncappedTarget que s'allunya del target mes a mes és una fuita de memòria fins que es demostri el contrari.
  • VPA i HPA sobre la mateixa mètrica es destrueixen mútuament. El bucle és real i triga hores a manifestar-se. controlledResources: ["memory"] quan l'HPA escala per CPU, o mode Off. La combinació òptima és HPA per mètrica de negoci (09-04) i VPA complet.
  • El redimensionament en calent arribarà i canviarà les regles, però avui no és base per a una estratègia de producció.
  • Compte amb la ResourceQuota: requests recomanat × maxReplicas de l'HPA hi ha de cabre, o l'error apareixerà al pic de trànsit setmanes després.

A Rutas Norte hem deixat el VPA en mode Off sobre api-reserves, botiga-web, postgres-reserves i redis-cache, actiu en Recreate sobre worker-notificacions i en Initial sobre informes-ocupacio. I hem descobert, amb dades, que api-reserves tenia la memòria per sota del seu lowerBound i que botiga-web reservava el triple de CPU de la que fa servir.

Amb això, cada pod demana el que necessita i el nombre de pods s'adapta al trànsit. Però queda un supòsit ocult en tot el que hem fet: hem donat per fet que, quan l'HPA demana trenta rèpliques d'api-reserves, hi ha lloc on posar-les. El clúster de Rutas Norte té quatre nodes. Trenta pods d'api-reserves més quinze de botiga-web més els vint de worker-notificacions no hi caben. El planificador els deixarà en Pending i l'HPA no hi podrà fer res: haurà demanat les rèpliques, el Deployment les haurà creat, i allà es quedaran, a la cua, mentre la venda del pont de maig cau exactament igual que l'any passat.

A la propera lliçó, Autoescalat de Clúster, pugem l'últim graó: com fer que el clúster afegeixi nodes quan els pods no hi caben, per què retirar-los és molt més delicat que afegir-los, quant triga de debò un node a estar llest (i per què això no salva una punta sobtada), i el truc del sobreaprovisionament amb pods de farciment que ja tenim gairebé totes les peces per entendre.

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