L'escalabilitat és la capacitat d'un sistema per gestionar un augment de la càrrega (més usuaris, més peticions, més dades) sense degradar el seu rendiment de manera inacceptable. És un dels atributs de qualitat més importants en l'arquitectura d'aplicacions: un sistema que funciona perfectament amb 100 usuaris pot col·lapsar amb 100.000 si no s'ha dissenyat per créixer. En aquesta lliçó aprendràs les dues estratègies fonamentals d'escalat, com gestionar l'estat per poder escalar, què són els balancejadors de càrrega i els seus algorismes, com dividir les dades mitjançant sharding i, finalment, les lleis matemàtiques (Amdahl i la Llei Universal d'Escalabilitat) que posen límits reals a fins a quin punt podem escalar.
Contingut
- Escalat vertical (scale up) vs escalat horitzontal (scale out)
- El problema de l'estat i les sessions
- Balancejadors de càrrega i algorismes de repartiment
- Sharding: partició de dades
- Els límits teòrics: Llei d'Amdahl i Llei Universal d'Escalabilitat
- Errors habituals i consells
- Exercicis
- Escalat vertical (scale up) vs escalat horitzontal (scale out)
Hi ha dues maneres bàsiques de donar més capacitat a un sistema:
- Escalat vertical (scale up): afegir més recursos a una única màquina (més CPU, més RAM, discos més ràpids). És com canviar el teu cotxe per un de més potent.
- Escalat horitzontal (scale out): afegir més màquines que treballen en paral·lel. És com tenir una flota de cotxes en lloc d'un de sol.
La taula següent compara ambdues estratègies en els aspectes que més importen a l'arquitecte:
| Aspecte | Vertical (scale up) | Horitzontal (scale out) |
|---|---|---|
| Com es creix | Màquina més gran | Més màquines |
| Límit màxim | Físic (maquinari més potent disponible) | Pràcticament il·limitat |
| Cost | Creix exponencialment al final | Creix de manera lineal |
| Punt únic de fallada | Sí (una sola màquina) | No (redundància natural) |
| Complexitat | Baixa (sense canvis al codi) | Alta (estat distribuït, xarxa) |
| Temps d'inactivitat en escalar | Sol requerir reinici | Zero (afegir nodes en calent) |
| Ideal per a | Bases de dades relacionals, càrregues petites/mitjanes | Serveis web sense estat, microserveis |
A la pràctica, els sistemes moderns combinen tots dos: s'escala verticalment fins a un punt raonable de cost/benefici i, a partir d'aquí, s'escala horitzontalment.
# Exemple d'escalat horitzontal a Kubernetes: # passem de 3 a 10 rèpliques d'un servei amb una sola comanda kubectl scale deployment mi-servicio --replicas=10
Aquesta comanda demana a Kubernetes que mantingui 10 còpies (rèpliques) idèntiques del contenidor mi-servicio en execució. L'orquestrador s'encarrega d'arrencar 7 instàncies noves i repartir-les entre els nodes disponibles. No cal tocar el codi: és escalat horitzontal pur.
- El problema de l'estat i les sessions
Per poder escalar horitzontalment, els nodes han de ser intercanviables: qualsevol petició s'ha de poder atendre en qualsevol instància. El gran enemic d'això és l'estat desat a la memòria local d'un node.
- Servei amb estat (stateful): desa dades de l'usuari a la seva pròpia memòria (per exemple, la sessió HTTP). Si la petició següent va a un altre node, aquest node no sap res de l'usuari.
- Servei sense estat (stateless): no desa res propi entre peticions; tota la informació necessària viatja a la petició o viu en un magatzem compartit (base de dades, memòria cau distribuïda).
La regla d'or és: dissenya serveis sense estat i externalitza l'estat.
// MALAMENT: sessió a la memòria local del node (stateful)
@RestController
public class CarritoController {
// Aquest Map viu a la memòria d'AQUEST node concret.
// Si el balancejador envia la següent petició a un altre node, el carret "desapareix".
private final Map<String, Carrito> carritos = new ConcurrentHashMap<>();
}El problema de l'exemple anterior és que el carret de la compra només existeix al node que el va crear. Vegem la versió correcta, que externalitza l'estat a un magatzem compartit (Redis):
// BÉ: estat externalitzat a Redis (stateless des del punt de vista del node)
@RestController
public class CarritoController {
private final RedisTemplate<String, Carrito> redis;
public CarritoController(RedisTemplate<String, Carrito> redis) {
this.redis = redis;
}
@GetMapping("/carrito/{id}")
public Carrito ver(@PathVariable String id) {
// El carret es llegeix de Redis, accessible des de QUALSEVOL node.
return redis.opsForValue().get("carrito:" + id);
}
}Aquí qualsevol node pot atendre la petició perquè el carret viu a Redis, fora de la memòria del procés. Això permet afegir o treure nodes sense perdre dades d'usuari.
Quan no es pot evitar l'estat al node, una solució intermèdia és l'afinitat de sessió (sticky sessions): el balancejador envia sempre el mateix usuari al mateix node. Funciona, però trenca la intercanviabilitat i complica l'escalat i la tolerància a fallades (si cau el node, es perd la sessió).
- Balancejadors de càrrega i algorismes de repartiment
Un balancejador de càrrega (load balancer) és el component que rep totes les peticions entrants i les reparteix entre els nodes disponibles. És la peça que fa possible l'escalat horitzontal de cara al client: aquest veu una única adreça, però al darrere hi ha moltes màquines.
Els algorismes de repartiment més habituals són:
| Algorisme | Com reparteix | Quan fer-lo servir |
|---|---|---|
| Round Robin | Per torns, un rere l'altre | Nodes homogenis i peticions similars |
| Weighted Round Robin | Per torns, però amb pesos | Nodes de potència diferent |
| Least Connections | Al node amb menys connexions actives | Peticions de durada variable |
| IP Hash | Segons el hash de la IP d'origen | Per mantenir afinitat de sessió |
| Least Response Time | Al node que respon més ràpid | Optimitzar la latència |
Vegem una configuració real de balanceig amb Nginx:
# Definim un grup de servidors backend
upstream backend {
least_conn; # Algorisme: menys connexions actives
server 10.0.0.1:8080 weight=2; # Node més potent: pes 2
server 10.0.0.2:8080; # Pes 1 per defecte
server 10.0.0.3:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend; # Reenvia les peticions al grup "backend"
}
}Línia a línia: upstream backend declara el conjunt de nodes. least_conn indica que Nginx enviarà cada petició al node amb menys connexions obertes en aquell moment. El weight=2 del primer servidor fa que rebi aproximadament el doble de trànsit perquè és més potent. Finalment, proxy_pass http://backend connecta el trànsit entrant amb aquest grup.
Convé distingir el nivell de xarxa en què opera el balancejador:
- Capa 4 (L4, transport): reparteix per IP i port, sense mirar el contingut. Molt ràpid.
- Capa 7 (L7, aplicació): entén HTTP, pot encaminar per URL, capçaleres o cookies. Més intel·ligent, una mica més lent.
- Sharding: partició de dades
Escalar els servidors d'aplicació és relativament fàcil perquè són sense estat. La base de dades és molt més difícil perquè té estat per definició. Quan una única base de dades no pot amb tot el volum, es recorre al sharding: dividir les dades en fragments (shards), cadascun en un servidor diferent.
La clau és triar una clau de partició (shard key) que reparteixi les dades de manera equilibrada:
Usuaris particionats per hash(user_id) % 3: hash(user_id) % 3 == 0 -> Shard 0 (servidor A) hash(user_id) % 3 == 1 -> Shard 1 (servidor B) hash(user_id) % 3 == 2 -> Shard 2 (servidor C)
Cada usuari viu en un únic shard, determinat pel seu user_id. Així, les consultes d'un usuari només impacten un servidor.
Estratègies habituals de sharding:
| Estratègia | Descripció | Risc |
|---|---|---|
| Per rang | Per intervals de la clau (A-M, N-Z) | Punts calents si la clau no és uniforme |
| Per hash | Hash de la clau mòdul N | Reparticionar en canviar N és costós |
| Per directori | Una taula de cerca decideix el shard | Punt únic de fallada al directori |
| Geogràfica | Per regió de l'usuari | Compleix la normativa de dades (p. ex. GDPR) |
El gran inconvenient del sharding són les consultes que creuen diversos shards (per exemple, "dona'm totes les comandes de l'últim mes de tots els usuaris"): requereixen consultar tots els servidors i combinar-ne els resultats, cosa que és lenta i complexa. Per això el sharding només s'aplica quan és estrictament necessari.
graph TD
C[Client] --> R[Router de Sharding]
R -->|user_id parell| S0[(Shard 0)]
R -->|user_id senar| S1[(Shard 1)]Aquest diagrama mostra com un router intermedi decideix, a partir de la clau de l'usuari, a quin shard dirigir cada operació. El client no sap que les dades estan repartides.
- Els límits teòrics: Llei d'Amdahl i Llei Universal d'Escalabilitat
No es pot escalar infinitament. Hi ha lleis matemàtiques que ho demostren.
Llei d'Amdahl
La Llei d'Amdahl diu que l'acceleració (speedup) d'un sistema en afegir processadors està limitada per la fracció del treball que no es pot paral·lelitzar (la part seqüencial).
Speedup(N) = 1 / ( (1 - P) + P/N ) P = fracció del treball que SÍ que es pot paral·lelitzar N = nombre de processadors/nodes
Si el 10% del treball és seqüencial (P = 0,9), tot i que tinguis infinits nodes el speedup màxim és 1 / (1 - 0,9) = 10x. És a dir: la part seqüencial posa un sostre absolut. Per això reduir aquesta fracció seqüencial (bloquejos, seccions crítiques, transaccions globals) és tan valuós.
Llei Universal d'Escalabilitat (USL)
La Llei d'Amdahl és optimista perquè ignora el cost de coordinar els nodes entre si. La Llei Universal d'Escalabilitat (USL) de Neil Gunther afegeix aquest cost i prediu una cosa més realista i de vegades sorprenent: a partir de cert punt, afegir més nodes empitjora el rendiment.
C(N) = N / ( 1 + α(N-1) + βN(N-1) ) α (alfa) = cost de contenció (esperar recursos compartits) β (beta) = cost de coherència (sincronitzar estat entre nodes)
El terme βN(N-1) creix de manera quadràtica: cada node nou s'ha de coordinar amb tots els altres. Quan aquest cost de coherència (β) domina, la corba de rendiment puja, arriba a un màxim i després baixa. La lliçó pràctica és clara: minimitza la comunicació i la sincronització entre nodes si vols escalar de debò.
Errors habituals i consells
- Desar estat a la memòria local del node. És l'error número u. Externalitza sempre les sessions a Redis o similar abans d'escalar horitzontalment.
- Abusar de les sticky sessions. Apedaça el símptoma però no la causa; dificulta el repartiment equilibrat i la tolerància a fallades.
- Escalar l'aplicació però oblidar la base de dades. De res no serveixen 50 nodes web si tots impacten una única base de dades saturada. Identifica el veritable coll d'ampolla.
- Aplicar sharding massa aviat. El sharding afegeix una complexitat enorme. Abans prova rèpliques de lectura, memòries cau i índexs.
- Ignorar la USL. Més nodes no sempre és més rendiment. Mesura sempre; no assumeixis un escalat lineal.
- Consell: mesura abans d'escalar. El coll d'ampolla sol estar on no l'esperes.
Exercicis
-
Escalat adequat. Tens una API REST sense estat que rep pics de trànsit impredictibles i un cost d'infraestructura ajustat. Escalaries vertical o horitzontalment, i per què?
-
Diagnòstic de sessió. Un usuari informa que, de manera aleatòria, el seu carret de la compra "es buida" després d'afegir productes. L'aplicació corre en 4 nodes darrere d'un balancejador Round Robin. Quina és la causa més probable i com ho solucionaries?
-
Càlcul amb Amdahl. Un procés té un 25% de treball seqüencial (P = 0,75). Quin és el speedup màxim teòric encara que disposis d'infinits nodes?
Solucions
-
Horitzontal. En ser sense estat, escala horitzontalment sense esforç. L'escalat horitzontal amb autoescalat permet afegir nodes només durant els pics i treure'ls després, optimitzant el cost enfront d'una màquina enorme sempre encesa. A més, evita el punt únic de fallada.
-
La causa és estat de sessió desat a la memòria local de cada node. Amb Round Robin, peticions consecutives del mateix usuari van a nodes diferents que no coneixen el seu carret. Solució correcta: externalitzar la sessió a un magatzem compartit (Redis). Solució pedaç temporal: activar sticky sessions (IP Hash) per forçar que l'usuari torni sempre al mateix node.
-
Aplicant Amdahl amb N tendint a infinit:
Speedup = 1 / (1 - P) = 1 / (1 - 0,75) = 1 / 0,25 = 4x. Encara que tinguis infinits nodes, mai no aniràs més de 4 vegades més ràpid.
Conclusió
Has après que escalar és triar entre créixer cap amunt (vertical) o cap als costats (horitzontal), que l'escalat horitzontal exigeix eliminar l'estat dels nodes, que el balancejador i els seus algorismes reparteixen la càrrega, que el sharding parteix les dades quan la base de dades no pot més, i que les lleis d'Amdahl i la USL posen límits reals a fins a quin punt podem créixer. Un sistema escalable no s'improvisa: es dissenya sense estat, es mesura constantment i es minimitza la coordinació.
Però escalar no n'hi ha prou: el sistema també ha de continuar funcionant quan alguna cosa falla. A la lliçó següent, Alta Disponibilitat i Tolerància a Fallades, veurem com mesurar i garantir que el servei estigui disponible encara que caiguin components individuals.
Curs d'Arquitectura d'Aplicacions
Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
