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
- Escalat horitzontal i vertical
- Què escala i què no a Aurora Libros
- El coll d'ampolla real: les connexions de PostgreSQL
- Rèpliques de lectura i PgBouncer
- Requisit previ: l'aplicació sense estat
- Balanceig de càrrega: L4 i L7
- El balanceig intern: Service,
kube-proxy, iptables i IPVS - Balanceig d'entrada i al món Compose/Swarm
- Algorismes i afinitat de sessió
- Escalat manual, comparat
- L'
HorizontalPodAutoscalera fons - L'HPA d'
aurora-api - Mètriques personalitzades
VerticalPodAutoscaleriCluster Autoscaler- Disponibilitat: PDB, repartiment i antiafinitat
- Prova de càrrega i lectura dels límits
- 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.
- Què escala i què no a Aurora Libros
| Servei | Escala horitzontalment? | Per què |
|---|---|---|
aurora-web |
Sí, trivialment | Serveix fitxers estàtics, sense estat |
aurora-api |
Sí | 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.
- 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.
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.
- 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çó.
- 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.
- 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.
- El balanceig intern: Service,
kube-proxy, iptables i IPVS
kube-proxy, iptables i IPVSQuan 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 nodeAmb 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.
- 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.
- 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:
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ó.
- 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 -wUn 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.
- L'
HorizontalPodAutoscaler a fons
HorizontalPodAutoscaler a fonsL'HPA és un controlador més: cada 15 segons llegeix les mètriques, aplica una fórmula i ajusta spec.replicas.
| 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
- L'HPA d'
aurora-api
aurora-apiapiVersion: 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 minutEl 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.
- 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.
VerticalPodAutoscaler i Cluster Autoscaler
VerticalPodAutoscaler i Cluster AutoscalerEl 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]
- 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.
- 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);
}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 × poolabans de fixarmaxReplicas. - 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. scaleDownagressiu. 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.
minAvailableigual areplicasen 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
maxReplicasa un número que el clúster pugui allotjar. Si no, tindràs Pods enPendingi 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 | auroraTres 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"}
9Nou 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 -cRepartiment 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 auroraevicting 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 4El 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
- Què és Docker?
- Instal·lant Docker
- Arquitectura de Docker
- Comandes Bàsiques de Docker
- Entenent les Imatges de Docker
- Creant el teu Primer Contenidor Docker
- El Projecte del Curs: la Plataforma Aurora Libros
Mòdul 2: Treballant amb Imatges Docker
- Docker Hub i Repositoris
- Construint Imatges Docker
- Conceptes Bàsics de Dockerfile
- Instruccions Avançades del Dockerfile
- Gestionant Imatges Docker
- Etiquetatge i Publicació d'Imatges
Mòdul 3: Contenidors Docker
- Executant Contenidors
- Cicle de Vida del Contenidor
- Gestionant Contenidors
- Inspecció i Depuració de Contenidors
- Xarxes a Docker
- Persistència de Dades amb Volums
- Límits de Recursos i Polítiques de Reinici
Mòdul 4: Docker Compose
- Introducció a Docker Compose
- Definint Serveis a Docker Compose
- Comandes de Docker Compose
- Aplicacions Multi-Contenidor
- Variables d'Entorn a Docker Compose
- Perfils, Overrides i Múltiples Entorns
- Desenvolupament Local amb Docker Compose
Mòdul 5: Conceptes Avançats de Docker
- Aprofundiment en Xarxes Docker
- Opcions d'Emmagatzematge Docker
- Millors Pràctiques de Seguretat a Docker
- Optimitzant Imatges Docker
- Builds Avançades amb BuildKit i Buildx
- Registre i Monitoratge a Docker
- El Runtime per Dins: Namespaces, Cgroups i Capes
Mòdul 6: Docker en Producció
- Preparar una Imatge per a Producció
- CI/CD amb Docker
- Orquestrant Contenidors amb Docker Swarm
- Introducció a Kubernetes
- Desplegant Contenidors Docker a Kubernetes
- Escalat i Balanceig de Càrrega
- Estratègies de Desplegament i Rollback
