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

  1. Per a què serveixen els models
  2. Models d'arquitectura
  3. Models d'interacció: síncron i asíncron
  4. Models de fallada
  5. Models de consistència: una primera visió
  6. Exemple de codi: un servei cataleg client-servidor
  7. Exemple de codi: catàlegs compartits en una xarxa P2P simulada
  8. Errors comuns i consells
  9. Exercicis
  10. Conclusió

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

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

  1. Presentació (web): rep les peticions HTTP, serveix contingut estàtic, gestiona sessions i TLS.
  2. Lògica de negoci (aplicació): executa les regles del domini (calcular preus, validar comandes, aplicar descomptes de campanya).
  3. 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.

  1. 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: comandes pregunta a inventari si 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, comandes publica l'esdeveniment "ComandaConfirmada" i no espera que analitica el 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.

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

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

  1. Exemple de codi: un servei cataleg client-servidor

Implementarem 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:

  1. asyncio.start_server crea un servidor que escolta al port 8765 de la màquina local. Per cada client que es connecta, asyncio crida atendre_client amb un parell d'objectes: reader (per llegir el que envia el client) i writer (per respondre-li).
  2. El bucle while True dins d'atendre_client permet que un mateix client enviï diverses consultes per la mateixa connexió. Quan readline() retorna una cadena buida, vol dir que el client ha tancat la connexió.
  3. 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.
  4. Al client, consultar és un exemple perfecte de comunicació síncrona (estil petició-resposta): envia la consulta i await 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).
  5. 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.

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

  1. No hi ha cap node especial. Cada MercatPeer té el mateix codi i les mateixes responsabilitats. Si Girona desaparegués, els altres continuarien intercanviant catàlegs.
  2. 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).
  3. 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.
  4. 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è:

  1. Un panell intern perquè l'equip d'administració consulti les comandes del dia.
  2. Que les tauletes dels productors en un mercat sense cobertura puguin compartir entre si les actualitzacions d'estoc fins que torni la connexió.
  3. 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:

  1. El procés d'inventari es reinicia automàticament després d'una fallada, recuperant l'estoc des del disc.
  2. Un cable de xarxa defectuós descarta el 5 % dels paquets entre comandes i inventari.
  3. Una consulta lenta fa que inventari trigui 15 segons a respondre.
  4. Un error de programació fa que inventari retorni 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:

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

  1. Crash-recovery. comandes veu que, durant uns segons, inventari no respon (error de connexió), i després torna a respondre amb l'estat desat. Els canvis que inventari tenia en memòria i no havia persistit s'han perdut.
  2. D'omissió. Algunes peticions (o respostes) no arriben. comandes veu que, de manera aparentment aleatòria, una de cada vint crides no rep resposta, encara que inventari estigui perfectament sa.
  3. De temporització. La resposta és correcta, però arriba tard. Si comandes té un timeout de 3 segons, veurà exactament el mateix que en el cas 2 (sense resposta), encara que la causa sigui completament diferent.
  4. Bizantí (encara que no sigui maliciós). El node retorna resultats incorrectes i inconsistents segons a qui respon. comandes no 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:

            if consulta == "vi":
                await asyncio.sleep(5)   # simula una consulta molt lenta

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

Mòdul 2: Comunicació en Sistemes Distribuïts

Mòdul 3: Consistència i Replicació

Mòdul 4: Emmagatzematge Distribuït

Mòdul 5: Computació Distribuïda

Mòdul 6: Seguretat en Sistemes Distribuïts

Mòdul 7: Monitoratge i Manteniment

Mòdul 8: Casos d'Estudi i Aplicacions

© Copyright 2026. Tots els drets reservats