Aurora Libros viu en un clúster amb tres rèpliques fixes de l'API. Funciona bé a les quatre de la tarda i s'ofega el dia que un diari ressenya La sombra del viento. Aquesta lliçó fa que la plataforma creixi sola amb la càrrega, reparteixi el trànsit amb criteri i et deixi veure, amb números, on és el límit real.

Contingut

  1. Escalat horitzontal i vertical
  2. Què escala i què no a Aurora Libros
  3. El coll d'ampolla real: les connexions de PostgreSQL
  4. Rèpliques de lectura i PgBouncer
  5. Requisit previ: l'aplicació sense estat
  6. Balanceig de càrrega: L4 i L7
  7. El balanceig intern: Service, kube-proxy, iptables i IPVS
  8. Balanceig d'entrada i al món Compose/Swarm
  9. Algorismes i afinitat de sessió
  10. Escalat manual, comparat
  11. L'HorizontalPodAutoscaler a fons
  12. L'HPA d'aurora-api
  13. Mètriques personalitzades
  14. VerticalPodAutoscaler i Cluster Autoscaler
  15. Disponibilitat: PDB, repartiment i antiafinitat
  16. Prova de càrrega i lectura dels límits

  1. Escalat horitzontal i vertical

Aspecte Vertical (scale up) Horitzontal (scale out)
Què canvia Més CPU i RAM a la mateixa instància Més instàncies iguals
Límit El node més gran que existeixi Pràcticament cap
Cost Creix de cop i no linealment Lineal i granular
Tolerància a fallades Cap: un punt únic Alta: en cauen unes i en queden d'altres
Requereix reiniciar Sí, gairebé sempre No
Temps de reacció Minuts Segons
Encaixa amb Bases de dades, memòries cau Serveis sense estat
A Kubernetes VerticalPodAutoscaler HorizontalPodAutoscaler

L'horitzontal és el natural en contenidors per una raó senzilla: arrencar una altra rèplica d'aurora-api:2.0.0 costa uns segons i no requereix permís de ningú, mentre que engrandir una màquina implica aturar-la. I aporta una cosa que el vertical no dona mai: redundància. Tres rèpliques no només aguanten el triple, sinó que sobreviuen a la pèrdua d'una.

  1. Què escala i què no a Aurora Libros

Servei Escala horitzontalment? Per què
aurora-web , trivialment Serveix fitxers estàtics, sense estat
aurora-api Sense estat: la memòria cau és fora, a aurora-cache
aurora-cache Només amb Redis Cluster Les dades estan repartides, no duplicades
aurora-db No sense més Escriptura en un únic primari; PVC ReadWriteOnce

L'asimetria de l'última fila és la realitat de gairebé qualsevol plataforma: el frontal escala amb un número, i la base de dades exigeix arquitectura. I com que tot el trànsit hi acaba passant, el límit del sistema és el seu.

  1. El coll d'ampolla real: les connexions de PostgreSQL

Cada connexió de PostgreSQL és un procés del sistema operatiu amb uns quants megabytes de memòria pròpia. El paràmetre max_connections val 100 per defecte, i aquest número no és una recomanació: és una paret.

repliques_d_api × DB_POOL_MAX  ≤  max_connections − reservades_per_a_superusuari

Amb el DB_POOL_MAX: "10" del teu ConfigMap i max_connections = 100:

Rèpliques d'API Connexions obertes Resultat
3 30 Correcte
8 80 Al límit
10 100 FATAL: sorry, too many clients already
20 (autoescalat) 200 La base de dades en rebutja la meitat

Aquest és el parany que fa fracassar el primer autoescalat de molta gent: l'HPA fa la seva feina, crea vint rèpliques per absorbir el pic... i la plataforma sencera cau, perquè vint pools de deu connexions tomben PostgreSQL. Escalar l'API sense mirar la base de dades no reparteix la càrrega: trasllada la fallada.

  1. Rèpliques de lectura i PgBouncer

Hi ha dues solucions i són complementàries.

Rèpliques de lectura. PostgreSQL replica en streaming cap a rèpliques de només lectura. aurora-api envia els SELECT del catàleg a una rèplica i les escriptures al primari. Funciona molt bé a Aurora Libros perquè la seva càrrega és gairebé tota de lectura, però introdueix el retard de replicació: un llibre acabat d'inserir pot trigar mil·lisegons a veure's a la rèplica, així que l'operació que acaba d'escriure ha de llegir del primari.

PgBouncer. Un pooler que es col·loca al davant i multiplexa: mil connexions de client sobre vint connexions reals.

# k8s/base/pgbouncer.yaml (extracte)
        - name: pgbouncer
          image: bitnami/pgbouncer:1.23
          env:
            - { name: POSTGRESQL_HOST,   value: aurora-db }
            - { name: PGBOUNCER_POOL_MODE, value: transaction }   # el mode que més comparteix
            - { name: PGBOUNCER_MAX_CLIENT_CONN, value: "1000" }
            - { name: PGBOUNCER_DEFAULT_POOL_SIZE, value: "20" }
Mode de pool Quan es retorna la connexió Multiplexació Limitació
session En desconnectar el client Escassa Gairebé com no tenir-lo
transaction En acabar cada transacció Alta Sense sentències preparades ni SET de sessió
statement Després de cada sentència Màxima Sense transaccions multisentència

Amb transaction i DB_HOST: pgbouncer, aurora-api pot escalar a cinquanta rèpliques mantenint vint connexions reals contra PostgreSQL. És, de bon tros, la intervenció amb millor relació entre esforç i resultat d'aquesta lliçó.

  1. Requisit previ: l'aplicació sense estat

Una rèplica només és intercanviable si no guarda res que les altres no tinguin. La regla pràctica: si apagues qualsevol Pod i cap usuari no ho nota, l'aplicació és sense estat.

Estat On NO pot viure On va a Aurora Libros
Sessió d'usuari Memòria del procés aurora-cache (Redis compartit)
Cistella de la compra Variable global aurora-cache amb TTL
Memòria cau de catàleg Memòria local aurora-cache, compartida per totes
Fitxers pujats Disc del contenidor Emmagatzematge d'objectes
Comptadors i mètriques Variable local Prometheus, agregant per rèplica

Guardar la sessió en memòria produeix la fallada més desconcertant d'una plataforma escalada: l'usuari inicia sessió (va a la rèplica 1), navega (rèplica 2) i apareix desconnectat. És exactament el problema que resol tenir la memòria cau fora del procés, i per això aurora-cache no és un luxe de rendiment: és el requisit que permet replicar l'API.

  1. Balanceig de càrrega: L4 i L7

Nivell Què inspecciona Pot repartir per Cost Exemples
L4 (transport) IP i port Connexió TCP Molt baix kube-proxy, IPVS, HAProxy en TCP
L7 (aplicació) Capçaleres, ruta, mètode, galetes Petició HTTP Més alt Ingress-nginx, Traefik, Envoy

La diferència pràctica es veu amb les connexions persistents. Un balancejador L4 reparteix connexions, no peticions: si un client obre una connexió keep-alive i n'envia mil peticions, les mil van al mateix Pod. Amb clients HTTP/1.1 normals amb prou feines es nota, però amb gRPC o HTTP/2 —una sola connexió de llarga vida— el repartiment L4 es torna terriblement desigual, i aquest és el motiu pel qual aquest tipus de trànsit necessita un balancejador L7.

  1. El balanceig intern: Service, kube-proxy, iptables i IPVS

Quan aurora-api resol aurora-cache, obté una IP virtual que no existeix en cap interfície de xarxa: és una adreça fictícia que kube-proxy intercepta amb regles del kernel.

flowchart LR
    P["Pod aurora-api"] -->|10.96.184.22:6379| K["kube-proxy<br/>iptables / IPVS"]
    K -->|DNAT 33%| A[Pod cache-1]
    K -->|DNAT 33%| B[Pod cache-2]
    K -->|DNAT 33%| C[Pod cache-3]
Mode de kube-proxy Com reparteix Complexitat Algorismes
iptables (per defecte) Regles amb probabilitat O(n): es degrada amb milers de serveis Només aleatori
IPVS Taula hash al kernel O(1) rr, lc, sh, dh, wrr
nftables Substitut modern d'iptables O(1) Aleatori
kubectl get svc aurora-api -n aurora -o jsonpath='{.spec.clusterIP}'
kubectl get endpointslices -n aurora -l kubernetes.io/service-name=aurora-api
sudo iptables -t nat -L KUBE-SVC-XXXX -n   # dins del node

Amb iptables, el repartiment s'implementa encadenant regles amb probabilitats decreixents: la primera accepta amb probabilitat 1/3, la següent 1/2 de la resta, i l'última recull el que queda. El resultat és un repartiment uniforme, però aleatori i sense memòria: no sap quantes connexions té cada Pod ni quant triga a respondre. Per tenir repartiment per menys connexions cal passar a IPVS. I per a polítiques de debò —reintents, tallacircuits, repartiment per latència— cal una malla de serveis com Istio o Linkerd.

  1. Balanceig d'entrada i al món Compose/Swarm

L'Ingress opera a L7 i el seu controlador parla directament amb els Pods, saltant-se la IP virtual del Service: consulta els EndpointSlices i balanceja ell mateix, cosa que li permet algorismes i reintents que kube-proxy no té.

  annotations:
    nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri"   # repartiment per URI
    nginx.ingress.kubernetes.io/proxy-next-upstream: "error timeout http_502"
    nginx.ingress.kubernetes.io/load-balance: "ewma"               # latència mòbil
Entorn Qui balanceja Algorisme per defecte
Compose amb --scale El DNS de Docker: retorna les IPs rotades Round-robin fluix (el client fa memòria cau)
Swarm amb ingress IPVS a cada node Round-robin
Swarm amb Nginx propi El teu upstream El que configuris
Kubernetes intern kube-proxy Aleatori (iptables)
Kubernetes d'entrada Ingress controller Round-robin, ewma, hash

El cas de Compose mereix una advertència: amb --scale api=3, el DNS intern retorna les tres IPs, però el client decideix quina fa servir i molts clients HTTP guarden la primera resolució per sempre. És la raó per la qual un docker compose up --scale sense un Nginx al davant reparteix molt pitjor del que sembla.

  1. Algorismes i afinitat de sessió

Algorisme Com tria Quan va bé Risc
Round-robin Per torns Peticions homogènies Ignora la càrrega real
Menys connexions El backend més ociós Peticions de durada desigual Necessita estat
Hash d'IP / URI Determinista per clau Memòries cau, afinitat Repartiment desigual
EWMA / latència El més ràpid darrerament Backends heterogenis Pot oscil·lar
Aleatori amb dues opcions En tria 2 i pren la menys carregada Escala molt bé

L'afinitat de sessió lliga cada client a un backend fix:

spec:
  sessionAffinity: ClientIP
  sessionAffinityConfig: { clientIP: { timeoutSeconds: 10800 } }

Sembla la solució fàcil al problema de la sessió en memòria, i és un mal negoci. Trenca el repartiment (un proxy corporatiu amb mil empleats és una sola IP, i tots cauen al mateix Pod), espatlla l'escalat (els Pods nous no reben trànsit existent, així que escalar no alleuja res immediatament) i converteix cada desplegament en una pèrdua de sessions. La solució correcta continua sent la de la secció 5: treure l'estat del procés. Aurora Libros no fa servir afinitat de sessió.

  1. Escalat manual, comparat

Plataforma Comanda Reconciliació
Compose docker compose up -d --scale aurora-api=5 Recrea contenidors; sense planificador
Swarm docker service scale aurora_aurora-api=5 Reparteix entre nodes segons restriccions
Kubernetes kubectl scale deploy/aurora-api --replicas=5 El scheduler col·loca segons requests
Kubernetes (condicional) kubectl scale --current-replicas=3 --replicas=5 Només si el número actual és l'esperat
kubectl scale deploy/aurora-api -n aurora --replicas=6
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api -w

Un avís que estalvia ensurts: si un HPA governa el Deployment, un kubectl scale manual dura fins al cicle següent de l'autoescalador, uns quinze segons. No és una fallada; és que l'estat desitjat el fixa ara l'HPA.

  1. L'HorizontalPodAutoscaler a fons

L'HPA és un controlador més: cada 15 segons llegeix les mètriques, aplica una fórmula i ajusta spec.replicas.

repliques_desitjades = ceil( repliques_actuals × (metrica_actual / metrica_objectiu) )
Actuals Ús mitjà de CPU Objectiu Càlcul Desitjades
3 90 % 70 % ceil(3 × 90/70) = ceil(3,86) 4
4 140 % 70 % ceil(4 × 2) 8
8 20 % 70 % ceil(8 × 0,29) 3
3 72 % 70 % 3 × 1,03 → dins de la tolerància (10 %) 3

Aquesta tolerància del 10 % és important: sense ella, l'HPA reaccionaria a cada fluctuació menor i la plataforma no pararia de crear i destruir Pods.

Requisit previ: metrics-server instal·lat i requests definides. El percentatge de l'HPA és relatiu a les requests, no al límit ni a la capacitat del node. Un Deployment sense requests deixa l'HPA sense denominador i el seu estat apareix com a <unknown>; és la causa número u de "el meu HPA no fa res".

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl top pods -n aurora

  1. L'HPA d'aurora-api

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: aurora-api, namespace: aurora }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: aurora-api }
  minReplicas: 3
  maxReplicas: 12          # 12 × 10 connexions = 120 > max_connections: vegeu la secció 3
  metrics:
    - type: Resource
      resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
    - type: Resource
      resource: { name: memory, target: { type: Utilization, averageUtilization: 80 } }
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0        # pujar de pressa: l'usuari està esperant
      policies:
        - { type: Percent, value: 100, periodSeconds: 30 }   # com a molt, doblar cada 30 s
        - { type: Pods,    value: 4,   periodSeconds: 30 }
      selectPolicy: Max
    scaleDown:
      stabilizationWindowSeconds: 300      # baixar a poc a poc: 5 min d'observació
      policies:
        - { type: Percent, value: 25, periodSeconds: 60 }    # com a molt, -25 % per minut

El bloc behavior és el que evita l'efecte io-io: sense ell, una ràfega crea rèpliques, la CPU mitjana baixa, l'HPA les destrueix, la càrrega es torna a concentrar i el cicle es repeteix indefinidament, amb Pods que neixen i moren sense arribar a ser útils.

L'asimetria és deliberada i mereix gravar-se: pujar és urgent, baixar no ho és. Escalar de menys costa peticions perdudes i clients; escalar de més costa uns cèntims de CPU ociosa durant cinc minuts. La finestra d'estabilització de 300 segons fa que l'HPA faci servir el màxim de les recomanacions dels últims cinc minuts abans de reduir.

I el maxReplicas: 12 no és un número rodó: surt del compte de la secció 3. Dotze rèpliques per deu connexions són 120, ja per sobre de max_connections. Amb PgBouncer al davant, aquest sostre podria pujar tranquil·lament.

  1. Mètriques personalitzades

La CPU és un mal indicador per a una API que espera molt per E/S: aurora-api pot estar al 20 % de CPU i amb la latència pels núvols esperant PostgreSQL. El que de debò descriu la seva càrrega són les peticions per segon i la latència.

    - type: Pods
      pods:
        metric: { name: http_peticions_per_segon }
        target: { type: AverageValue, averageValue: "50" }

Això requereix un adaptador que exposi les mètriques de Prometheus per l'API custom.metrics.k8s.io (prometheus-adapter o KEDA). El circuit complet és: la teva API exposa /metrics → Prometheus la recull (ja ho vas muntar a 05-06) → l'adaptador la publica com a mètrica de Kubernetes → l'HPA la consumeix. Val la pena quan el consum de CPU no es correlaciona amb la càrrega percebuda, que és el cas de gairebé qualsevol API lligada a base de dades.

  1. VerticalPodAutoscaler i Cluster Autoscaler

El VPA ajusta requests i limits en lloc del nombre de rèpliques. El seu gran valor és al mode Off, que només recomana: et diu, amb dades de setmanes, quins valors haurien de tenir els teus contenidors, i així ajustes els manifestos sense endevinar. En mode Auto recrea els Pods per aplicar els valors nous, cosa que el fa incompatible amb l'HPA sobre la mateixa mètrica: si tots dos actuen sobre la CPU, es barallen.

El Cluster Autoscaler treballa un nivell més amunt: quan hi ha Pods en Pending perquè no caben en cap node, demana una màquina nova al proveïdor de núvol; quan un node fa estona que està infrautilitzat i els seus Pods caben en d'altres, el drena i l'apaga. És la peça que fa que l'HPA no xoqui contra el sostre de capacitat, i també la que introdueix el retard real de l'escalat: crear un node triga minuts, no segons.

flowchart TB
    C[La càrrega puja] --> H[HPA: més Pods]
    H --> Q{Hi caben?}
    Q -->|sí| OK[Programats en segons]
    Q -->|no| P[Pods en Pending]
    P --> CA[Cluster Autoscaler: node nou]
    CA --> OK2[Programats en minuts]

  1. Disponibilitat: PDB, repartiment i antiafinitat

Escalar no serveix de res si les dotze rèpliques són al mateix node i aquest node cau.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: aurora-api }
spec:
  minAvailable: 2                # mai menys de 2 durant manteniments
  selector: { matchLabels: { app.kubernetes.io/name: aurora-api } }
      topologySpreadConstraints:
        - maxSkew: 1                                  # com a molt, 1 Pod de diferència
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway           # preferència, no requisit
          labelSelector: { matchLabels: { app.kubernetes.io/name: aurora-api } }
Mecanisme Què garanteix Contra què protegeix
PodDisruptionBudget Un mínim disponible durant interrupcions voluntàries kubectl drain, actualització de nodes
topologySpreadConstraints Repartiment equilibrat entre nodes o zones Caiguda d'un node o d'una zona
podAntiAffinity Rèpliques en nodes diferents (regla dura o tova) Caiguda d'un node

El PDB només cobreix les interrupcions voluntàries: si un node s'apaga de cop, ningú no pregunta res. I hi ha un parany clàssic: minAvailable: 2 amb replicas: 2 bloqueja qualsevol drenatge per sempre, perquè no se'n pot treure cap sense incomplir-lo. Expressa sempre el PDB en funció del mínim de l'HPA, no del número actual.

  1. Prova de càrrega i lectura dels límits

apiVersion: batch/v1
kind: Job
metadata: { name: carrega-llibres, namespace: aurora }
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: k6
          image: grafana/k6:latest
          args: ["run","--vus","200","--duration","5m","/guions/carrega.js"]
          volumeMounts: [{ name: guions, mountPath: /guions }]
      volumes: [{ name: guions, configMap: { name: k6-guions } }]
// carrega.js — 200 usuaris virtuals demanant el catàleg
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = { thresholds: { http_req_duration: ['p(95)<800'] } };
export default function () {
  const r = http.get('http://aurora-api:3000/llibres');
  check(r, { 'estat 200': (x) => x.status === 200 });
  sleep(0.2);
}
kubectl apply -f k8s/carrega.yaml
kubectl get hpa aurora-api -n aurora -w
NAME         TARGETS              MINPODS  MAXPODS  REPLICAS
aurora-api   12%/70%, 31%/80%     3        12       3
aurora-api   148%/70%, 44%/80%    3        12       3
aurora-api   148%/70%, 44%/80%    3        12       6
aurora-api   96%/70%, 41%/80%     3        12       9
aurora-api   64%/70%, 38%/80%     3        12       9
aurora-api   9%/70%, 30%/80%      3        12       3
Moment Rèpliques CPU mitjana p50 p95 Errors
Repòs (20 pet./s) 3 12 % 8 ms 24 ms 0
Pic, abans de l'HPA 3 148 % 310 ms 1 840 ms 12
Durant l'escalat 6 96 % 96 ms 420 ms 0
Estabilitzat 9 64 % 21 ms 88 ms 0
Després de la càrrega (5 min) 3 9 % 8 ms 22 ms 0

El p95 va passar de 1 840 ms a 88 ms sense tocar res: l'HPA ho va fer en poc més d'un minut. Fixa't en la fila del pic: 148 % d'utilització vol dir que cada Pod fa servir 1,48 vegades la seva request de CPU, és a dir, està patint throttling contra el seu límit; els dotze errors són peticions que van superar el temps d'espera del client.

Símptoma sota càrrega Coll d'ampolla Què fer
CPU de l'API al màxim, BD tranquil·la API Escalar rèpliques: l'HPA ho cobreix
API ociosa, latència alta, BD al 100 % Base de dades Rèpliques de lectura, índexs, més memòria cau
too many clients already Connexions PgBouncer, baixar DB_POOL_MAX
Tot ociós i tot i així lent Xarxa o dependència externa Traces distribuïdes, revisar DNS i keep-alive
Memòria creixent fins a OOMKilled Fuita a l'aplicació Perfilar el heap; l'HPA no arregla una fuita
Proporció d'encerts de memòria cau baixa TTL o clau de memòria cau Revisar CACHE_TTL i la clau

L'última fila del bloc anterior és la que més importa de la lliçó: escalar l'API només ajuda quan el coll d'ampolla és l'API. En els altres casos, afegir rèpliques empitjora el problema, perquè multiplica la pressió sobre el component que ja estava saturat.

Advertència. Una prova de càrrega contra un entorn compartit pot degradar serveis d'altres equips i disparar alertes de seguretat. Acorda sempre la finestra, l'origen del trànsit i els llindars amb els responsables d'infraestructura i seguretat, i no llancis mai càrrega contra sistemes que no controles.

Errors Habituals i Consells

  • Escalar l'API sense mirar la base de dades. L'HPA crea vint rèpliques, PostgreSQL rebutja connexions i la caiguda és total. Calcula rèpliques × pool abans de fixar maxReplicas.
  • HPA sense requests. Els objectius apareixen com a <unknown> i no escala mai. El percentatge és relatiu a la petició.
  • Sense metrics-server. L'HPA no té d'on llegir. kubectl top pods és la comprovació de trenta segons.
  • scaleDown agressiu. Sense finestra d'estabilització, la plataforma oscil·la i cap rèplica arriba a ser útil.
  • Afinitat de sessió com a pedaç. Trenca el repartiment i no arregla el disseny. Treu l'estat del procés.
  • minAvailable igual a replicas en un PDB. Bloqueja tots els manteniments del clúster.
  • Provar la càrrega contra una API amb la memòria cau calenta. Mesures Redis, no el teu sistema. Buida la memòria cau o varia les claus.
  • Consell: mesura abans i després amb els mateixos percentils. El p95 i el p99 expliquen la història; la mitjana l'amaga.
  • Consell: fixa maxReplicas a un número que el clúster pugui allotjar. Si no, tindràs Pods en Pending i un autoescalat que aparenta funcionar sense fer res.

Exercicis

Exercici 1. Calcula i demostra el límit de connexions: esbrina el max_connections d'aurora-db, dedueix quantes rèpliques d'API hi caben amb DB_POOL_MAX: 10 i provoca la fallada escalant per sobre d'aquest número.

Exercici 2. Munta l'HPA sobre aurora-api, genera càrrega amb k6 i documenta la taula de latències i rèpliques abans, durant i després. Explica per què la baixada triga molt més que la pujada.

Exercici 3. Comprova que les rèpliques estan repartides i que la plataforma sobreviu a un manteniment: aplica un PDB i topologySpreadConstraints, drena un node i observa què fa Kubernetes.

Solucions

Solució 1.

kubectl exec -n aurora aurora-db-0 -- psql -U aurora -tAc 'SHOW max_connections;'
kubectl exec -n aurora aurora-db-0 -- psql -U aurora -tAc \
  "SELECT count(*), usename FROM pg_stat_activity WHERE usename='aurora' GROUP BY usename;"
# 100
# 30 | aurora

Tres rèpliques per deu connexions són exactament les 30 obertes: el DB_POOL_MAX del ConfigMap no és teòric, cada Pod obre el seu pool complet en arrencar encara que no el faci servir. El sostre teòric és (100 − 3 reservades) / 10 = 9 rèpliques.

kubectl scale deploy/aurora-api -n aurora --replicas=12
sleep 30
kubectl logs -n aurora -l app.kubernetes.io/name=aurora-api --tail=2 | grep -i fatal
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api | grep -c Running
{"nivell":"error","missatge":"fallada de connexio",
 "detall":"FATAL: sorry, too many clients already"}
9

Nou Pods Running de dotze, i els altres tres en CrashLoopBackOff: els seus pools no van aconseguir obrir-se. Però el greu no són aquests tres, sinó l'efecte sobre els que sí que van arrencar, que comencen a veure errors intermitents en renovar connexions.

kubectl apply -f k8s/base/pgbouncer.yaml
kubectl patch cm aurora-config -n aurora --type=merge -p '{"data":{"DB_HOST":"pgbouncer"}}'
kubectl rollout restart deploy/aurora-api -n aurora
kubectl exec -n aurora aurora-db-0 -- psql -U aurora -tAc \
  "SELECT count(*) FROM pg_stat_activity WHERE usename='aurora';"
# 20
Configuració Rèpliques d'API Connexions reals a PostgreSQL Sostre de rèpliques
Directa 12 120 (rebutja) 9
Amb PgBouncer (transaction, pool 20) 12 20 Centenars

Dotze rèpliques i vint connexions reals: PgBouncer ha desacoblat completament l'escalat de l'API del límit de la base de dades. Aquesta és la raó per la qual maxReplicas deixa de ser un compte de connexions i es pot fixar per capacitat de còmput.

La lliçó de fons és de mètode: el límit d'un sistema gairebé mai no és on escales. Aquí escalaves l'API i el que es trencava era PostgreSQL, un servei que ni tan sols havies tocat. Abans de pujar maxReplicas, cal preguntar-se sempre quin recurs compartit es multiplica amb cada rèplica.

Solució 2.

kubectl apply -f k8s/hpa.yaml
kubectl get hpa aurora-api -n aurora
kubectl apply -f k8s/carrega.yaml          # el Job de k6 amb 200 usuaris virtuals
kubectl get hpa aurora-api -n aurora -w &
kubectl logs -f job/carrega-llibres -n aurora | tail -6
     http_req_duration..: p(50)=21ms  p(95)=88ms  p(99)=141ms
     http_req_failed....: 0.00%  ✓ 0  ✗ 74213
     iterations.........: 74213  247.3/s
     ✓ estat 200
Fase t Rèpliques CPU (% de request) p95 Errors
Repòs 0 3 12 % 24 ms 0
Inici de la càrrega +15 s 3 148 % 1 840 ms 12
Primer escalat +45 s 6 96 % 420 ms 0
Segon escalat +75 s 9 64 % 88 ms 0
Fi de la càrrega +5 min 9 9 % 22 ms 0
Reducció +10 min 3 9 % 22 ms 0

Les dues columnes de la dreta resumeixen el resultat: el p95 va caure de 1 840 ms a 88 ms, vint vegades, sense que ningú toqués res, i els dotze errors del primer minut són el preu d'haver començat amb tres rèpliques.

Els temps revelen l'asimetria del behavior. La pujada va ser en dos salts de 30 segons —stabilizationWindowSeconds: 0 i la política de doblar com a molt— i en 75 segons la plataforma estava estabilitzada. La baixada va començar cinc minuts després que la càrrega acabés, perquè scaleDown.stabilizationWindowSeconds: 300 obliga l'HPA a fer servir el màxim recomanat en aquella finestra; i després va baixar a poc a poc, un 25 % per minut.

Aquesta lentitud és intencionada, i la justificació és econòmica abans que tècnica. Baixar de pressa té un risc asimètric: si la càrrega torna a pujar trenta segons després —cosa molt habitual, perquè el trànsit arriba a ràfegues—, et trobes altre cop amb tres rèpliques ofegades i un altre minut de latències dolentes. Mantenir nou rèpliques cinc minuts de més costa cèntims; quedar-te curt al segon pic costa clients.

Un detall metodològic: les 247 iteracions per segon de l'informe són el rendiment sostingut, no el pic. En mesurar, importa més aquest número al costat del p95 que el màxim instantani, que gairebé sempre s'aconsegueix a costa de latències inacceptables.

Solució 3.

kubectl apply -f k8s/pdb.yaml
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api \
  -o custom-columns=POD:.metadata.name,NODE:.spec.nodeName --no-headers | awk '{print $2}' | sort | uniq -c
      3 aurora-worker
      3 aurora-worker2

Repartiment perfecte gràcies a topologySpreadConstraints amb maxSkew: 1. Sense aquesta restricció, el scheduler només mira els recursos lliures i és perfectament capaç de posar les sis al mateix node si hi cabien.

kubectl drain aurora-worker --ignore-daemonsets --delete-emptydir-data --timeout=120s
kubectl get pods -n aurora -l app.kubernetes.io/name=aurora-api -o wide | tail -4
kubectl get pdb aurora-api -n aurora
evicting pod aurora/aurora-api-6d4f8b7c9-2xkpq
evicting pod aurora/aurora-api-6d4f8b7c9-mv7rn
aurora-api-6d4f8b7c9-k9wtz   1/1  Running  aurora-worker2
aurora-api-6d4f8b7c9-p2mvx   1/1  Running  aurora-worker2
NAME         MIN AVAILABLE   ALLOWED DISRUPTIONS
aurora-api   2               4

El detall que cal observar és que les expulsions es van fer d'una en una i esperant. El drenatge no expulsa alhora tots els Pods del node: demana permís al PDB abans de cadascun, i si expulsar el següent deixés menys de dos disponibles, espera que el reemplaçament estigui preparat en un altre node. Per això ALLOWED DISRUPTIONS va baixant durant l'operació.

# Amb un PDB mal calibrat, el drenatge es bloqueja per sempre
kubectl patch pdb aurora-api -n aurora -p '{"spec":{"minAvailable":6}}'
kubectl drain aurora-worker2 --ignore-daemonsets --timeout=30s
# error: Cannot evict pod ... violate the pod disruption budget.

Aquí hi ha el parany de la secció 15, en directe: amb minAvailable: 6 i sis rèpliques, cap expulsió no és admissible i el drenatge falla indefinidament. En una actualització nocturna del clúster, aquest PDB deixaria el manteniment penjat sense que ningú entengui per què.

La regla que se'n dedueix: expressa el PDB en funció del mínim de l'HPA, mai del nombre actual de rèpliques. Amb minReplicas: 3, un minAvailable: 2 (o millor, maxUnavailable: 1) dona marge tant en repòs com amb dotze rèpliques.

I la limitació que aquest exercici no cobreix: tot això protegeix davant d'interrupcions voluntàries. Si aurora-worker s'apagués de cop, els seus tres Pods desapareixerien sense que el PDB digués res, i només el repartiment entre nodes —que sí que has garantit— evitaria que Aurora Libros es quedés sense cap rèplica viva.

Conclusió

La plataforma ja creix i s'encongeix sola. Distingeixes l'escalat horitzontal del vertical i saps quins serveis d'Aurora Libros accepten cadascun: l'API i el frontal repliquen sense més perquè no guarden estat, i la base de dades no, per dos motius que has mesurat —el PVC ReadWriteOnce i, sobretot, el max_connections—. Aquesta va ser la troballa més valuosa: el límit del sistema no era on escalaves. Dotze rèpliques per deu connexions tombaven PostgreSQL, i PgBouncer en mode transaction ho va resoldre deixant dotze rèpliques sobre vint connexions reals, amb les rèpliques de lectura com l'altra meitat de la resposta.

Entens per què la memòria cau compartida a aurora-cache no és un luxe sinó el requisit que permet replicar l'API, i per què l'afinitat de sessió és un pedaç que trenca el repartiment en comptes d'arreglar el disseny. Saps què separa L4 de L7, com kube-proxy implementa la IP virtual amb regles probabilístiques d'iptables —i què hi guanya IPVS—, com l'Ingress balanceja parlant directament amb els EndpointSlices, i per què un docker compose up --scale reparteix pitjor del que sembla.

Has muntat l'HorizontalPodAutoscaler amb la seva fórmula, la seva tolerància del 10 % i un behavior deliberadament asimètric: pujar en segons perquè l'usuari espera, baixar en cinc minuts perquè el trànsit arriba a ràfegues. I ho has provat de debò amb k6: el p95 va caure de 1 840 ms a 88 ms en poc més d'un minut mentre l'HPA portava les rèpliques de 3 a 9, i va tornar sol a 3 en acabar. Coneixes les mètriques personalitzades com a pas següent quan la CPU no descriu la càrrega, el VPA en mode recomanació i el Cluster Autoscaler amb el seu retard de minuts. I has assegurat la disponibilitat amb PodDisruptionBudget i topologySpreadConstraints, veient el drenatge expulsar d'una en una i comprovant de primera mà com un PDB mal calibrat bloqueja un manteniment per sempre. Amb la taula que tradueix cada símptoma al seu coll d'ampolla real, perquè escalar l'API només ajuda quan el problema és l'API.

Queda una última peça. Aurora Libros aguanta la càrrega, però cada vegada que publiques una versió nova continua havent-hi un moment delicat. A la lliçó següent, Estratègies de Desplegament i Rollback, veuràs com conviuen la versió vella i la nova: rolling update amb maxSurge i maxUnavailable, blue-green, canari i shadow, amb el seu cost i el seu risc; assajaràs un desplegament fallit d'aurora-api:2.1.0 que no passa la readiness per veure com s'atura tot sol i desfer-lo; i afrontaràs el problema que cap estratègia no resol tota sola, el de les migracions de base de dades.

Docker: De Principiant a Avançat

Mòdul 1: Introducció a Docker

Mòdul 2: Treballant amb Imatges Docker

Mòdul 3: Contenidors Docker

Mòdul 4: Docker Compose

Mòdul 5: Conceptes Avançats de Docker

Mòdul 6: Docker en Producció

Mòdul 7: Ecosistema i Eines de Docker

© Copyright 2026. Tots els drets reservats