Die Architektur basiert auf einem deterministischen Transaktionssequenzer, der Operationen mithilfe von Hybrid Logical Clocks anstelle von physischer Zeit anordnet, wodurch Abhängigkeiten von Uhrenabweichungen vermieden werden, die in TrueTime-basierten Systemen auftreten. Der Koordinator implementiert eine Variante des Calvin-Protokolls, bei dem Transaktionsabsichten an eine Mehrheit der Shard-Leiter vor der Ausführung repliziert werden, um Serialisierbarkeit durch deterministische Planung anstelle von verteiltem Sperren zu gewährleisten. TLA+-Spezifikationen modellieren die Zustandsübergänge des Koordinators und verifizieren formal, dass das System Sicherheit (strikte Serialisierbarkeit) und Lebendigkeit (alle bestätigten Transaktionen werden schließlich abgeschlossen) auch während partieller Netzwerkpartitionen aufrechterhält.
Der Koordinator speichert Transaktionsprotokolle in einem WAL (Write-Ahead Log) unter Verwendung des Paxos-Konsens für Haltbarkeit über Verfügbarkeitszonen hinweg. Shard-Proxys abstrahieren die zugrunde liegenden Speichermaschinen—ob PostgreSQL, MongoDB oder Cassandra—und bieten eine einheitliche Schnittstelle für die Ausführungsmaschine. Die Konflikterkennung verwendet einen Abhängigkeitsgraphen, der während der Sequenzierungsphase erstellt wird, um gleichzeitige Ausführungen von nicht-konfligierenden Transaktionen zu ermöglichen und die Äquivalenz zu einer seriellen Reihenfolge aufrechtzuerhalten.
Eine globale Investmentbank benötigte die Migration ihres Trade-Abwicklungssystems von einer monolithischen Oracle-Datenbank zu einer sharded Architektur, die sich über AWS- und Azure-Regionen erstreckt. Die entscheidende Herausforderung bestand darin, die atomare Abwicklung von Trades zu gewährleisten, die mehrere Vermögensklassen betrafen, die in verschiedenen Datenbanktechnologien gespeichert waren—Aktien in PostgreSQL und Derivate in ScyllaDB—ohne atomare Uhren oder GPS-Zeitquellen zur Synchronisierung einzusetzen.
Eine vorgeschlagene Lösung verwendete Standard-XA-Transaktionen mit einem Zwei-Phasen-Commit (2PC) Protokoll, das von einem Narayana-Transaktionsmanager verwaltet wurde. Dieser Ansatz bot starke Konsistenz und umfassende Unterstützung des Ökosystems, führte jedoch zu blockierendem Verhalten, wenn ein Koordinatorausfall während der Vorbereitungsphase auftrat, was dazu führte, dass Shards Sperren unbestimmt hielten und die Anforderungen an die Lebendigkeit während der Netzwerkinstabilität zwischen den Cloud-Diensten verletzten.
Eine weitere Alternative betrachtete das Saga-Muster, das über das Axon Framework implementiert wurde und kompensierende Transaktionen für Rollbackszenarien nutzte. Während dies hohe Verfügbarkeit bot und verteilte Sperrungen vermied, opferte es die strikte Serialisierbarkeit—unacceptable für die finanzielle Abwicklung, wo Zwischenzustände niemals sichtbar sein dürfen, und die Entschädigungslogik für irreversible externe Marktoperationen stellte sich als unüberwindbar komplex heraus.
Die ausgewählte Architektur implementierte einen deterministischen Koordinator, inspiriert von Calvin, mit formaler Überprüfung durch TLA+. Das System sequenzierte alle Abwicklungstransaktionen durch eine replizierte Zustandsmaschine, die Raft für das Koordinationsprotokoll verwendete, und führte sie dann in derselben Reihenfolge auf allen Shards unter Verwendung idempotenter gespeicherter Prozeduren aus. Dies beseitigte die Notwendigkeit für verteilte Sperrungen während der Ausführung und erlaubte der TLA+-Modellprüfung, mathematisch nachzuweisen, dass das System nicht feststecken oder Abwicklungen während willkürlicher Netzwerkpartitionen verlieren konnte.
Die Bereitstellung führte zu einer Reduzierung der Abwicklungslatenz um 40 % im Vergleich zum Legacy-Oracle-System, während die vollständigen ACID-Garantien über die Clouds hinweg aufrechterhalten wurden. Während eines späteren regionalen AWS-Ausfalls setzte das System die Verarbeitung von Trades ohne manuelles Eingreifen fort und validierte damit die formell nachgewiesenen Lebendigkeitseigenschaften.
Was ist der grundlegende Unterschied zwischen strenger Serialisierbarkeit und Linearität, und warum zielt ein verteilter Transaktionskoordinator typischerweise auf ersteres ab, anstatt auf letzteres?
Strikte Serialisierbarkeit kombiniert Serialisierbarkeit (Transaktionen erscheinen, als würden sie in einer sequentiellen Reihenfolge ausgeführt) mit Linearitäts' realen zeitlichen Einschränkungen (Transaktionen werden abgeschlossen, bevor nachfolgende beginnen). Während Linearität auf Operationen mit einem einzelnen Objekt anwendbar ist, erweitert die strikte Serialisierbarkeit dies auf Transaktionen mit mehreren Objekten. Kandidaten verwechseln dies oft und entwerfen Systeme, die eine lineare Lesbarkeit für einen einzelnen Schlüssel gewährleisten, aber Anomalien wie Schreibabweichungen über mehrere Schlüssel hinweg nicht verhindern können. Ein Koordinator erreicht strikte Serialisierbarkeit, indem er eine globale Reihenfolge der Transaktionen festlegt—häufig durch eine Sequenzierungsschicht oder einen Zeitstempel-Orakel—während Linearität allein shard-spezifisch ohne plattformübergreifende Reihenfolgen-Garantien erfüllt werden kann.
Warum blockiert das Zwei-Phasen-Commit (2PC)-Protokoll unbestimmt während eines Koordinatorausfalls, und wie versagt das Drei-Phasen-Commit (3PC) darin, dies unter Netzwerkpartitionen zu lösen?
Im 2PC, sobald ein Teilnehmer während der Vorbereitungsphase "ja" stimmt, hält er Sperren, bis er die globale Commit-/Abbruchentscheidung vom Koordinator erhält. Wenn der Koordinator nach dem Empfang aller Stimmen, aber bevor die Entscheidung bekannt gegeben wird, ausfällt, bleiben die Teilnehmer unsicher und gesperrt, was die Verfügbarkeit verletzt. 3PC versucht, dies zu lösen, indem es eine Vor-Commit-Phase und zeitüberschreitungsbasierten Fortschritt hinzufügt, aber unter Netzwerkpartitionen kann ein Teilnehmer nicht zwischen einem gescheiterten Koordinator und einem partitionierten unterscheiden. Dies führt zu Split-Brain-Szenarien, in denen unterschiedliche Partitionen widersprüchliche Entscheidungen treffen, was die Konsistenz verletzt. Das grundlegende Problem ist, dass die FLP-Unmöglichkeit beweist, dass deterministischer Konsens in asynchronen Systemen mit auch nur einem fehlerhaften Prozess unmöglich ist, was bedeutet, dass jedes Commit-Protokoll zwischen Blockierung (Sicherheit) und potenzieller Inkonsistenz (Lebendigkeit) während bestimmter Fehlerarten wählen muss.
Wie verifiziert TLA+ Lebendigkeits-Eigenschaften in einem Transaktionskoordinator, und welche spezifischen temporalen Logikoperatoren drücken "letztendlich abgeschlossen" im Vergleich zu "niemals Daten verlieren" aus?
TLA+ verwendet temporale Logik, um anzugeben, dass gute Dinge schließlich geschehen (Lebendigkeit), während schlechte Dinge niemals geschehen (Sicherheit). Die Lebendigkeits-Eigenschaft, dass alle initiierten Transaktionen schließlich abgeschlossen werden, wird mit dem letztendlich-Operator (◇) ausgedrückt, typischerweise geschrieben als Initiated(t) ~> Committed(t) (führt-zu), was bedeutet, dass, wenn Transaktion t initiiert, sie schließlich ausgeführt oder abgebrochen wird. Sicherheitseigenschaften wie "niemals Daten verlieren" verwenden den immer-Operator (□), geschrieben als □(Committed(t) ⇒ ◇(Query(t) = Value)), was bedeutet, dass einmal abgeschlossen, der Wert immer schließlich lesbar ist. Kandidaten übersehen oft, dass die Überprüfung der Lebendigkeit Fairness-Annahmen erfordert—schwache Fairness (WF_vars(Action)) stellt sicher, dass, wenn eine Aktion aktiviert bleibt, sie schließlich eintreten muss, um unendliche Stottern zu verhindern, bei dem der Koordinator einfach aufhört, Schritte zu unternehmen. Ohne diese Fairness-Constraints würden TLA+-Modelle trivially Lebendigkeitseigenschaften erfüllen, indem sie nichts tun.