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

  1. Escalat vertical (scale up) vs escalat horitzontal (scale out)
  2. El problema de l'estat i les sessions
  3. Balancejadors de càrrega i algorismes de repartiment
  4. Sharding: partició de dades
  5. Els límits teòrics: Llei d'Amdahl i Llei Universal d'Escalabilitat
  6. Errors habituals i consells
  7. Exercicis

  1. 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.

  1. 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ó).

  1. 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.

  1. 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è 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.

  1. 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

  1. 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è?

  2. 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?

  3. 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

  1. 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.

  2. 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.

  3. 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

Mòdul 2: Principis i Tàctiques de Disseny

Mòdul 3: Estils i Patrons Arquitectònics

Mòdul 4: Arquitectures Distribuïdes i Microserveis

Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria

Mòdul 6: Disseny Dirigit pel Domini (DDD)

Mòdul 7: Dades i Persistència

Mòdul 8: Arquitectura al Núvol i Desplegament

Mòdul 9: Qualitat, Seguretat i Observabilitat

Mòdul 10: Evolució, Governança i Casos Pràctics

© Copyright 2026. Tots els drets reservats