La lliçó anterior va acabar amb una taula en què Quilòmetre Zero assignava un model de consistència diferent a cada dada: linealitzabilitat per a l'última unitat de formatge-curat, garanties de sessió per al cistell de l'Anna, consistència eventual per al comptador de visites. Va quedar pendent la pregunta òbvia: si la linealitzabilitat és la garantia més còmoda per al programador, per què no donar-la a tot? La resposta és que té un preu que no es paga en euros sinó en disponibilitat quan la xarxa falla i en latència quan no falla, i aquest preu està formalitzat en dos resultats: el teorema CAP i la seva extensió PACELC.
Pocs resultats de la informàtica se citen tant i s'entenen tan poc com CAP. Aquesta lliçó l'enuncia amb la precisió amb què el van demostrar Gilbert i Lynch, explica per què la "P" no és una opció que es pugui descartar i per què l'elecció real és què sacrificar durant una partició, i desmunta els malentesos més estesos ("tria'n dos de tres", "AP vol dir sense consistència"). Després presentarem PACELC, que afegeix el que a CAP li falta (el compromís entre latència i consistència quan tot funciona), i classificarem amb ell els sistemes que apareixeran a la resta del curs. Aplicarem tots dos a les dades de Quilòmetre Zero amb una taula de decisions justificades, i una simulació en Python mostrarà el mateix parell de rèpliques inv-bcn/inv-vlc comportant-se en mode CP (rebutjant escriptures sense quòrum) i en mode AP (acceptant, divergint i perdent una reserva en reconciliar). Acabarem amb les crítiques modernes al teorema, que no l'invaliden però sí que obliguen a fer-lo servir amb més cura. Els mecanismes concrets amb què un sistema CP es posa d'acord (consens) i els quòrums de lectura i escriptura són les dues lliçons següents.
Contingut
- L'enunciat precís de CAP
- Per què la partició no és opcional
- L'elecció real: CP o AP durant una partició
- Malentesos comuns
- PACELC: què passa quan no hi ha partició
- Taula de sistemes classificats
- Decidir per cas d'ús a Quilòmetre Zero
- Simulació: el mateix sistema en mode CP i en mode AP
- Crítiques i matisos: harvest, yield i "deixeu de dir CP o AP a les bases de dades"
- Errors comuns i consells
- Exercicis
- Conclusió
- L'enunciat precís de CAP
Eric Brewer va presentar CAP com a conjectura en una xerrada de l'any 2000; Seth Gilbert i Nancy Lynch el van demostrar formalment el 2002. La demostració és senzilla, però només si les tres lletres estan ben definides, i aquí és on comença la confusió, perquè en l'ús col·loquial cadascuna significa alguna cosa més vaga del que diu el teorema:
| Lletra | Nom col·loquial | Definició a Gilbert-Lynch |
|---|---|---|
| C | Consistència | Linealitzabilitat (03-01): existeix un ordre total de les operacions que respecta el temps real i en què cada lectura retorna l'última escriptura. Res a veure amb la C d'ACID. |
| A | Disponibilitat | Tota petició rebuda per un node que no ha fallat ha de produir una resposta (no un error, no un timeout), en temps finit. Observa que és una propietat de cada node viu, no del sistema "en conjunt": si un node viu respon "ara no et puc atendre", el sistema no és disponible en el sentit CAP, encara que un altre node sí que pogués. |
| P | Tolerància a particions | La xarxa pot perdre arbitràriament missatges entre nodes (una partició: dos grups de nodes vius que no es poden comunicar). El sistema ha de continuar complint les seves garanties encara que això passi. |
Amb aquestes definicions, el teorema diu: en un sistema distribuït asíncron en què hi pot haver particions, és impossible garantir simultàniament linealitzabilitat i disponibilitat.
La demostració cap en un paràgraf. Sigui un registre amb valor inicial v0 replicat en dos nodes, N1 i N2, i una partició que els separa. Un client escriu v1 a N1. Per disponibilitat, N1 ha de respondre "fet" sense esperar N2 (amb qui no pot parlar). Un altre client llegeix després a N2. Per disponibilitat, N2 ha de respondre; però N2 no ha rebut res de N1, així que només pot respondre v0. Aquest historial (escriptura de v1 acabada, lectura posterior que retorna v0) és exactament l'historial 1 de 03-01: no és linealitzable. Per tant, o N1 no respon (sacrifica A), o N2 retorna un valor antic (sacrifica C). No hi ha una tercera opció.
sequenceDiagram
participant Anna
participant N1 as inv-bcn
participant N2 as inv-vlc
participant Marc
Note over N1,N2: Partició: cap missatge no passa entre inv-bcn i inv-vlc
Anna->>N1: escriure estoc = 0
N1-->>Anna: ok (si és disponible, no pot esperar N2)
N1--xN2: replicar estoc = 0 (es perd)
Marc->>N2: llegir estoc
N2-->>Marc: 1 (si és disponible, només té el valor antic)
Note over Anna,Marc: L'Anna va acabar abans que en Marc comencés i en Marc va llegir el valor vell: no linealitzable
- Per què la partició no és opcional
Una lectura ingènua del teorema ("tria'n dos de C, A i P") suggereix que es pot renunciar a P i quedar-se amb C i A. En un sistema distribuït real aquesta opció no existeix, per dues raons que ja coneixem del Mòdul 1:
- La xarxa és imperfecta (fal·làcia 1 de 01-04). Cables tallats, switches que es reinicien, taules de rutes mal aplicades, un firewall que descarta paquets, un node tan sobrecarregat que no respon a temps (recorda que en un sistema asíncron, 01-02, "lent" i "particionat" són indistingibles). La pregunta no és si hi haurà particions sinó quantes vegades l'any.
- Renunciar a P vol dir que, quan passa una partició, el sistema deixa de complir C, A o totes dues de manera no controlada. És a dir, "CA" no és una elecció de disseny sinó l'absència d'una: el sistema es comportarà d'alguna manera durant la partició, i si el dissenyador no l'ha decidida, serà la pitjor.
L'únic que sí que es pot fer és reduir la probabilitat i l'abast de les particions: nodes al mateix rack, xarxes redundants, o directament un sol node. Una base de dades PostgreSQL en una sola màquina és "CA" en el sentit trivial que no hi ha res a particionar, i per això el monòlit de Quilòmetre Zero de 01-06 mai no va haver de pensar en això. Tan bon punt inventari té inv-bcn a Barcelona i inv-vlc a València, unides per 350 km de fibra que no controla, P està decidida.
- L'elecció real: CP o AP durant una partició
Amb P fixada, el teorema es redueix a una decisió: quan la partició passa, què fa un node que no pot parlar amb els altres?
- CP (consistència per sobre de disponibilitat): el node que no es pot coordinar rebutja l'operació (o la bloqueja fins que la partició es curi). Al diagrama, N1 respondria a l'Anna "ara no puc confirmar la reserva", i N2 respondria a en Marc "no et puc garantir un valor actual". Ningú no llegeix dades antigues, però part del sistema, o tot, deixa de servir. A la pràctica, un sistema CP acostuma a deixar operatiu el costat de la partició que té majoria (quòrum) i apagar el minoritari; com es decideix qui té majoria és el consens de 03-03.
- AP (disponibilitat per sobre de consistència): tots dos nodes continuen responent amb el que tenen. L'Anna reserva a N1, en Marc llegeix (i potser reserva) a N2, cada costat acumula escriptures que l'altre no veu, i quan la partició es cura cal reconciliar: decidir què passa amb dues reserves del mateix últim formatge. La reconciliació pot ser tan simple com "guanya l'última" (amb pèrdua de dades) o tan sofisticada com un CRDT de 03-01 (sense pèrdua, però sense invariants).
Fixa't que l'elecció no és global ni permanent: és per operació i durant la partició. Un sistema pot ser CP per a les escriptures i AP per a les lectures; pot ser CP per a l'estoc i AP per al cistell; i fora de la partició, totes dues lletres es compleixen sense conflicte. Un sistema ben dissenyat es comporta de manera idèntica el 99,9 % del temps sigui CP o AP; l'etiqueta només descriu què fa en el 0,1 % restant.
- Malentesos comuns
| Malentès | Per què és fals |
|---|---|
| "Tria'n dos de tres" | P no es tria; es pateix. L'elecció és CP o AP, i només durant la partició. |
| "AP vol dir sense consistència" | AP vol dir sense linealitzabilitat durant la partició. Un sistema AP pot oferir consistència causal, garanties de sessió i consistència eventual forta amb CRDT (totes les de la meitat inferior del diagrama de 03-01). De fet, la consistència causal és el model més fort compatible amb A. |
| "CP vol dir que el sistema cau tan bon punt hi ha partició" | CP vol dir que algun node rebutja algunes peticions. El costat majoritari de la partició continua funcionant; només els nodes aïllats deixen de servir. Amb 5 nodes i un d'aïllat, el 80 % del sistema continua sent C i disponible en el sentit col·loquial. |
| "El meu sistema és CA" | Només si és un únic node. Si hi ha dos nodes amb xarxa entre ells, la partició és possible i el sistema tindrà un comportament (decidit o no) davant seu. |
| "L'A de CAP és la disponibilitat dels 'nous' de 01-03" | No. L'A de CAP és una propietat binària i formal ("tot node viu respon"), no un percentatge de temps en servei. Un sistema CP pot tenir un 99,99 % de disponibilitat operativa si les particions són rares i curtes. |
| "CAP decideix l'arquitectura sencera" | CAP parla d'un registre replicat durant una partició. No diu res sobre latència, sobre transaccions, sobre particionament de dades ni sobre tolerància a fallades de nodes (que no són particions). Per això cal PACELC. |
- PACELC: què passa quan no hi ha partició
Daniel Abadi va observar el 2012 que CAP ignora l'estat normal del sistema. La major part del temps no hi ha cap partició, i tanmateix els sistemes continuen prenent decisions de compromís: cada escriptura linealitzable s'ha de coordinar amb altres rèpliques abans de respondre, i aquesta coordinació costa un o diversos viatges de xarxa. PACELC ho formula així:
Si hi ha Partició, triar entre Availability i Consistency; Else (si no n'hi ha), triar entre Latency i Consistency.
La segona meitat és la que descriu el dia a dia. Amb inv-bcn i inv-vlc connectades i sanes, una reserva linealitzable (EC) requereix que inv-bcn esperi la confirmació d'inv-vlc abans de dir "fet" a l'Anna: uns 10 ms d'anada i tornada Barcelona-València, més al percentil 99. Una reserva en mode EL respon tan bon punt s'escriu localment i replica en segon pla; l'Anna veu una resposta en 1 ms, a canvi que inv-vlc pugui anar uns mil·lisegons per darrere (la propagació retardada de la simulació de 03-01). Les quatre combinacions resultants:
| Classificació | Durant partició | Sense partició | Perfil |
|---|---|---|---|
| PA/EL | Disponible, divergeix | Ràpid, replicació asíncrona | Màxima disponibilitat i velocitat; el programa ha de tolerar anomalies sempre |
| PA/EC | Disponible, divergeix | Consistent, espera les rèpliques | Poc comú: paga latència normalment però renuncia a C sota partició |
| PC/EL | Rebutja sense quòrum | Ràpid, replicació asíncrona | Comú en bases de dades amb líder i rèpliques asíncrones: consistent via el líder, però els seguidors s'endarrereixen |
| PC/EC | Rebutja sense quòrum | Consistent, espera les rèpliques | Màxima seguretat; latència de coordinació en tota operació |
PACELC no és un teorema sinó un marc de classificació, però converteix la discussió abstracta en una pregunta concreta que es pot fer a qualsevol magatzem: "què fas durant una partició, i quants viatges de xarxa fas per escriptura quan no n'hi ha?".
- Taula de sistemes classificats
Els sistemes que apareixeran a la resta del curs, classificats amb PACELC. Molts són configurables, i per això la classificació indica la configuració:
| Sistema | Configuració | PACELC | Comentari |
|---|---|---|---|
| PostgreSQL, un líder + rèpliques asíncrones | Per defecte | PC/EL | Les escriptures van al líder (consistents entre elles); les lectures a rèpliques poden ser antigues; si el líder queda aïllat, deixa d'acceptar escriptures (o el failover produeix split-brain, 03-04) |
| PostgreSQL, replicació síncrona | synchronous_standby_names |
PC/EC | Cada commit espera l'standby; si l'standby no respon, els commits es bloquegen (03-04) |
| Cassandra | ONE/ANY en escriptura i lectura |
PA/EL | Qualsevol rèplica accepta; reconciliació per marca de temps (LWW) |
| Cassandra | QUORUM en escriptura i lectura |
PC/EC (per operació) | Amb quòrum es rebutja el que no assoleix majoria; latència d'esperar diverses rèpliques (04-04) |
| DynamoDB | Lectures eventualment consistents | PA/EL | El mode per defecte i el més barat |
| DynamoDB | Lectures fortament consistents | PC/EC | El doble de cost per lectura i sense disponibilitat multiregió |
| MongoDB | w:1, lectura del primari |
PC/EL (matisat) | El primari accepta escriptures; durant partició, el costat sense majoria degrada el seu primari; escriptures w:1 poden retrocedir en un failover |
| MongoDB | w:majority, readConcern: linearizable |
PC/EC | Coordinació a cada operació |
| Google Spanner | Sempre | PC/EC | Serialitzabilitat estricta amb TrueTime (01-05); Google argumenta que les seves particions són tan rares que "és CA a la pràctica" |
| etcd / ZooKeeper / Consul | Sempre | PC/EC | Consens (Raft/ZAB) a cada escriptura; el costat sense majoria rebutja; base de la coordinació (03-03) |
| Redis (un node) | — | Trivialment C i A | No hi ha partició possible; amb Redis Cluster o Sentinel passa a ser PA/EL amb possibles pèrdues en failover |
| DNS | — | PA/EL | L'exemple clàssic de consistència eventual (els TTL) |
Dues observacions sobre la taula. Primera: la mateixa base de dades apareix en files diferents, perquè l'elecció es fa per configuració i sovint per operació; això és el que Kleppmann critica a l'apartat 9. Segona: els sistemes de coordinació (etcd, ZooKeeper) són sempre PC/EC i accepten el cost de bon grat, perquè la seva funció és precisament ser la font de veritat sobre qui és líder o quina és la configuració vigent; un sistema de coordinació AP seria inútil.
- Decidir per cas d'ús a Quilòmetre Zero
Amb el marc a la mà, tornem a la taula de 03-01 i justifiquem cada elecció pensant en què passa durant una partició entre Barcelona i València (o entre el núvol on viuen els serveis i les botigues físiques dels productors) i en la latència acceptable la resta del temps:
| Dada | Pregunta clau | Decisió | Justificació |
|---|---|---|---|
Estoc de formatge-curat (última unitat) |
És pitjor vendre el que no hi ha o no poder vendre durant la partició? | CP per a la reserva; lectures del catàleg AP | Vendre un formatge inexistent suposa cancel·lar una comanda cobrada i una compensació (03-05); rebutjar la reserva 30 segons durant una partició és un "torna-ho a provar". Amb moltes unitats, la mateixa dada es pot tractar com a AP (vegeu més avall) |
Estoc de tomaquet-rosa (200 unitats) |
Quin dany fa una sobrevenda? | AP amb reconciliació per operacions | Amb marge d'estoc, acceptar reserves a tots dos costats i sumar els descomptes en reconciliar (no LWW) rarament produeix un negatiu; Quilòmetre Zero pot definir un llindar: per sota de 5 unitats, el producte passa a mode CP |
| Cistell de l'Anna | Què passa si l'Anna no pot afegir al cistell durant 30 segons? | AP amb garanties de sessió | Un cistell indisponible és una venda perduda; un cistell amb un article duplicat després de reconciliar és una molèstia que l'usuari corregeix. Es reconcilia per unió (mai no perdre un article afegit, a l'estil d'Amazon) i amb read-your-writes |
Pagaments: cobrament de P-2026-000124 |
Es pot cobrar dues vegades o cobrar sense registre? | CP | Un cobrament és irreversible i regulat. Si pagaments no pot confirmar amb quòrum, la comanda queda "pendent de pagament" (03-05) i es reintenta amb la mateixa clau d'idempotència de 02-05. Latència extra acceptada |
Posicions de furgoneta-3 |
Importa perdre o desordenar una posició? | AP (EL) | Deu posicions per minut; perdre'n una és irrellevant i la següent la corregeix. Coordinar cada posició seria absurd. Consistència seqüencial per repartidor és suficient (03-01) |
| Comptador de vendes de la "Setmana de la Verema" | Pot diferir uns segons entre rèpliques? | AP amb CRDT (GCounter) |
Convergència garantida sense coordinació; els increments mai no es perden |
| Configuració: qui és el relay outbox actiu | N'hi pot haver dos alhora? | CP (etcd) | Dos relays publicant dupliquen esdeveniments; millor cap durant uns segons que dos. És l'elecció de líder de 03-03 |
| Catàleg (noms, preus, fotos) | Quant pot trigar un canvi de preu a veure's? | AP eventual | Un preu antic durant uns segons és acceptable; el preu que es cobra es fixa a comandes en crear la comanda, no al catàleg |
La regla que emergeix: CP per a allò que és irreversible o disputat (diners, última unitat, lideratge); AP per a allò que es pot corregir, unir o simplement ignorar (cistells, posicions, comptadors, catàleg). I la fila de tomaquet-rosa mostra que la decisió pot dependre de l'estat de la dada, no només del seu tipus.
- Simulació: el mateix sistema en mode CP i en mode AP
Construirem dues rèpliques d'inventari que poden operar en qualsevol dels dos modes, i una xarxa que es pot partir. En mode CP, una reserva només es confirma si l'accepten totes dues rèpliques (amb dos nodes, el "quòrum" és la unanimitat; a 03-04 veurem quòrums majoritaris amb N=3). En mode AP, cada rèplica accepta localment i anota la marca de temps; en curar-se la partició, es reconcilia amb last-write-wins.
# km0/simulacions/particio_cp_ap.py
from dataclasses import dataclass, field
class SenseQuorum(Exception):
"""El node no es pot coordinar amb prou rèpliques."""
class SenseEstoc(Exception):
"""No queden unitats."""
@dataclass
class Registre:
unitats: int
marca: int # instant de l'última escriptura (rellotge de simulació)
origen: str # rèplica que va fer l'última escriptura
@dataclass
class Replica:
nom: str
mode: str # "CP" o "AP"
estoc: dict[str, Registre] = field(default_factory=dict)
reserves: list[tuple[str, str]] = field(default_factory=list) # (client, producte) acceptades aquí
class Xarxa:
"""Connecta les rèpliques; es pot partir i curar."""
def __init__(self, repliques: dict[str, Replica]):
self.repliques = repliques
self.particionada = False
def abastable(self, des_de: str, fins_a: str) -> bool:
return des_de == fins_a or not self.particionada
class Inventari:
def __init__(self, mode: str):
self.repliques = {n: Replica(n, mode) for n in ("inv-bcn", "inv-vlc")}
self.xarxa = Xarxa(self.repliques)
self.rellotge = 0
# --- utilitats --------------------------------------------------------
def carregar(self, producte: str, unitats: int) -> None:
for r in self.repliques.values():
r.estoc[producte] = Registre(unitats, self.rellotge, "carrega")
def _altres(self, nom: str) -> list[Replica]:
return [r for n, r in self.repliques.items() if n != nom]
# --- operació principal ----------------------------------------------
def reservar(self, client: str, replica: str, producte: str) -> str:
self.rellotge += 1
local = self.repliques[replica]
if local.estoc[producte].unitats <= 0:
raise SenseEstoc(f"{replica}: no queda {producte}")
nou = Registre(local.estoc[producte].unitats - 1, self.rellotge, replica)
if local.mode == "CP":
# Només confirmem si TOTES les rèpliques accepten (quòrum = unanimitat amb N=2)
for altra in self._altres(replica):
if not self.xarxa.abastable(replica, altra.nom):
raise SenseQuorum(f"{replica}: no arribo a {altra.nom}; reserva rebutjada")
for r in self.repliques.values(): # aplicar a totes, atòmicament
r.estoc[producte] = nou
local.reserves.append((client, producte))
return f"{replica}: reserva de {producte} per a {client} CONFIRMADA (estoc={nou.unitats})"
# Mode AP: acceptar localment i replicar si es pot; si no, ja es reconciliarà
local.estoc[producte] = nou
local.reserves.append((client, producte))
for altra in self._altres(replica):
if self.xarxa.abastable(replica, altra.nom):
altra.estoc[producte] = nou
return f"{replica}: reserva de {producte} per a {client} acceptada (estoc local={nou.unitats})"
def llegir(self, replica: str, producte: str) -> int:
return self.repliques[replica].estoc[producte].unitats
# --- reconciliació després de la partició (només té sentit en AP) ------
def reconciliar_lww(self, producte: str) -> str:
bcn, vlc = self.repliques["inv-bcn"], self.repliques["inv-vlc"]
a, b = bcn.estoc[producte], vlc.estoc[producte]
guanyador = a if a.marca >= b.marca else b
bcn.estoc[producte] = vlc.estoc[producte] = guanyador
return (f"LWW: guanya l'escriptura d'{guanyador.origen} (marca {guanyador.marca}); "
f"totes dues rèpliques queden amb estoc={guanyador.unitats}")
def escenari(mode: str) -> None:
print(f"\n===================== MODE {mode} =====================")
inv = Inventari(mode)
inv.carregar("formatge-curat", 1) # l'últim formatge curat de la Formatgeria Montblanc
print("estoc inicial:", inv.llegir("inv-bcn", "formatge-curat"), "/", inv.llegir("inv-vlc", "formatge-curat"))
inv.xarxa.particionada = True
print("-- partició entre inv-bcn i inv-vlc --")
for client, replica in (("Anna", "inv-bcn"), ("Marc", "inv-vlc")):
try:
print(inv.reservar(client, replica, "formatge-curat"))
except (SenseQuorum, SenseEstoc) as e:
print(f"ERROR -> {e}")
print("durant la partició, lectures:", inv.llegir("inv-bcn", "formatge-curat"), "/", inv.llegir("inv-vlc", "formatge-curat"))
inv.xarxa.particionada = False
print("-- partició curada --")
if mode == "AP":
print(inv.reconciliar_lww("formatge-curat"))
reserves = [(c, r.nom) for r in inv.repliques.values() for c, _ in r.reserves]
print("reserves acceptades:", reserves)
print("estoc final:", inv.llegir("inv-bcn", "formatge-curat"), "/", inv.llegir("inv-vlc", "formatge-curat"))
if len(reserves) > 1:
print(f"!!! {len(reserves)} reserves d'1 unitat: l'estoc final hauria de ser {1 - len(reserves)}; "
f"LWW ha PERDUT {len(reserves) - 1} reserva/es i hi ha sobrevenda")
if __name__ == "__main__":
escenari("CP")
escenari("AP")Sortida:
===================== MODE CP =====================
estoc inicial: 1 / 1
-- partició entre inv-bcn i inv-vlc --
ERROR -> inv-bcn: no arribo a inv-vlc; reserva rebutjada
ERROR -> inv-vlc: no arribo a inv-bcn; reserva rebutjada
durant la partició, lectures: 1 / 1
-- partició curada --
reserves acceptades: []
estoc final: 1 / 1
===================== MODE AP =====================
estoc inicial: 1 / 1
-- partició entre inv-bcn i inv-vlc --
inv-bcn: reserva de formatge-curat per a Anna acceptada (estoc local=0)
inv-vlc: reserva de formatge-curat per a Marc acceptada (estoc local=0)
durant la partició, lectures: 0 / 0
-- partició curada --
LWW: guanya l'escriptura d'inv-vlc (marca 2); totes dues rèpliques queden amb estoc=0
reserves acceptades: [('Anna', 'inv-bcn'), ('Marc', 'inv-vlc')]
estoc final: 0 / 0
!!! 2 reserves d'1 unitat: l'estoc final hauria de ser -1; LWW ha PERDUT 1 reserva/es i hi ha sobrevendaQuè està passant, pas a pas:
- Mode CP: durant la partició,
reservarcomprova si arriba a l'altra rèplica abans de tocar res. No hi arriba, i llançaSenseQuorum: ni l'Anna ni en Marc poden reservar. El sistema ha sacrificat disponibilitat (dos nodes vius que responen amb error) i ha conservat la linealitzabilitat: no hi ha cap historial anòmal possible perquè no hi ha hagut escriptures. En curar-se la partició no hi ha res a reconciliar. Amb N=2, aquest mode és fràgil: qualsevol partició apaga tot el sistema. Amb N=3 i quòrum de majoria (03-03 i 03-04), el costat amb dos nodes continuaria reservant. - Mode AP: cada rèplica accepta la reserva del seu client i descompta la seva còpia local. Totes dues responen, totes dues queden a zero, i totes dues creuen haver venut l'últim formatge: la lectura "0 / 0" sembla coherent però amaga dues reserves. En reconciliar amb LWW, el sistema compara marques, es queda amb l'escriptura d'
inv-vlc(marca 2) i descarta la d'inv-bcn. L'estoc final (0) és incorrecte (hauria de ser -1, cosa que vol dir que un dels dos clients no rebrà el seu formatge) i, pitjor, la reserva de l'Anna ha desaparegut de l'estat de l'estoc encara que continua a la llista de reserves d'inv-bcn. Ningú no se n'assabentarà fins que la Formatgeria Montblanc rebi dues comandes per a una unitat.
L'experiment mostra les dues cares del teorema amb cruesa: CP protegeix l'invariant al preu de rebutjar clients; AP manté tots els clients contents durant la partició i els trenca l'invariant després. I mostra també que LWW és la pitjor reconciliació possible per a dades que es disputen: descarta silenciosament una escriptura sencera. Una reconciliació per operacions (sumar els descomptes de tots dos costats: 1 - 1 - 1 = -1, detectar el negatiu i compensar la comanda d'en Marc) o un llindar que canviï a mode CP en quedar poques unitats, com suggeria la taula de l'apartat 7, serien les alternatives; les estratègies de resolució de conflictes es tracten a 03-04, i la compensació de la comanda d'en Marc, a 03-05.
- Crítiques i matisos: harvest, yield i "deixeu de dir CP o AP a les bases de dades"
CAP és correcte com a teorema, però el seu ús com a etiqueta de sistemes ha rebut crítiques serioses que convé conèixer:
- És massa estret. Només cobreix un model de consistència (linealitzabilitat), un tipus de fallada (partició) i una noció de disponibilitat molt particular. No diu res de latència (per això Abadi va proposar PACELC), de fallades de nodes que no són particions, de transaccions ni de particionament de dades. Un sistema pot ser irreprotxablement "CP" i tot i així perdre dades per un disc corrupte, o ser "AP" i estar caigut per un error de desplegament.
- Kleppmann (2015), "Please stop calling databases CP or AP". Martin Kleppmann argumenta que les definicions de CAP són tan específiques que gairebé cap sistema real no hi encaixa netament: MongoDB no és "CP" perquè les lectures de secundaris no són linealitzables ni les escriptures
w:1sobreviuen a un failover; Cassandra no és "AP" en el sentit formal quan es fa servir amb quòrum; i "disponible" en el sentit de Gilbert-Lynch (tot node viu respon) és una propietat que gairebé ningú no vol literalment (un node aïllat que respon amb dades de fa una hora és "disponible" en algun sentit útil?). La seva proposta: descriure els sistemes per les garanties concretes que ofereixen (el vocabulari de 03-01) i pel seu comportament davant fallades concretes, en lloc de per una lletra. La taula de l'apartat 6 s'ha construït amb aquest esperit: cada fila indica la configuració i el comportament, no només l'etiqueta. - Brewer (2012), "CAP twelve years later". El mateix Brewer va matisar que "dos de tres" és enganyós, que l'elecció és per operació i durant la partició, i que l'interessant és dissenyar la gestió de la partició: detectar-la, entrar en un mode explícit de partició (limitar operacions, registrar el que es fa), i en curar-se, recuperar (reconciliar, compensar). La simulació de l'apartat 8 en mode AP no té precisament aquesta tercera fase ben feta.
- Harvest i yield (Fox i Brewer, 1999). Una manera més fina de pensar la disponibilitat. Yield és la fracció de peticions que es responen; harvest és la fracció de les dades que reflecteix la resposta. Un cercador amb l'índex en 10 fragments que en perd un pot respondre el 100 % de les consultes (yield 1) amb el 90 % de les dades (harvest 0,9). Aplicat a Quilòmetre Zero: durant una partició, la pàgina d'un mercat pot mostrar els productors abastables i ometre els altres amb un avís, en lloc de fallar sencera. És una forma de degradació elegant que CAP, amb la seva A binària, no sap descriure.
La lliçó d'aquestes crítiques no és abandonar CAP sinó fer-lo servir com el que és: un recordatori formal que la coordinació té un preu i que aquest preu s'ha de decidir per dada i per operació, amb el vocabulari precís de 03-01 i no amb dues lletres.
Errors Comuns i Consells
- Presentar un sistema com a "CA". Si algú ho diu d'un sistema amb més d'un node, o no ha pensat en la partició o està descrivint un sol servidor. Pregunta què fa cada node quan no pot parlar amb els altres.
- Etiquetar tota la plataforma amb una lletra. Quilòmetre Zero no és CP ni AP; l'estoc de l'última unitat és CP i el cistell és AP. La decisió és per dada i per operació, i de vegades per estat de la dada.
- Triar AP i reconciliar amb LWW sense mirar què es perd. LWW és l'estratègia per defecte de molts magatzems (Cassandra, entre d'altres) i és correcta per a dades on l'última escriptura realment és la bona (posició d'un repartidor, perfil d'usuari editat per una sola persona). Per a dades que s'acumulen o es disputen, descarta escriptures senceres. Comprova-ho amb una simulació com la de l'apartat 8 abans d'acceptar la configuració per defecte.
- Confondre un node lent amb una partició... o no confondre'ls. En un sistema asíncron no hi ha cap diferència observable. Un sistema CP tractarà un node molt lent com a aïllat i deixarà de comptar-hi; això és correcte, però vol dir que els timeouts (02-03, 07-04) formen part de la decisió CAP, i uns timeouts massa curts converteixen la congestió en particions freqüents.
- Oblidar l'E de PACELC. El cost de la consistència es paga cada dia en latència, no només durant particions. Si la reserva linealitzable de
formatge-curatrequereix coordinació Barcelona-València, el percentil 99 deReservarEstocho reflectirà. Mesura abans de decidir (07-01). - Consell: dissenya explícitament les tres fases de Brewer per a cada dada AP: com es detecta la partició, quines operacions es permeten mentre dura i com es reconcilia després. Si no pots descriure la tercera fase, aquesta dada hauria de ser CP.
- Consell: quan algú et pregunti "això és CP o AP?", respon amb les garanties concretes: "les reserves són linealitzables via quòrum i es rebutgen si no hi ha majoria; les lectures del catàleg són eventualment consistents amb un retard típic de 200 ms".
Exercicis
Exercici 1: Classificar decisions
Per a cadascuna de les dades següents de Quilòmetre Zero, decideix CP o AP durant una partició, indica l'elecció L o C en absència de partició i justifica-ho en dues frases. Després, indica quina estratègia de reconciliació faries servir en les AP.
- La llista de desitjos de la Llúcia (productes marcats per a més tard).
- El nombre de reserves actives d'un repartidor (que limita quantes comandes més se li poden assignar; màxim 8).
- La valoració mitjana (1-5 estrelles) del Celler Roure Alt.
- L'estat d'una comanda (
creada→pagada→en_repartiment→lliurada).
Exercici 2: Estendre la simulació amb un llindar
Modifica Inventari perquè el mode es decideixi per producte i per estat: si l'estoc del producte a la rèplica local és més gran que un llindar (per exemple 5), la reserva es processa en mode AP; si és menor o igual, en mode CP. Carrega tomaquet-rosa amb 20 unitats i formatge-curat amb 1, parteix la xarxa i fes que l'Anna i en Marc reservin tots dos productes des de rèpliques diferents. Mostra la sortida i explica què s'ha guanyat respecte dels dos modes purs.
Exercici 3: Reconciliació per operacions
Substitueix reconciliar_lww per reconciliar_per_operacions, que en lloc de triar una escriptura calculi l'estoc real com estoc_inicial - reserves_a_bcn - reserves_a_vlc i, si el resultat és negatiu, retorni la llista de reserves que s'han de compensar (les últimes acceptades, per marca de temps). Executa l'escenari AP amb 1 unitat i amb 2 unitats i comenta la diferència amb LWW. Quina informació necessita aquesta reconciliació que LWW no necessitava?
Solucions
Solució 1:
- Llista de desitjos: AP / EL. Una llista de desitjos indisponible és una molèstia sense cost, i un article que apareix dues vegades és trivial d'arreglar. Reconciliació per unió (OR-Set, un CRDT de conjunt: mai no es perd un "afegir"; un "treure" només elimina els "afegir" que havia vist).
- Reserves actives d'un repartidor: CP / EC. El límit de 8 és un invariant del tipus "no descomptar per sota de zero": si dos costats de la partició assignen comandes al mateix repartidor, pot acabar amb 10. Com que és una decisió d'assignació (no de cara al client final), un rebuig temporal és acceptable: la comanda queda "pendent d'assignació" com a l'cas 5 de l'exercici 3 de 02-05.
- Valoració mitjana: AP / EL. És un agregat estadístic; una dècima d'estrella de diferència entre rèpliques durant uns segons és irrellevant. Reconciliació amb dos
GCounter(suma de puntuacions i nombre de vots), que convergeixen sense pèrdua. - Estat d'una comanda: CP / EC per a les transicions, encara que amb matís. L'estat avança per una màquina d'estats amb transicions irreversibles (una comanda lliurada no torna a "pagada") i cada transició la fa un únic servei (
pagamentsmarca pagada,repartimentmarca lliurada), així que el conflicte real és rar; però dos costats acceptant transicions diferents (cancelladaen un,en_repartimenten l'altre) crearien un estat sense sentit. Les escriptures han d'anar al líder dekm0_comandesamb quòrum; les lectures de "les meves comandes" poden ser AP amb garanties de sessió.
Solució 2:
LLINDAR = 5
def reservar(self, client: str, replica: str, producte: str) -> str:
local = self.repliques[replica]
mode = "AP" if local.estoc[producte].unitats > LLINDAR else "CP"
for r in self.repliques.values():
r.mode = mode # la resta del mètode fa servir local.mode com abans
return self._reservar_amb_mode(client, replica, producte) # el cos original(Reanomenant el reservar original a _reservar_amb_mode.) Amb tomaquet-rosa a 20 unitats, totes dues reserves durant la partició s'accepten en mode AP i, en reconciliar per operacions, l'estoc queda a 18 sense sobrevenda; amb formatge-curat a 1, totes dues es rebutgen en mode CP. S'ha guanyat disponibilitat per al 99 % dels productes (que tenen estoc de sobres) mantenint la protecció de l'invariant només quan importa. El cost: la decisió es pren amb l'estoc local, que durant la partició pot ser més gran que el real (l'altre costat ha descomptat sense que ho sapiguem), així que el llindar ha de ser més gran que el nombre de reserves plausibles durant la partició més llarga esperada. És un exemple del que Brewer anomena "gestionar la partició" en lloc de patir-la.
Solució 3:
Primer, reservar ha de conservar la marca de cada reserva acceptada (l'estoc només desa l'última escriptura, així que l'historial cal anotar-lo a part). Canvia el camp de Replica a reserves: list[tuple[int, str, str]] (marca, client, producte) i els dos append a local.reserves.append((self.rellotge, client, producte)) (i, a escenari, la comprensió que llista les reserves passa a for _, c, _ in r.reserves). Després:
def reconciliar_per_operacions(self, producte: str, estoc_inicial: int) -> str:
reserves = sorted(
(marca, client, r.nom)
for r in self.repliques.values()
for marca, client, p in r.reserves if p == producte
)
real = estoc_inicial - len(reserves) # sumar TOTES les operacions de tots dos costats
for r in self.repliques.values():
r.estoc[producte] = Registre(max(real, 0), self.rellotge, "reconciliacio")
a_compensar = reserves[real:] if real < 0 else [] # les últimes |real| per marca de temps
return (f"estoc real={real}; a compensar: "
f"{[(client, replica) for _, client, replica in a_compensar]}")Amb 1 unitat: estoc real=-1; a compensar: [('Marc', 'inv-vlc')]. Es conserva la reserva de l'Anna (marca 1), l'estoc queda a 0 i comandes rep l'ordre de cancel·lar la comanda d'en Marc (la compensació d'una saga, 03-05). Amb 2 unitats: estoc real=0; a compensar: []. Davant de LWW, que deixava l'estoc a 0 "per casualitat" i perdia una reserva sense que ningú ho sabés, aquesta reconciliació produeix un estat correcte i una llista explícita de danys a reparar. El que necessita, i LWW no, és l'historial d'operacions de cada costat (no només l'últim valor) i una regla de negoci per decidir a qui compensar. És la diferència entre replicar estats i replicar operacions, que reapareixerà a 03-04.
Conclusió
El teorema CAP, enunciat amb precisió, diu que un sistema distribuït no pot garantir alhora linealitzabilitat i que tot node viu respongui mentre la xarxa està partida; i com que la partició no és opcional tan bon punt hi ha dos nodes i un cable, l'elecció real és què sacrificar durant la partició: CP rebutja les operacions que no pot coordinar, AP les accepta i reconcilia després. Hem desmuntat els malentesos habituals (no se'n "trien dos de tres"; AP no és "sense consistència" sinó sense linealitzabilitat, i admet garanties causals i de sessió; CP no vol dir caiguda total), i hem afegit PACELC per descriure el cost diari de la consistència en latència quan no hi ha partició, amb una taula que classifica PostgreSQL, Cassandra, DynamoDB, MongoDB, Spanner i etcd per configuració. Aplicat a Quilòmetre Zero, el criteri ha estat clar: CP per a allò irreversible o disputat (pagaments, última unitat, lideratge), AP per a allò que es pot unir, corregir o ignorar (cistell, posicions, comptadors, catàleg), amb la subtilesa que una mateixa dada pot canviar de mode segons el seu estat. La simulació ha fet visibles totes dues cares: el mode CP va rebutjar l'Anna i en Marc durant la partició, i el mode AP els va acceptar tots dos i va perdre silenciosament una reserva en reconciliar amb last-write-wins. Les crítiques de Kleppmann i els conceptes de harvest i yield ens recorden que les dues lletres són un punt de partida, no una descripció, i que el que cal documentar són les garanties concretes i les tres fases de la gestió de la partició.
Hem dit diverses vegades que un sistema CP "rebutja les escriptures sense quòrum" i que "el costat amb majoria continua funcionant", com si fos evident com un grup de nodes, amb missatges que es perden i sense rellotge global, decideix quina és la majoria, qui mana i quin valor és el definitiu. No és gens evident: és un dels problemes més difícils de la informàtica distribuïda, resolt per algorismes amb nom propi que Quilòmetre Zero necessitarà, entre altres coses, perquè una sola instància del relay outbox de 02-05 publiqui en cada moment. Aquest problema és el consens, i Paxos i Raft són el tema de la lliçó següent.
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
