Grundlagen

Verteilte Systeme

Sobald zwei Rechner über ein Netz zusammenarbeiten, wird Selbstverständliches schwer: Wurde die Überweisung ausgeführt oder nicht? Wer hat das Sagen, wenn der Chef ausfällt? Und warum macht ein gut gemeinter Neuversuch den Ausfall manchmal schlimmer?

Lesestand

Ein verteiltes System besteht aus mehreren Rechnern, die über ein Netz zusammenarbeiten – heute also fast jede größere Anwendung. Das Grundproblem: Nachrichten können verloren gehen, sich verspäten oder doppelt ankommen, und ein Rechner kann ausfallen, während die anderen weiterlaufen. Ein Absender, der keine Antwort bekommt, weiß nicht, ob seine Anfrage ankam.

Daraus folgen die Grundregeln: Jeder Aufruf braucht ein Timeout. Neuversuche sind nur sicher, wenn Operationen idempotent sind. „Genau einmal zustellen“ gibt es nicht – nur „mindestens einmal“ plus Erkennen von Doppelten. Und wenn mehrere Rechner sich einigen müssen, wer das Sagen hat, braucht man ein Konsensverfahren wie Raft, das mit einer Mehrheit arbeitet.

Acht falsche Annahmen

Schon in den 1990er-Jahren schrieben Ingenieure bei Sun Microsystems die Annahmen auf, die Entwickler verteilter Systeme stillschweigend treffen – und die alle falsch sind:

Die AnnahmeDie Wirklichkeit
Das Netz ist zuverlässig.Pakete gehen verloren, Verbindungen brechen ab, Router fallen aus.
Die Latenz ist null.Jeder Aufruf kostet Millisekunden – über Kontinente hinweg Hunderte.
Die Bandbreite ist unbegrenzt.Große Antworten und viele kleine Aufrufe summieren sich.
Das Netz ist sicher.Alles, was übers Netz geht, muss verschlüsselt und geprüft werden.
Die Topologie ändert sich nicht.Container kommen und gehen, IP-Adressen wechseln.
Es gibt einen Administrator.Viele Teams, viele Zuständigkeiten, niemand sieht alles.
Transport kostet nichts.Serialisierung, Verschlüsselung und Datenverkehr kosten Rechenzeit und Geld.
Das Netz ist einheitlich.Unterschiedliche Hardware, Protokolle und Versionen.

Dazu kommt das Kernproblem: Teilausfälle. Auf einem einzelnen Rechner läuft ein Programm oder es stürzt ab. In einem verteilten System kann ein Teil ausfallen, während der Rest weitermacht – und von außen sieht ein ausgefallener Rechner genauso aus wie ein langsamer oder ein schlecht erreichbarer.

Wurde es ausgeführt oder nicht?

Ein Bestelldienst schickt eine Überweisung an den Zahlungsdienst. Die Antwort bleibt aus. Was tun?

Überweisung von 100 €
Was im Netz passiert
Was der Absender bei Timeout tut

Aus Sicht des Absenders sehen „Anfrage verloren“ und „Antwort verloren“ identisch aus: Es kam nichts zurück. Deshalb gibt es nur zwei Zustellgarantien, die man tatsächlich bauen kann:

≤1

Höchstens einmal

Nie wiederholen. Nichts wird doppelt ausgeführt, aber manches gar nicht. Gut für Dinge, die man verlieren darf – Messwerte, Statistiken.

≥1

Mindestens einmal

Wiederholen, bis eine Bestätigung kommt. Nichts geht verloren, aber manches kommt doppelt an. Der Standard bei Nachrichtenvermittlern.

=1

Effektiv einmal

Mindestens einmal zustellen, beim Empfänger Doppelte erkennen und ignorieren. Das ist alles, was „exactly once“ in der Praxis je bedeutet.

Eine Operation ist idempotent, wenn mehrfaches Ausführen dasselbe bewirkt wie einmaliges. „Setze den Status auf versandt“ ist idempotent, „Buche 100 € ab“ nicht. Nicht-idempotente Operationen macht man idempotent, indem der Absender jeder Anfrage eine eindeutige Kennung mitgibt und der Empfänger sich merkt, welche er schon ausgeführt hat.

Konsens: Wer hat das Sagen?

Viele Systeme brauchen genau einen Rechner, der Entscheidungen trifft – einen Anführer (Leader). Er ordnet alle Schreibvorgänge, damit keine Widersprüche entstehen. Fällt er aus, muss ein neuer gewählt werden, und zwar so, dass es nie zwei gleichzeitig gibt. Das leistet ein Konsensverfahren. Das bekannteste ist Raft (2014), entworfen ausdrücklich, um verständlicher zu sein als sein Vorgänger Paxos. Raft steckt in etcd, dem Gedächtnis von Kubernetes, und in vielen Datenbanken. Die Kernidee: Alles braucht eine Mehrheit.

Fünf Rechner, ein Anführer

Stark vereinfacht. Volle Blöcke: bestätigte Einträge (von einer Mehrheit gespeichert). Schraffiert: noch unbestätigt. In echtem Raft laufen Wahlen automatisch nach zufälligen Wartezeiten ab.

Weil jede Entscheidung eine Mehrheit braucht und es nur eine Mehrheit geben kann, kann es nie zwei Anführer geben, die beide etwas durchsetzen – auch nicht, wenn das Netz sich teilt. Die Minderheit bleibt einfach stehen; das ist die Entscheidung für Konsistenz im CAP-Theorem. Daraus folgt eine wichtige Faustregel: Ein Cluster aus drei Rechnern verkraftet einen Ausfall, einer aus fünf zwei. Ein vierter Rechner bringt nichts – für eine Mehrheit von vier braucht man drei, er verkraftet also ebenfalls nur einen Ausfall.

Timeouts, Neuversuche und der Ansturm danach

Ohne Timeout wartet ein Aufruf im Zweifel ewig – und mit ihm ein Thread, eine Verbindung, ein Nutzer. Also: immer ein Timeout. Nach einem Timeout liegt ein Neuversuch nahe. Aber Vorsicht: Wenn ein Dienst wegen Überlastung ausfällt und alle Aufrufer im selben Takt neu versuchen, trifft ihn bei der Rückkehr eine Welle, die ihn gleich wieder umwirft.

Ein Dienst kommt zurück

200 Aufrufer, deren erste Anfrage bei Sekunde 0 scheitert. Der Dienst ist 5 Sekunden lang aus und schafft danach 40 Anfragen pro halbe Sekunde. Kommen mehr, schafft er sogar weniger – Überlast kostet ihn zusätzlich Kraft. Was scheitert, wird erneut versucht.

Dazu gehören zwei weitere Muster: Ein Circuit Breaker (Sicherung) zählt Fehler und hört nach zu vielen ganz auf, einen kranken Dienst anzurufen – er antwortet sofort mit einem Fehler oder einem Ersatzwert und probiert erst nach einer Weile wieder vorsichtig. Und ein Bulkhead (Schott) begrenzt, wie viele Threads oder Verbindungen ein einzelner Abhängiger belegen darf, damit ein langsamer Dienst nicht die ganze Anwendung blockiert.

Uhren lügen

Jeder Rechner hat seine eigene Uhr, und keine geht genau. Über NTP werden sie abgeglichen, aber Abweichungen von Millisekunden sind normal, manchmal mehr – und eine Uhr kann bei einer Korrektur sogar rückwärts springen. „Was zuerst passierte“ lässt sich deshalb über Rechnergrenzen hinweg nicht an Zeitstempeln ablesen. Verteilte Systeme benutzen stattdessen logische Uhren: Zähler, die mit jeder Nachricht weitergegeben werden und eine Reihenfolge garantieren, die zur Ursache-Wirkung-Beziehung passt (Lamport-Uhren, Versionsvektoren). Googles Datenbank Spanner geht einen anderen Weg: Atomuhren und GPS in jedem Rechenzentrum, und jede Zeitangabe hat ein ausdrückliches Unsicherheitsintervall.

Für Entwickler übersetzt

// Idempotenter Endpunkt: gleiche Kennung, gleiche Antwort – keine zweite Ausführung
@PostMapping("/ueberweisungen")
Ergebnis ueberweise(@RequestHeader("Idempotency-Key") UUID key, @RequestBody Auftrag a) {
    return ergebnisse.findById(key)                 // schon verarbeitet?
            .orElseGet(() -> service.fuehreAus(key, a)); // Buchung + Ergebnis mit Key in EINER Transaktion,
}                                                     // Key mit UNIQUE-Constraint gegen Gleichzeitiges

// Resilience4j: Neuversuch mit exponentiellem Backoff und Zufall, plus Sicherung
Retry retry = Retry.of("zahlung", RetryConfig.custom()
        .maxAttempts(4)
        .intervalFunction(IntervalFunction.ofExponentialRandomBackoff(Duration.ofMillis(200), 2.0))
        .build());
CircuitBreaker cb = CircuitBreaker.ofDefaults("zahlung");

Supplier<Ergebnis> aufruf = Decorators.ofSupplier(() -> zahlung.belaste(auftrag))
        .withCircuitBreaker(cb).withRetry(retry).decorate();
MusterWofür
Transactional OutboxDatenbank ändern und ein Ereignis senden – zuverlässig: Das Ereignis wird in derselben Transaktion in eine Tabelle geschrieben und von dort verschickt. Sonst gibt es entweder Ereignisse ohne Änderung oder Änderungen ohne Ereignis.
SagaEin Ablauf über mehrere Dienste, bei dem jeder Schritt eine Ausgleichsaktion hat – statt einer verteilten Transaktion, die alle Dienste sperrt.
Timeouts überallHTTP-Client, Datenbank, Nachrichtenvermittler – jeder Aufruf über das Netz. Und das Timeout des Aufrufers muss größer sein als die Summe dessen, was dahinter passiert.
ZeitSystem.currentTimeMillis() verschiedener Rechner nicht vergleichen, um eine Reihenfolge festzustellen. Für Dauern auf einem Rechner System.nanoTime() – das springt nicht.

Häufige Irrtümer

„Unser Nachrichtensystem garantiert exactly once.“ – Innerhalb eines Systems, mit Transaktionen und Deduplizierung, lässt sich das herstellen. Über Systemgrenzen hinweg – bis in die Datenbank des Empfängers oder zu einer externen API – muss der Empfänger Doppelte selbst erkennen.
„Timeout heißt: Es ist nicht passiert.“ – Timeout heißt: Man weiß es nicht. Die Anfrage kann ausgeführt worden sein, nur die Antwort fehlt.
„Mehr Rechner, mehr Ausfallsicherheit.“ – Bei Mehrheitsverfahren zählen ungerade Zahlen. Vier Rechner verkraften nicht mehr Ausfälle als drei, haben aber mehr, was ausfallen kann.
„Ein Neuversuch kann nicht schaden.“ – Ohne Idempotenz führt er zu Doppelbuchungen, ohne Backoff zu Lastwellen. Und wiederholt jede Schicht einer Aufrufkette dreimal, werden aus einem Fehler ganz unten 27 Aufrufe.
„Die Uhren sind ja per NTP synchron.“ – Auf Millisekunden ungefähr, manchmal schlechter, und sie können springen. Für die Reihenfolge von Ereignissen auf verschiedenen Rechnern taugen sie nicht.

Verwandte Begriffe