Un sistema distribuït és un conjunt d'ordinadors independents que, des del punt de vista de l'usuari, es comporten com un únic sistema coherent. Al món actual, pràcticament qualsevol aplicació de certa envergadura (una banca en línia, una plataforma de comerç electrònic o un sistema de gestió de pòlisses d'assegurances) és un sistema distribuït. Comprendre'n els fonaments és imprescindible abans d'abordar els microserveis, perquè gairebé tots els problemes difícils de les arquitectures modernes neixen de la naturalesa distribuïda del sistema, no dels microserveis en si.
En aquesta lliçó establirem què significa "distribuït", desmuntarem les famoses fal·làcies de la computació distribuïda i analitzarem els tres grans reptes que tot arquitecte ha d'interioritzar: la latència, els errors parcials i la consistència.
Contingut
- Què és un sistema distribuït
- Per què distribuïm: avantatges i motivacions
- Les vuit fal·làcies de la computació distribuïda
- Repte 1: la latència de la xarxa
- Repte 2: els errors parcials
- Repte 3: la consistència de les dades
- Models de comunicació
- Errors comuns i consells
- Exercicis
- Conclusió
- Què és un sistema distribuït
Una definició clàssica, atribuïda a Andrew Tanenbaum, diu:
Un sistema distribuït és una col·lecció d'ordinadors independents que apareixen davant els seus usuaris com un únic sistema coherent.
Les tres idees clau són:
- Independents: cada node té la seva pròpia memòria, el seu propi rellotge i pot fallar per separat.
- Coordinats: els nodes cooperen intercanviant missatges a través d'una xarxa.
- Transparència: idealment, l'usuari no percep que hi ha diverses màquines.
Una característica fonamental, formulada per Leslie Lamport, ho resumeix amb ironia:
Un sistema distribuït és aquell en què l'error d'un ordinador que ni tan sols sabies que existia pot deixar inservible el teu propi ordinador.
graph LR
C[Client] --> GW[API Gateway]
GW --> S1[Servei Pòlisses]
GW --> S2[Servei Clients]
S1 --> DB1[(BD Pòlisses)]
S2 --> DB2[(BD Clients)]
S1 -.missatges.-> S2En aquest diagrama, cada node (gateway, serveis, bases de dades) és un procés independent que es comunica per xarxa. Les fletxes contínues representen crides directes i la fletxa discontínua una comunicació asíncrona entre serveis.
- Per què distribuïm: avantatges i motivacions
Distribuir un sistema afegeix complexitat, així que només val la pena quan n'obtenim beneficis reals:
| Motivació | Descripció |
|---|---|
| Escalabilitat | Afegir més màquines per atendre més càrrega (escalat horitzontal). |
| Disponibilitat | Si una rèplica cau, una altra continua donant servei. |
| Tolerància a errors | El sistema continua operatiu malgrat errors parcials. |
| Latència geogràfica | Apropar les dades a l'usuari (CDN, regions). |
| Aïllament | Equips i mòduls evolucionen de manera independent. |
La regla d'or: no distribueixis per moda. Distribueix quan un únic servidor ja no n'hi ha prou o quan l'organització necessita autonomia entre equips.
- Les vuit fal·làcies de la computació distribuïda
El 1994, Peter Deutsch i altres enginyers de Sun Microsystems van enumerar les suposicions errònies que els desenvolupadors fan repetidament en construir sistemes distribuïts. Cadascuna d'elles és falsa, i oblidar-ho provoca bugs subtils i caigudes en producció.
| # | Fal·làcia | Realitat |
|---|---|---|
| 1 | La xarxa és fiable | Els paquets es perden, les connexions es tallen. |
| 2 | La latència és zero | Cada salt de xarxa costa mil·lisegons o més. |
| 3 | L'amplada de banda és infinita | Hi ha límits; transferir dades grans satura la xarxa. |
| 4 | La xarxa és segura | Hi ha atacs, interceptacions i suplantacions. |
| 5 | La topologia no canvia | Servidors apareixen i desapareixen contínuament. |
| 6 | Hi ha un únic administrador | Múltiples equips, proveïdors i configuracions. |
| 7 | El cost de transport és zero | Serialitzar i moure dades consumeix CPU i diners. |
| 8 | La xarxa és homogènia | Conviuen diferents protocols, versions i maquinari. |
El següent exemple en Java mostra codi que cau en la fal·làcia 1 i 2 (la xarxa és fiable i la latència és zero):
// MALAMENT: assumeix que la crida sempre funciona i és instantània
public Cliente obtenerCliente(String id) {
// Sense timeout: si el servei remot no respon,
// aquest fil queda bloquejat indefinidament.
return clienteRestClient.get("/clientes/" + id);
}Aquí no hi ha timeout, ni reintents, ni gestió d'error. Si el servei remot triga o falla, el fil es queda penjat. La versió correcta exigeix timeouts explícits i gestió d'errors, cosa que veurem a la lliçó de resiliència.
- Repte 1: la latència de la xarxa
La latència és el temps que triga un missatge a viatjar d'un node a un altre. A diferència d'una crida a un mètode local (nanosegons), una crida per xarxa costa ordres de magnitud més. Una referència mental útil (números aproximats de Jeff Dean):
| Operació | Temps aproximat |
|---|---|
| Accés a memòria cau L1 | 0,5 ns |
| Accés a memòria RAM | 100 ns |
| Lectura d'SSD | 150.000 ns (0,15 ms) |
| Anada i tornada al mateix centre de dades | 500.000 ns (0,5 ms) |
| Anada i tornada entre continents | 150.000.000 ns (150 ms) |
La conseqüència pràctica és que fer moltes crides petites per xarxa és molt car. Aquest antipatró es coneix com a "N+1 de xarxa":
// MALAMENT: una crida de xarxa per cada comanda (problema N+1)
List<Pedido> pedidos = pedidoService.listar(); // 1 crida
for (Pedido p : pedidos) {
Cliente c = clienteService.obtener(p.getClienteId()); // N crides
p.setCliente(c);
}Si hi ha 100 comandes, això són 101 crides de xarxa. La solució és agrupar (batching), demanant tots els clients de cop:
// BÉ: dues crides en total, independentment del nombre de comandes
List<Pedido> pedidos = pedidoService.listar();
Set<String> ids = pedidos.stream()
.map(Pedido::getClienteId)
.collect(Collectors.toSet());
Map<String, Cliente> clientes = clienteService.obtenerVarios(ids); // 1 crida
pedidos.forEach(p -> p.setCliente(clientes.get(p.getClienteId())));Reduïm de 101 a 2 crides: la diferència de rendiment és enorme.
- Repte 2: els errors parcials
En un sistema monolític, o tot funciona o tot falla. En un de distribuït apareix un estat nou i perillós: l'error parcial, en què uns components funcionen i altres no.
El cas més difícil és la incertesa: quan enviem una petició i no rebem resposta, no sabem si:
- la petició no va arribar al destí,
- va arribar i es va processar però la resposta es va perdre,
- el destí la continua processant lentament.
sequenceDiagram
participant A as Servei A
participant B as Servei B
A->>B: Cobrar 100 EUR
Note over B: Cobra correctament
B--xA: Resposta perduda (timeout)
Note over A: Reintent? Risc de cobrar dues vegadesAixò obliga a dissenyar operacions idempotents: una operació és idempotent si executar-la diverses vegades produeix el mateix resultat que executar-la una sola vegada. Així un reintent és segur.
// Operació idempotent mitjançant clau d'idempotència
public void cobrar(String idempotencyKey, BigDecimal importe) {
// Si ja hem processat aquesta clau, no tornem a cobrar
if (repositorioCobros.existe(idempotencyKey)) {
return; // ja fet, reintent segur
}
procesarPago(importe);
repositorioCobros.guardar(idempotencyKey);
}La idempotencyKey (un identificador únic que el client genera i reenvia en cada reintent) permet detectar duplicats. Sense ella, un reintent podria cobrar dues vegades.
- Repte 3: la consistència de les dades
Quan les dades estan replicades o repartides entre nodes, mantenir-les coherents és difícil. Distingim dos models bàsics:
| Model | Descripció | Exemple d'ús |
|---|---|---|
| Consistència forta | Tota lectura retorna el darrer valor escrit. | Saldo bancari, control d'estoc crític. |
| Consistència eventual | Les rèpliques convergeixen "amb el temps"; pot haver-hi lectures desfasades. | Comptador de "m'agrada", catàleg de productes. |
La consistència forta és còmoda per al programador però cara en rendiment i disponibilitat. La consistència eventual escala millor però obliga a tolerar dades temporalment desactualitzades. A la lliçó sobre el Teorema CAP aprofundirem en per què no es pot tenir tot alhora.
- Models de comunicació
Existeixen dues grans formes de comunicar nodes, que estudiarem en detall més endavant:
- Síncrona (petició/resposta): qui crida espera la resposta (REST, gRPC). Senzilla però acobla disponibilitat: si B està caigut, A se'n veu afectat.
- Asíncrona (missatgeria/esdeveniments): qui crida publica un missatge i continua (cues, esdeveniments). Desacobla, però introdueix complexitat i consistència eventual.
# Exemple conceptual d'esdeveniment publicat en un bus de missatges evento: PolizaCreada version: 1 datos: polizaId: "POL-00123" ramo: "hogar" fechaAlta: "2026-06-30"
Aquest esdeveniment descriu un fet ocorregut ("una pòlissa va ser creada"). Altres serveis poden subscriure-s'hi i reaccionar sense que l'emissor sàpiga qui són, cosa que redueix l'acoblament.
Errors Comuns i Consells
- Tractar les crides de xarxa com a crides locals: sempre hi haurà errors i latència. Posa timeouts a totes les crides remotes.
- Oblidar la idempotència: si has de reintentar, assegura't que l'operació sigui segura de repetir.
- Distribuir prematurament: un monòlit ben fet sol ser millor punt de partida que deu microserveis mal definits.
- Ignorar l'observabilitat: sense logs correlacionats, mètriques i traces, depurar un sistema distribuït és gairebé impossible. Usa un identificador de correlació que viatgi en totes les peticions.
- Confiar en els rellotges: els rellotges de diferents màquines no estan perfectament sincronitzats; no usis marques de temps per ordenar esdeveniments crítics sense un mecanisme adequat.
Exercicis
- Enumera les vuit fal·làcies de la computació distribuïda i indica, per a cadascuna, una conseqüència pràctica si la ignores en el disseny.
- Tens un servei de pagaments que rep peticions per xarxa. Explica per què un simple reintent pot provocar un cobrament doble i dissenya, en pseudocodi Java, una solució idempotent.
- Estima quantes vegades més lenta és una crida entre continents (150 ms) enfront d'un accés a memòria RAM (100 ns). Raona quina implicació té això per al disseny d'APIs.
Solucions
-
Per exemple: La xarxa és fiable → si no gestiones errors, una connexió tallada deixa l'operació en estat inconsistent. La latència és zero → si dissenyes APIs "xerraires" amb moltes anades i tornades, el rendiment es degrada. (La resta es completa de manera anàloga amb la taula de la secció 3.)
-
Un reintent provoca cobrament doble perquè, si la primera petició sí que es va processar però la seva resposta es va perdre, el client creu que va fallar i la reenvia. Solució:
public void cobrar(String idempotencyKey, BigDecimal importe) {
if (repositorioCobros.existe(idempotencyKey)) {
return; // ja cobrat, ignorem el duplicat
}
procesarPago(importe);
repositorioCobros.guardar(idempotencyKey);
}La clau és registrar la clau d'idempotència de manera atòmica juntament amb el cobrament.
- 150 ms = 150.000.000 ns; 150.000.000 / 100 = 1.500.000 vegades més lenta. Implicació: cal minimitzar el nombre de crides remotes (batching, memòries cau, agregació de dades) en lloc de dissenyar APIs que requereixin moltes anades i tornades.
Conclusió
Hem vist que un sistema distribuït és un conjunt de nodes independents que cooperen per xarxa i que semblen un de sol. Les vuit fal·làcies ens recorden que la xarxa no és fiable, ni instantània, ni segura, ni gratuïta. Els tres grans reptes (latència, errors parcials i consistència) condicionen totes les decisions d'arquitectura que prendrem a partir d'ara.
Amb aquests fonaments clars, a la lliçó següent, Arquitectura de Microserveis, veurem com un estil arquitectònic concret aprofita (i alhora pateix) la naturalesa distribuïda per construir sistemes escalables i mantenibles.
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
