Späť na blog

Split-Brain z Posunu Hodín Dozadu: Wall Time v Lease-Based Systémoch

|
| distributed-systems, debugging, time, leader-election, ntp

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:

  1. Nikdy nepoužívaj wall-clock pre meranie durations
  2. Fencing tokeny bránia zápisom od starého leadera
  3. Kratšie leases = menšie split-brain okno
  4. Monitoruj úpravy hodín

Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Split-Brain z Posunu Hodín Dozadu: Wall Time v Lease-Based Systémoch". https://www.michal-drozd.com/sk/blog/clock-step-backwards-split-brain/ (Publikované 22. januára 2025).