En un programa que corre en una sola màquina, la pregunta "què va passar primer?" sempre té resposta: n'hi ha prou amb mirar el rellotge o l'ordre de les instruccions. En un sistema distribuït, aquesta pregunta es torna sorprenentment difícil. Cada node té el seu propi rellotge, aquests rellotges mai no coincideixen exactament i els missatges triguen un temps variable a arribar. Quan l'Anna, des de Barcelona, i en Marc, des de València, premen "comprar" sobre l'última unitat de formatge curat de la Formatgeria Montblanc, decidir qui va ser primer no és una qüestió de mirar dues marques de temps.
Aquesta lliçó explica per què no existeix un rellotge global, fins a quin punt podem sincronitzar els rellotges físics (NTP, PTP) i, sobretot, presenta la solució que Leslie Lamport va proposar el 1978 i que continua sent fonamental: substituir el temps físic per un ordre lògic basat en la causalitat. Implementarem pas a pas els rellotges de Lamport i els rellotges vectorials en Python, veurem com detecten esdeveniments concurrents i acabarem amb una visió dels rellotges híbrids i de TrueTime, que combinen el millor de tots dos mons.
Contingut
- Per què no hi ha un rellotge global
- Rellotges físics: deriva i sincronització (NTP i PTP)
- El problema d'ordenar esdeveniments: l'Anna, en Marc i l'últim formatge
- La relació "va passar abans" (happens-before)
- Rellotges lògics de Lamport
- Rellotges vectorials
- Rellotges híbrids i TrueTime: una visió general
- Els rellotges lògics a la pràctica
- Errors comuns i consells
- Exercicis
- Conclusió
- Per què no hi ha un rellotge global
Un "rellotge global" seria un instant de temps compartit i exacte que tots els nodes poguessin consultar. No existeix per tres raons que s'acumulen:
- Cada node té el seu propi rellotge de maquinari, un oscil·lador de quars la freqüència del qual depèn de la temperatura, l'antiguitat i la fabricació. Dos rellotges idèntics, posats en hora alhora, divergeixen inevitablement.
- Consultar el rellotge d'un altre node porta temps, i aquest temps és variable i desconegut. Si preguntes "quina hora és?" i la resposta triga 30 ms a arribar, l'hora que t'han donat és de fa 15 ms? De fa 5? De fa 25? No ho saps.
- Els nodes s'aturen sense saber-ho. Un procés es pot aturar durant desenes o centenars de mil·lisegons per una recol·lecció de brossa, per una interrupció de la màquina virtual o per sobrecàrrega de CPU. Quan es reprèn, creu que "ara" és l'instant en què es va aturar.
La conseqüència pràctica: dues marques de temps preses en màquines diferents no es poden comparar amb una precisió millor que l'error de sincronització entre elles. Si aquest error és de 10 ms i les marques difereixen en 2 ms, l'ordre real és indeterminat.
- Rellotges físics: deriva i sincronització (NTP i PTP)
Deriva
La deriva (drift) és la velocitat a la qual un rellotge es desvia del temps real. Un quars típic de servidor té una deriva d'unes 50 parts per milió (ppm): es desvia 50 microsegons per cada segon, és a dir, uns 4,3 segons al dia. Sembla poc, però per a un sistema que processa centenars de comandes per segon, 4 segons són una eternitat. Per això els rellotges se sincronitzen periòdicament amb una font de referència.
NTP (Network Time Protocol)
NTP és el protocol que fan servir gairebé tots els sistemes operatius per posar en hora els seus rellotges a través d'Internet. Funciona en una jerarquia d'"estrats": els servidors d'estrat 0 són rellotges atòmics o receptors GPS; els d'estrat 1 se sincronitzen directament amb ells; els d'estrat 2 amb els d'estrat 1, etc. Un client NTP envia una petició, anota quan l'ha enviat i quan ha rebut la resposta, i fa servir les marques de temps del servidor per estimar el desfasament, assumint que el viatge d'anada triga el mateix que el de tornada. Aquesta suposició és la font principal d'error.
| Entorn | Precisió típica de NTP |
|---|---|
| Internet pública | 5 a 100 ms |
| Xarxa local ben configurada | 0,5 a 5 ms |
| Amb servidor GPS local | 0,1 a 1 ms |
A més, NTP ajusta el rellotge de dues maneres: lliscant (slew), accelerant o frenant lleugerament el rellotge fins a corregir el desfasament, o saltant (step), si el desfasament és gran. Un salt enrere vol dir que el rellotge pot marcar dues vegades el mateix instant, o que una mesura de durada (fi - inici) pot sortir negativa. Per això els sistemes operatius ofereixen un rellotge monotònic (time.monotonic() en Python), que mai no retrocedeix i serveix per mesurar durades, davant del rellotge de paret (time.time()), que serveix per saber la data i l'hora però pot saltar.
PTP (Precision Time Protocol)
PTP (IEEE 1588) assoleix precisions de microsegons o menys gràcies al fet que les marques de temps les posen les mateixes targetes de xarxa i els commutadors per maquinari, eliminant la variabilitat del programari. Requereix maquinari compatible a tota la ruta, per la qual cosa es fa servir en centres de dades, en finances d'alta freqüència i en telecomunicacions, no a l'Internet obert.
Fins i tot amb PTP, els rellotges tenen un error. I aquest error, per petit que sigui, és suficient perquè dos esdeveniments "gairebé simultanis" no es puguin ordenar amb certesa. Necessitem una altra idea.
- El problema d'ordenar esdeveniments: l'Anna, en Marc i l'últim formatge
Suposem que Quilòmetre Zero ha replicat el servei inventari en dues ciutats per reduir la latència: una rèplica a Barcelona (inv-bcn) i una altra a València (inv-vlc). Queda una unitat de formatge curat de la Formatgeria Montblanc. L'Anna compra des de Barcelona i en Marc des de València, gairebé alhora.
| Esdeveniment | Node | Hora segons el rellotge local |
|---|---|---|
| L'Anna prem "comprar" | inv-bcn |
10:00:00.120 |
| En Marc prem "comprar" | inv-vlc |
10:00:00.118 |
Si ens refiem dels rellotges, en Marc va ser 2 ms abans. Però el rellotge d'inv-vlc està sincronitzat per NTP amb un error estimat de ±15 ms. Així que la "veritable" hora d'en Marc és entre 10:00:00.103 i 10:00:00.133: pot haver estat abans o després que l'Anna. Els rellotges físics no poden decidir.
I ara la pregunta clau: importa realment "qui va ser primer" en temps físic? El que el sistema necessita és que totes dues rèpliques prenguin la mateixa decisió (una de les dues compres guanya, l'altra rep "sense estoc") i que aquesta decisió respecti les relacions de causa i efecte que sí que coneixem. Per exemple, si l'Anna va consultar l'estoc, va veure "1 unitat" i després va comprar, la seva compra va passar després de la seva consulta, i això sí que és un fet.
Aquesta és la idea de Lamport: renunciar al temps físic i quedar-nos només amb el que podem saber amb certesa, la causalitat.
- La relació "va passar abans" (happens-before)
Lamport va definir la relació "va passar abans" (que s'escriu a → b, "a va passar abans que b") a partir de només tres regles:
- Mateix procés. Si
aibsón esdeveniments del mateix procés iapassa abans queben la seva execució, aleshoresa → b. - Enviament i recepció. Si
aés l'enviament d'un missatge ibés la recepció d'aquest mateix missatge, aleshoresa → b. - Transitivitat. Si
a → bib → c, aleshoresa → c.
I una definició fonamental: si ni a → b ni b → a, aleshores a i b són concurrents (a ∥ b). Concurrent no vol dir "al mateix temps": vol dir que cap dels dos no pot haver influït en l'altre, perquè no hi ha cap cadena de missatges que els connecti. Per això, per al sistema, el seu ordre és indiferent: qualsevol ordre és igual de vàlid, sempre que tots els nodes triïn el mateix.
sequenceDiagram
participant A as inv-bcn (Anna)
participant Q as Formatgeria Montblanc
participant M as inv-vlc (Marc)
Q->>A: a1: estoc = 1 (missatge)
Q->>M: m1: estoc = 1 (missatge)
Note over A: a2: Anna consulta estoc
Note over M: m2: Marc consulta estoc
Note over A: a3: Anna compra
Note over M: m3: Marc compra
A->>M: a4: "he venut la unitat"
Note over M: m4: rep avís de Barcelona
En aquest diagrama: a2 → a3 (mateix procés), a3 → a4 → m4 (enviament i recepció, més transitivitat). Però a3 i m3 (les dues compres) són concurrents: no hi ha cap cadena de missatges entre elles. El sistema no pot saber quina va ser "realment" primer, i tampoc no ho necessita: el que necessita és una regla determinista per desempatar.
- Rellotges lògics de Lamport
Un rellotge lògic de Lamport és un comptador enter per procés que assigna a cada esdeveniment una marca L(e) de manera que es compleix la condició de rellotge: si a → b, aleshores L(a) < L(b). L'algorisme té tres regles, en paral·lel a les tres de happens-before:
- Abans de cada esdeveniment local, el procés incrementa el seu comptador:
L = L + 1. - En enviar un missatge, el procés incrementa el seu comptador i adjunta el valor al missatge.
- En rebre un missatge amb marca
Lm, el procés faL = max(L, Lm) + 1.
La regla 3 és la clau: garanteix que la recepció sempre té una marca més gran que l'enviament, i que a partir d'aquest moment el receptor "sap" que existeix com a mínim aquest temps lògic.
Implementació en Python
from dataclasses import dataclass, field
@dataclass
class Missatge:
origen: str
contingut: str
marca: int # el rellotge de Lamport de l'emissor en el moment de l'enviament
@dataclass
class ProcesLamport:
"""Un node amb el seu rellotge lògic de Lamport i un registre d'esdeveniments."""
nom: str
rellotge: int = 0
registre: list = field(default_factory=list)
def _anotar(self, descripcio: str) -> None:
self.registre.append((self.rellotge, self.nom, descripcio))
print(f" [{self.nom} L={self.rellotge:>2}] {descripcio}")
def esdeveniment_local(self, descripcio: str) -> None:
# Regla 1: incrementar abans de l'esdeveniment
self.rellotge += 1
self._anotar(descripcio)
def enviar(self, contingut: str) -> Missatge:
# Regla 2: incrementar i adjuntar la marca al missatge
self.rellotge += 1
self._anotar(f"envia '{contingut}'")
return Missatge(self.nom, contingut, self.rellotge)
def rebre(self, msg: Missatge) -> None:
# Regla 3: avançar el rellotge si l'emissor anava per davant, i sumar-hi un
self.rellotge = max(self.rellotge, msg.marca) + 1
self._anotar(f"rep '{msg.contingut}' de {msg.origen} (marca {msg.marca})")
if __name__ == "__main__":
bcn = ProcesLamport("inv-bcn")
vlc = ProcesLamport("inv-vlc")
formatgeria = ProcesLamport("formatgeria")
print("La Formatgeria Montblanc publica l'estoc:")
m_bcn = formatgeria.enviar("estoc formatge curat = 1")
m_vlc = formatgeria.enviar("estoc formatge curat = 1")
bcn.rebre(m_bcn)
vlc.rebre(m_vlc)
print("\nL'Anna (Barcelona) i en Marc (València) actuen de manera concurrent:")
bcn.esdeveniment_local("Anna consulta estoc: veu 1 unitat")
bcn.esdeveniment_local("Anna compra la unitat")
vlc.esdeveniment_local("Marc consulta estoc: veu 1 unitat")
vlc.esdeveniment_local("Marc compra la unitat")
print("\nBarcelona avisa València de la venda:")
avis = bcn.enviar("venuda unitat de formatge curat")
vlc.rebre(avis)
vlc.esdeveniment_local("detecta conflicte amb la compra de Marc")
print("\nOrdre total d'esdeveniments (marca, nom de procés):")
tots = sorted(bcn.registre + vlc.registre + formatgeria.registre)
for marca, nom, desc in tots:
print(f" {marca:>2} {nom:<11} {desc}")La sortida:
La Formatgeria Montblanc publica l'estoc: [formatgeria L= 1] envia 'estoc formatge curat = 1' [formatgeria L= 2] envia 'estoc formatge curat = 1' [inv-bcn L= 2] rep 'estoc formatge curat = 1' de formatgeria (marca 1) [inv-vlc L= 3] rep 'estoc formatge curat = 1' de formatgeria (marca 2) L'Anna (Barcelona) i en Marc (València) actuen de manera concurrent: [inv-bcn L= 3] Anna consulta estoc: veu 1 unitat [inv-bcn L= 4] Anna compra la unitat [inv-vlc L= 4] Marc consulta estoc: veu 1 unitat [inv-vlc L= 5] Marc compra la unitat Barcelona avisa València de la venda: [inv-bcn L= 5] envia 'venuda unitat de formatge curat' [inv-vlc L= 6] rep 'venuda unitat de formatge curat' de inv-bcn (marca 5) [inv-vlc L= 7] detecta conflicte amb la compra de Marc Ordre total d'esdeveniments (marca, nom de procés): 1 formatgeria envia 'estoc formatge curat = 1' 2 formatgeria envia 'estoc formatge curat = 1' 2 inv-bcn rep 'estoc formatge curat = 1' de formatgeria (marca 1) 3 inv-bcn Anna consulta estoc: veu 1 unitat 3 inv-vlc rep 'estoc formatge curat = 1' de formatgeria (marca 2) 4 inv-bcn Anna compra la unitat 4 inv-vlc Marc consulta estoc: veu 1 unitat 5 inv-bcn envia 'venuda unitat de formatge curat' 5 inv-vlc Marc compra la unitat 6 inv-vlc rep 'venuda unitat de formatge curat' de inv-bcn (marca 5) 7 inv-vlc detecta conflicte amb la compra de Marc
El mateix escenari, dibuixat com a diagrama de seqüència amb la marca de Lamport de cada esdeveniment:
sequenceDiagram
participant Q as formatgeria
participant A as inv-bcn
participant M as inv-vlc
Note over Q: L=1 envia estoc=1
Q->>A: marca 1
Note over Q: L=2 envia estoc=1
Q->>M: marca 2
Note over A: L=2 rep (max(0,1)+1)
Note over M: L=3 rep (max(0,2)+1)
Note over A: L=3 Anna consulta
Note over A: L=4 Anna compra
Note over M: L=4 Marc consulta
Note over M: L=5 Marc compra
Note over A: L=5 envia "venuda"
A->>M: marca 5
Note over M: L=6 rep (max(5,5)+1)
Note over M: L=7 detecta conflicte
Observacions importants:
- La condició de rellotge es compleix. Cada recepció té una marca més gran que el seu enviament (1 → 2, 2 → 3, 5 → 6), i dins de cada procés les marques creixen. Tota cadena causal té marques creixents.
- Hi ha empats. "Anna compra" (
inv-bcn, 4) i "Marc consulta" (inv-vlc, 4) tenen la mateixa marca. Lamport resol els empats amb un criteri arbitrari però determinista: si les marques coincideixen, ordena pel nom (o identificador) del procés. És el que fasorted(...)amb les tuples(marca, nom, descripcio). Així, tots els nodes que apliquin la mateixa regla obtindran el mateix ordre total. - L'ordre total no és "l'ordre real", i no importa. A l'ordre final, "Anna compra" (4) precedeix "Marc compra" (5). Va ser així en el temps físic? No ho sabem ni ho podem saber. Però és un ordre consistent amb tota la causalitat coneguda i és el mateix per a tothom, que és exactament el que necessitàvem per decidir qui s'emporta el formatge.
- La limitació de Lamport. Si
a → b, aleshoresL(a) < L(b). Però el recíproc és fals: deL(a) < L(b)no se'n dedueixa → b. "Anna compra" (4) té una marca més petita que "Marc compra" (5), però són esdeveniments concurrents: no hi ha cap relació causal entre ells. Mirant només les marques de Lamport no podem distingir "va passar abans" de "concurrent". Per a això calen els rellotges vectorials.
- Rellotges vectorials
Un rellotge vectorial substitueix l'enter únic per un vector amb un comptador per procés. Si hi ha tres processos, cada esdeveniment porta una marca com {inv-bcn: 3, inv-vlc: 1, formatgeria: 2}, que es llegeix com "aquest esdeveniment coneix fins a l'esdeveniment 3 de Barcelona, l'1 de València i el 2 de la formatgeria". Les regles:
- Abans de cada esdeveniment local, el procés
iincrementa la seva pròpia component:V[i] = V[i] + 1. - En enviar, incrementa la seva component i adjunta el vector complet al missatge.
- En rebre un missatge amb vector
Vm, faV[k] = max(V[k], Vm[k])per a cada componentk, i després incrementa la seva pròpia component.
I la comparació, que és el que hi guanyem:
V(a) ≤ V(b)si totes les components deV(a)són més petites o iguals que les deV(b).a → bsi i només siV(a) ≤ V(b)iV(a) ≠ V(b).a ∥ b(concurrents) si niV(a) ≤ V(b)niV(b) ≤ V(a): és a dir, cadascun té alguna component més gran que l'altre.
Ara el recíproc sí que es compleix: comparant vectors podem saber amb certesa si dos esdeveniments estan causalment relacionats o són concurrents.
Implementació en Python
from dataclasses import dataclass, field
@dataclass(frozen=True)
class MarcaVectorial:
"""Un vector de comptadors, immutable, amb les operacions de comparació."""
valors: tuple # una tupla d'enters, un per procés, en ordre fix
processos: tuple # noms dels processos, en el mateix ordre
def __le__(self, altra: "MarcaVectorial") -> bool:
return all(a <= b for a, b in zip(self.valors, altra.valors))
def va_passar_abans(self, altra: "MarcaVectorial") -> bool:
return self <= altra and self.valors != altra.valors
def concurrent(self, altra: "MarcaVectorial") -> bool:
return not (self <= altra) and not (altra <= self)
def __str__(self) -> str:
return "{" + ", ".join(f"{p}:{v}" for p, v in zip(self.processos, self.valors)) + "}"
@dataclass
class MissatgeV:
origen: str
contingut: str
marca: MarcaVectorial
class ProcesVectorial:
"""Un node amb rellotge vectorial. Tots els processos han de conèixer la llista completa."""
def __init__(self, nom: str, processos: list[str]):
self.nom = nom
self.processos = tuple(processos)
self.index = processos.index(nom) # la meva posició al vector
self.vector = [0] * len(processos)
self.esdeveniments: dict[str, MarcaVectorial] = {} # etiqueta -> marca de l'esdeveniment
def _marca_actual(self) -> MarcaVectorial:
return MarcaVectorial(tuple(self.vector), self.processos)
def _anotar(self, etiqueta: str, descripcio: str) -> MarcaVectorial:
marca = self._marca_actual()
self.esdeveniments[etiqueta] = marca
print(f" [{self.nom} {marca}] {etiqueta}: {descripcio}")
return marca
def esdeveniment_local(self, etiqueta: str, descripcio: str) -> None:
self.vector[self.index] += 1 # regla 1
self._anotar(etiqueta, descripcio)
def enviar(self, etiqueta: str, contingut: str) -> MissatgeV:
self.vector[self.index] += 1 # regla 2
marca = self._anotar(etiqueta, f"envia '{contingut}'")
return MissatgeV(self.nom, contingut, marca)
def rebre(self, etiqueta: str, msg: MissatgeV) -> None:
# regla 3: màxim component a component, i després avançar la pròpia
self.vector = [max(meu, seu) for meu, seu in zip(self.vector, msg.marca.valors)]
self.vector[self.index] += 1
self._anotar(etiqueta, f"rep '{msg.contingut}' de {msg.origen}")
if __name__ == "__main__":
noms = ["inv-bcn", "inv-vlc", "formatgeria"]
bcn = ProcesVectorial("inv-bcn", noms)
vlc = ProcesVectorial("inv-vlc", noms)
formatgeria = ProcesVectorial("formatgeria", noms)
print("La Formatgeria Montblanc publica l'estoc:")
m1 = formatgeria.enviar("q1", "estoc formatge curat = 1")
m2 = formatgeria.enviar("q2", "estoc formatge curat = 1")
bcn.rebre("a1", m1)
vlc.rebre("m1", m2)
print("\nL'Anna i en Marc actuen de manera concurrent:")
bcn.esdeveniment_local("a2", "Anna consulta estoc: veu 1 unitat")
bcn.esdeveniment_local("a3", "Anna compra la unitat")
vlc.esdeveniment_local("m2", "Marc consulta estoc: veu 1 unitat")
vlc.esdeveniment_local("m3", "Marc compra la unitat")
print("\nBarcelona avisa València:")
avis = bcn.enviar("a4", "venuda unitat de formatge curat")
vlc.rebre("m4", avis)
print("\nRelacions causals:")
esdeveniments = {**bcn.esdeveniments, **vlc.esdeveniments, **formatgeria.esdeveniments}
for x, y in [("a2", "a3"), ("a3", "m4"), ("a3", "m3"), ("m3", "a3"), ("q1", "m3")]:
vx, vy = esdeveniments[x], esdeveniments[y]
if vx.va_passar_abans(vy):
relacio = f"{x} -> {y} (va passar abans)"
elif vy.va_passar_abans(vx):
relacio = f"{y} -> {x} (va passar abans)"
else:
relacio = f"{x} || {y} (CONCURRENTS)"
print(f" {x}={vx} {y}={vy} => {relacio}")La sortida:
La Formatgeria Montblanc publica l'estoc:
[formatgeria {inv-bcn:0, inv-vlc:0, formatgeria:1}] q1: envia 'estoc formatge curat = 1'
[formatgeria {inv-bcn:0, inv-vlc:0, formatgeria:2}] q2: envia 'estoc formatge curat = 1'
[inv-bcn {inv-bcn:1, inv-vlc:0, formatgeria:1}] a1: rep 'estoc formatge curat = 1' de formatgeria
[inv-vlc {inv-bcn:0, inv-vlc:1, formatgeria:2}] m1: rep 'estoc formatge curat = 1' de formatgeria
L'Anna i en Marc actuen de manera concurrent:
[inv-bcn {inv-bcn:2, inv-vlc:0, formatgeria:1}] a2: Anna consulta estoc: veu 1 unitat
[inv-bcn {inv-bcn:3, inv-vlc:0, formatgeria:1}] a3: Anna compra la unitat
[inv-vlc {inv-bcn:0, inv-vlc:2, formatgeria:2}] m2: Marc consulta estoc: veu 1 unitat
[inv-vlc {inv-bcn:0, inv-vlc:3, formatgeria:2}] m3: Marc compra la unitat
Barcelona avisa València:
[inv-bcn {inv-bcn:4, inv-vlc:0, formatgeria:1}] a4: envia 'venuda unitat de formatge curat'
[inv-vlc {inv-bcn:4, inv-vlc:4, formatgeria:2}] m4: rep 'venuda unitat de formatge curat' de inv-bcn
Relacions causals:
a2={inv-bcn:2, inv-vlc:0, formatgeria:1} a3={inv-bcn:3, inv-vlc:0, formatgeria:1} => a2 -> a3 (va passar abans)
a3={inv-bcn:3, inv-vlc:0, formatgeria:1} m4={inv-bcn:4, inv-vlc:4, formatgeria:2} => a3 -> m4 (va passar abans)
a3={inv-bcn:3, inv-vlc:0, formatgeria:1} m3={inv-bcn:0, inv-vlc:3, formatgeria:2} => a3 || m3 (CONCURRENTS)
m3={inv-bcn:0, inv-vlc:3, formatgeria:2} a3={inv-bcn:3, inv-vlc:0, formatgeria:1} => m3 || a3 (CONCURRENTS)
q1={inv-bcn:0, inv-vlc:0, formatgeria:1} m3={inv-bcn:0, inv-vlc:3, formatgeria:2} => q1 -> m3 (va passar abans)Analitzem el que hi hem guanyat:
- Detectem la concurrència.
a3(Anna compra) téinv-bcn:3més gran quem3, peròm3(Marc compra) téinv-vlc:3més gran quea3. Cadascun "sap alguna cosa" que l'altre no sap: són concurrents, i el sistema ho pot detectar mecànicament. Amb Lamport, això era impossible. - Confirmem la causalitat.
a3 → m4: quan València rep l'avís de Barcelona, el seu vector absorbeix el de Barcelona (inv-bcn:4), i a partir d'aquí tot el que passi a València "sap" de la compra de l'Anna. El vector dem4domina el d'a3en totes les components. - Un detall subtil:
q1 → m3. La formatgeria va enviarq1a Barcelona, no a València, així que com és quem3"sap" deq1? Perquèq2va passar després deq1a la formatgeria (mateixa component: 2 > 1), im1va rebreq2. La transitivitat es propaga sola a través del vector. - El preu. Cada marca ocupa tants enters com processos hi ha. Amb 3 processos és trivial; amb 1.000 rèpliques o amb milions de clients, no. A la pràctica es fan servir vectors només amb els nodes que escriuen (no els clients), o variants compactades (dotted version vectors), o s'accepta la pèrdua d'informació de Lamport quan no cal detectar concurrència.
- Rellotges híbrids i TrueTime: una visió general
Els rellotges lògics resolen l'ordenació causal, però perden una cosa valuosa: la relació amb el temps real. Una marca de Lamport 4732 no diu res de si l'esdeveniment va passar aquest matí o l'any passat, i hi ha molts usos (auditoria, caducitats, "dona'm les comandes de l'última hora") que necessiten temps físic. Dues famílies de solucions combinen tots dos mons:
- Rellotges lògics híbrids (HLC, Hybrid Logical Clocks, 2014). Una marca HLC té dues parts: el temps físic (segons NTP) i un comptador lògic. Es comporta com un rellotge de Lamport (respecta la causalitat: si
a → b,HLC(a) < HLC(b)), però la seva part física es manté sempre a prop del temps real (amb un error acotat pel de NTP). El comptador lògic només intervé per desempatar esdeveniments que el temps físic no pot ordenar. Els fan servir bases de dades distribuïdes com CockroachDB o MongoDB. - TrueTime (Google Spanner, 2012). En lloc de donar un instant, l'API de TrueTime retorna un interval
[mes_aviat, mes_tard]que amb certesa conté l'instant real, gràcies a rellotges atòmics i GPS a cada centre de dades, que mantenen l'interval en uns pocs mil·lisegons. Spanner assigna a cada transacció una marca de temps i, abans de confirmar-la, espera que l'interval d'incertesa hagi passat del tot (commit wait). Així garanteix que si la transacció A va acabar abans que comencés la B en temps real, aleshores la marca d'A és més petita que la de B, a qualsevol part del món. És una solució cara (maquinari especialitzat) però conceptualment elegant: converteix la incertesa del rellotge en una espera explícita i acotada.
| Mecanisme | Ordena per causalitat | Detecta concurrència | Relació amb el temps real | Mida de la marca | Requisits |
|---|---|---|---|---|---|
| Rellotge físic (NTP) | No (amb errors) | No | Sí (±ms) | 1 enter | Cap |
| Lamport | Sí | No | Cap | 1 enter | Cap |
| Vectorial | Sí | Sí | Cap | N enters | Conèixer els N processos |
| HLC | Sí | No | Sí (±error NTP) | 2 enters | NTP |
| TrueTime | Sí (amb espera) | Implícita | Sí (interval garantit) | Interval | Rellotges atòmics/GPS |
- Els rellotges lògics a la pràctica
On apareixen aquests mecanismes en sistemes reals, i a Quilòmetre Zero?
- Ordenació de missatges i esdeveniments. Quan el servei
repartimentrep posicions dels repartidors per 4G, poden arribar desordenades. Si cada posició porta un comptador per repartidor (un rellotge de Lamport d'un sol procés),repartimentpot descartar posicions antigues que arriben tard. Els sistemes de missatgeria (lliçó 02-04) ofereixen garanties d'ordre basades en la mateixa idea de números de seqüència. - Detecció de conflictes en replicació. Quan dues rèpliques d'
inventari(o dues còpies del cistell de l'Anna, una al seu mòbil i una altra al servidor) es modifiquen de manera concurrent, els vectors de versió permeten detectar que hi ha un conflicte real (dues escriptures concurrents) davant d'una simple actualització (una escriptura que va passar després de l'altra). El sistema pot aleshores resoldre el conflicte (amb una regla de negoci, demanant-ho a l'usuari, o quedant-se amb totes dues versions). És el mecanisme que va popularitzar Amazon Dynamo i que estudiarem a la lliçó 03-04. - Instantànies consistents. Perquè
analiticacalculi "l'estoc total a les 12:00" sobre dades repartides en moltes rèpliques sense aturar el sistema, cal saber quins esdeveniments incloure. Els rellotges lògics permeten definir talls consistents (l'algorisme de Chandy-Lamport) que no incloguin un efecte sense la seva causa. - Marques de temps de transaccions. Les bases de dades distribuïdes assignen marques de temps (HLC, TrueTime) a les transaccions per decidir quina versió d'una dada veu cada lectura. Apareixerà al Mòdul 3 i a la lliçó 04-04.
La regla pràctica que es deriva de tot això: mai no facis servir marques de temps físiques preses en màquines diferents per decidir l'ordre d'operacions que afecten la coherència de les dades. Fes-les servir per al que serveixen (dates per a humans, caducitats, mètriques) i fes servir comptadors o vectors per a l'ordenació.
Errors Comuns i Consells
- Fer servir
time.time()per mesurar durades. El rellotge de paret pot saltar enrere per un ajust de NTP i donar durades negatives o absurdes. Per mesurar quant triga alguna cosa, fes servir sempretime.monotonic(). - Ordenar esdeveniments de nodes diferents per la seva marca de temps física. És l'error d'"últim a escriure guanya" (last-writer-wins) basat en rellotges: amb rellotges desincronitzats, una escriptura més antiga pot "guanyar" una de més recent i perdre dades silenciosament. Si es fa servir aquesta estratègia, cal ser conscient que pot perdre escriptures.
- Creure que Lamport detecta concurrència. De
L(a) < L(b)no se'n dedueix res sobre la relació causal entreaib. Si necessites saber si dos esdeveniments són concurrents, necessites vectors. - Oblidar el desempat determinista. Un ordre total de Lamport exigeix una regla per als empats (habitualment l'identificador de procés). Sense ella, dos nodes poden ordenar de manera diferent dos esdeveniments amb la mateixa marca.
- Vectors que creixen sense control. Si cada client que escriu afegeix una component al vector, el vector creix sense límit. Cal decidir qui "compta" com a procés (normalment les rèpliques, no els clients) i podar components antigues.
- Consell: a cada missatge o registre que travessi la xarxa, inclou-hi sempre dues coses: una marca de temps física (per als humans i per a les mètriques) i un número de seqüència o vector (per a l'ordenació). Costen pocs bytes i estalvien hores de depuració.
Exercicis
Exercici 1: Traçar rellotges de Lamport a mà
Tres processos, comandes, pagaments i analitica, comencen amb rellotge 0. Passen, en aquest ordre d'execució, els esdeveniments següents:
comandesesdeveniment local "crea comanda de la Llúcia".comandesenvia "cobra 14,50 €" apagaments.analiticaesdeveniment local "inicia informe".pagamentsrep el missatge decomandes.pagamentsesdeveniment local "cobrament acceptat".pagamentsenvia "cobrament OK" acomandesi també aanalitica(dos enviaments consecutius).analiticarep "cobrament OK".comandesrep "cobrament OK".
Calcula la marca de Lamport de cada esdeveniment i indica dos esdeveniments que siguin concurrents encara que les seves marques siguin diferents.
Exercici 2: Detectar conflictes al cistell
L'Anna modifica el seu cistell des del mòbil (procés mobil) i des del navegador (procés web), i tots dos se sincronitzen amb el servidor (procés servidor). Fent servir la classe ProcesVectorial, simula:
servidorenvia el cistell inicial amobili aweb(dos enviaments).mobilrep, i afegeix "formatge fresc" (esdeveniment local).webrep, i afegeix "vi negre" (esdeveniment local).mobilenvia el seu cistell alservidor; elservidorel rep.webenvia el seu cistell alservidor; elservidorel rep.
Comprova amb concurrent() si les dues modificacions són concurrents i explica què hauria de fer el servidor. Després, canvia l'ordre perquè web rebi el cistell del servidor després que aquest hagi integrat el de mobil, i comprova que ara la modificació de web passa després de la de mobil.
Exercici 3: Rellotge de Lamport per a posicions de repartidors
El servei repartiment rep posicions d'un repartidor per una xarxa que les desordena. Escriu una classe SeguimentRepartidor amb un mètode rebre_posicio(sequencia, lat, lon) que només actualitzi la posició actual si sequencia és més gran que l'última aplicada, i que compti quantes posicions ha descartat per antigues. Simula l'arribada de les seqüències [1, 2, 4, 3, 5, 7, 6, 8] i mostra la posició final i el nombre de descarts. Quina informació es perd amb aquesta estratègia i quan seria acceptable?
Solucions
Solució 1:
| Pas | Procés | Esdeveniment | Càlcul | Marca |
|---|---|---|---|---|
| 1 | comandes | crea comanda | 0 + 1 | 1 |
| 2 | comandes | envia "cobra" | 1 + 1 | 2 |
| 3 | analitica | inicia informe | 0 + 1 | 1 |
| 4 | pagaments | rep "cobra" (marca 2) | max(0, 2) + 1 | 3 |
| 5 | pagaments | cobrament acceptat | 3 + 1 | 4 |
| 6a | pagaments | envia "cobrament OK" a comandes | 4 + 1 | 5 |
| 6b | pagaments | envia "cobrament OK" a analitica | 5 + 1 | 6 |
| 7 | analitica | rep "cobrament OK" (marca 6) | max(1, 6) + 1 | 7 |
| 8 | comandes | rep "cobrament OK" (marca 5) | max(2, 5) + 1 | 6 |
Esdeveniments concurrents amb marques diferents: "inicia informe" (analitica, 1) i "cobrament acceptat" (pagaments, 4). No hi ha cap cadena de missatges entre ells (analitica encara no havia rebut res), així que són concurrents, encara que 1 < 4. Un altre parell: "crea comanda" (1) i "inicia informe" (1), amb marques iguals i també concurrents. Fixa't que "inicia informe" (1) i "rep cobrament OK" a comandes (6) també són concurrents: el 6 de comandes descendeix de pagaments, no d'analitica.
Solució 2:
noms = ["servidor", "mobil", "web"]
servidor = ProcesVectorial("servidor", noms)
mobil = ProcesVectorial("mobil", noms)
web = ProcesVectorial("web", noms)
c1 = servidor.enviar("s1", "cistell inicial")
c2 = servidor.enviar("s2", "cistell inicial")
mobil.rebre("mv1", c1)
mobil.esdeveniment_local("mv2", "afegeix formatge fresc")
web.rebre("w1", c2)
web.esdeveniment_local("w2", "afegeix vi negre")
servidor.rebre("s3", mobil.enviar("mv3", "cistell amb formatge"))
servidor.rebre("s4", web.enviar("w3", "cistell amb vi"))
mv2, w2 = mobil.esdeveniments["mv2"], web.esdeveniments["w2"]
print(mv2, w2, "concurrents:", mv2.concurrent(w2))El resultat és {servidor:1, mobil:2, web:0} davant de {servidor:2, mobil:0, web:2}: concurrents. Cap modificació no sabia de l'altra. El servidor no s'ha de quedar amb l'última que ha arribat (perdria el formatge o el vi): ha de fusionar totes dues (el cistell amb formatge i vi) o, si la fusió no és òbvia (per exemple, totes dues han canviat la quantitat del mateix producte), preguntar a l'Anna.
A la variant seqüencial (el servidor integra primer el cistell de mobil i després envia el cistell actualitzat a web, que aleshores afegeix el vi), la marca de w2 serà una cosa com {servidor:3, mobil:3, web:2}, que domina mv2: mv2.va_passar_abans(w2) és True. No hi ha conflicte: la modificació de web ja coneixia la de mobil, i el servidor la pot aplicar sense més.
Solució 3:
class SeguimentRepartidor:
def __init__(self, nom: str):
self.nom = nom
self.ultima_sequencia = 0
self.posicio = None
self.descartades = 0
def rebre_posicio(self, sequencia: int, lat: float, lon: float) -> None:
if sequencia <= self.ultima_sequencia:
self.descartades += 1
print(f" descartada seq {sequencia} (ja aplicada la {self.ultima_sequencia})")
return
self.ultima_sequencia = sequencia
self.posicio = (lat, lon)
print(f" aplicada seq {sequencia}: {self.posicio}")
seguiment = SeguimentRepartidor("furgoneta-3")
arribades = [1, 2, 4, 3, 5, 7, 6, 8]
for seq in arribades:
seguiment.rebre_posicio(seq, round(41.38 + seq * 0.001, 3), round(2.17 + seq * 0.001, 3))
print(f"Posició final: {seguiment.posicio}, descartades: {seguiment.descartades}")La posició final és la de la seqüència 8 i es descarten 2 posicions (la 3 i la 6). Es perd el recorregut complet: si analitica volgués reconstruir la ruta exacta, li faltarien dos punts. L'estratègia és acceptable quan només importa la posició actual (mostrar el repartidor al mapa), que és el cas del seguiment en temps real; si cal l'històric, caldria desar totes les posicions i ordenar-les per seqüència després, en lloc de descartar-les.
Conclusió
No existeix un rellotge global: cada node té el seu, amb deriva pròpia, i sincronitzar-los (NTP amb precisió de mil·lisegons, PTP de microsegons) redueix l'error però mai no l'elimina. Per això l'ordre de dos esdeveniments passats en màquines diferents no es pot decidir comparant les seves marques de temps físiques, com mostra el cas de l'Anna i en Marc comprant l'últim formatge.
La sortida de Lamport va ser canviar la pregunta: en lloc de "què va passar abans en el temps?", preguntar "què pot haver causat què?". La relació va passar abans captura exactament aquesta causalitat, els rellotges de Lamport la converteixen en un ordre total que tots els nodes comparteixen (a costa de no distingir la concurrència), i els rellotges vectorials hi afegeixen la capacitat de detectar quan dos esdeveniments són concurrents, que és la base de la detecció de conflictes en replicació. Els rellotges híbrids i TrueTime reconcilien la causalitat amb el temps real per als sistemes que necessiten totes dues coses.
Amb això tanquem els fonaments conceptuals del mòdul. La lliçó següent, Del Monòlit a la Plataforma Distribuïda: el Cas Quilòmetre Zero, recull tot el que hem vist —fallades parcials, models, avantatges i costos, fal·làcies, temps— i ho aplica a un disseny concret: l'arquitectura objectiu que construirem durant la resta del curs.
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
