Split-Brain z Posunu Hodín Dozadu: Wall Time v Lease-Based Systémoch
Najdivnejsi split-brain, aky som videl, zacal tym, ze cas isiel dozadu. “Dva nody sa stali leaderom súčasne.” Príčina: 2-sekundová NTP korekcia hodín dozadu spôsobila že oba nody verili že ich lease je stále platný. Miešanie currentTimeMillis() s duration-based timeoutmi je skrytá pasca.
Prostredie: Custom leader election používajúca database-backed leases, NTP-synchronizované nody
Problém
Split-Brain Incident
Časová os:
10:00:00 Node A získava lease (expiruje o 10:00:30)
10:00:15 Node A: "Som leader, lease platný do 10:00:30"
10:00:16 NTP posunie hodiny Node A DOZADU o 2 sekundy
10:00:14* Node A: "Hodiny hovoria 10:00:14, lease platný do 10:00:30"
"Mám ešte 16 sekúnd!" (v skutočnosti len 14)
10:00:30 Lease expiruje v databáze
10:00:31 Node B získava lease (expiruje o 10:01:01)
10:00:31 Node B: "Som leader!"
10:00:28* Node A stále si myslí že je 10:00:28
Node A: "Stále som leader! Zostávajú 2 sekundy!"
Výsledok: Oba nody si myslia že sú leader!
Zraniteľný Kód
// Bežný lease-based leader election vzor
public class LeaseManager {
private Instant leaseExpiry;
public boolean isLeader() {
// BUG: Používa wall clock čas
return Instant.now().isBefore(leaseExpiry);
}
public void acquireLease(Duration leaseDuration) {
// Ukladá expiry ako wall-clock čas
leaseExpiry = Instant.now().plus(leaseDuration);
writeToDatabase(leaseExpiry);
}
}
// Ak hodiny skočia dozadu:
// - leaseExpiry zostáva na pôvodnej wall-clock hodnote
// - Instant.now() vracia skorší čas
// - isLeader() vracia true na "extra" čas
Príčina
Wall Clock vs Monotonic Time
Wall Clock (System.currentTimeMillis(), Instant.now()):
├── Môže skočiť dopredu (NTP sync)
├── Môže skočiť dozadu (NTP korekcia)
├── Ovplyvnený DST, leap seconds
└── NEVHODNÝ pre meranie durations!
Monotonic Clock (System.nanoTime()):
├── Pohybuje sa len dopredu
├── Neovplyvnený NTP
├── Rýchlosť sa môže mierne líšiť
└── Vhodný pre meranie durations
Pasca:
┌─────────────────────────────────────────────────┐
│ Duration timeout = start_wall_time + 30 sekúnd │
│ │
│ Ak wall clock skočí dozadu o 2 sekundy: │
│ Timeout sa javí ako 32 sekúnd! │
└─────────────────────────────────────────────────┘
Ako NTP Spôsobuje Skoky Času
# NTP typicky slew-uje čas (postupná úprava)
# Ale ak je drift príliš veľký, STEP-uje (okamžitý skok)
# Skontroluj NTP status:
chronyc tracking
# System time: 0.000000002 seconds slow of NTP time
# Last offset: -0.000000814 seconds # Malý slew
# ALEBO
# Last offset: -2.345 seconds # Veľký step!
# Vynúť NTP step (nebezpečné v produkcii):
# chronyc makestep
# Bežné príčiny veľkých skokov:
# - VM suspend/resume
# - Container migrácia
# - Network partition resolving
# - Nový node joining cluster
Diagnostika
Kontrola Skokov Času
# Monitoruj skoky času skriptom
#!/bin/bash
PREV=$(date +%s.%N)
while true; do
sleep 0.1
NOW=$(date +%s.%N)
DIFF=$(echo "$NOW - $PREV - 0.1" | bc)
if (( $(echo "$DIFF > 0.5 || $DIFF < -0.5" | bc -l) )); then
echo "SKOK ČASU: $DIFF sekúnd v $(date)"
fi
PREV=$NOW
done
Kontrola NTP Logov
# chrony logy
journalctl -u chronyd | grep -E "(makestep|System clock)"
# Hľadaj:
# System clock was stepped by -2.345 seconds
Detekcia Split-Brain
-- Ak používaš database-backed leases
SELECT node_id, lease_acquired_at, lease_expires_at
FROM leader_leases
WHERE is_active = true;
-- Má vrátiť presne 1 riadok
-- Viac riadkov = split-brain!
Riešenie
Možnosť 1: Použi Monotonic Time pre Durations
public class SafeLeaseManager {
private long leaseAcquiredNanos; // Monotonic
private long leaseDurationNanos;
public boolean isLeader() {
// Používa monotonic time - nemôže byť ovplyvnený úpravami hodín
long elapsed = System.nanoTime() - leaseAcquiredNanos;
return elapsed < leaseDurationNanos;
}
public void acquireLease(Duration leaseDuration) {
leaseAcquiredNanos = System.nanoTime();
leaseDurationNanos = leaseDuration.toNanos();
// Stále zapíš wall-clock expiry pre externú viditeľnosť
writeToDatabase(Instant.now().plus(leaseDuration));
}
}
Možnosť 2: Použi Fencing Tokeny
// Fencing token: monotónne rastúce číslo
// Aj keď si dva nody myslia že sú leader,
// len ten s vyšším tokenom môže zapisovať
public class FencedLeaseManager {
private long fencingToken;
public boolean acquireLease() {
// Atomicky inkrementuj a prečítaj fencing token
Long newToken = database.incrementAndGet("lease_fencing_token");
if (newToken != null) {
this.fencingToken = newToken;
return true;
}
return false;
}
public void writeWithFence(String key, Object value) {
// Databáza odmietne zápisy s nižším fencing tokenom
database.conditionalWrite(key, value, this.fencingToken);
}
}
Možnosť 3: Server-Side Validácia Lease
// Nedôveruj client-side lease kontrolám
// Vždy validuj lease na koordinačnom bode
// Klient požaduje prácu ako leader
// Server validuje lease ZAKAŽDÝM
public class LeaseCoordinator {
public Result executeAsLeader(String nodeId, Work work) {
// Získaj aktuálny lease z databázy (zdroj pravdy)
Lease currentLease = database.getCurrentLease();
if (!currentLease.heldBy(nodeId)) {
throw new NotLeaderException();
}
if (currentLease.isExpired()) { // Server čas!
throw new LeaseExpiredException();
}
return work.execute();
}
}
Možnosť 4: Skráť Lease + Heartbeat
// Namiesto 30-sekundového lease s jednou kontrolou
// Použi 5-sekundový lease s kontinuálnym obnovovaním
public class HeartbeatLeaseManager {
private static final Duration LEASE_DURATION = Duration.ofSeconds(5);
private static final Duration HEARTBEAT_INTERVAL = Duration.ofSeconds(1);
public void maintainLeadership() {
while (shouldBeLeader) {
boolean renewed = renewLease(LEASE_DURATION);
if (!renewed) {
stepDown();
return;
}
Thread.sleep(HEARTBEAT_INTERVAL.toMillis());
}
}
// Ak hodiny skočia dozadu:
// - Lease expiruje v databáze do 5 sekúnd
// - Iný node môže získať do 5 sekúnd
// - Omnoho menšie okno pre split-brain
}
Monitoring
groups:
- name: time-sync
rules:
- alert: NTPClockStep
expr: |
abs(node_ntp_offset_seconds) > 0.5
for: 1m
labels:
severity: warning
annotations:
summary: "Veľký NTP offset na {{ $labels.instance }}"
- alert: MultipleLeaders
expr: |
count(leader_election_is_leader == 1) > 1
for: 30s
labels:
severity: critical
annotations:
summary: "Split-brain detekovaný - viacero leaderov!"
Checklist
## Clock Step Split-Brain
### Symptómy
- [ ] Dva nody nárokujú leadership súčasne
- [ ] Dátová nekonzistencia po NTP sync
- [ ] VM resume spôsobuje problémy
- [ ] Lease-based systémy sa správajú nesprávne
### Diagnostika
- [ ] Skontroluj NTP logy pre skoky času
- [ ] Monitoruj pre viacero leaderov
- [ ] Prezri lease check kód pre wall-clock použitie
- [ ] Skontroluj VM suspend/resume eventy
### Riešenia
- [ ] Použi monotonic time pre duration kontroly
- [ ] Implementuj fencing tokeny
- [ ] Server-side validácia lease
- [ ] Skráť lease duration + heartbeat
- [ ] Alertuj na NTP clock skoky
Záver
Lekcia: “Použi leases pre jednoduchosť” je nebezpečná rada bez pochopenia rozdielu medzi wall-clock a monotonic-time. 2-sekundová NTP korekcia môže premeniť starostlivo navrhnutý leader election na split-brain chaos.
Kľúčové princípy:
- Nikdy nepoužívaj wall-clock pre meranie durations
- Fencing tokeny bránia zápisom od starého leadera
- Kratšie leases = menšie split-brain okno
- Monitoruj úpravy hodín
Súvisiace články
- Distributed System Testing - Testovanie pre time-related zlyhania
- Idempotency Keys Replica Lag - Ďalšia konzistenčná pasca
Súvisiace články
Certifikat nie je expirnuty, vas node ano: Time Drift rozbitie TLS a JWT v Kubernetes
Sporadicke TLS handshake zlyhania a JWT zamietnutia napriec sluzbami. Vsetko prejde ked to skontrolujete. Vinik: hodiny nodu sa posunuli alebo skocili, a NTP to opravilo skor nez ste to stihli zachytit.
Dvojité Účtovanie z Idempotency Keys: Pasca Replica Lag
Perfektná idempotency logika, ale zákazníci sú stále účtovaní dvakrát. Príčina: kontrola idempotency keys voči read replice ktorá je sekundy za primary počas špičiek.
Gossip Protocol Ghost Nodes: IP Reuse Strašiaci Váš Cluster
Nový node sa pripája ku clusteru ale je odmietaný. IP starého nodu je stále v blackliste failure detection gossip protokolu. Zombie membership záznam žije ďalej.
Elasticsearch Hot Shard Problém: Keď Jeden Node Robí Všetku Prácu
5 data nodov ale jeden je na 100% CPU. Nerovnomerné routing kľúče vytvárajú hot shardy. Ukážem ako detekovať skew a opraviť ho pomocou routing stratégií.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.