A principis dels anys noranta, Peter Deutsch, enginyer de Sun Microsystems, va recopilar una llista de suposicions falses que els programadors fan, gairebé sense adonar-se'n, quan escriuen per primera vegada programari que es comunica per xarxa. James Gosling (creador de Java) va completar la llista fins a les vuit fal·làcies de la computació distribuïda que avui són un clàssic. El seu valor rau en el fet que cadascuna és tan natural que la donem per certa en escriure codi i només descobrim que era falsa quan el sistema falla en producció.
Aquesta lliçó repassa les vuit fal·làcies una per una. Per a cadascuna veurem què significa, com es manifesta concretament a Quilòmetre Zero i quina mesura de disseny la contraresta, assenyalant la lliçó del curs on aquesta mesura es desenvolupa. Acabarem amb un exemple de codi en què una crida "ingènua" del servei comandes al servei inventari s'enfronta a una xarxa que perd paquets i triga un temps variable, i veurem com un simple timeout en canvia per complet el comportament.
Contingut
- Origen i sentit de les fal·làcies
- Fal·làcia 1: la xarxa és fiable
- Fal·làcia 2: la latència és zero
- Fal·làcia 3: l'amplada de banda és infinita
- Fal·làcia 4: la xarxa és segura
- Fal·làcia 5: la topologia no canvia
- Fal·làcia 6: hi ha un únic administrador
- Fal·làcia 7: el cost de transport és zero
- Fal·làcia 8: la xarxa és homogènia
- Taula resum: fal·làcia, símptoma, contramesura, lliçó
- Exemple de codi:
comandescridainventarien una xarxa imperfecta - Errors comuns i consells
- Exercicis
- Conclusió
- Origen i sentit de les fal·làcies
Les fal·làcies no són errors de programació: són errors de model mental. Quan al monòlit de Quilòmetre Zero el mòdul comandes cridava inventari.descomptar_estoc(producte_id, 1), aquesta crida era una funció Python: sempre arribava, sempre responia en microsegons, ningú no la podia interceptar i no costava diners. En separar comandes i inventari en dos serveis, la línia de codi pot continuar sent gairebé idèntica (inventari.descomptar_estoc(...) a través d'un client RPC), però totes aquestes propietats han desaparegut. Si el programador no canvia el seu model mental, escriurà codi correcte per al monòlit i incorrecte per al sistema distribuït.
Per això convé recórrer les vuit fal·làcies amb un exemple concret al cap. El nostre exemple serà, principalment, la interacció comandes → inventari en confirmar una comanda, i el flux de posicions que els repartidors envien al servei repartiment per 4G.
- Fal·làcia 1: la xarxa és fiable
Què significa creure-la. Que tot missatge enviat arriba a la seva destinació, exactament una vegada i sense corrompre's.
La realitat. Els paquets es perden (congestió, cables defectuosos, commutadors que es reinicien), es dupliquen (reintents de capes inferiors) i arriben desordenats. TCP amaga part d'això (retransmet i reordena), però no pot fer res si l'enllaç cau del tot, si l'altre extrem es reinicia o si un tallafoc comença a descartar connexions. I, el més important, quan una petició no obté resposta, l'emissor no pot saber si s'ha perdut la petició o la resposta.
A Quilòmetre Zero. En confirmar la comanda de l'Anna, comandes envia a inventari "descompta una unitat de formatge curat". Si la petició es perd, l'estoc no es descompta i la comanda es confirma sense existències. Si la petició arriba, inventari descompta, però la resposta es perd, comandes creu que ha fallat; si reintenta, es descomptaran dues unitats.
Contramesures. Confirmacions i reintents, però amb operacions idempotents (que es puguin repetir sense efectes addicionals) i identificadors únics de petició (lliçó 02-05); cues de missatges amb lliurament garantit (02-04); i estratègies de reintent amb espera exponencial (07-04).
- Fal·làcia 2: la latència és zero
Què significa creure-la. Que una crida remota triga el mateix que una crida local, és a dir, pràcticament res.
La realitat. Una crida a funció en memòria triga nanosegons. Una crida per xarxa dins del mateix centre de dades triga entre 0,5 i 2 ms; entre ciutats europees, 20-40 ms; entre continents, 100-200 ms; per 4G des d'un mòbil, entre 50 i 500 ms, amb una variabilitat enorme. És a dir, una crida remota és entre 10.000 i 1.000.000 de vegades més lenta que una de local. I el problema s'agreuja quan les crides s'encadenen: el patró "N+1" (fer una crida per cada element d'una llista) que al monòlit era un descuit tolerable, en un sistema distribuït converteix una pàgina en un desastre.
A Quilòmetre Zero. La pàgina del cistell de la Llúcia mostra 15 productes. Si la nova versió de comandes fa una crida a cataleg per cada producte per obtenir-ne el nom i el preu, són 15 crides × 20 ms = 300 ms només en esperes de xarxa, en sèrie. I les posicions dels repartidors arriben amb retards variables de 80 a 900 ms, de manera que la posició "actual" al mapa sempre té una certa antiguitat i de vegades arriba desordenada.
Contramesures. Dissenyar interfícies de gra gruixut (una crida que retorna 15 productes, no 15 crides); paral·lelitzar les crides independents; fer servir memòria cau (04-05); situar les dades a prop de qui les fa servir (replicació geogràfica, 03-04); comunicació asíncrona per al que no necessita resposta immediata (02-04). I, en qualsevol cas, mesurar la latència de cada crida (07-01).
- Fal·làcia 3: l'amplada de banda és infinita
Què significa creure-la. Que es pot enviar qualsevol quantitat de dades sense que la mida importi.
La realitat. Encara que l'amplada de banda ha crescut enormement, continua sent finita i compartida. A més, la latència i l'amplada de banda interactuen: enviar 10 MB per un enllaç de 100 Mb/s triga gairebé un segon, per baixa que sigui la latència. I a les xarxes mòbils, l'amplada de banda és escassa, variable i, sovint, de pagament per volum.
A Quilòmetre Zero. La primera versió del servei cataleg retornava, a cada cerca, els productes complets amb les fotos codificades en base64 dins del JSON: 2 MB per resposta. Amb 400 cerques/s durant la campanya, són 800 MB/s, més del que dona la interfície de xarxa. Un altre cas: l'app del repartidor enviava, a cada actualització de posició, tot l'historial del dia en lloc de només la posició nova.
Contramesures. Enviar només el necessari (paginació, camps seleccionables); formats de serialització compactes com Protocol Buffers (02-03); compressió; separar les dades grans (fotos) en un emmagatzematge d'objectes i enviar només referències (04-03); agregar missatges petits en lots quan el volum ho justifiqui.
- Fal·làcia 4: la xarxa és segura
Què significa creure-la. Que només els components legítims poden llegir o enviar missatges a la xarxa.
La realitat. Qualsevol missatge que travessa una xarxa pot ser llegit, modificat o fabricat per qui tingui accés a aquesta xarxa. I "la xarxa interna" és un concepte cada vegada més difús: contenidors, núvols públics, xarxes de proveïdors, portàtils d'empleats, dispositius mòbils. En un monòlit, la crida de comandes a inventari no podia ser interceptada perquè no sortia del procés. Ara sí.
A Quilòmetre Zero. Si inventari accepta qualsevol petició de "descompta estoc" sense verificar qui l'envia, qualsevol que arribi a la xarxa interna (un contenidor compromès, un empleat descontent) pot buidar l'estoc del Celler Roure Alt. Si les posicions dels repartidors viatgen sense xifrar, un atacant a la mateixa wifi pot seguir-ne els moviments, o injectar posicions falses.
Contramesures. Xifratge en trànsit (TLS) per a totes les comunicacions, incloses les internes (06-02); autenticació mútua entre serveis amb mTLS (06-04); autenticació i autorització d'usuaris amb tokens (06-01); gestió segura de secrets (06-04); una passarel·la que centralitzi els controls (06-05). Principi general: confiança zero, no es confia en una petició pel fet de venir de "dins".
- Fal·làcia 5: la topologia no canvia
Què significa creure-la. Que les màquines són sempre a la mateixa adreça IP, que el mateix node sempre és al mateix lloc i que el mapa de la xarxa és estable.
La realitat. En qualsevol sistema modern, els nodes apareixen i desapareixen constantment: escalat automàtic, reinicis, desplegaments, fallades de maquinari, migracions entre zones. Un contenidor pot tenir una adreça IP diferent cada vegada que arrenca. Els enllaços de xarxa canvien de ruta. El codi que té adreces fixes escrites ("inventari és a 10.0.3.17") funciona fins al primer redesplegament.
A Quilòmetre Zero. Durant la Setmana del Formatge Artesà s'afegeixen 6 instàncies de cataleg i, en acabar, es retiren. comandes necessita trobar les instàncies vives a cada moment. I quan inventari es redesplega amb una versió nova, els seus contenidors canvien d'IP: si comandes tenia l'adreça antiga, comença a fallar sense que res estigui "trencat".
Contramesures. Descobriment de serveis (registre dinàmic d'instàncies i resolució per nom, no per adreça) i orquestradors que ho gestionen automàticament (07-05); balancejadors de càrrega que encaminen cap a les instàncies sanes; comprovacions de salut que retiren les instàncies que no responen (07-03); dissenyar els serveis perquè no depenguin de la identitat de la màquina on corren.
- Fal·làcia 6: hi ha un únic administrador
Què significa creure-la. Que una sola persona (o equip) coneix, controla i configura tota la xarxa i tots els sistemes implicats.
La realitat. Tan bon punt el sistema creix, hi intervenen múltiples parts: l'equip d'infraestructura, cada equip de servei, el proveïdor de núvol, la passarel·la de pagaments externa, els operadors de telefonia mòbil dels repartidors. Ningú no té la visió completa. Un canvi de configuració que fa un equip pot trencar-ne un altre; una actualització del proveïdor extern pot canviar el comportament d'una API; una política de xarxa del núvol pot bloquejar un port.
A Quilòmetre Zero. La passarel·la de pagaments externa anuncia que deixa d'admetre una versió antiga de TLS, i pagaments deixa de funcionar un dimarts al matí sense que ningú de Quilòmetre Zero hagi tocat res. L'equip de repartiment canvia el format d'un esdeveniment, i analitica (d'un altre equip) comença a descartar missatges.
Contramesures. Contractes explícits i versionats entre serveis (02-03); configuració centralitzada i auditada; automatització de la infraestructura perquè la configuració sigui reproduïble (07-05); observabilitat per detectar ràpidament els canvis de comportament (Mòdul 7); acords de nivell de servei amb els proveïdors externs i un disseny que en toleri les fallades (07-04).
- Fal·làcia 7: el cost de transport és zero
Què significa creure-la. Que moure dades per la xarxa no costa res, ni en diners ni en recursos de computació.
La realitat. Hi ha dos costos. El primer és de computació: per enviar un objecte per la xarxa cal serialitzar-lo (convertir-lo a bytes), i en rebre'l, deserialitzar-lo; per a conjunts de dades grans, aquesta feina pot consumir més CPU que la mateixa lògica de negoci. El segon és econòmic: els proveïdors de núvol cobren pel trànsit que surt dels seus centres de dades (i de vegades entre zones), i les xarxes mòbils cobren per volum. El que al monòlit era gratuït (passar un objecte Python d'un mòdul a un altre), ara té un preu.
A Quilòmetre Zero. El servei analitica es dissenya inicialment per consultar cada nit totes les comandes del dia demanant-les a comandes en JSON: 2 milions de registres serialitzats, transmesos i deserialitzats, cada nit. Un mes després, la factura de trànsit entre zones del núvol sorprèn la direcció, i el procés triga més a serialitzar que a calcular.
Contramesures. Formats binaris eficients (02-03); portar el càlcul on són les dades en lloc de moure les dades al càlcul, que és el principi de MapReduce i Spark (05-02, 05-03); fluxos d'esdeveniments que es processen a mesura que passen en lloc d'abocaments massius (05-04); situar els serveis que més es parlen a la mateixa zona.
- Fal·làcia 8: la xarxa és homogènia
Què significa creure-la. Que tots els nodes fan servir el mateix maquinari, el mateix sistema operatiu, el mateix llenguatge, les mateixes versions de biblioteques i el mateix format de dades.
La realitat. Qualsevol sistema real és una barreja: llenguatges diferents, versions diferents del mateix servei convivint durant un desplegament, representacions diferents dels nombres (com es codifica un decimal? i una data?), jocs de caràcters diferents, mides de xarxa diferents (una interfície de 10 Gb/s al centre de dades davant del 4G al mòbil).
A Quilòmetre Zero. El servei repartiment rep posicions d'una app Android (que envia el timestamp en mil·lisegons), d'una app iOS (que l'envia en segons amb decimals) i d'un dispositiu GPS de furgoneta (que l'envia com a text en un format propi). Durant un desplegament gradual, la versió 1 i la versió 2 d'inventari conviuen, i la 2 retorna un camp nou que la versió antiga de comandes no espera. Un preu de 9,90 € viatja com a 9.9 (coma flotant) i arriba com a 9.899999.
Contramesures. Formats d'intercanvi amb esquema explícit i independents del llenguatge (02-03); regles de compatibilitat cap endavant i cap enrere a les API (afegir camps sense trencar els clients antics); tipus de dades precisos per a diners (decimals, no flotants) i dates (UTC amb zona explícita); contenidors per homogeneïtzar l'entorn d'execució (07-05).
- Taula resum: fal·làcia, símptoma, contramesura, lliçó
| # | Fal·làcia | Símptoma a Quilòmetre Zero | Contramesura principal | Lliçó |
|---|---|---|---|---|
| 1 | La xarxa és fiable | Comandes confirmades sense estoc, o estoc descomptat dues vegades | Reintents amb idempotència, cues amb lliurament garantit | 02-04, 02-05, 07-04 |
| 2 | La latència és zero | Cistell que triga 300 ms per 15 crides encadenades; posicions de repartidors desordenades | Interfícies de gra gruixut, memòria cau, asincronia, mesurament | 02-04, 04-05, 07-01 |
| 3 | L'amplada de banda és infinita | Respostes de 2 MB amb fotos en base64 | Paginació, serialització compacta, emmagatzematge d'objectes | 02-03, 04-03 |
| 4 | La xarxa és segura | Qualsevol a la xarxa interna pot buidar l'estoc | TLS, mTLS, autenticació entre serveis, confiança zero | 06-01, 06-02, 06-04, 06-05 |
| 5 | La topologia no canvia | Adreces IP escrites al codi que fallen després de cada desplegament | Descobriment de serveis, comprovacions de salut, orquestració | 07-03, 07-05 |
| 6 | Hi ha un únic administrador | La passarel·la de pagaments canvia i pagaments deixa de funcionar sense tocar res |
Contractes versionats, infraestructura com a codi, observabilitat | 02-03, 07-05, Mòdul 7 |
| 7 | El cost de transport és zero | Factura de trànsit i CPU disparades per abocaments nocturns en JSON | Formats binaris, portar el càlcul a les dades, fluxos d'esdeveniments | 02-03, 05-02, 05-04 |
| 8 | La xarxa és homogènia | Timestamps en tres formats; preus amb errors d'arrodoniment; versions incompatibles | Esquemes explícits, compatibilitat d'API, tipus precisos | 02-03, 07-05 |
- Exemple de codi:
comandes crida inventari en una xarxa imperfecta
comandes crida inventari en una xarxa imperfectaSimularem, sense sockets reals, les fal·làcies 1 i 2: una xarxa que perd peticions amb una certa probabilitat i que triga un temps variable a lliurar-les. Sobre aquesta xarxa, el servei comandes intentarà descomptar estoc a inventari. Primer amb un client ingenu (que assumeix que la xarxa és fiable i ràpida) i després amb un que fa servir un timeout.
Farem servir asyncio per modelar l'espera: asyncio.sleep representarà el temps de viatge del missatge.
import asyncio
import random
import time
class XarxaImperfecta:
"""Simula una xarxa amb pèrdua de paquets i latència variable.
- prob_perdua: probabilitat que un missatge no arribi mai.
- latencia_min / latencia_max: segons que triga un missatge a arribar
(triats a l'atzar dins d'aquest rang, a cada enviament).
"""
def __init__(self, prob_perdua: float, latencia_min: float, latencia_max: float):
self.prob_perdua = prob_perdua
self.latencia_min = latencia_min
self.latencia_max = latencia_max
async def transmetre(self, missatge: dict) -> dict:
"""Lliura el missatge després d'una latència aleatòria... o mai."""
if random.random() < self.prob_perdua:
# El paquet s'ha perdut. A la xarxa real no passa RES: no hi ha
# error, no hi ha excepció, simplement no arriba mai cap resposta.
await asyncio.sleep(float("inf"))
await asyncio.sleep(random.uniform(self.latencia_min, self.latencia_max))
return missatge
class ServeiInventari:
"""Un inventari molt simple, amb l'estoc de cada producte."""
def __init__(self):
self.estoc = {"formatge-curat": 5, "tomaquet-rosa": 20, "vi-crianca": 12}
self.peticions_ateses = 0
async def descomptar(self, producte: str, quantitat: int) -> dict:
self.peticions_ateses += 1
if self.estoc.get(producte, 0) < quantitat:
return {"ok": False, "motiu": "sense estoc"}
self.estoc[producte] -= quantitat
return {"ok": True, "estoc_restant": self.estoc[producte]}
class ClientComandesIngenu:
"""Crida inventari com si fos una funció local."""
def __init__(self, xarxa: XarxaImperfecta, inventari: ServeiInventari):
self.xarxa = xarxa
self.inventari = inventari
async def confirmar_comanda(self, client: str, producte: str) -> str:
peticio = {"op": "descomptar", "producte": producte, "quantitat": 1}
# Viatge d'anada per la xarxa, processament, i viatge de tornada.
await self.xarxa.transmetre(peticio)
resposta = await self.inventari.descomptar(producte, 1)
await self.xarxa.transmetre(resposta)
if resposta["ok"]:
return f"Comanda de {client} confirmada (en queden {resposta['estoc_restant']} u.)"
return f"Comanda de {client} rebutjada: {resposta['motiu']}"
class ClientComandesAmbTimeout(ClientComandesIngenu):
"""Igual que l'ingenu, però no espera més de 'timeout' segons."""
def __init__(self, xarxa, inventari, timeout: float):
super().__init__(xarxa, inventari)
self.timeout = timeout
async def confirmar_comanda(self, client: str, producte: str) -> str:
try:
return await asyncio.wait_for(
super().confirmar_comanda(client, producte), timeout=self.timeout
)
except asyncio.TimeoutError:
return (f"Comanda de {client}: SENSE RESPOSTA d'inventari en "
f"{self.timeout} s (s'ha descomptat l'estoc? no ho sabem)")
async def processar_lot(client_comandes, comandes: list[tuple[str, str]], nom: str):
"""Processa diverses comandes, una darrere l'altra, i mesura quant triga."""
print(f"\n--- {nom} ---")
inici = time.monotonic()
for client, producte in comandes:
t0 = time.monotonic()
resultat = await client_comandes.confirmar_comanda(client, producte)
print(f"[{time.monotonic() - t0:5.2f} s] {resultat}")
print(f"Total: {time.monotonic() - inici:.2f} s; "
f"inventari va atendre {client_comandes.inventari.peticions_ateses} peticions")
async def main():
random.seed(16)
comandes = [("Anna", "formatge-curat"), ("Marc", "formatge-curat"),
("Llúcia", "vi-crianca"), ("Anna", "tomaquet-rosa")]
# Escenari A: xarxa gairebé perfecta. El client ingenu sembla funcionar bé.
xarxa_bona = XarxaImperfecta(prob_perdua=0.0, latencia_min=0.01, latencia_max=0.03)
await processar_lot(ClientComandesIngenu(xarxa_bona, ServeiInventari()),
comandes, "A: client ingenu, xarxa bona")
# Escenari B: xarxa amb un 25 % de pèrdua i latència de fins a 1,5 s.
xarxa_dolenta = XarxaImperfecta(prob_perdua=0.25, latencia_min=0.05, latencia_max=1.5)
# B1: client ingenu. El protegim amb un límit global de 6 s perquè,
# si no, el programa es quedaria penjat PER SEMPRE al primer paquet perdut.
try:
await asyncio.wait_for(
processar_lot(ClientComandesIngenu(xarxa_dolenta, ServeiInventari()),
comandes, "B1: client ingenu, xarxa dolenta"),
timeout=6.0,
)
except asyncio.TimeoutError:
print("!!! El lot sencer s'ha quedat penjat: el client ingenu espera "
"indefinidament un paquet que no arribarà mai")
# B2: client amb timeout d'1 s per crida.
inventari = ServeiInventari()
await processar_lot(ClientComandesAmbTimeout(xarxa_dolenta, inventari, timeout=1.0),
comandes, "B2: client amb timeout, xarxa dolenta")
print(f"Estoc final a inventari: {inventari.estoc}")
if __name__ == "__main__":
asyncio.run(main())Executem-ho i analitzem la sortida (els temps exactes varien lleugerament entre màquines):
--- A: client ingenu, xarxa bona ---
[ 0.04 s] Comanda de Anna confirmada (en queden 4 u.)
[ 0.05 s] Comanda de Marc confirmada (en queden 3 u.)
[ 0.03 s] Comanda de Llúcia confirmada (en queden 11 u.)
[ 0.05 s] Comanda de Anna confirmada (en queden 19 u.)
Total: 0.17 s; inventari va atendre 4 peticions
--- B1: client ingenu, xarxa dolenta ---
[ 2.37 s] Comanda de Anna confirmada (en queden 4 u.)
!!! El lot sencer s'ha quedat penjat: el client ingenu espera indefinidament un paquet que no arribarà mai
--- B2: client amb timeout, xarxa dolenta ---
[ 1.00 s] Comanda de Anna: SENSE RESPOSTA d'inventari en 1.0 s (s'ha descomptat l'estoc? no ho sabem)
[ 0.98 s] Comanda de Marc confirmada (en queden 3 u.)
[ 1.00 s] Comanda de Llúcia: SENSE RESPOSTA d'inventari en 1.0 s (s'ha descomptat l'estoc? no ho sabem)
[ 1.00 s] Comanda de Anna: SENSE RESPOSTA d'inventari en 1.0 s (s'ha descomptat l'estoc? no ho sabem)
Total: 3.98 s; inventari va atendre 3 peticions
Estoc final a inventari: {'formatge-curat': 3, 'tomaquet-rosa': 20, 'vi-crianca': 11}Què ens ensenya cada part:
XarxaImperfecta.transmetreés el cor de la simulació. Fixa't en com es modela la pèrdua: no amb una excepció, sinó ambawait asyncio.sleep(float("inf")), una espera infinita. Això és fidel a la realitat: la xarxa no avisa que ha perdut un paquet. És la fal·làcia 1 en estat pur.- Escenari A. Amb una xarxa gairebé perfecta, el client ingenu funciona bé, i això és precisament el perillós: el codi passa totes les proves al portàtil del desenvolupador i a l'entorn de proves, on la xarxa és bona.
- Escenari B1. Amb un 25 % de pèrdua, la primera comanda triga gairebé 2,4 segons (fal·làcia 2: la latència no és zero, i aquí és de fins a 1,5 s per trajecte) i la segona es queda penjada per sempre. Hem hagut d'embolcallar el lot en un
wait_forde 6 segons només perquè el programa acabi. En un servidor real, això es tradueix en fils o connexions bloquejats que s'acumulen fins a esgotar els recursos, exactament el que li va passar al monòlit a la Setmana de la Verema. - Escenari B2. El client amb timeout mai no es penja: cada crida triga com a màxim 1 segon, i el lot acaba en 4 segons. Però observa amb atenció les últimes línies:
comandesnomés té constància d'una comanda confirmada (la d'en Marc), i tanmateixinventariva atendre 3 peticions: l'estoc de formatge curat va baixar de 5 a 3 (dos descomptes, no un) i el de vi de 12 a 11, encara que la comanda de la Llúcia es va donar per fallida. És a dir: la comanda de formatge de l'Anna i la de vi de la Llúcia sí que van arribar a inventari i van descomptar estoc; el que es va perdre va ser la resposta, no la petició. La comanda de tomàquets de l'Anna, en canvi, no va arribar mai. Des decomandesels tres casos són indistingibles.
El timeout resol el problema del bloqueig, però deixa oberta la pregunta més important: quan expira, l'operació s'ha fet o no? El missatge "s'ha descomptat l'estoc? no ho sabem" és literalment cert. Resoldre aquesta ambigüitat exigeix idempotència i reintents segurs (02-05), i decidir quan deixar d'intentar-ho per no sobrecarregar un servei malalt és el paper del circuit breaker (07-04). Aquesta lliçó s'atura al timeout, que és la contramesura mínima i imprescindible: cap crida remota no s'ha de fer mai sense un límit de temps.
Errors Comuns i Consells
- Provar només en xarxes bones. L'escenari A demostra que un codi que ignora les fal·làcies funciona perfectament en desenvolupament. Cal provar amb pèrdua de paquets i latència injectades (la lliçó 07-06 tracta l'enginyeria del caos precisament per a això).
- Posar timeouts "generosos" per evitar falses fallades. Un timeout de 60 segons no protegeix de res: 60 segons de connexions bloquejades sota càrrega és una caiguda. El timeout ha de ser una mica més gran que la latència normal de l'operació (per exemple, el percentil 99), no "prou gran perquè no salti mai".
- Tractar el timeout com a "l'operació no s'ha fet". Com mostra l'escenari B2, després d'un timeout l'operació pot haver-se executat. Qualsevol reintent ha de ser segur davant de duplicats.
- Escriure adreces IP o noms de host al codi. Fal·làcia 5. Tota adreça ha de venir de la configuració o d'un sistema de descobriment.
- Suposar que "intern" vol dir "segur". Fal·làcia 4. Xifrar i autenticar també entre serveis propis.
- Fer servir
floatper a diners o timestamps sense zona horària. Fal·làcia 8. Són les dues fonts més habituals de discrepàncies entre serveis escrits per equips diferents. - Consell: quan revisis codi que fa una crida remota, comprova la "llista de les tres T": té Timeout?, és Tolerant a duplicats (idempotent)?, està Traçada (es registra quant ha trigat i si ha fallat)? Si en falta alguna, la crida no està a punt per a producció.
Exercicis
Exercici 1: Diagnòstic de fal·làcies
Per a cadascun dels incidents següents de Quilòmetre Zero, indica quina fal·làcia (o fal·làcies) es va creure certa i quina contramesura hi aplicaries:
- Després de migrar
inventaria contenidors,comandesfalla cada matí a les 6:00, just quan es redesplegainventariamb les dades actualitzades dels productors. - En un mercat rural, l'app del repartidor consumeix 400 MB de dades mòbils al dia i la bateria dura la meitat.
- Un desenvolupador escriu un bucle que, per a cadascuna de les 300 comandes del dia, crida
pagamentsper comprovar-ne l'estat. L'informe triga 2 minuts. - Una anàlisi de seguretat mostra que un contenidor d'
analitica(que només hauria de llegir) pot enviar peticions de "descomptar estoc" ainventarii són acceptades.
Exercici 2: Ajustar el timeout
Fent servir la simulació de l'apartat 11, amb la xarxa_dolenta (pèrdua 25 %, latència 0,05-1,5 s per trajecte), raona i després comprova experimentalment:
- Quin timeout cal perquè cap petició que sí que arriba es doni per perduda (és a dir, perquè el timeout només salti per pèrdua real)?
- Quina proporció de les crides fallarà per pèrdua real, tenint en compte que cada crida fa dos trajectes (anada i tornada)?
- Modifica
mainper executar 200 comandes amb el client amb timeout i comptar quantes es confirmen, quantes donen timeout i quantes peticions atén realmentinventari. Compara els dos últims nombres i explica'n la diferència.
Exercici 3: Un reintent ingenu
Afegeix a ClientComandesAmbTimeout un paràmetre reintents de manera que, després d'un timeout, torni a intentar la mateixa crida fins a aquest nombre de vegades. Executa el lot de 4 comandes amb reintents=3 i observa l'estoc final de formatge-curat. Què ha passat i per què? (No cal resoldre-ho: només diagnosticar-ho. La solució s'estudia a 02-05).
Solucions
Solució 1:
- Fal·làcia 5 (la topologia no canvia).
comandesté en memòria cau (o configurada) l'adreça dels contenidors antics d'inventari, que desapareixen amb el redesplegament. Contramesura: descobriment de serveis i resolució per nom a cada crida, amb comprovacions de salut (07-05, 07-03). - Fal·làcies 3 i 7 (amplada de banda infinita, cost de transport zero), i probablement la 2 (envia massa sovint). L'app envia massa dades o massa vegades. Contramesura: enviar només la posició nova (no l'historial), en format compacte, amb una freqüència adaptada (més baixa quan el repartidor està aturat), i agrupar posicions quan no hi ha urgència (02-03, 08-02).
- Fal·làcia 2 (la latència és zero). És el patró N+1: 300 crides × 400 ms = 2 minuts. Contramesura: una crida de gra gruixut a
pagamentsque retorni l'estat de les 300 comandes d'una vegada, o millor, quepagamentspubliqui els canvis d'estat com a esdeveniments quecomandesja tingui emmagatzemats localment (02-04). - Fal·làcia 4 (la xarxa és segura).
inventarino autentica qui el crida ni en comprova els permisos. Contramesura: autenticació entre serveis amb mTLS i autorització per identitat del servei (06-04); principi de mínim privilegi.
Solució 2:
- Cada crida fa dos trajectes, cadascun de fins a 1,5 s, així que el pitjor cas d'una crida que sí que arriba és de 3 s. Amb un timeout de 3 s (o una mica més, pel temps de processament), cap timeout no seria un "fals positiu". Però fixa't en el preu: cada pèrdua real bloqueja el client 3 segons. Aquesta tensió (timeout curt = falses fallades; timeout llarg = bloqueigs llargs) no té solució perfecta.
- Una crida es completa només si tots dos trajectes tenen èxit: 0,75 × 0,75 = 0,5625. És a dir, el 43,75 % de les crides fallarà per pèrdua real. D'aquestes, aproximadament la meitat (0,25 × 0,75 / 0,4375 ≈ 43 %) hauran arribat a
inventarii només n'hauran perdut la resposta.
async def experiment(n: int = 200):
random.seed(11)
xarxa = XarxaImperfecta(prob_perdua=0.25, latencia_min=0.05, latencia_max=1.5)
inventari = ServeiInventari()
inventari.estoc["tomaquet-rosa"] = 10_000 # perquè no s'esgoti
client = ClientComandesAmbTimeout(xarxa, inventari, timeout=3.0)
confirmades = timeouts = 0
for _ in range(n):
r = await client.confirmar_comanda("Anna", "tomaquet-rosa")
if "confirmada" in r:
confirmades += 1
else:
timeouts += 1
print(f"confirmades={confirmades} timeouts={timeouts} "
f"ateses per inventari={inventari.peticions_ateses} "
f"estoc descomptat={10_000 - inventari.estoc['tomaquet-rosa']}")Amb 200 comandes s'obté confirmades=109 timeouts=91 ateses per inventari=144 estoc descomptat=144 (els valors concrets poden variar lleugerament segons la temporització de cada màquina). La diferència entre les peticions ateses (144) i les confirmades (109) són les 35 comandes que sí que van descomptar estoc però que el client creu que van fallar; i el 45,5 % de timeouts observat coincideix amb el 43,75 % teòric. En un sistema real, aquests 35 clients veurien un error, probablement ho tornarien a intentar i l'estoc es descomptaria dues vegades. (L'experiment triga uns minuts perquè les esperes són reals; pots reduir les latències proporcionalment per accelerar-lo).
Solució 3:
class ClientComandesAmbReintents(ClientComandesAmbTimeout):
def __init__(self, xarxa, inventari, timeout: float, reintents: int):
super().__init__(xarxa, inventari, timeout)
self.reintents = reintents
async def confirmar_comanda(self, client: str, producte: str) -> str:
for intent in range(1, self.reintents + 1):
resultat = await super().confirmar_comanda(client, producte)
if "SENSE RESPOSTA" not in resultat:
return resultat + f" (intent {intent})"
return f"Comanda de {client}: fallida després de {self.reintents} intents"En executar el lot de 4 comandes amb reintents=3, és habitual veure que l'estoc descomptat de formatge-curat és superior al nombre de comandes de formatge confirmades: per exemple, amb la llavor 16, les dues comandes de formatge acaben com a "fallida després de 3 intents" i, tanmateix, l'estoc ha baixat de 5 a 2 (tres descomptes que ningú no ha confirmat). Cada vegada que una petició arriba a inventari i se'n perd la resposta, el reintent torna a descomptar. El reintent converteix la pèrdua de missatges en duplicació d'efectes. La solució (que cada petició porti un identificador únic i que inventari recordi quines ja ha processat, és a dir, idempotència) es desenvolupa a la lliçó 02-05.
Conclusió
Les vuit fal·làcies de Deutsch i Gosling són un catàleg dels errors de model mental que cometem en passar de crides locals a crides per xarxa: creure que la xarxa és fiable, que la latència és zero, que l'amplada de banda és infinita, que la xarxa és segura, que la topologia no canvia, que hi ha un únic administrador, que transportar dades no costa i que tot és homogeni. Per a cadascuna hem vist un símptoma concret a Quilòmetre Zero i la contramesura que el curs desenvoluparà més endavant, resumides a la taula de l'apartat 10.
La simulació ha mostrat la lliçó més important de manera tangible: un codi que ignora les fal·làcies funciona perfectament en una xarxa bona i es penja per sempre en una xarxa dolenta; un timeout evita el bloqueig, però deixa sense resposta la pregunta de si l'operació s'ha executat, i un reintent ingenu converteix aquest dubte en duplicats. Cap crida remota no s'hauria de fer sense timeout, i cap operació amb efectes no s'hauria de reintentar sense ser idempotent.
Hi ha una fal·làcia implícita que no és a la llista de Deutsch però que subjau a molts problemes: la que tots els nodes comparteixen la mateixa noció del temps. Quan l'Anna i en Marc compren l'última unitat de formatge des de dues ciutats diferents, qui va ser primer? La lliçó següent, Temps, Rellotges i Ordenació d'Esdeveniments, mostra per què aquesta pregunta no té una resposta òbvia i quines eines (rellotges lògics i vectorials) ens permeten respondre-la de manera útil.
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
