En un programa que corre en una sola máquina, la pregunta "¿qué pasó primero?" tiene siempre respuesta: basta con mirar el reloj o el orden de las instrucciones. En un sistema distribuido, esa pregunta se vuelve sorprendentemente difícil. Cada nodo tiene su propio reloj, esos relojes nunca coinciden exactamente y los mensajes tardan un tiempo variable en llegar. Cuando Ana, desde Barcelona, y Marc, desde Valencia, pulsan "comprar" sobre la última unidad de queso curado de Quesería Montblanc, decidir quién fue primero no es una cuestión de mirar dos marcas de tiempo.

Esta lección explica por qué no existe un reloj global, hasta qué punto podemos sincronizar los relojes físicos (NTP, PTP) y, sobre todo, presenta la solución que Leslie Lamport propuso en 1978 y que sigue siendo fundamental: sustituir el tiempo físico por un orden lógico basado en la causalidad. Implementaremos paso a paso los relojes de Lamport y los relojes vectoriales en Python, veremos cómo detectan eventos concurrentes y terminaremos con una visión de los relojes híbridos y de TrueTime, que combinan lo mejor de ambos mundos.

Contenido

  1. Por qué no hay un reloj global
  2. Relojes físicos: deriva y sincronización (NTP y PTP)
  3. El problema de ordenar eventos: Ana, Marc y el último queso
  4. La relación "sucedió antes" (happens-before)
  5. Relojes lógicos de Lamport
  6. Relojes vectoriales
  7. Relojes híbridos y TrueTime: una visión general
  8. Los relojes lógicos en la práctica
  9. Errores comunes y consejos
  10. Ejercicios
  11. Conclusión

  1. Por qué no hay un reloj global

Un "reloj global" sería un instante de tiempo compartido y exacto que todos los nodos pudieran consultar. No existe por tres razones que se acumulan:

  1. Cada nodo tiene su propio reloj de hardware, un oscilador de cuarzo cuya frecuencia depende de la temperatura, la antigüedad y la fabricación. Dos relojes idénticos, puestos en hora a la vez, divergen inevitablemente.
  2. Consultar el reloj de otro nodo lleva tiempo, y ese tiempo es variable y desconocido. Si preguntas "¿qué hora es?" y la respuesta tarda 30 ms en llegar, ¿la hora que te han dado es de hace 15 ms? ¿De hace 5? ¿De hace 25? No lo sabes.
  3. Los nodos se pausan sin saberlo. Un proceso puede detenerse durante decenas o cientos de milisegundos por una recolección de basura, por una interrupción de la máquina virtual o por sobrecarga de CPU. Cuando reanuda, cree que "ahora" es el instante en que se detuvo.

La consecuencia práctica: dos marcas de tiempo tomadas en máquinas distintas no se pueden comparar con precisión mejor que el error de sincronización entre ellas. Si ese error es de 10 ms y las marcas difieren en 2 ms, el orden real es indeterminado.

  1. Relojes físicos: deriva y sincronización (NTP y PTP)

Deriva

La deriva (drift) es la velocidad a la que un reloj se desvía del tiempo real. Un cuarzo típico de servidor tiene una deriva de unas 50 partes por millón (ppm): se desvía 50 microsegundos por cada segundo, es decir, unos 4,3 segundos al día. Parece poco, pero para un sistema que procesa cientos de pedidos por segundo, 4 segundos es una eternidad. Por eso los relojes se sincronizan periódicamente con una fuente de referencia.

NTP (Network Time Protocol)

NTP es el protocolo que usan casi todos los sistemas operativos para poner en hora sus relojes a través de Internet. Funciona en una jerarquía de "estratos": los servidores de estrato 0 son relojes atómicos o receptores GPS; los de estrato 1 se sincronizan directamente con ellos; los de estrato 2 con los de estrato 1, etc. Un cliente NTP envía una petición, anota cuándo la envió y cuándo recibió la respuesta, y usa las marcas de tiempo del servidor para estimar el desfase, asumiendo que el viaje de ida tarda lo mismo que el de vuelta. Esa suposición es la fuente principal de error.

Entorno Precisión típica de NTP
Internet pública 5 a 100 ms
Red local bien configurada 0,5 a 5 ms
Con servidor GPS local 0,1 a 1 ms

Además, NTP ajusta el reloj de dos formas: deslizando (slew), acelerando o frenando ligeramente el reloj hasta corregir el desfase, o saltando (step), si el desfase es grande. Un salto hacia atrás significa que el reloj puede marcar dos veces el mismo instante, o que una medida de duración (fin - inicio) puede salir negativa. Los sistemas operativos ofrecen por ello un reloj monotónico (time.monotonic() en Python), que nunca retrocede y sirve para medir duraciones, frente al reloj de pared (time.time()), que sirve para saber la fecha y hora pero puede saltar.

PTP (Precision Time Protocol)

PTP (IEEE 1588) alcanza precisiones de microsegundos o menos gracias a que las marcas de tiempo las ponen las propias tarjetas de red y los conmutadores por hardware, eliminando la variabilidad del software. Requiere hardware compatible en toda la ruta, por lo que se usa en centros de datos, en finanzas de alta frecuencia y en telecomunicaciones, no en Internet abierta.

Incluso con PTP, los relojes tienen un error. Y ese error, por pequeño que sea, es suficiente para que dos eventos "casi simultáneos" no puedan ordenarse con certeza. Necesitamos otra idea.

  1. El problema de ordenar eventos: Ana, Marc y el último queso

Supongamos que Kilómetro Cero ha replicado el servicio inventario en dos ciudades para reducir la latencia: una réplica en Barcelona (inv-bcn) y otra en Valencia (inv-vlc). Queda una unidad de queso curado de Quesería Montblanc. Ana compra desde Barcelona y Marc desde Valencia, casi a la vez.

Evento Nodo Hora según el reloj local
Ana pulsa "comprar" inv-bcn 10:00:00.120
Marc pulsa "comprar" inv-vlc 10:00:00.118

Si nos fiamos de los relojes, Marc fue 2 ms antes. Pero el reloj de inv-vlc está sincronizado por NTP con un error estimado de ±15 ms. Así que la "verdadera" hora de Marc está entre 10:00:00.103 y 10:00:00.133: puede haber sido antes o después que Ana. Los relojes físicos no pueden decidir.

Y ahora la pregunta clave: ¿importa realmente "quién fue primero" en tiempo físico? Lo que el sistema necesita es que ambas réplicas tomen la misma decisión (una de las dos compras gana, la otra recibe "sin stock") y que esa decisión respete las relaciones de causa y efecto que sí conocemos. Por ejemplo, si Ana consultó el stock, vio "1 unidad" y luego compró, su compra sucedió después de su consulta, y eso sí es un hecho.

Esta es la idea de Lamport: renunciar al tiempo físico y quedarnos solo con lo que podemos saber con certeza, la causalidad.

  1. La relación "sucedió antes" (happens-before)

Lamport definió la relación "sucedió antes" (que se escribe a → b, "a sucedió antes que b") a partir de solo tres reglas:

  1. Mismo proceso. Si a y b son eventos del mismo proceso y a ocurre antes que b en su ejecución, entonces a → b.
  2. Envío y recepción. Si a es el envío de un mensaje y b es la recepción de ese mismo mensaje, entonces a → b.
  3. Transitividad. Si a → b y b → c, entonces a → c.

Y una definición fundamental: si ni a → b ni b → a, entonces a y b son concurrentes (a ∥ b). Concurrente no significa "al mismo tiempo": significa que ninguno pudo haber influido en el otro, porque no hay ninguna cadena de mensajes que los conecte. Por eso, para el sistema, su orden es indiferente: cualquier orden es igual de válido, siempre que todos los nodos elijan el mismo.

sequenceDiagram
    participant A as inv-bcn (Ana)
    participant Q as Quesería Montblanc
    participant M as inv-vlc (Marc)
    Q->>A: a1: stock = 1 (mensaje)
    Q->>M: m1: stock = 1 (mensaje)
    Note over A: a2: Ana consulta stock
    Note over M: m2: Marc consulta stock
    Note over A: a3: Ana compra
    Note over M: m3: Marc compra
    A->>M: a4: "he vendido la unidad"
    Note over M: m4: recibe aviso de Barcelona

En este diagrama: a2 → a3 (mismo proceso), a3 → a4 → m4 (envío y recepción, más transitividad). Pero a3 y m3 (las dos compras) son concurrentes: no hay ninguna cadena de mensajes entre ellas. El sistema no puede saber cuál fue "realmente" primero, y tampoco lo necesita: lo que necesita es una regla determinista para desempatar.

  1. Relojes lógicos de Lamport

Un reloj lógico de Lamport es un contador entero por proceso que asigna a cada evento una marca L(e) de forma que se cumple la condición de reloj: si a → b, entonces L(a) < L(b). El algoritmo tiene tres reglas, en paralelo a las tres de happens-before:

  1. Antes de cada evento local, el proceso incrementa su contador: L = L + 1.
  2. Al enviar un mensaje, el proceso incrementa su contador y adjunta el valor al mensaje.
  3. Al recibir un mensaje con marca Lm, el proceso hace L = max(L, Lm) + 1.

La regla 3 es la clave: garantiza que la recepción siempre tiene una marca mayor que el envío, y que a partir de ese momento el receptor "sabe" que existe al menos ese tiempo lógico.

Implementación en Python

from dataclasses import dataclass, field


@dataclass
class Mensaje:
    origen: str
    contenido: str
    marca: int          # el reloj de Lamport del emisor en el momento del envío


@dataclass
class ProcesoLamport:
    """Un nodo con su reloj lógico de Lamport y un registro de eventos."""
    nombre: str
    reloj: int = 0
    registro: list = field(default_factory=list)

    def _anotar(self, descripcion: str) -> None:
        self.registro.append((self.reloj, self.nombre, descripcion))
        print(f"  [{self.nombre} L={self.reloj:>2}] {descripcion}")

    def evento_local(self, descripcion: str) -> None:
        # Regla 1: incrementar antes del evento
        self.reloj += 1
        self._anotar(descripcion)

    def enviar(self, contenido: str) -> Mensaje:
        # Regla 2: incrementar y adjuntar la marca al mensaje
        self.reloj += 1
        self._anotar(f"envía '{contenido}'")
        return Mensaje(self.nombre, contenido, self.reloj)

    def recibir(self, msg: Mensaje) -> None:
        # Regla 3: adelantar el reloj si el emisor iba por delante, y avanzar uno
        self.reloj = max(self.reloj, msg.marca) + 1
        self._anotar(f"recibe '{msg.contenido}' de {msg.origen} (marca {msg.marca})")


if __name__ == "__main__":
    bcn = ProcesoLamport("inv-bcn")
    vlc = ProcesoLamport("inv-vlc")
    queseria = ProcesoLamport("queseria")

    print("Quesería Montblanc publica el stock:")
    m_bcn = queseria.enviar("stock queso curado = 1")
    m_vlc = queseria.enviar("stock queso curado = 1")
    bcn.recibir(m_bcn)
    vlc.recibir(m_vlc)

    print("\nAna (Barcelona) y Marc (Valencia) actúan de forma concurrente:")
    bcn.evento_local("Ana consulta stock: ve 1 unidad")
    bcn.evento_local("Ana compra la unidad")
    vlc.evento_local("Marc consulta stock: ve 1 unidad")
    vlc.evento_local("Marc compra la unidad")

    print("\nBarcelona avisa a Valencia de la venta:")
    aviso = bcn.enviar("vendida unidad de queso curado")
    vlc.recibir(aviso)
    vlc.evento_local("detecta conflicto con la compra de Marc")

    print("\nOrden total de eventos (marca, nombre de proceso):")
    todos = sorted(bcn.registro + vlc.registro + queseria.registro)
    for marca, nombre, desc in todos:
        print(f"  {marca:>2}  {nombre:<9} {desc}")

La salida:

Quesería Montblanc publica el stock:
  [queseria L= 1] envía 'stock queso curado = 1'
  [queseria L= 2] envía 'stock queso curado = 1'
  [inv-bcn L= 2] recibe 'stock queso curado = 1' de queseria (marca 1)
  [inv-vlc L= 3] recibe 'stock queso curado = 1' de queseria (marca 2)

Ana (Barcelona) y Marc (Valencia) actúan de forma concurrente:
  [inv-bcn L= 3] Ana consulta stock: ve 1 unidad
  [inv-bcn L= 4] Ana compra la unidad
  [inv-vlc L= 4] Marc consulta stock: ve 1 unidad
  [inv-vlc L= 5] Marc compra la unidad

Barcelona avisa a Valencia de la venta:
  [inv-bcn L= 5] envía 'vendida unidad de queso curado'
  [inv-vlc L= 6] recibe 'vendida unidad de queso curado' de inv-bcn (marca 5)
  [inv-vlc L= 7] detecta conflicto con la compra de Marc

Orden total de eventos (marca, nombre de proceso):
   1  queseria  envía 'stock queso curado = 1'
   2  inv-bcn   recibe 'stock queso curado = 1' de queseria (marca 1)
   2  queseria  envía 'stock queso curado = 1'
   3  inv-bcn   Ana consulta stock: ve 1 unidad
   3  inv-vlc   recibe 'stock queso curado = 1' de queseria (marca 2)
   4  inv-bcn   Ana compra la unidad
   4  inv-vlc   Marc consulta stock: ve 1 unidad
   5  inv-bcn   envía 'vendida unidad de queso curado'
   5  inv-vlc   Marc compra la unidad
   6  inv-vlc   recibe 'vendida unidad de queso curado' de inv-bcn (marca 5)
   7  inv-vlc   detecta conflicto con la compra de Marc

El mismo escenario, dibujado como diagrama de secuencia con la marca de Lamport de cada evento:

sequenceDiagram
    participant Q as queseria
    participant A as inv-bcn
    participant M as inv-vlc
    Note over Q: L=1 envía stock=1
    Q->>A: marca 1
    Note over Q: L=2 envía stock=1
    Q->>M: marca 2
    Note over A: L=2 recibe (max(0,1)+1)
    Note over M: L=3 recibe (max(0,2)+1)
    Note over A: L=3 Ana consulta
    Note over A: L=4 Ana compra
    Note over M: L=4 Marc consulta
    Note over M: L=5 Marc compra
    Note over A: L=5 envía "vendida"
    A->>M: marca 5
    Note over M: L=6 recibe (max(5,5)+1)
    Note over M: L=7 detecta conflicto

Observaciones importantes:

  1. La condición de reloj se cumple. Cada recepción tiene una marca mayor que su envío (1 → 2, 2 → 3, 5 → 6), y dentro de cada proceso las marcas crecen. Toda cadena causal tiene marcas crecientes.
  2. Hay empates. "Ana compra" (inv-bcn, 4) y "Marc consulta" (inv-vlc, 4) tienen la misma marca. Lamport resuelve los empates con un criterio arbitrario pero determinista: si las marcas coinciden, ordena por el nombre (o identificador) del proceso. Es lo que hace sorted(...) con las tuplas (marca, nombre, descripcion). Así, todos los nodos que apliquen la misma regla obtendrán el mismo orden total.
  3. El orden total no es "el orden real", y no importa. En el orden final, "Ana compra" (4) precede a "Marc compra" (5). ¿Fue así en el tiempo físico? No lo sabemos ni lo podemos saber. Pero es un orden consistente con toda la causalidad conocida y es el mismo para todos, que es exactamente lo que necesitábamos para decidir quién se lleva el queso.
  4. La limitación de Lamport. Si a → b, entonces L(a) < L(b). Pero el recíproco es falso: de L(a) < L(b) no se deduce a → b. "Ana compra" (4) tiene una marca menor que "Marc compra" (5), pero son eventos concurrentes: no hay ninguna relación causal entre ellos. Mirando solo las marcas de Lamport no podemos distinguir "sucedió antes" de "concurrente". Para eso hacen falta los relojes vectoriales.

  1. Relojes vectoriales

Un reloj vectorial sustituye el entero único por un vector con un contador por proceso. Si hay tres procesos, cada evento lleva una marca como {inv-bcn: 3, inv-vlc: 1, queseria: 2}, que se lee como "este evento conoce hasta el evento 3 de Barcelona, el 1 de Valencia y el 2 de la quesería". Las reglas:

  1. Antes de cada evento local, el proceso i incrementa su propia componente: V[i] = V[i] + 1.
  2. Al enviar, incrementa su componente y adjunta el vector completo al mensaje.
  3. Al recibir un mensaje con vector Vm, hace V[k] = max(V[k], Vm[k]) para cada componente k, y después incrementa su propia componente.

Y la comparación, que es lo que ganamos:

  • V(a) ≤ V(b) si todas las componentes de V(a) son menores o iguales que las de V(b).
  • a → b si y solo si V(a) ≤ V(b) y V(a) ≠ V(b).
  • a ∥ b (concurrentes) si ni V(a) ≤ V(b) ni V(b) ≤ V(a): es decir, cada uno tiene alguna componente mayor que el otro.

Ahora el recíproco se cumple: comparando vectores podemos saber con certeza si dos eventos están causalmente relacionados o son concurrentes.

Implementación en Python

from dataclasses import dataclass, field


@dataclass(frozen=True)
class MarcaVectorial:
    """Un vector de contadores, inmutable, con las operaciones de comparación."""
    valores: tuple          # una tupla de enteros, uno por proceso, en orden fijo
    procesos: tuple         # nombres de los procesos, en el mismo orden

    def __le__(self, otra: "MarcaVectorial") -> bool:
        return all(a <= b for a, b in zip(self.valores, otra.valores))

    def sucedio_antes(self, otra: "MarcaVectorial") -> bool:
        return self <= otra and self.valores != otra.valores

    def concurrente(self, otra: "MarcaVectorial") -> bool:
        return not (self <= otra) and not (otra <= self)

    def __str__(self) -> str:
        return "{" + ", ".join(f"{p}:{v}" for p, v in zip(self.procesos, self.valores)) + "}"


@dataclass
class MensajeV:
    origen: str
    contenido: str
    marca: MarcaVectorial


class ProcesoVectorial:
    """Un nodo con reloj vectorial. Todos los procesos deben conocer la lista completa."""

    def __init__(self, nombre: str, procesos: list[str]):
        self.nombre = nombre
        self.procesos = tuple(procesos)
        self.indice = procesos.index(nombre)          # mi posición en el vector
        self.vector = [0] * len(procesos)
        self.eventos: dict[str, MarcaVectorial] = {}  # etiqueta -> marca del evento

    def _marca_actual(self) -> MarcaVectorial:
        return MarcaVectorial(tuple(self.vector), self.procesos)

    def _anotar(self, etiqueta: str, descripcion: str) -> MarcaVectorial:
        marca = self._marca_actual()
        self.eventos[etiqueta] = marca
        print(f"  [{self.nombre} {marca}] {etiqueta}: {descripcion}")
        return marca

    def evento_local(self, etiqueta: str, descripcion: str) -> None:
        self.vector[self.indice] += 1                 # regla 1
        self._anotar(etiqueta, descripcion)

    def enviar(self, etiqueta: str, contenido: str) -> MensajeV:
        self.vector[self.indice] += 1                 # regla 2
        marca = self._anotar(etiqueta, f"envía '{contenido}'")
        return MensajeV(self.nombre, contenido, marca)

    def recibir(self, etiqueta: str, msg: MensajeV) -> None:
        # regla 3: máximo componente a componente, y luego avanzar el propio
        self.vector = [max(mio, suyo) for mio, suyo in zip(self.vector, msg.marca.valores)]
        self.vector[self.indice] += 1
        self._anotar(etiqueta, f"recibe '{msg.contenido}' de {msg.origen}")


if __name__ == "__main__":
    nombres = ["inv-bcn", "inv-vlc", "queseria"]
    bcn = ProcesoVectorial("inv-bcn", nombres)
    vlc = ProcesoVectorial("inv-vlc", nombres)
    queseria = ProcesoVectorial("queseria", nombres)

    print("Quesería Montblanc publica el stock:")
    m1 = queseria.enviar("q1", "stock queso curado = 1")
    m2 = queseria.enviar("q2", "stock queso curado = 1")
    bcn.recibir("a1", m1)
    vlc.recibir("m1", m2)

    print("\nAna y Marc actúan de forma concurrente:")
    bcn.evento_local("a2", "Ana consulta stock: ve 1 unidad")
    bcn.evento_local("a3", "Ana compra la unidad")
    vlc.evento_local("m2", "Marc consulta stock: ve 1 unidad")
    vlc.evento_local("m3", "Marc compra la unidad")

    print("\nBarcelona avisa a Valencia:")
    aviso = bcn.enviar("a4", "vendida unidad de queso curado")
    vlc.recibir("m4", aviso)

    print("\nRelaciones causales:")
    eventos = {**bcn.eventos, **vlc.eventos, **queseria.eventos}
    for x, y in [("a2", "a3"), ("a3", "m4"), ("a3", "m3"), ("m3", "a3"), ("q1", "m3")]:
        vx, vy = eventos[x], eventos[y]
        if vx.sucedio_antes(vy):
            relacion = f"{x} -> {y}  (sucedió antes)"
        elif vy.sucedio_antes(vx):
            relacion = f"{y} -> {x}  (sucedió antes)"
        else:
            relacion = f"{x} || {y}  (CONCURRENTES)"
        print(f"  {x}={vx}  {y}={vy}  =>  {relacion}")

La salida:

Quesería Montblanc publica el stock:
  [queseria {inv-bcn:0, inv-vlc:0, queseria:1}] q1: envía 'stock queso curado = 1'
  [queseria {inv-bcn:0, inv-vlc:0, queseria:2}] q2: envía 'stock queso curado = 1'
  [inv-bcn {inv-bcn:1, inv-vlc:0, queseria:1}] a1: recibe 'stock queso curado = 1' de queseria
  [inv-vlc {inv-bcn:0, inv-vlc:1, queseria:2}] m1: recibe 'stock queso curado = 1' de queseria

Ana y Marc actúan de forma concurrente:
  [inv-bcn {inv-bcn:2, inv-vlc:0, queseria:1}] a2: Ana consulta stock: ve 1 unidad
  [inv-bcn {inv-bcn:3, inv-vlc:0, queseria:1}] a3: Ana compra la unidad
  [inv-vlc {inv-bcn:0, inv-vlc:2, queseria:2}] m2: Marc consulta stock: ve 1 unidad
  [inv-vlc {inv-bcn:0, inv-vlc:3, queseria:2}] m3: Marc compra la unidad

Barcelona avisa a Valencia:
  [inv-bcn {inv-bcn:4, inv-vlc:0, queseria:1}] a4: envía 'vendida unidad de queso curado'
  [inv-vlc {inv-bcn:4, inv-vlc:4, queseria:2}] m4: recibe 'vendida unidad de queso curado' de inv-bcn

Relaciones causales:
  a2={inv-bcn:2, inv-vlc:0, queseria:1}  a3={inv-bcn:3, inv-vlc:0, queseria:1}  =>  a2 -> a3  (sucedió antes)
  a3={inv-bcn:3, inv-vlc:0, queseria:1}  m4={inv-bcn:4, inv-vlc:4, queseria:2}  =>  a3 -> m4  (sucedió antes)
  a3={inv-bcn:3, inv-vlc:0, queseria:1}  m3={inv-bcn:0, inv-vlc:3, queseria:2}  =>  a3 || m3  (CONCURRENTES)
  m3={inv-bcn:0, inv-vlc:3, queseria:2}  a3={inv-bcn:3, inv-vlc:0, queseria:1}  =>  m3 || a3  (CONCURRENTES)
  q1={inv-bcn:0, inv-vlc:0, queseria:1}  m3={inv-bcn:0, inv-vlc:3, queseria:2}  =>  q1 -> m3  (sucedió antes)

Analicemos lo que hemos ganado:

  1. Detectamos la concurrencia. a3 (Ana compra) tiene inv-bcn:3 mayor que m3, pero m3 (Marc compra) tiene inv-vlc:3 mayor que a3. Cada uno "sabe algo" que el otro no sabe: son concurrentes, y el sistema puede detectarlo mecánicamente. Con Lamport, esto era imposible.
  2. Confirmamos la causalidad. a3 → m4: cuando Valencia recibe el aviso de Barcelona, su vector absorbe el de Barcelona (inv-bcn:4), y a partir de ahí todo lo que ocurra en Valencia "sabe" de la compra de Ana. El vector de m4 domina al de a3 en todas las componentes.
  3. Un detalle sutil: q1 → m3. La quesería envió q1 a Barcelona, no a Valencia, así que ¿cómo es que m3 "sabe" de q1? Porque q2 sucedió después de q1 en la quesería (misma componente: 2 > 1), y m1 recibió q2. La transitividad se propaga sola a través del vector.
  4. El precio. Cada marca ocupa tantos enteros como procesos hay. Con 3 procesos es trivial; con 1.000 réplicas o con millones de clientes, no. En la práctica se usan vectores con solo los nodos que escriben (no los clientes), o variantes compactadas (dotted version vectors), o se acepta la pérdida de información de Lamport cuando no hace falta detectar concurrencia.

  1. Relojes híbridos y TrueTime: una visión general

Los relojes lógicos resuelven la ordenación causal, pero pierden algo valioso: la relación con el tiempo real. Una marca de Lamport 4732 no dice nada de si el evento ocurrió esta mañana o el año pasado, y hay muchos usos (auditoría, caducidades, "dame los pedidos de la última hora") que necesitan tiempo físico. Dos familias de soluciones combinan ambos mundos:

  • Relojes lógicos híbridos (HLC, Hybrid Logical Clocks, 2014). Una marca HLC tiene dos partes: el tiempo físico (según NTP) y un contador lógico. Se comporta como un reloj de Lamport (respeta la causalidad: si a → b, HLC(a) < HLC(b)), pero su parte física se mantiene siempre cerca del tiempo real (con un error acotado por el de NTP). El contador lógico solo interviene para desempatar eventos que el tiempo físico no puede ordenar. Los usan bases de datos distribuidas como CockroachDB o MongoDB.
  • TrueTime (Google Spanner, 2012). En lugar de dar un instante, la API de TrueTime devuelve un intervalo [más_temprano, más_tarde] que con certeza contiene el instante real, gracias a relojes atómicos y GPS en cada centro de datos, que mantienen el intervalo en unos pocos milisegundos. Spanner asigna a cada transacción una marca de tiempo y, antes de confirmarla, espera a que el intervalo de incertidumbre haya pasado por completo (commit wait). Así garantiza que si la transacción A terminó antes de que empezara la B en tiempo real, entonces la marca de A es menor que la de B, en cualquier parte del mundo. Es una solución cara (hardware especializado) pero conceptualmente elegante: convierte la incertidumbre del reloj en una espera explícita y acotada.
Mecanismo Ordena por causalidad Detecta concurrencia Relación con tiempo real Tamaño de la marca Requisitos
Reloj físico (NTP) No (con errores) No Sí (±ms) 1 entero Ninguno
Lamport No Ninguna 1 entero Ninguno
Vectorial Ninguna N enteros Conocer los N procesos
HLC No Sí (±error NTP) 2 enteros NTP
TrueTime Sí (con espera) Implícita Sí (intervalo garantizado) Intervalo Relojes atómicos/GPS

  1. Los relojes lógicos en la práctica

¿Dónde aparecen estos mecanismos en sistemas reales, y en Kilómetro Cero?

  • Ordenación de mensajes y eventos. Cuando el servicio reparto recibe posiciones de los repartidores por 4G, pueden llegar desordenadas. Si cada posición lleva un contador por repartidor (un reloj de Lamport de un solo proceso), reparto puede descartar posiciones antiguas que llegan tarde. Los sistemas de mensajería (lección 02-04) ofrecen garantías de orden basadas en la misma idea de números de secuencia.
  • Detección de conflictos en replicación. Cuando dos réplicas de inventario (o dos copias del carrito de Ana, una en su móvil y otra en el servidor) se modifican de forma concurrente, los vectores de versión permiten detectar que hay un conflicto real (dos escrituras concurrentes) frente a una simple actualización (una escritura que sucedió después de la otra). El sistema puede entonces resolver el conflicto (con una regla de negocio, pidiéndoselo al usuario, o quedándose con ambas versiones). Es el mecanismo que popularizó Amazon Dynamo y que estudiaremos en la lección 03-04.
  • Instantáneas consistentes. Para que analitica calcule "el stock total a las 12:00" sobre datos repartidos en muchas réplicas sin parar el sistema, hace falta saber qué eventos incluir. Los relojes lógicos permiten definir cortes consistentes (el algoritmo de Chandy-Lamport) que no incluyan un efecto sin su causa.
  • Marcas de tiempo de transacciones. Las bases de datos distribuidas asignan marcas de tiempo (HLC, TrueTime) a las transacciones para decidir qué versión de un dato ve cada lectura. Aparecerá en el Módulo 3 y en la lección 04-04.

La regla práctica que se deriva de todo esto: nunca uses marcas de tiempo físicas tomadas en máquinas distintas para decidir el orden de operaciones que afectan a la coherencia de los datos. Úsalas para lo que sirven (fechas para humanos, caducidades, métricas) y usa contadores o vectores para la ordenación.

Errores Comunes y Consejos

  • Usar time.time() para medir duraciones. El reloj de pared puede saltar hacia atrás por un ajuste de NTP y dar duraciones negativas o absurdas. Para medir cuánto tarda algo, usa siempre time.monotonic().
  • Ordenar eventos de distintos nodos por su marca de tiempo física. Es el error de "último en escribir gana" (last-writer-wins) basado en relojes: con relojes desincronizados, una escritura más antigua puede "ganar" a una más reciente y perder datos silenciosamente. Si se usa esa estrategia, hay que ser consciente de que puede perder escrituras.
  • Creer que Lamport detecta concurrencia. De L(a) < L(b) no se deduce nada sobre la relación causal entre a y b. Si necesitas saber si dos eventos son concurrentes, necesitas vectores.
  • Olvidar el desempate determinista. Un orden total de Lamport exige una regla para los empates (habitualmente el identificador de proceso). Sin ella, dos nodos pueden ordenar de forma distinta dos eventos con la misma marca.
  • Vectores que crecen sin control. Si cada cliente que escribe añade una componente al vector, el vector crece sin límite. Hay que decidir quién "cuenta" como proceso (normalmente las réplicas, no los clientes) y podar componentes antiguas.
  • Consejo: en cada mensaje o registro que atraviese la red, incluye siempre dos cosas: una marca de tiempo física (para los humanos y para las métricas) y un número de secuencia o vector (para la ordenación). Cuestan pocos bytes y ahorran horas de depuración.

Ejercicios

Ejercicio 1: Trazar relojes de Lamport a mano

Tres procesos, pedidos, pagos y analitica, empiezan con reloj 0. Ocurren, en este orden de ejecución, los eventos siguientes:

  1. pedidos evento local "crea pedido de Lucía".
  2. pedidos envía "cobra 14,50 €" a pagos.
  3. analitica evento local "inicia informe".
  4. pagos recibe el mensaje de pedidos.
  5. pagos evento local "cobro aceptado".
  6. pagos envía "cobro OK" a pedidos y también a analitica (dos envíos consecutivos).
  7. analitica recibe "cobro OK".
  8. pedidos recibe "cobro OK".

Calcula la marca de Lamport de cada evento e indica dos eventos que sean concurrentes aunque sus marcas sean distintas.

Ejercicio 2: Detectar conflictos en el carrito

Ana modifica su carrito desde el móvil (proceso movil) y desde el navegador (proceso web), y ambos se sincronizan con el servidor (proceso servidor). Usando la clase ProcesoVectorial, simula:

  1. servidor envía el carrito inicial a movil y a web (dos envíos).
  2. movil recibe, y añade "queso fresco" (evento local).
  3. web recibe, y añade "vino tinto" (evento local).
  4. movil envía su carrito al servidor; el servidor lo recibe.
  5. web envía su carrito al servidor; el servidor lo recibe.

Comprueba con concurrente() si las dos modificaciones son concurrentes y explica qué debería hacer el servidor. Después, cambia el orden para que web reciba el carrito del servidor después de que este haya integrado el de movil, y comprueba que ahora la modificación de web sucede después de la de movil.

Ejercicio 3: Reloj de Lamport para posiciones de repartidores

El servicio reparto recibe posiciones de un repartidor por una red que las desordena. Escribe una clase SeguimientoRepartidor con un método recibir_posicion(secuencia, lat, lon) que solo actualice la posición actual si secuencia es mayor que la última aplicada, y que cuente cuántas posiciones ha descartado por antiguas. Simula la llegada de las secuencias [1, 2, 4, 3, 5, 7, 6, 8] y muestra la posición final y el número de descartes. ¿Qué información se pierde con esta estrategia y cuándo sería aceptable?

Soluciones

Solución 1:

Paso Proceso Evento Cálculo Marca
1 pedidos crea pedido 0 + 1 1
2 pedidos envía "cobra" 1 + 1 2
3 analitica inicia informe 0 + 1 1
4 pagos recibe "cobra" (marca 2) max(0, 2) + 1 3
5 pagos cobro aceptado 3 + 1 4
6a pagos envía "cobro OK" a pedidos 4 + 1 5
6b pagos envía "cobro OK" a analitica 5 + 1 6
7 analitica recibe "cobro OK" (marca 6) max(1, 6) + 1 7
8 pedidos recibe "cobro OK" (marca 5) max(2, 5) + 1 6

Eventos concurrentes con marcas distintas: "inicia informe" (analitica, 1) y "cobro aceptado" (pagos, 4). No hay ninguna cadena de mensajes entre ellos (analitica no había recibido nada todavía), así que son concurrentes, aunque 1 < 4. Otro par: "crea pedido" (1) e "inicia informe" (1), con marcas iguales y también concurrentes. Fíjate en que "inicia informe" (1) y "recibe cobro OK" en pedidos (6) también son concurrentes: el 6 de pedidos desciende de pagos, no de analitica.

Solución 2:

nombres = ["servidor", "movil", "web"]
servidor = ProcesoVectorial("servidor", nombres)
movil = ProcesoVectorial("movil", nombres)
web = ProcesoVectorial("web", nombres)

c1 = servidor.enviar("s1", "carrito inicial")
c2 = servidor.enviar("s2", "carrito inicial")
movil.recibir("mv1", c1)
movil.evento_local("mv2", "añade queso fresco")
web.recibir("w1", c2)
web.evento_local("w2", "añade vino tinto")
servidor.recibir("s3", movil.enviar("mv3", "carrito con queso"))
servidor.recibir("s4", web.enviar("w3", "carrito con vino"))

mv2, w2 = movil.eventos["mv2"], web.eventos["w2"]
print(mv2, w2, "concurrentes:", mv2.concurrente(w2))

El resultado es {servidor:1, movil:2, web:0} frente a {servidor:2, movil:0, web:2}: concurrentes. Ninguna modificación sabía de la otra. El servidor no debe quedarse con la última que llegó (perdería el queso o el vino): debe fusionar ambas (el carrito con queso y vino) o, si la fusión no es obvia (por ejemplo, ambas cambiaron la cantidad del mismo producto), preguntar a Ana.

En la variante secuencial (el servidor integra primero el carrito de movil y después envía el carrito actualizado a web, que entonces añade el vino), la marca de w2 será algo como {servidor:3, movil:3, web:2}, que domina a mv2: mv2.sucedio_antes(w2) es True. No hay conflicto: la modificación de web ya conocía la de movil, y el servidor puede aplicarla sin más.

Solución 3:

class SeguimientoRepartidor:
    def __init__(self, nombre: str):
        self.nombre = nombre
        self.ultima_secuencia = 0
        self.posicion = None
        self.descartadas = 0

    def recibir_posicion(self, secuencia: int, lat: float, lon: float) -> None:
        if secuencia <= self.ultima_secuencia:
            self.descartadas += 1
            print(f"  descartada seq {secuencia} (ya aplicada la {self.ultima_secuencia})")
            return
        self.ultima_secuencia = secuencia
        self.posicion = (lat, lon)
        print(f"  aplicada seq {secuencia}: {self.posicion}")


seguimiento = SeguimientoRepartidor("furgoneta-3")
llegadas = [1, 2, 4, 3, 5, 7, 6, 8]
for seq in llegadas:
    seguimiento.recibir_posicion(seq, round(41.38 + seq * 0.001, 3), round(2.17 + seq * 0.001, 3))
print(f"Posición final: {seguimiento.posicion}, descartadas: {seguimiento.descartadas}")

La posición final es la de la secuencia 8 y se descartan 2 posiciones (la 3 y la 6). Se pierde el recorrido completo: si analitica quisiera reconstruir la ruta exacta, le faltarían dos puntos. La estrategia es aceptable cuando solo importa la posición actual (mostrar el repartidor en el mapa), que es el caso del seguimiento en tiempo real; si hace falta el histórico, habría que guardar todas las posiciones y ordenarlas por secuencia después, en lugar de descartar.

Conclusión

No existe un reloj global: cada nodo tiene el suyo, con deriva propia, y sincronizarlos (NTP con precisión de milisegundos, PTP de microsegundos) reduce el error pero nunca lo elimina. Por eso el orden de dos eventos ocurridos en máquinas distintas no puede decidirse comparando sus marcas de tiempo físicas, como muestra el caso de Ana y Marc comprando el último queso.

La salida de Lamport fue cambiar la pregunta: en lugar de "¿qué ocurrió antes en el tiempo?", preguntar "¿qué pudo haber causado qué?". La relación sucedió antes captura exactamente esa causalidad, los relojes de Lamport la convierten en un orden total que todos los nodos comparten (a costa de no distinguir la concurrencia), y los relojes vectoriales añaden la capacidad de detectar cuándo dos eventos son concurrentes, que es la base de la detección de conflictos en replicación. Los relojes híbridos y TrueTime reconcilian la causalidad con el tiempo real para los sistemas que necesitan ambos.

Con esto cerramos los fundamentos conceptuales del módulo. La siguiente lección, Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero, recoge todo lo visto —fallos parciales, modelos, ventajas y costes, falacias, tiempo— y lo aplica a un diseño concreto: la arquitectura objetivo que construiremos durante el resto del curso.

Curso de Arquitecturas Distribuidas

Módulo 1: Introducción a los Sistemas Distribuidos

Módulo 2: Comunicación en Sistemas Distribuidos

Módulo 3: Consistencia y Replicación

Módulo 4: Almacenamiento Distribuido

Módulo 5: Computación Distribuida

Módulo 6: Seguridad en Sistemas Distribuidos

Módulo 7: Monitoreo y Mantenimiento

Módulo 8: Casos de Estudio y Aplicaciones

© Copyright 2026. Todos los derechos reservados