Quan parlem d'"un sistema distribuït" podem estar referint-nos a coses molt diferents: un navegador que parla amb un servidor web, una xarxa d'intercanvi de fitxers sense cap servidor central, o una plataforma amb desenes de serveis que s'envien esdeveniments entre si. Per raonar amb precisió necessitem models: descripcions simplificades que fixen com s'organitzen els nodes, com interactuen i què podem suposar sobre el seu comportament quan alguna cosa va malament.
En aquesta lliçó presentem tres famílies de models que apareixeran constantment a la resta del curs: els models d'arquitectura (com es reparteixen els papers entre els nodes), els models d'interacció i de fallada (quines suposicions fem sobre la comunicació i els errors) i, només com a avançament, els models de consistència (quines garanties dona el sistema sobre les dades). Els aplicarem a Quilòmetre Zero per veure com cada model encaixa —o no— amb les seves necessitats.
Contingut
- Per a què serveixen els models
- Models d'arquitectura
- Models d'interacció: síncron i asíncron
- Models de fallada
- Models de consistència: una primera visió
- Exemple de codi: un servei
catalegclient-servidor - Exemple de codi: catàlegs compartits en una xarxa P2P simulada
- Errors comuns i consells
- Exercicis
- Conclusió
- Per a què serveixen els models
Un model és un conjunt de supòsits explícits. Quan diem "aquest algorisme funciona en un sistema asíncron amb fallades de tipus crash-stop", estem declarant exactament en quines condicions l'algorisme és correcte i, sobretot, en quines no ho és. Sense models, les discussions d'arquitectura es converteixen en intercanvis d'opinions; amb models, es converteixen en raonaments verificables.
Les tres famílies responen a tres preguntes diferents:
| Família | Pregunta que respon | Exemple de resposta |
|---|---|---|
| Arquitectura | Quin paper juga cada node i com es relacionen? | "Client-servidor en tres capes" |
| Interacció i fallada | Què podem suposar sobre els temps i sobre els errors? | "Asíncron, amb fallades crash-stop" |
| Consistència | Quines garanties ofereix el sistema sobre les dades que retorna? | "Consistència eventual" |
- Models d'arquitectura
2.1 Client-servidor
És el model més antic i el més estès. Els nodes es divideixen en dos papers: els servidors ofereixen un servei (esperen peticions i les responen) i els clients el consumeixen (envien peticions i esperen respostes). Els papers són asimètrics: el servidor no inicia converses.
El monòlit inicial de Quilòmetre Zero n'és un exemple pur:
flowchart LR
A[Anna - navegador] -->|"GET /cataleg?q=formatge"| S
M[Marc - app mobil] -->|"POST /comandes"| S
L[Llucia - navegador] -->|"GET /comandes/1234"| S
S["Servidor Quilòmetre Zero<br/>(app Python + PostgreSQL)"]
S -->|resposta HTML/JSON| A
S -->|resposta JSON| M
S -->|resposta HTML| L
Característiques:
- Avantatges: senzill d'entendre, l'estat està centralitzat (fàcil de mantenir coherent), la seguretat es concentra en un punt.
- Inconvenients: el servidor és un coll d'ampolla i un punt únic de fallada; l'escalabilitat depèn de fins on pugui créixer aquest servidor (o de replicar-lo, cosa que introdueix nous problemes).
Convé aclarir que "client" i "servidor" són papers, no màquines: a Quilòmetre Zero, el mòdul comandes és servidor per a l'app mòbil i, alhora, client del mòdul inventari quan li demana que comprovi l'estoc.
2.2 Multicapa (n-tier)
Una evolució natural del model client-servidor consisteix a separar el servidor en capes amb responsabilitats diferents, cadascuna potencialment en màquines diferents. La versió més habitual és la de tres capes:
- Presentació (web): rep les peticions HTTP, serveix contingut estàtic, gestiona sessions i TLS.
- Lògica de negoci (aplicació): executa les regles del domini (calcular preus, validar comandes, aplicar descomptes de campanya).
- Dades: emmagatzema i recupera la informació de manera persistent.
Aplicat a Quilòmetre Zero després de l'incident de la Setmana de la Verema, un primer pas raonable seria aquest:
flowchart TB
subgraph Capa1["Capa de presentació"]
W1[Servidor web 1]
W2[Servidor web 2]
end
subgraph Capa2["Capa d'aplicació"]
A1[App Python 1]
A2[App Python 2]
A3[App Python 3]
end
subgraph Capa3["Capa de dades"]
DB[(PostgreSQL)]
end
Clients[Anna, Marc, Llucia] --> LB[Balancejador]
LB --> W1
LB --> W2
W1 --> A1
W1 --> A2
W2 --> A2
W2 --> A3
A1 --> DB
A2 --> DB
A3 --> DB
L'important d'aquest model és que cada capa s'escala de manera independent: si el problema és de CPU a la lògica de negoci, s'afegeixen instàncies de l'aplicació; si és de trànsit web, s'afegeixen servidors web. A més, cada capa només parla amb les adjacents, cosa que simplifica el raonament i la seguretat.
Fixa't, però, que la capa de dades continua sent un únic node: la multicapa alleuja el problema de la Setmana de la Verema (la capa d'aplicació ja no s'ofega), però no el resol, perquè PostgreSQL continuarà esgotant les seves connexions. Hi tornarem a la lliçó 01-06 i als mòduls 3 i 4.
2.3 Peer-to-peer (P2P)
A l'extrem oposat al client-servidor hi ha el model entre iguals: tots els nodes (peers) tenen el mateix paper, actuen simultàniament com a clients i com a servidors, i no hi ha cap autoritat central. Exemples clàssics: BitTorrent, les xarxes de blockchain, o el protocol de descobriment de molts sistemes d'emmagatzematge (Cassandra fa servir gossip entre iguals, com veurem a 04-04).
A Quilòmetre Zero podríem imaginar un escenari P2P per als mercats físics: cada mercat local té un petit ordinador amb el catàleg dels seus productors, i els mercats s'intercanvien els catàlegs directament entre ells, sense dependre del servidor central (que pot estar caigut, o no tenir cobertura en una zona rural).
flowchart LR
M1["Mercat Girona<br/>(Formatgeria Montblanc)"]
M2["Mercat Lleida<br/>(Horta La Vega)"]
M3["Mercat Tarragona<br/>(Celler Roure Alt)"]
M4["Mercat València"]
M1 <-->|intercanvi de catàlegs| M2
M1 <--> M3
M2 <--> M3
M3 <--> M4
M2 <--> M4
| Aspecte | Client-servidor | Peer-to-peer |
|---|---|---|
| Papers | Asimètrics | Simètrics |
| Punt únic de fallada | Sí (el servidor) | No |
| Escalabilitat | Limitada pel servidor | Creix amb el nombre de peers |
| Coherència de dades | Fàcil (estat centralitzat) | Difícil (estat repartit, sense autoritat) |
| Seguretat i control | Centralitzats | Difícils (de qui et refies?) |
| Descobriment | Trivial (adreça coneguda) | Complex (com trobo els altres?) |
2.4 Arquitectures orientades a serveis i microserveis
La quarta família és la que dominarà la segona meitat del curs, així que aquí només la presentem com a model. La idea és descompondre l'aplicació en serveis independents, cadascun responsable d'una àrea funcional, amb el seu propi desplegament i (habitualment) les seves pròpies dades. Els serveis es comuniquen entre si mitjançant peticions (RPC, HTTP) o mitjançant esdeveniments (cues de missatges).
Per a Quilòmetre Zero, els serveis corresponen als seus dominis funcionals: cataleg, comandes, inventari, pagaments, repartiment i analitica.
flowchart LR
GW[Gateway] --> C[cataleg]
GW --> P[comandes]
GW --> R[repartiment]
P --> I[inventari]
P --> PA[pagaments]
P -.->|esdeveniment ComandaCreada| AN[analitica]
R -.->|esdeveniment PosicioActualitzada| AN
Aquest model combina trets dels anteriors: cada servei és client-servidor respecte als altres, i internament sol desplegar-se en capes. El seu gran avantatge és la independència (de desplegament, d'escalat, de fallada i d'equip); el seu gran cost, la complexitat de tenir desenes de peces comunicant-se per xarxa. Ho desenvoluparem a la lliçó 08-01, i tota l'evolució de Quilòmetre Zero (lliçó 01-06) va encaminada cap a aquest model.
- Models d'interacció: síncron i asíncron
La paraula "síncron" es fa servir amb dos significats diferents en sistemes distribuïts, i confondre'ls és font de molts malentesos. Els separarem amb cura.
3.1 Comunicació síncrona davant d'asíncrona (estil d'interacció)
Es refereix a com es comporta l'emissor després d'enviar un missatge:
- Comunicació síncrona: l'emissor envia la petició i es bloqueja esperant la resposta. És l'estil "petició-resposta": cridar una funció remota, fer una petició HTTP. Exemple a Quilòmetre Zero:
comandespregunta ainventarisi hi ha estoc i no continua fins a obtenir la resposta. - Comunicació asíncrona: l'emissor envia el missatge i continua amb la seva feina. La resposta, si n'hi ha, arribarà més tard per un altre canal (una cua, una crida de retorn). Exemple: quan es confirma una comanda,
comandespublica l'esdeveniment "ComandaConfirmada" i no espera queanaliticael processi.
| Síncrona | Asíncrona | |
|---|---|---|
| Acoblament temporal | Alt: tots dos han d'estar disponibles alhora | Baix: el receptor pot processar més tard |
| Simplicitat del codi | Alta (es llegeix com una crida normal) | Menor (cal gestionar respostes diferides) |
| Comportament davant de fallada del receptor | L'emissor falla o espera | El missatge espera a la cua |
| Exemple típic | HTTP, RPC, gRPC (Mòdul 2, lliçons 02-02 i 02-03) | Cues de missatges, esdeveniments (lliçons 02-04 i 02-05) |
3.2 Sistemes síncrons davant d'asíncrons (supòsits sobre el temps)
Aquest segon significat és més teòric i més profund. Es refereix a què podem suposar sobre els temps del sistema:
- Sistema síncron: existeixen límits coneguts per a (a) el temps que triga un missatge a arribar, (b) el temps que triga un node a executar un pas i (c) la deriva dels rellotges. Sota aquests supòsits, si un node no respon dins del temps màxim, sabem amb certesa que ha fallat.
- Sistema asíncron: no existeix cap límit: un missatge pot trigar un temps arbitrari (però finit) a arribar, i un node pot trigar un temps arbitrari a fer un pas. Sota aquests supòsits, és impossible distingir un node lent d'un node caigut.
- Parcialment síncron: el model més realista. El sistema es comporta com a asíncron durant períodes de temps (congestió, pauses de recol·lecció de brossa, sobrecàrrega), però "la major part del temps" respecta certs límits. Gairebé tots els sistemes reals (i gairebé tots els algorismes pràctics de consens, que veurem a 03-03) assumeixen aquest model.
Per què importa? Perquè hi ha un resultat teòric cèlebre (el resultat d'impossibilitat FLP, de Fischer, Lynch i Paterson, 1985) que demostra que en un sistema purament asíncron en què un sol node pugui fallar, no existeix cap algorisme determinista que garanteixi assolir el consens. No és una limitació d'enginyeria: és una impossibilitat matemàtica. Els sistemes reals la sortegen assumint sincronia parcial (timeouts) o acceptant garanties probabilístiques. Per això, quan a Quilòmetre Zero el servei comandes espera una resposta d'inventari, l'única eina que té és un timeout: una aposta sobre quant és "massa temps", mai una certesa. La lliçó 01-04 explora aquesta idea amb codi.
- Models de fallada
Un model de fallada descriu de quines maneres es pot comportar malament un component. Com més ampli és el model, més difícil (i més car) és tolerar-lo. De menor a major gravetat:
| Model | Descripció | Exemple a Quilòmetre Zero | Cost de tolerar-lo |
|---|---|---|---|
| Crash-stop (aturada per fallada) | El node funciona correctament fins que s'atura, i ja no torna (o torna com a node nou, sense recordar res) | El servidor de pagaments s'apaga per una fallada d'alimentació |
Baix: n'hi ha prou amb replicar |
| Crash-recovery (fallada i recuperació) | El node s'atura però pot reiniciar-se, conservant el seu estat persistent (disc) però no el volàtil (memòria) | comandes es reinicia després de quedar-se sense memòria; recorda les comandes desades a la base de dades, però no les que tenia en memòria |
Mitjà: cal gestionar la recuperació |
| D'omissió | El node no rep o no envia alguns missatges (però els que envia són correctes) | La xarxa perd la petició de comandes a inventari; o inventari la processa però la resposta es perd |
Mitjà: reintents, confirmacions |
| De temporització | El node respon correctament però fora del temps esperat | inventari triga 8 segons a respondre per una consulta lenta a la base de dades |
Mitjà: timeouts (i ambigüitat) |
| Arbitrari o bizantí | El node pot fer qualsevol cosa: enviar dades incorrectes, mentir, comportar-se de manera inconsistent amb nodes diferents, ser maliciós | Un node compromès d'inventari respon "hi ha estoc" a comandes i "no hi ha estoc" a analitica |
Molt alt: calen 3f+1 nodes per tolerar f bizantins (lliçó 03-03) |
Algunes observacions pràctiques:
- Les fallades d'omissió i de temporització, des del punt de vista de l'emissor, són indistingibles d'un crash: en els tres casos, l'únic que observa és que no arriba resposta a temps.
- La immensa majoria dels sistemes empresarials (inclòs Quilòmetre Zero) assumeixen el model crash-recovery amb fallades d'omissió, i no toleren fallades bizantines: es confia que els nodes propis no menteixen. La tolerància bizantina es reserva per a sistemes on participen parts que no es refien entre si (blockchains, sistemes crítics d'aviació).
- Un model de fallada també diu quantes fallades simultànies tolerem: "el sistema continua funcionant si fallen fins a f dels n nodes".
- Models de consistència: una primera visió
La tercera família respon a quines garanties ofereix el sistema quan hi ha diverses còpies de les dades (cosa que, com veurem, és gairebé inevitable tan bon punt distribuïm). Això és matèria del Mòdul 3 sencer, així que aquí n'hi ha prou amb tenir una intuïció dels dos extrems:
| Model | Garantia | Preu | Exemple a Quilòmetre Zero |
|---|---|---|---|
| Consistència forta | Tota lectura retorna l'últim valor escrit, en qualsevol còpia, com si només n'existís una | Latència i disponibilitat: cal coordinar les còpies abans de respondre | L'estoc de l'última unitat de formatge de la Formatgeria Montblanc: l'Anna i en Marc no la poden comprar tots dos |
| Consistència eventual | Si deixen de produir-se escriptures, totes les còpies acabaran convergint al mateix valor; mentrestant, una lectura pot retornar un valor antic | Lectures obsoletes durant un temps | El nombre de "m'agrada" d'un producte, o el catàleg de fotos: no passa res si la Llúcia veu la foto antiga durant uns segons |
Entre tots dos extrems hi ha una escala de models intermedis (lectura de les teves pròpies escriptures, consistència causal, etc.) que s'estudien a la lliçó 03-01, i el famós teorema CAP (03-02) explica per què no es pot tenir tot alhora. De moment, queda't amb la idea: triar el model de consistència és una decisió de negoci per dada, no una propietat global del sistema.
- Exemple de codi: un servei
cataleg client-servidor
cataleg client-servidorImplementarem el model client-servidor en la seva forma més elemental, amb asyncio i sense cap dependència externa. El servidor serà una primera versió (molt simplificada) del servei cataleg de Quilòmetre Zero: manté un petit catàleg en memòria i respon a consultes de text. El protocol serà deliberadament primitiu (una línia de text per petició i una resposta JSON per línia) perquè els protocols "de debò" són el tema de la lliçó 02-01.
Desa aquest codi com a cataleg_servidor.py:
import asyncio
import json
CATALEG = [
{"id": 1, "nom": "Tomàquet rosa", "productor": "Horta La Vega", "preu": 3.20},
{"id": 2, "nom": "Formatge curat d'ovella", "productor": "Formatgeria Montblanc", "preu": 14.50},
{"id": 3, "nom": "Formatge fresc", "productor": "Formatgeria Montblanc", "preu": 6.80},
{"id": 4, "nom": "Vi negre criança", "productor": "Celler Roure Alt", "preu": 9.90},
{"id": 5, "nom": "Carbassó", "productor": "Horta La Vega", "preu": 2.10},
]
def cercar(text: str) -> list[dict]:
"""Retorna els productes el nom o productor dels quals conté el text."""
text = text.lower()
return [
p for p in CATALEG
if text in p["nom"].lower() or text in p["productor"].lower()
]
async def atendre_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""S'executa una vegada per cada client que es connecta."""
adreca = writer.get_extra_info("peername")
print(f"[servidor] connexió des de {adreca}")
try:
while True:
linia = await reader.readline() # espera una petició
if not linia: # el client ha tancat la connexió
break
consulta = linia.decode().strip()
resultat = cercar(consulta)
resposta = json.dumps(resultat, ensure_ascii=False) + "\n"
writer.write(resposta.encode()) # envia la resposta
await writer.drain() # espera que surti per la xarxa
print(f"[servidor] '{consulta}' -> {len(resultat)} resultats")
finally:
writer.close()
await writer.wait_closed()
print(f"[servidor] connexió tancada amb {adreca}")
async def main():
servidor = await asyncio.start_server(atendre_client, "127.0.0.1", 8765)
print("[servidor] cataleg escoltant a 127.0.0.1:8765")
async with servidor:
await servidor.serve_forever()
if __name__ == "__main__":
asyncio.run(main())I aquest com a cataleg_client.py:
import asyncio
import json
async def consultar(consulta: str) -> list[dict]:
"""Obre una connexió, envia una consulta i retorna la resposta."""
reader, writer = await asyncio.open_connection("127.0.0.1", 8765)
writer.write((consulta + "\n").encode())
await writer.drain()
linia = await reader.readline() # es bloqueja fins a rebre la resposta
writer.close()
await writer.wait_closed()
return json.loads(linia.decode())
async def main():
for consulta in ["formatge", "vega", "vi"]:
productes = await consultar(consulta)
print(f"Consulta '{consulta}':")
for p in productes:
print(f" - {p['nom']} ({p['productor']}): {p['preu']:.2f} €")
if __name__ == "__main__":
asyncio.run(main())Executa primer el servidor en una terminal i després el client en una altra. Analitzem les peces:
asyncio.start_servercrea un servidor que escolta al port 8765 de la màquina local. Per cada client que es connecta,asynciocridaatendre_clientamb un parell d'objectes:reader(per llegir el que envia el client) iwriter(per respondre-li).- El bucle
while Truedins d'atendre_clientpermet que un mateix client enviï diverses consultes per la mateixa connexió. Quanreadline()retorna una cadena buida, vol dir que el client ha tancat la connexió. await writer.drain()és la part "distribuïda" del codi: el servidor no controla quan sortiran els bytes per la xarxa;drain()espera que el sistema operatiu els hagi acceptat. És el primer lloc on el codi toca la realitat física de la xarxa.- Al client,
consultarés un exemple perfecte de comunicació síncrona (estil petició-resposta): envia la consulta iawait reader.readline()no continua fins que arriba la resposta. Si el servidor no respongués mai, el client es quedaria esperant per sempre: no hi hem posat cap timeout (ho arreglarem a 01-04). - Els papers estan clarament separats: el servidor espera i el client inicia. Aquest és el model client-servidor.
Prova de llançar dos o tres clients alhora: gràcies a asyncio, el servidor els atén de manera concurrent sense necessitat de fils. I prova també de matar el servidor mentre un client està connectat: el client rebrà un error de connexió, que és la manera com el model de fallada crash-stop es manifesta al codi.
- Exemple de codi: catàlegs compartits en una xarxa P2P simulada
Per al model P2P no farem servir sockets, sinó una simulació en què cada mercat és un objecte i la "xarxa" és una llista de veïns. La idea és implementar un intercanvi de catàlegs per gossip (xafarderia): cada peer, periòdicament, tria un veí a l'atzar i li explica el que sap; tots dos es queden amb la unió dels seus coneixements.
import random
class MercatPeer:
"""Un mercat local que coneix el seu propi catàleg i aprèn els dels altres."""
def __init__(self, nom: str, productes_propis: dict[str, str]):
self.nom = nom
# cataleg: nom de producte -> productor. Comença només amb el que és propi.
self.cataleg = dict(productes_propis)
self.veins: list["MercatPeer"] = []
def connectar(self, altre: "MercatPeer") -> None:
"""Enllaç bidireccional: tots dos peers es consideren veïns."""
self.veins.append(altre)
altre.veins.append(self)
def ronda_gossip(self) -> None:
"""Tria un veí a l'atzar i hi intercanvia catàlegs."""
if not self.veins:
return
vei = random.choice(self.veins)
# Cadascun aprèn el que no sabia de l'altre. No hi ha servidor central:
# tots dos són alhora emissor i receptor.
nous_per_a_mi = set(vei.cataleg) - set(self.cataleg)
nous_per_a_ell = set(self.cataleg) - set(vei.cataleg)
self.cataleg.update({k: vei.cataleg[k] for k in nous_per_a_mi})
vei.cataleg.update({k: self.cataleg[k] for k in nous_per_a_ell})
if nous_per_a_mi or nous_per_a_ell:
print(f" {self.nom} <-> {vei.nom}: "
f"vaig aprendre {sorted(nous_per_a_mi)}, vaig ensenyar {sorted(nous_per_a_ell)}")
def tots_convergeixen(peers: list[MercatPeer]) -> bool:
"""True si tots els peers tenen exactament el mateix catàleg."""
referencia = peers[0].cataleg
return all(p.cataleg == referencia for p in peers)
if __name__ == "__main__":
random.seed(3)
girona = MercatPeer("Girona", {"Formatge curat d'ovella": "Formatgeria Montblanc"})
lleida = MercatPeer("Lleida", {"Tomàquet rosa": "Horta La Vega"})
tarragona = MercatPeer("Tarragona", {"Vi negre criança": "Celler Roure Alt"})
valencia = MercatPeer("València", {"Taronges": "Horta del Túria"})
# Topologia: no tots es coneixen entre si (com en una xarxa P2P real)
girona.connectar(lleida)
girona.connectar(tarragona)
lleida.connectar(tarragona)
tarragona.connectar(valencia)
lleida.connectar(valencia)
peers = [girona, lleida, tarragona, valencia]
ronda = 0
while not tots_convergeixen(peers):
ronda += 1
print(f"Ronda {ronda}:")
for peer in peers:
peer.ronda_gossip()
print(f"\nTots els mercats han convergit en {ronda} rondes.")
print("Catàleg complet a València:")
for producte, productor in sorted(valencia.cataleg.items()):
print(f" - {producte} ({productor})")Punts que convé destacar:
- No hi ha cap node especial. Cada
MercatPeerté el mateix codi i les mateixes responsabilitats. Si Girona desaparegués, els altres continuarien intercanviant catàlegs. - El coneixement es propaga per contagi. València no està connectada amb Girona, però acaba coneixent el formatge de la Formatgeria Montblanc a través de Tarragona o Lleida. Aquest és exactament el mecanisme de gossip que fan servir sistemes com Cassandra per difondre l'estat del clúster (lliçó 04-04).
- La convergència és eventual. Durant diverses rondes, mercats diferents tenen catàlegs diferents: és un exemple tangible de consistència eventual (apartat 5). Al final tots coincideixen, però no sabem per endavant quantes rondes caldran.
- Simplificacions importants. Només afegim productes, mai no els modifiquem ni els eliminem. Si Lleida i Girona tinguessin versions diferents del preu del mateix producte, quina guanya? Aquest problema (resolució de conflictes) requereix els rellotges lògics de la lliçó 01-05 i les tècniques de replicació de 03-04.
Errors Comuns i Consells
- Confondre els dos significats de "síncron". "Crida síncrona" parla de l'estil de programació (bloquejar-se esperant resposta); "sistema síncron" parla de supòsits sobre límits de temps. Es pot fer una crida síncrona en un sistema asíncron (i és el més normal).
- Assumir que un timeout detecta fallades. Un timeout només detecta que no ha arribat resposta a temps. En un sistema asíncron (o parcialment síncron), això no permet concloure que l'altre node estigui caigut. Tot disseny que tracti "timeout" com a "fallada segura" acabarà tenint problemes de duplicats o de decisions contradictòries.
- Dissenyar per a fallades bizantines quan no cal. Tolerar nodes maliciosos multiplica el cost i la complexitat. Si tots els nodes són teus i són a la teva infraestructura, el model crash-recovery és gairebé sempre suficient.
- Tractar el model d'arquitectura com una elecció única. Els sistemes reals combinen models: microserveis que internament són multicapa, amb components P2P per al descobriment i coordinació centralitzada per a certes decisions. Quilòmetre Zero acabarà sent una barreja.
- Oblidar que les capes no arreglen la base de dades. Un error freqüent en passar de monòlit a multicapa és replicar l'aplicació i deixar una única base de dades: l'aplicació escala, però el coll d'ampolla es trasllada a les dades.
- Consell: quan documentis un disseny, escriu explícitament el model de fallada assumit ("tolerem la caiguda de fins a un node de cada servei; no tolerem nodes maliciosos"). Aquesta frase evita moltes discussions i moltes sorpreses.
Exercicis
Exercici 1: Triar arquitectura
Per a cada necessitat de Quilòmetre Zero, indica quin model d'arquitectura (client-servidor simple, multicapa, P2P o serveis) hi encaixa millor i per què:
- Un panell intern perquè l'equip d'administració consulti les comandes del dia.
- Que les tauletes dels productors en un mercat sense cobertura puguin compartir entre si les actualitzacions d'estoc fins que torni la connexió.
- Que el mòdul de pagaments es pugui desplegar i escalar sense afectar la resta de la plataforma.
Exercici 2: Classificar fallades
Indica el model de fallada (crash-stop, crash-recovery, omissió, temporització, bizantí) que descriu millor cada situació, i explica què veu el servei comandes en cada cas:
- El procés d'
inventaries reinicia automàticament després d'una fallada, recuperant l'estoc des del disc. - Un cable de xarxa defectuós descarta el 5 % dels paquets entre
comandesiinventari. - Una consulta lenta fa que
inventaritrigui 15 segons a respondre. - Un error de programació fa que
inventariretorni estoc negatiu a alguns clients i positiu a d'altres per al mateix producte.
Exercici 3: Timeout al client
Modifica cataleg_client.py perquè l'espera de la resposta tingui un límit de 2 segons fent servir asyncio.wait_for. Si se supera, el client ha d'imprimir un missatge d'error i continuar amb la consulta següent. Després, modifica el servidor perquè "s'adormi" 5 segons (await asyncio.sleep(5)) abans de respondre a la consulta "vi", i comprova el comportament. Quin model de fallada estàs simulant?
Solucions
Solució 1:
- Client-servidor simple (o multicapa lleugera). És una eina interna amb pocs usuaris i sense requisits d'escala ni d'alta disponibilitat. Afegir-hi complexitat no aporta res.
- Peer-to-peer. Les tauletes han de funcionar sense servidor central i compartir informació directament entre elles; un mecanisme de gossip com el de l'apartat 7 hi encaixa perfectament. Quan torni la connexió, els canvis se sincronitzaran amb el servidor (cosa que plantejarà conflictes: Mòdul 3).
- Serveis (microserveis). És exactament la motivació d'aquest model: independència de desplegament i d'escalat per àrea funcional. És el camí que prendrà Quilòmetre Zero.
Solució 2:
- Crash-recovery.
comandesveu que, durant uns segons,inventarino respon (error de connexió), i després torna a respondre amb l'estat desat. Els canvis queinventaritenia en memòria i no havia persistit s'han perdut. - D'omissió. Algunes peticions (o respostes) no arriben.
comandesveu que, de manera aparentment aleatòria, una de cada vint crides no rep resposta, encara queinventariestigui perfectament sa. - De temporització. La resposta és correcta, però arriba tard. Si
comandesté un timeout de 3 segons, veurà exactament el mateix que en el cas 2 (sense resposta), encara que la causa sigui completament diferent. - Bizantí (encara que no sigui maliciós). El node retorna resultats incorrectes i inconsistents segons a qui respon.
comandesno veu cap error: rep respostes amb aspecte vàlid però amb contingut fals. És el tipus de fallada més perillós precisament perquè no es detecta amb timeouts ni amb reintents.
Solució 3:
async def consultar(consulta: str, timeout: float = 2.0) -> list[dict]:
reader, writer = await asyncio.open_connection("127.0.0.1", 8765)
try:
writer.write((consulta + "\n").encode())
await writer.drain()
# wait_for cancel·la l'espera si supera el límit i llança TimeoutError
linia = await asyncio.wait_for(reader.readline(), timeout=timeout)
return json.loads(linia.decode())
finally:
writer.close()
await writer.wait_closed()
async def main():
for consulta in ["formatge", "vega", "vi"]:
try:
productes = await consultar(consulta)
except asyncio.TimeoutError:
print(f"Consulta '{consulta}': el servidor no ha respost a temps")
continue
print(f"Consulta '{consulta}':")
for p in productes:
print(f" - {p['nom']} ({p['productor']}): {p['preu']:.2f} €")Al servidor, dins d'atendre_client, afegeix abans de calcular la resposta:
En executar-ho, les consultes "formatge" i "vega" funcionen i "vi" produeix el missatge de timeout. Estàs simulant una fallada de temporització: el servidor està sa i respondrà correctament (encara que ja ningú no llegeixi la resposta). Fixa't que el client no pot saber si el servidor va lent o està caigut; és l'ambigüitat del model asíncron en acció. Si la consulta hagués estat una operació amb efectes (per exemple, "crear comanda"), el client no sabria si s'ha creat o no, i reintentar-ho podria duplicar-la.
Conclusió
Els models ens donen un llenguatge precís per parlar de sistemes distribuïts. Hem vist els models d'arquitectura —client-servidor, multicapa, peer-to-peer i serveis— i com cadascun reparteix els papers i els riscos de manera diferent; els models d'interacció, distingint amb cura la comunicació síncrona/asíncrona (estil de crida) dels sistemes síncrons/asíncrons (supòsits sobre el temps), amb la conseqüència fonamental que en un sistema asíncron no es pot distingir un node lent d'un de caigut; els models de fallada, des del senzill crash-stop fins al bizantí, amb el seu cost creixent de tolerància; i una primera visió dels models de consistència, que el Mòdul 3 desenvoluparà.
Al codi hem construït un servei cataleg client-servidor amb asyncio, i una xarxa P2P simulada de mercats que difonen els seus catàlegs per gossip, veient en acció la consistència eventual.
Amb aquest vocabulari ja podem avaluar amb rigor què guanya i què perd Quilòmetre Zero en distribuir-se. Això és exactament el que farem a la lliçó següent, Avantatges i Desafiaments dels Sistemes Distribuïts, on quantificarem l'escalabilitat, la disponibilitat i els costos ocults de la distribució.
Curs d'Arquitectures Distribuïdes
Mòdul 1: Introducció als Sistemes Distribuïts
- Conceptes Bàsics de Sistemes Distribuïts
- Models de Sistemes Distribuïts
- Avantatges i Desafiaments dels Sistemes Distribuïts
- Les Fal·làcies de la Computació Distribuïda
- Temps, Rellotges i Ordenació d'Esdeveniments
- Del Monòlit a la Plataforma Distribuïda: el Cas Quilòmetre Zero
Mòdul 2: Comunicació en Sistemes Distribuïts
- Protocols de Comunicació
- RPC i RMI
- gRPC i Serialització de Dades
- Missatgeria i Cues de Missatges
- Patrons de Comunicació Asíncrona
Mòdul 3: Consistència i Replicació
- Models de Consistència
- El Teorema CAP i PACELC
- Algorismes de Consens
- Replicació de Dades
- Transaccions Distribuïdes i Sagues
Mòdul 4: Emmagatzematge Distribuït
- Particionament de Dades i Hashing Consistent
- Sistemes de Fitxers Distribuïts
- Emmagatzematge d'Objectes
- Bases de Dades Distribuïdes
- Memòries Cau Distribuïdes
Mòdul 5: Computació Distribuïda
- Models de Computació Distribuïda
- MapReduce i Hadoop
- Spark i Computació en Memòria
- Processament de Fluxos de Dades
- Planificació de Treballs i Pipelines de Dades
Mòdul 6: Seguretat en Sistemes Distribuïts
- Autenticació i Autorització
- Xifratge i Protecció de Dades
- Gestió d'Identitats
- Seguretat entre Serveis: mTLS i Gestió de Secrets
- Passarel·les d'API, Limitació de Taxa i Auditoria
Mòdul 7: Monitoratge i Manteniment
- Monitoratge de Sistemes Distribuïts
- Logs Centralitzats i Traçabilitat Distribuïda
- Gestió de Fallades i Recuperació
- Patrons de Resiliència: Timeouts, Reintents i Circuit Breaker
- Automatització i Orquestració
- Proves en Sistemes Distribuïts i Enginyeria del Caos
