⏱️
Tempo distribuído: NTP, clock skew, monotonic vs wall
⏱ 12 min de leitura·+60 XP
Tempo parece simples — até você perceber que dois servidores no mesmo datacenter podem discordar em 100ms, que um leap second pode "voltar no tempo" e quebrar logs, e que a ordem de eventos em sistemas distribuídos não pode depender de relógio físico.
Wall clock vs monotonic clock
import time
# Wall clock (CLOCK_REALTIME): hora real do mundo
# Pode ser ajustado a qualquer momento: NTP, settime, leap second
inicio = time.time()
time.sleep(0.1)
fim = time.time()
print(f"Wall: {fim - inicio:.4f}s") # correto normalmente, mas pode ser negativo!
# Monotonic clock (CLOCK_MONOTONIC): contador que só avança
# Garantia: nunca recua, mesmo com ajuste de NTP
inicio = time.monotonic()
time.sleep(0.1)
fim = time.monotonic()
print(f"Monotonic: {fim - inicio:.4f}s") # sempre >= 0
# Performance counter: máxima resolução disponível no OS
inicio = time.perf_counter()
# ... código a medir ...
fim = time.perf_counter()
print(f"Perf counter: {(fim - inicio) * 1000:.3f}ms")
# Regra de ouro:
# time.time() → timestamps absolutos (logs, DB, expiração)
# time.monotonic() → medir duração, timeouts, intervalos
# time.perf_counter() → benchmarks de alta precisão
# Exemplo do problema com wall clock:
# Servidor faz NTP slew enquanto você mede:
# inicio = time.time() → 1713430000.000
# [NTP ajusta -200ms]
# fim = time.time() → 1713429999.900
# duração = fim - inicio = -0.100s ← duração NEGATIVA!
# time.monotonic() nunca teria esse problema.NTP: como relógios de rede se sincronizam
# NTP (Network Time Protocol) — RFC 5905
# Hierarquia de strata:
# Stratum 0: GPS, relógio atômico, GPS disciplined oscillator
# Stratum 1: servidores diretamente conectados ao stratum 0
# Stratum 2: servidores sincronizados com stratum 1 via NTP
# Stratum 3+: cascata (cada hop adiciona incerteza)
# Algoritmo de medição de offset:
# Cliente envia com timestamp T1
# Servidor recebe em T2, responde em T3
# Cliente recebe em T4
#
# RTT = (T4 - T1) - (T3 - T2) # round-trip time real
# offset = ((T2 - T1) + (T3 - T4)) / 2 # ajuste necessário
# NTP ajusta o relógio de duas formas:
# 1. Slew: ajuste gradual (≤500ppm) — menos disruptivo
# 2. Step: ajuste instantâneo (só se offset > 128ms por padrão)
# Verificar sincronização NTP no Linux:
# timedatectl show-timesync # Linux systemd
# chronyc tracking # chrony (mais preciso que ntpd)
# ntpq -p # ntpd classic
# Saída típica de chronyc tracking:
# Reference ID : A29F2303 (162.159.200.123 — Cloudflare NTP)
# Stratum : 3
# System time : 0.000042153 seconds fast of NTP time
# Last offset : +0.000038471 seconds
# RMS offset : 0.000019234 seconds
# Frequency : 12.345 ppm fast
# Residual freq : -0.002 ppm
# Skew : 0.143 ppm
# Root delay : 0.009823456 seconds
# Root dispersion : 0.001234567 seconds
import subprocess
def get_ntp_offset_ms():
"""Lê offset NTP atual via chronyc (Linux)."""
try:
result = subprocess.run(
["chronyc", "tracking"],
capture_output=True, text=True, timeout=5
)
for line in result.stdout.splitlines():
if "System time" in line:
# "System time : 0.000042153 seconds fast of NTP time"
parts = line.split(":")
offset_str = parts[1].strip().split()[0]
return float(offset_str) * 1000 # ms
except Exception:
return NoneClock skew em sistemas distribuídos
| Método de sincronização | Precisão típica | Uso |
|---|---|---|
| NTP público (pool.ntp.org) | ±10–100ms | Servidores comuns |
| NTP local (datacenter) | ±1–10ms | Infraestrutura corporativa |
| PTP (IEEE 1588) | ±1µs–1ms | Redes financeiras, telecom |
| GPS disciplined oscillator | ±1–100ns | Stratum 0, Google Spanner |
| AWS Time Sync Service | ±1ms | EC2 via hypervisor |
| Google TrueTime API | ±7ms (bounded) | Spanner — retorna intervalo |
# O problema de clock skew para ordenação de eventos:
# Servidor A: timestamp 10:00:00.050 → operação A
# Servidor B: timestamp 10:00:00.040 → operação B
# Clock skew de A: +50ms acima do real
# Clock skew de B: -10ms abaixo do real
# Timestamps dizem: A aconteceu depois de B
# Realidade: B aconteceu depois de A (skew de 60ms total)
# Implicações reais:
# - Logs de audit podem ter ordem incorreta
# - Replicação de banco pode aplicar updates na ordem errada
# - Rate limiting baseado em timestamp pode ser bypassado
# - Certificates com not-before/not-after dependem de clock correto
import datetime
import time
def timestamp_com_monotonic():
"""
Combina timestamp absoluto (wall) com monotonic para detectar regressão.
Se wall clock regredir, usa monotonic para estimar o tempo correto.
"""
wall = time.time()
mono = time.monotonic()
return {"wall": wall, "mono": mono, "iso": datetime.datetime.utcfromtimestamp(wall).isoformat()}
# Para sistemas que precisam de ordem global sem GPS:
# → Lamport timestamps (lógico, sem clock físico)
# → Vector clocks (detecta concorrência real)
# → HLC — Hybrid Logical Clock (CockroachDB)Lamport timestamps e Vector Clocks
# Lamport Timestamps: relógio lógico para causalidade
# Regra: send incrementa, receive toma max(local, received) + 1
class LamportClock:
def __init__(self, node_id: str):
self.node_id = node_id
self.counter = 0
def tick(self) -> int:
"""Evento local: incrementa."""
self.counter += 1
return self.counter
def send(self) -> int:
"""Ao enviar mensagem: incrementa e retorna timestamp."""
self.counter += 1
return self.counter
def receive(self, received_ts: int) -> int:
"""Ao receber mensagem: max + 1."""
self.counter = max(self.counter, received_ts) + 1
return self.counter
# Simulação:
node_a = LamportClock("A")
node_b = LamportClock("B")
ts_a1 = node_a.tick() # A: evento local → L(a1) = 1
ts_send = node_a.send() # A: envia para B → L(send) = 2
ts_b1 = node_b.receive(ts_send) # B: recebe de A → L(b1) = 3
ts_b2 = node_b.tick() # B: evento local → L(b2) = 4
print(f"A1={ts_a1}, A→B={ts_send}, B1={ts_b1}, B2={ts_b2}")
# A1=1, A→B=2, B1=3, B2=4
# Garantia: L(a_send) < L(b_receive) — a→b é provada causalmente
# Limitação: L(x) < L(y) NÃO prova x→y (pode ser concorrente)
# Vector Clocks: detecta concorrência real
# Cada nó mantém vetor de contadores por nó conhecido
class VectorClock:
def __init__(self, node_id: str, nodes: list):
self.node_id = node_id
self.clock = {n: 0 for n in nodes}
def tick(self):
self.clock[self.node_id] += 1
return dict(self.clock)
def send(self):
self.clock[self.node_id] += 1
return dict(self.clock)
def receive(self, received: dict):
for node, ts in received.items():
self.clock[node] = max(self.clock.get(node, 0), ts)
self.clock[self.node_id] += 1
def happened_before(self, vc_a: dict, vc_b: dict) -> bool:
"""vc_a → vc_b se todos os componentes de A ≤ B e pelo menos um <."""
all_le = all(vc_a.get(n, 0) <= vc_b.get(n, 0) for n in vc_b)
some_lt = any(vc_a.get(n, 0) < vc_b.get(n, 0) for n in vc_b)
return all_le and some_ltGoogle TrueTime e Spanner
# Google TrueTime API (Spanner):
# Em vez de retornar UM timestamp, retorna um intervalo [earliest, latest]
# Garante que a hora real está dentro do intervalo
# Intervalo típico: ±7ms (GPS + atomic clocks em cada datacenter)
# Pseudocódigo do conceito TrueTime:
class TrueTime:
def now(self):
"""Retorna (earliest, latest) com garantia que hora real está no intervalo."""
uncertainty_ms = 7 # garantia Google TrueTime via GPS
wall_ms = time.time() * 1000
return (wall_ms - uncertainty_ms, wall_ms + uncertainty_ms)
def after(self, t: float) -> bool:
"""Garante que agora é definitivamente DEPOIS de t."""
earliest, _ = self.now()
return earliest > t # só retorna True se até o pior caso seja > t
def before(self, t: float) -> bool:
"""Garante que agora é definitivamente ANTES de t."""
_, latest = self.now()
return latest < t
# Commit wait em Spanner:
# 1. Líder escolhe timestamp s = TT.now().latest
# 2. Aguarda até TT.after(s) → "commit wait"
# 3. Só então confirma — garante que NENHUM servidor tem s no futuro
# Resultado: external consistency sem coordenação global
# CockroachDB usa HLC (Hybrid Logical Clock):
# HLC = (wall_time, logical_counter)
# - wall_time: do relógio físico (NTP)
# - logical_counter: contador que avança quando wall_time empata
# Propriedade: HLC.now() ≥ max(todos os HLC vistos) E ≈ wall_time
# Mais robusto que Lamport (legível) sem precisar de GPS
import struct
def hlc_encode(wall_ms: int, logical: int) -> bytes:
"""Hybrid Logical Clock — 8 bytes: 48 bits wall + 16 bits logical."""
# wall em ms cabe em 48 bits até ano 2^48ms ≈ ano 10889
packed = (wall_ms << 16) | (logical & 0xFFFF)
return struct.pack(">Q", packed)
def hlc_decode(b: bytes):
packed = struct.unpack(">Q", b)[0]
wall_ms = packed >> 16
logical = packed & 0xFFFF
return wall_ms, logical✅
Regras práticas: sempre use
time.monotonic() para medir duração e timeouts — nunca time.time(). Use UTC para todos os timestamps armazenados. NTP típico tem ±10–100ms de skew — nunca assuma ordem de eventos em sistemas distribuídos baseado só em timestamp. Para ordenação causal sem GPS: Lamport timestamps (simples) ou HLC (CockroachDB). Para garantia forte de ordem global: TrueTime + commit wait (Spanner/Cloud Spanner).💡
Próximo: CPU: pipeline, cache e branch prediction — o que acontece dentro do processador em cada instrução.
🧩
Quiz rápido
3 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito
Continue lendo