TechCorp ja veu el que passa (06-01, 06-02) i sobreviu a les fallades (06-03). Queda la tercera pregunta que la Marta porta fent des de 01-05: el Black Friday en què el catàleg va rebre ×20 trànsit i la botiga va estar 50 minuts sense vendre. Al monòlit l'única palanca era "una màquina més gran"; a Kubernetes tenim rèpliques, autoescalat i la possibilitat d'escalar només el servei que ho necessita. Però escalar no és gratis ni automàtic: cal saber què fa escalable un servei, com es diu al clúster que creixi, com es comprova abans amb proves de càrrega i, sobretot, on són els colls d'ampolla que cap rèplica no arregla (la base de dades, un event loop bloquejat, un pool mal dimensionat). Aquesta lliçó recorre aquestes capes de fora cap endins.
Contingut
- Escalat vertical i horitzontal: què fa escalable un servei
- Autoescalat a Kubernetes: HPA per a
servei-cataleg - Escalar consumidors per longitud de cua: KEDA per a
servei-inventari requests,PodDisruptionBudgeti l'escalat del clúster- El cas Black Friday: dimensionar i comprovar amb k6
- Memòria cau: Redis a Catàleg i memòria cau HTTP al gateway
- Pools de connexions: el compte que cal fer
- Consultes, índexs i l'event loop de Node
- Escalar la missatgeria: cues competidores i ordre
- Escalar la base de dades: rèpliques de lectura i particionament
- Taula "símptoma → on mirar → remei"
- Escalat vertical i horitzontal: què fa escalable un servei
| Vertical (scale up) | Horitzontal (scale out) | |
|---|---|---|
| Què és | Més CPU/memòria al mateix procés | Més rèpliques del mateix procés |
| A Kubernetes | Apujar resources.limits (05-02) |
Apujar replicas / HPA |
| Límit | La mida del node; Node.js fa servir un sol fil, així que més CPU gairebé no ajuda | L'estat compartit (BD, cues) |
| Cost del canvi | Reinici del pod | Cap si el servei és "escalable" |
| Quan | Bases de dades, processos amb estat | Serveis HTTP i consumidors sense estat |
Un servei és horitzontalment escalable quan dues rèpliques es comporten igual que una i no es fan nosa. La plantilla de 04-02 ja ho garanteix, i convé recordar per què:
- Sense estat al procés. Res en memòria que una segona rèplica necessiti: ni sessions (el JWT de 07-01 viatja amb la petició), ni memòries cau "de debò" (Redis, apartat 6), ni fitxers locals. La memòria cau en memòria de la plantilla (
Mapamb TTL) és tolerable només si perdre-la no trenca res. - Connexions limitades i conegudes. Cada rèplica obre el seu pool de
pg(apartat 7) i el seu canal de RabbitMQ; N rèpliques = N pools. Si no es compta, la BD es queda sense connexions abans que el servei sense CPU. - Arrencada i aturada ràpides i netes.
readinessProbe(05-02) per no rebre trànsit abans d'hora;SIGTERM(06-03) per no perdre peticions en reduir rèpliques. - Feines periòdiques que toleren rèpliques. El relay de l'outbox i el vigilant (06-03) fan servir
FOR UPDATE SKIP LOCKEDperquè dues rèpliques no es trepitgin; sense això, escalar Comandes duplicaria esdeveniments.
- Autoescalat a Kubernetes: HPA per a
servei-cataleg
servei-catalegL'HorizontalPodAutoscaler observa una mètrica i ajusta replicas del Deployment entre un mínim i un màxim. Per a Catàleg, el servei que multiplica ×20:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: servei-cataleg
namespace: techcorp
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: servei-cataleg
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60 # % sobre resources.requests.cpu
- type: Pods
pods:
metric:
name: http_requests_per_second # mètrica personalitzada servida per Prometheus Adapter
target:
type: AverageValue
averageValue: "150" # peticions/s per pod
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- { type: Percent, value: 100, periodSeconds: 60 } # com a molt duplicar cada minut
scaleDown:
stabilizationWindowSeconds: 300 # esperar 5 min abans de reduir
policies:
- { type: Pods, value: 2, periodSeconds: 60 }Línia a línia:
scaleTargetRef: elDeploymentde 05-02. A partir d'ara no es fixareplicasal manifest (l'HPA el trepitjaria); Kustomize deixa el camp fora i l'HPA mana.minReplicas: 2per disponibilitat (un pod pot morir en qualsevol moment);maxReplicas: 20és el sostre de cost i també protegeix MongoDB de rebre 200 connexions.- CPU al 60 %: el percentatge es calcula sobre
resources.requests.cpu(100m a 05-02), no sobrelimits. Ambrequests: 100m, l'HPA afegeix rèpliques quan la mitjana supera 60m. Per això requests ha de reflectir el consum real en règim normal (apartat 4). - Mètrica personalitzada
http_requests_per_second: ve de l'http_requests_totalde 06-01 a través del Prometheus Adapter, que exposa consultes PromQL com a mètriques de l'APIcustom.metrics.k8s.io. La seva configuració (fragment):
rules:
- seriesQuery: 'http_requests_total{namespace!="",pod!=""}'
resources: { overrides: { namespace: { resource: namespace }, pod: { resource: pod } } }
name: { matches: "^(.*)_total$", as: "${1}_per_second" }
metricsQuery: 'sum(rate(<<.Series>>{<<.LabelMatchers>>}[2m])) by (<<.GroupBy>>)'Converteix http_requests_total en http_requests_per_second per pod amb rate de 2 minuts. Amb dues mètriques, l'HPA calcula les rèpliques necessàries per a cadascuna i agafa la més gran.
behavior: sense això, l'HPA reacciona amb les polítiques per defecte. Aquí pugem ràpid (duplicar per minut: en Black Friday no hi ha temps) i baixem a poc a poc (5 minuts d'estabilització, 2 pods per minut) per evitar el flapping amb pics curts.
Es comprova amb kubectl get hpa -n techcorp (TARGETS 45%/60%, 80/150) i kubectl describe hpa servei-cataleg mostra cada decisió. A 05-04 escalàvem a mà amb kubectl scale; aquesta ordre continua servint per a un ajust puntual, però l'HPA la revertirà al cicle següent (15 s).
- Escalar consumidors per longitud de cua: KEDA per a
servei-inventari
servei-inventariInventari no rep HTTP: consumeix inventari.comandes. La seva càrrega no es veu a la CPU fins que ja va tard; es veu a rabbitmq_queue_messages_ready. L'HPA no parla RabbitMQ, però KEDA (Kubernetes Event-Driven Autoscaling) sí: crea i gestiona un HPA per sota a partir d'scalers per a cues, Prometheus, cron, etc.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: servei-inventari
namespace: techcorp
spec:
scaleTargetRef:
name: servei-inventari
minReplicaCount: 2
maxReplicaCount: 10
cooldownPeriod: 120
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: inventari.comandes
mode: QueueLength
value: "50" # objectiu: 50 missatges pendents per rèplica
authenticationRef:
name: keda-rabbitmq-auth # TriggerAuthentication que apunta al Secret comandes-rabbitmq (05-02)mode: QueueLength, value: 50: KEDA volrèpliques = ceil(missatges_llestos / 50); amb 400 missatges esperant passa a 8 rèpliques.cooldownPeriod: 120: dos minuts amb la cua buida abans de reduir.authenticationRefreutilitza les credencials delSecretde RabbitMQ; no es posen al YAML.- KEDA també pot escalar a zero (
minReplicaCount: 0) per a feines esporàdiques; per a Inventari mantenim 2 perquè el primer missatge no ha d'esperar una arrencada.
Escalar consumidors té un efecte sobre l'ordre dels missatges que es tracta a l'apartat 9, i un límit clar: més rèpliques d'Inventari vol dir més càrrega sobre el seu PostgreSQL. maxReplicaCount: 10 surt del compte de l'apartat 7.
requests, PodDisruptionBudget i l'escalat del clúster
requests, PodDisruptionBudget i l'escalat del clústerTres peces de l'equip de Plataforma que fan possible tot l'anterior:
resources.requestsreals. L'scheduler col·loca pods segons requests; l'HPA calcula percentatges sobre ells. Sirequests.cpu: 100mperò el servei fa servir 300m en repòs, l'HPA veurà "300 %" i escalarà al màxim sense motiu; si en fa servir 20m, mai no escalarà. S'ajusten mirantcontainer_cpu_usage_seconds_total(06-01) del p50 en horari normal, ilimitsamb marge per a pics (05-02: 100m/500m per a Comandes; Catàleg passa a 200m/1000m després de les proves de l'apartat 5).PodDisruptionBudget: en reduir rèpliques, buidar un node (kubectl drain) o actualitzar el clúster, Kubernetes pot matar diversos pods alhora. El PDB hi posa un terra:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: servei-cataleg, namespace: techcorp }
spec:
minAvailable: 1 # o maxUnavailable: 1 per a Deployments grans
selector: { matchLabels: { app: servei-cataleg } }Amb això, un drain que deixaria Catàleg amb zero pods espera que un altre node l'aixequi. Va a la base de Kustomize de tots els serveis amb minReplicas ≥ 2.
- Cluster Autoscaler (o Karpenter): quan l'HPA demana 20 rèpliques i no caben als nodes, els pods queden
Pending; l'autoescalador del clúster afegeix nodes (i els treu en baixar la càrrega). És responsabilitat de Plataforma/proveïdor; per als equips de servei n'hi ha prou de saber que "rèplica pendent durant minuts" vol dir que el clúster està creixent, i que triga 2-4 minuts: per això Catàleg entra al Black Friday ambminReplicasapujat a 6 (un overlay de Kustomize temporal), sense confiar només en la reacció.
- El cas Black Friday: dimensionar i comprovar amb k6
Dades de 01-05: ~3.000 comandes/dia en un dia normal (≈0,03/s de mitjana, amb pics de 0,5/s); Catàleg unes 200 peticions/s en hora punta. En Black Friday: catàleg ×20 (4.000/s) i comandes ×3 (pics d'1,5/s, unes 130 comandes/minut).
Dimensionar és mesurar quant aguanta una rèplica i dividir. Per a això hi ha les proves de càrrega amb k6, al repositori de cada servei sota proves/carrega/:
// servei-cataleg/proves/carrega/cataleg.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 200 }, // rampa fins a 200 usuaris virtuals
{ duration: '5m', target: 200 }, // altiplà
{ duration: '2m', target: 1000 }, // pic ×5 sobre l'altiplà
{ duration: '5m', target: 1000 },
{ duration: '2m', target: 0 } // baixada
],
thresholds: {
http_req_duration: ['p(95)<300'], // p95 per sota de 300 ms
http_req_failed: ['rate<0.01'], // menys de l'1 % d'errors
checks: ['rate>0.99']
}
};
const IDS = ['p-501,p-777', 'p-501', 'p-777,p-812,p-903'];
export default function () {
const ids = IDS[Math.floor(Math.random() * IDS.length)];
const res = http.get(`${__ENV.URL_BASE}/v1/productes?ids=${ids}`, { headers: { 'X-Request-Id': `k6-${__VU}-${__ITER}` } });
check(res, { 'estat 200': (r) => r.status === 200, 'té productes': (r) => r.json('productes').length > 0 });
sleep(0.5);
}stagesdescriu la forma del trànsit: rampes i altiplans, amb un pic que simula l'allau. Cada usuari virtual (VU) executa la funció en bucle amb mig segon de pausa: 1.000 VU ≈ 2.000 peticions/s si el servidor respon a l'instant.thresholdsconverteix la prova en passa/no passa: són els mateixos objectius que el panell de Grafana de 06-01 pinta en vermell (300 ms) i que 06-05 formalitzarà com a SLO. S'executa a CI contra staging (k6 run -e URL_BASE=https://staging.api.techcorp.example proves/carrega/cataleg.js) des d'unJobdel pipeline de 05-03, en una etapa manual abans de campanyes.X-Request-Idamb prefixk6-: els logs de la prova es distingeixen (i es poden excloure) a Loki.
Com llegir el resum de k6:
http_req_duration..............: avg=48ms min=6ms med=31ms max=2.1s p(90)=110ms p(95)=240ms
✓ { expected_response:true }.: p(95)<300
http_req_failed................: 0.32% ✓ 41 ✗ 12760
http_reqs......................: 12801 1830/s
vus_max........................: 1000p(95)=240msamb1830/ssobre 2 rèpliques ⇒ una rèplica sosté ~900 peticions/s dins de l'objectiu. Per a 4.000/s calen almenys 5; amb marge ×2 per a l'HPA i el soroll,minReplicas: 6a la campanya imaxReplicas: 20de sostre. Ambrequests.cpu: 200m, la CPU al 60 % es creua molt abans que les 150 peticions/s per pod, així que la mètrica de CPU serà la que escali a la pràctica.- Un
max=2.1saïllat ambp(95)sa sol ser una arrencada de pod o un GC; es comprova a la traça (06-02) abans de preocupar-se. - Si el p95 no baixa en afegir rèpliques, el coll és al darrere: MongoDB, xarxa o el gateway. Aquí entren els apartats següents.
- Comandes ×3 són 1,5 comandes/s: les seves 2 rèpliques sobren de llarg; el que cal vigilar a Comandes és la saga (RabbitMQ, Inventari, Pagaments), no HTTP.
- Memòria cau: Redis a Catàleg i memòria cau HTTP al gateway
La manera més barata de servir 4.000 peticions/s és no calcular-les. GET /v1/productes?ids= a 03-01 ja retorna Cache-Control: public, max-age=30; ara afegim memòria cau en dues capes:
Cache-aside a productesServei amb Redis. Abans d'anar a MongoDB es mira Redis; si hi falta, es llegeix, es desa amb TTL i es retorna:
// servei-cataleg/src/serveis/productesServei.js
function crearProductesServei({ repositori, redis, ttlSegons = 30, logger }) {
async function obtenirPerIds(ids) {
const claus = ids.map((id) => `producte:${id}`);
const enCau = await redis.mget(claus); // una sola anada i tornada
const resultat = new Map();
const falten = [];
ids.forEach((id, i) => (enCau[i] ? resultat.set(id, JSON.parse(enCau[i])) : falten.push(id)));
if (falten.length) {
const llegits = await repositori.cercarPerIds(falten);
const pipeline = redis.pipeline();
for (const p of llegits) { resultat.set(p.id, p); pipeline.set(`producte:${p.id}`, JSON.stringify(p), 'EX', ttlSegons); }
await pipeline.exec();
logger.debug({ encerts: ids.length - falten.length, fallades: falten.length }, 'memòria cau de productes');
}
return ids.map((id) => resultat.get(id)).filter(Boolean);
}
return { obtenirPerIds };
}mgetipipelineagrupen les operacions: una petició amb 3 ids fa 2 anades i tornades a Redis, no 6.- El TTL de 30 s coincideix amb el
max-ageHTTP: ningú no veu un preu més vell del que ja prometíem. - Invalidació per esdeveniment: Catàleg consumeix el seu propi esdeveniment
producte.actualitzat(publicat quan l'equip d'Experiència de compra canvia un producte) i executaredis.del('producte:p-501'); així un canvi de preu es veu a l'instant i no als 30 s. Un comptadorcache_encerts_total{resultat="encert|fallada"}(06-01) mesura l'eficàcia: a Catàleg, amb 30 s de TTL, s'espera > 95 % d'encerts. - Si Redis no respon, se salta la memòria cau (timeout de 50 ms i
catchque registrawarni va a MongoDB): la memòria cau mai no pot ser causa de fallada (06-03).
Trenca això la regla de la Marta de 04-01 ("dos motors de base de dades, PostgreSQL i MongoDB, i prou")? No: Redis aquí és una memòria cau, no una base de dades: no desa res que no sigui a MongoDB, es pot buidar en qualsevol moment (FLUSHDB) i el servei continua funcionant. La regla és sobre on viu la veritat, no sobre quins processos hi ha al clúster. Redis es desplega amb Helm a techcorp (una instància amb rèplica, 512 Mi) i es configura amb REDIS_URL al ConfigMap.
Memòria cau HTTP al gateway i CDN. Traefik (o un CDN al davant) pot desar en memòria cau respostes public, max-age=30 de GET /v1/productes per URL: el 90 % de les lectures d'un producte popular en Black Friday ni tan sols arriba a Catàleg. Requereix que la clau de memòria cau sigui estable (els ids ordenats: el client de 04-04 ja els ordena) i que les respostes personalitzades (GET /v1/comandes, amb Authorization) portin Cache-Control: private, no-store.
- Pools de connexions: el compte que cal fer
Cada rèplica obre un pool de connexions a PostgreSQL. El compte que cal fer abans d'apujar maxReplicas:
connexions a PostgreSQL = rèpliques × mida del pool × processos per rèplica (+ relay + vigilant + jobs)Per a servei-comandes: pg.Pool({ max: 10 }) × 2 rèpliques + relay i vigilant al mateix pool = 20 connexions. Amb maxReplicas: 10 serien 100 només de Comandes, i PostgreSQL porta max_connections = 100 per defecte per a tots els serveis que comparteixin instància (Clients, Pagaments, Inventari tenen bases separades però, en dev, el mateix servidor). Opcions:
- Abaixar
maxdel pool: per a un servei Node amb 100 ms per consulta, 10 connexions sostenen ~100 consultes/s per rèplica; sol sobrar. Comandes passa amax: 5. - Posar un pooler al davant (PgBouncer en mode transacció): 200 connexions d'aplicació es multiplexen en 20 de reals. És l'habitual amb moltes rèpliques; Plataforma l'afegeix quan algun servei supera 8 rèpliques.
- Configurar timeouts del pool (
connectionTimeoutMillis: 2000,idleTimeoutMillis: 30000) perquè l'espera per una connexió sigui un error ràpid (06-03), no una penjada. - Vigilar la saturació (USE, 06-01):
prom-clientpot exposarpg_pool_esperant(pool.waitingCount) ipg_pool_actives; una cua d'espera creixent amb CPU baixa és la signatura d'un pool petit.
El mateix s'aplica al driver de MongoDB (maxPoolSize, 100 per defecte: massa per a 20 rèpliques → 20) i als canals de RabbitMQ (un per consumidor i un per publicar, no un per missatge).
- Consultes, índexs i l'event loop de Node
Escalar rèpliques no arregla una consulta lenta: la multiplica. Eines per ordre d'ús:
EXPLAIN ANALYZE a PostgreSQL. La consulta d'obtenir(id) de Comandes (04-04) fa LEFT JOIN a clients_ref (la còpia local de dades del client que manté el consumidor comandes.clients):
EXPLAIN ANALYZE
SELECT p.*, c.nom AS client_nom
FROM comandes p LEFT JOIN clients_ref c ON c.id = p.client_id
WHERE p.id = 'com-88213';Nested Loop Left Join (cost=0.71..16.76 rows=1 width=180) (actual time=0.041..0.043 rows=1 loops=1)
-> Index Scan using comandes_pkey on comandes p (actual time=0.022..0.023 rows=1 loops=1)
Index Cond: (id = 'com-88213'::text)
-> Index Scan using clients_ref_pkey on clients_ref c (actual time=0.010..0.010 rows=1 loops=1)
Planning Time: 0.180 ms
Execution Time: 0.071 msDos Index Scan: perfecte. El que cal témer és un Seq Scan sobre comandes a la consulta de llistat per client (WHERE client_id = $1 ORDER BY creat_en DESC), que a 3.000 comandes/dia en un any recorre un milió de files: CREATE INDEX comandes_client_creat_idx ON comandes (client_id, creat_en DESC) i la paginació per cursor de 03-01 (WHERE (creat_en, id) < ($cursor…)) fan servir el mateix índex i mantenen el cost constant encara que la taula creixi. L'extensió pg_stat_statements diu quines consultes consumeixen més temps total; és el primer que es mira quan la BD està alta de CPU.
Índexs a MongoDB. assegurarIndexs() de 04-02 crea { _id } (implícit) i { categoria: 1, nom: 1 }; db.productes.find({...}).explain('executionStats') ha de mostrar IXSCAN, no COLLSCAN, i totalDocsExamined proper a nReturned.
Un sol fil. Node atén milers de connexions amb un fil perquè mai no el bloqueja; una operació síncrona de 100 ms (el calcularRecomanacions que la traça de 06-02 va destapar, un JSON.parse de 20 MB, bcrypt síncron, una regex catastròfica) atura totes les peticions d'aquesta rèplica durant 100 ms. La mètrica nodejs_eventloop_lag_seconds (06-01) ho delata; el remei és fer-ho asíncron, treure-ho a un esdeveniment, o, si és còmput pur i imprescindible, moure-ho a worker_threads (menció: un pool de fils per a tasques intensives en CPU; a TechCorp no n'hi ha cap).
Detalls que sumen: compression() a Express per a respostes grans (llistats de productes: 60-80 % menys bytes), keep-alive als clients HTTP (el fetch de Node 20 ho fa per defecte: evita un handshake TCP per petició entre Comandes i Catàleg), no serialitzar cossos gegants als esdeveniments (comanda.creada porta ids i quantitats, no el catàleg sencer) i prefetch de RabbitMQ d'acord amb el cost del missatge: 10 per a Inventari (consultes ràpides), 2-3 per a Pagaments (crides de segons al PSP).
- Escalar la missatgeria: cues competidores i ordre
Diverses rèpliques d'Inventari consumint inventari.comandes és el patró de consumidors competidors: RabbitMQ reparteix els missatges en round-robin entre els consumidors connectats, cada missatge es lliura a un, i el rendiment creix gairebé linealment fins que la BD diu prou. És el que KEDA explota a l'apartat 3.
El preu és l'ordre: dos missatges de la mateixa comanda (comanda.creada i, un segon després, comanda.cancellada per un client penedit) poden anar a rèpliques diferents i processar-se en ordre invers. Com es tracta a TechCorp:
- Dissenyar els consumidors per al desordre: la màquina d'estats de Comandes ignora transicions impossibles (
pagament.confirmatsobreCANCELLADAno fa res, 06-03) i cada esdeveniment portaocorregut_enper descartar els més antics que l'estat actual. És la solució que ja tenim i n'hi ha prou per al 99 % dels casos. - Particionar per clau quan l'ordre és imprescindible: l'exchange
x-consistent-hashde RabbitMQ (o Kafka amb particions percomandaId, 03-02) envia tots els missatges d'un mateixcomandaIda la mateixa cua/consumidor. Més complex; TechCorp no ho necessita avui. - El cas oposat: un sol consumidor garanteix ordre però no escala; només per a cues de molt baix volum.
Escalar el mateix RabbitMQ: en producció, clúster de 3 nodes amb quorum queues (x-queue-type: quorum, replicades per Raft: un node caigut no perd la cua) en comptes de les clàssiques; ho declara Plataforma al chart de 05-02 i no canvia el codi.
- Escalar la base de dades: rèpliques de lectura i particionament
Els serveis s'escalen sols; la base de dades és el sostre. Palanques, de menor a major esforç:
- Menys consultes: memòria cau (apartat 6), pools correctes (7), índexs (8). Resol la majoria dels casos.
- Rèpliques de lectura: PostgreSQL amb streaming replication; el
ComandaRepositorifa servir dos pools (COMANDES_DB_URLper a escriptures i la saga,COMANDES_DB_LECTURA_URLper a les vistes de consulta del CQRS de 02-05:GET /v1/comandes?clientId=). Preu: retard de replicació de mil·lisegons a segons; unGET /v1/comandes/{id}just després delPOSTha d'anar al primari o acceptarRetry-After(03-01 ja retorna 202 iLocation, així que el client espera). En un proveïdor gestionat és una casella. - Particionament (sharding, menció): repartir
comandesper rang de data (PARTITION BY RANGE (creat_en), mensual, per arxivar) o per hash declient_identre diverses instàncies. TechCorp ho avaluarà quan la taula superi desenes de milions de files; avui és innecessari. MongoDB té sharding natiu per clau, amb la mateixa advertència. - Escalat vertical de la instància: la palanca legítima per a les bases de dades: més CPU, RAM i IOPS. Costa diners, no complexitat.
Regla: primer mesurar (pg_stat_statements, latència de consultes a les traces de 06-02), després triar la palanca més barata que resolgui el símptoma.
- Taula "símptoma → on mirar → remei"
| Símptoma (mètrica de 06-01) | On mirar | Remei probable |
|---|---|---|
p95 d'http_request_duration_seconds puja amb la càrrega i CPU del pod > 80 % |
kubectl top pod, HPA |
Més rèpliques: HPA amb requests ben posats |
p95 puja però CPU baixa i pg_pool_esperant > 0 |
Pool de pg, pg_stat_activity |
Ampliar pool o PgBouncer; revisar consultes lentes |
nodejs_eventloop_lag_seconds > 0,1 s |
Traça amb forats (06-02), codi síncron | Eliminar l'operació bloquejant; worker_threads si és còmput |
rabbitmq_queue_messages_ready{queue="inventari.comandes"} creix sense parar |
Rèpliques d'Inventari, la seva BD | KEDA / més consumidors; comprovar que la BD d'Inventari aguanta |
cache_encerts_total{resultat="fallada"} alt a Catàleg |
Redis (caigut? TTL massa baix), invalidacions excessives | Revisar TTL, mida de Redis, esdeveniment producte.actualitzat |
| BD al 100 % de CPU amb rèpliques de servei ocioses | pg_stat_statements, EXPLAIN ANALYZE |
Índexs, memòria cau, rèplica de lectura |
Pods Pending en escalar |
kubectl describe pod ("Insufficient cpu") |
Cluster Autoscaler; ajustar requests; apujar minReplicas abans de la campanya |
| Molts 429 al gateway amb serveis sans | Rate limit del gateway (03-04) | Apujar el límit per a clients legítims; el 429 està fent la seva feina |
| La latència puja després d'un desplegament amb la mateixa càrrega | Comparar per version (06-01), traça |
Regressió de rendiment: revertir el canary (05-04) i perfilar |
Errors Comuns i Consells
- HPA sobre un
Deploymentambreplicasfixat a Git. Argo CD (05-03) i l'HPA es barallen: l'un posa 2, l'altre 8, cada minut. Treurereplicasdel manifest (oignoreDifferencesa Argo). requestsinventats. Ambrequests.cpu: 1000mper a un servei que fa servir 50m, l'HPA mai no supera el 5 % i no escala mai; amb 10m, escala al màxim de seguida. Mesurar.- Escalar el servei i oblidar la base de dades. 20 rèpliques × 10 connexions = 200 >
max_connections. El compte de l'apartat 7 abans d'apujar el màxim. - Memòria cau sense invalidació ni TTL. Preus vells durant hores. TTL sempre; esdeveniment d'invalidació quan importi.
- Memòria cau que es converteix en dependència. Si Redis cau i el servei falla, la memòria cau ha deixat de ser memòria cau. Timeout curt i fallback a la BD.
- Proves de càrrega contra producció o des del portàtil. Els resultats no volen dir res (o tomben producció). Staging amb dades i mida realistes, generador de càrrega dins del clúster o en una màquina dedicada.
- Fixar-se en la mitjana.
avg=48msamaga un p95 de 240 i un p99 de 900. Percentils sempre. - Afegir rèpliques per arreglar una consulta sense índex. La BD empitjora. Índex primer.
- Consell: fer la prova de k6 abans de cada campanya i desar el resum al costat del tag de la imatge; comparar amb l'anterior detecta regressions.
- Consell: cada servei documenta el seu "full de capacitat": peticions/s per rèplica al p95 objectiu,
requests/limits, mida del pool,min/maxde l'HPA i la data de l'última prova de càrrega.
Exercicis
Exercici 1: dimensionar Comandes i la seva base de dades
Una rèplica de servei-comandes sosté 40 POST /v1/comandes/s al p95 < 500 ms amb pg.Pool({ max: 5 }). Per al Black Friday s'esperen pics d'1,5 comandes/s d'HTTP més el consumidor de la saga i el relay. Proposa minReplicas/maxReplicas, calcula les connexions màximes a PostgreSQL i digues si cal PgBouncer.
Exercici 2: KEDA per a Notificacions
servei-notificacions consumeix notificacions.comandes i envia correus a través d'un proveïdor que admet 20 enviaments/s per connexió. Escriu l'ScaledObject amb valors justificats i explica quin límit impedeix apujar maxReplicaCount sense més.
Exercici 3: diagnòstic
Durant la prova de k6, el p95 de Catàleg passa de 90 ms amb 200 VU a 1,2 s amb 1.000 VU; l'HPA ha pujat a 12 rèpliques, la CPU mitjana dels pods és del 25 % i cache_encerts_total{resultat="encert"} és del 40 %. On és el coll d'ampolla i quines dues coses comprovaries primer?
Solucions
Exercici 1
1,5 comandes/s és el 4 % de la capacitat d'una rèplica: minReplicas: 2 (disponibilitat, no capacitat) i maxReplicas: 4 sobra amb escreix per a pics i per a la feina asíncrona. Connexions: 4 rèpliques × 5 = 20 al primari, més el Job de migracions ocasional (05-02) i el vigilant/relay que comparteixen el pool: ~20-22. Amb max_connections = 100 compartides per Comandes, Clients, Pagaments i Inventari, Comandes consumeix una cinquena part: acceptable, no cal PgBouncer encara. S'anota al full de capacitat que si maxReplicas supera 8 o s'afegeixen més serveis a la mateixa instància, es revisa.
Exercici 2
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: servei-notificacions, namespace: techcorp }
spec:
scaleTargetRef: { name: servei-notificacions }
minReplicaCount: 1 # les notificacions toleren segons d'espera; 1 basta en repòs
maxReplicaCount: 5
cooldownPeriod: 300
triggers:
- type: rabbitmq
metadata: { protocol: amqp, queueName: notificacions.comandes, mode: QueueLength, value: "100" }
authenticationRef: { name: keda-rabbitmq-auth }value: 100 perquè un correu triga ~50 ms i 100 missatges es buiden en 5 s per rèplica; no hi ha pressa de negoci. El límit real és el proveïdor de correu: 20 enviaments/s per connexió i probablement una quota global; 5 rèpliques × 20 = 100/s, que ja és a prop de la quota contractada. Apujar maxReplicaCount sense apujar la quota només produiria 429 del proveïdor (que el consumidor tractaria com a transitori i enviaria a notificacions.comandes.reintent, 06-03). El límit ve de la dependència externa, no del clúster.
Exercici 3
Rèpliques al 25 % de CPU i latència disparada: el coll no és Catàleg, és al darrere. La pista és la memòria cau: 40 % d'encerts és molt baix per a un TTL de 30 s amb productes populars; el 60 % de les peticions baixen a MongoDB. Comprovar primer (1) Redis: està responent o el timeout de 50 ms està saltant (warn a Loki "memòria cau no disponible") de manera que tot va a la BD?; els ids arriben en ordre diferent i generen claus diferents? (aquí les claus són per producte, així que seria l'execució del pipeline); (2) MongoDB: db.currentOp(), CPU de la instància i explain de cercarPerIds: amb _id indexat hauria de ser ràpid; si la instància està saturada de connexions (12 rèpliques × maxPoolSize 100 = 1.200), aquí és el problema: abaixar maxPoolSize a 20 i, si és Redis, arreglar-ho abans de tornar a llançar la prova. Afegir més rèpliques no ajudaria; probablement empitjoraria.
Conclusió
Escalar a TechCorp deixa de ser "una màquina més gran" per convertir-se en un conjunt de decisions mesurables. Un servei de la plantilla és escalable perquè no desa estat, limita les seves connexions i arrenca i s'atura amb netedat; l'HorizontalPodAutoscaler de servei-cataleg (2-20 rèpliques, CPU al 60 % i http_requests_per_second via Prometheus Adapter, amb behavior asimètric) i l'ScaledObject de KEDA per a servei-inventari sobre inventari.comandes decideixen les rèpliques; requests reals, PodDisruptionBudget i el Cluster Autoscaler sostenen aquestes decisions; k6 (proves/carrega/cataleg.js, p(95)<300) les comprova abans del Black Friday. A sota, el rendiment es guanya amb memòria cau cache-aside a Redis (TTL 30 s, invalidació per producte.actualitzat, mai dependència), pools de connexions que caben a max_connections, índexs comprovats amb EXPLAIN ANALYZE, un event loop sense bloquejos, consumidors competidors que toleren el desordre i rèpliques de lectura per a les vistes de consulta. Tenim observabilitat, resiliència i capacitat; el que falta és acordar què és "anar bé" i què fer quan no: quanta latència i quants errors acceptem, quan ha de sonar el telèfon de la guàrdia i com es gestiona i s'aprèn d'un incident. Aquest és el tancament del mòdul: SLOs, alertes i gestió d'incidents.
Curs de Microserveis
Mòdul 1: Introducció als Microserveis
- Conceptes Bàsics de Microserveis
- Avantatges i Desavantatges dels Microserveis
- Comparació amb l'Arquitectura Monolítica
- Quan Adoptar Microserveis: Criteris de Decisió
- El Cas Pràctic del Curs: la Botiga Online de TechCorp
Mòdul 2: Disseny de Microserveis
- Principis de Disseny de Microserveis
- Descomposició d'Aplicacions Monolítiques
- Definició de Bounded Contexts
- Gestió de Dades: una Base de Dades per Servei
- Consistència Distribuïda: Sagues, CQRS i Event Sourcing
Mòdul 3: Comunicació entre Microserveis
- APIs RESTful
- Missatgeria Asíncrona
- Protocols de Comunicació: gRPC, GraphQL
- API Gateway i Backend for Frontend
- Descobriment de Serveis i Balanceig de Càrrega
- Contractes i Versionat d'APIs
Mòdul 4: Implementació de Microserveis
- Elecció de Tecnologies i Eines
- Desenvolupament d'un Microservei Simple
- Gestió de Configuració
- Integració Pràctica: Consumir APIs i Publicar Esdeveniments
- Proves en Microserveis: Unitàries, d'Integració i de Contracte
Mòdul 5: Desplegament i Orquestració
- Contenidors i Docker
- Orquestració amb Kubernetes
- CI/CD per a Microserveis
- Estratègies de Desplegament: Rolling, Blue-Green i Canary
- Service Mesh: Istio i Linkerd
Mòdul 6: Monitoratge i Manteniment
- Monitoratge i Logging
- Traçabilitat Distribuïda amb OpenTelemetry
- Gestió d'Errors i Recuperació
- Escalabilitat i Rendiment
- SLOs, Alertes i Gestió d'Incidents
Mòdul 7: Seguretat en Microserveis
- Autenticació i Autorització
- Seguretat en la Comunicació
- Pràctiques de Seguretat
- Seguretat en Contenidors i Kubernetes
