Blog ·
Zwei Bestellungen, eine Maschine
Ein dedizierter Server lässt sich genau einmal verkaufen. Den ganzen Pool zu sperren kostete still eine Reservierung, und half nur eine Sperre je Maschine.
Von Provibr
Ein VPS-Tarif verkauft absichtlich mehr, als da ist, und niemand liegt deswegen wach. Eine dedizierte Maschine kann das nicht: es gibt sie einmal, und glauben zwei Bestellungen beide, sie bekommen zu haben, dann hört ein Kunde am Montagmorgen, dass das am Freitag Bezahlte jemand anderem gehört.
Die Anforderung ist einen Satz lang. Reservieren vor Nebenwirkungen, und Kapazität nach einem Absturz oder einem Fehlschlag transaktional wieder freigeben. Beim Bauen entstanden zwei Messungen, die den Entwurf geändert haben, und eine davon kostete uns eine Maschine, die einfach frei dastand.
Der Schiedsrichter ist ein Index
Die Regel, dass zwei Bestellungen niemals dieselbe Maschine halten können, steht in einem teilweisen eindeutigen Index auf der Gerätekennung, beschränkt auf Reservierungen im Zustand gehalten. Nicht in der Anwendung, nicht in einer Sperre, die der Code nimmt, nicht in einem Kommentar.
Das ist bewusst dieselbe Form wie der Belegungsindex auf Höheneinheiten. Die Anwendungsschicht liefert eine lesbare Ablehnung vor dem Zusammenstoß, und die Datenbank entscheidet, wer tatsächlich gewinnt. Nur eine der beiden kann maßgeblich sein, und es muss die sein, die kein Codepfad umgehen kann, den jemand nächstes Jahr hinzufügt.
Gehalten ist der einzige Zustand, der etwas hält, und das steht als Tabelle neben dem Index und nicht als boolesche Spalte, denn ein Wahrheitswert neben dem Zustand ist eine zweite Wahrheit, die nur stimmt, solange jeder Schreiber daran denkt. Ein Test prüft die Tabelle gegen den Index.
Es gibt vier Zustände, und zwei davon sind mit Absicht ein Paar: gehalten, umgewandelt, abgelaufen, abgebrochen. Die verlockende Vereinfachung ist ein einziges freigegeben für die letzten beiden, und sie machte die Frage „hat jemand das abgeblasen, oder ist es liegen geblieben“ unbeantwortbar, genau die Frage, die man bekommt, wenn ein Kunde wegen einer nicht zustande gekommenen Bestellung anruft. Ein Aufräumlauf, der still denselben Zustand schreibt wie ein Mensch, löscht den Unterschied zwischen einer Entscheidung und einer Zeitüberschreitung.
Den ganzen Pool zu sperren kostete eine Reservierung
Die erste Fassung wählte die Kandidatenmaschinen aus und sperrte sie in einer Abfrage mit FOR UPDATE SKIP LOCKED, wobei bereits Gesperrtes übersprungen wurde. Das liest sich gut und ist falsch, und nur ein Nebenläufigkeitstest hat es gezeigt.
Acht gleichzeitige Bestellungen auf drei geeignete Maschinen sollten drei Reservierungen ergeben. Es wurden zwei. Die erste Transaktion griff alle drei Zeilen, benutzte eine und hielt die anderen beiden bis zu ihrem Abschluss; alle übrigen sprangen an allen dreien vorbei und fanden nichts. Der Pool war nicht erschöpft, sondern in Geiselhaft.
Die Lösung ist eine Sperre je Kandidat: nimm den ersten, und hat ihn jemand anders, geh zum nächsten. Derselbe Test noch einmal: drei Reservierungen auf drei verschiedenen Maschinen und fünf ehrliche Ablehnungen. Zwei Bestellungen auf eine einzige Maschine ergeben eine Reservierung und eine Ablehnung.
Diese Lösung bringt ihr eigenes Risiko mit, nämlich die Länge der Schleife. Bei hundert gleichzeitigen Bestellungen liefe die letzte Transaktion hundert Kandidaten ab, bevor sie gewinnt, und eine lange Transaktion hält alle anderen auf. Deshalb endet die Suche nach fünfundzwanzig Kandidaten, und darüber ist „versuchen Sie es gleich noch einmal“ die ehrliche Antwort.
Beachten Sie, dass Überspringen und Scheitern getrennt gezählt werden. Ein Kandidat, den jemand anders hielt, wird übersprungen, und das ist kein Versuch, denn es ist nichts schiefgegangen. Ein Kandidat, der ein Einfügerennen verloren hat, ist ein Versuch, und davon gibt es höchstens fünf.
Eine Eindeutigkeitsverletzung vergiftet die Transaktion
Das ist die Falle, um die herum die Wiederholschleife gebaut ist, und es ist leicht, Code zu schreiben, der korrekt aussieht und beim ersten echten Zusammenstoß umfällt.
Innerhalb einer PostgreSQL-Transaktion bricht eine Verletzung eines eindeutigen Schlüssels die Transaktion ab. Jede folgende Anweisung wird dann mit einem anderen Fehler abgelehnt, „die laufende Transaktion ist abgebrochen“, bis jemand zurückrollt. Eine Schleife, die die Verletzung fängt und den nächsten Kandidaten versucht, funktioniert also überhaupt nicht: das Einfügen des zweiten Kandidaten scheitert aus einem Grund, der nichts mit dem zweiten Kandidaten zu tun hat.
Jeder Versuch läuft daher innerhalb eines Sicherungspunkts, was in unserem Abfragebauer schlicht eine geschachtelte Transaktion ist. Auf den Sicherungspunkt zurückrollen, die äußere Transaktion ist wieder gesund, nächster Kandidat.
Dazu gehört ein Detail darüber, wie die Verletzung gefangen wird. Unser Abfragebauer verpackt Treiberfehler in einen eigenen Fehlertyp und hängt den echten an die Ursache, ein Codevergleich auf oberster Ebene sieht also nichts. Er muss die Ursachenkette ablaufen, und er muss auch auf den Bedingungsnamen schauen, denn diese Tabelle hat zwei eindeutige Bedingungen, und sie bedeuten Gegensätzliches: die eine sagt, die Maschine ist vergeben, die andere sagt, diese Bestellung hat bereits eine Reservierung, und hier ist sie.
Die Ablehnung muss sagen, welcher Art sie ist
Der Zuteiler baut die Kandidatenliste zweimal, und der Unterschied zwischen beiden Ergebnissen ist eine Mitteilung an einen Kunden.
Der erste Durchgang ist die vollständige Liste, mit einem Grund für jede Maschine, die ausschied: falscher Standort, falscher Hersteller, nicht einsatzbereit und so fort. Der zweite Durchgang ist das, was sich in diesem Augenblick tatsächlich sperren ließ. Ergab die vollständige Liste einen Gewinner und die sperrbare nicht, dann gab es etwas zu verkaufen, und jemand war den Bruchteil einer Sekunde früher. Das ist ein anderer Satz als „es gibt nichts“: der eine heißt versuchen Sie es gleich noch einmal, der andere heißt kaufen Sie mehr Eisen. Sie verdienen verschiedene Schaltflächen.
Der eingefrorene Grund für die gewählte Maschine wandert in die Reservierungszeile, denn einen Monat später steht jedes Gerät anders da, und es lässt sich nicht mehr rekonstruieren, warum der Zuteiler damals diese nahm. Die Zeile sagt es dann selbst.
Nach dem Wiederholungsschlüssel wird zweimal gefragt, und dieses zweite Mal ist noch etwas, das nur ein Nebenläufigkeitstest gefunden hat. Fünf gleichzeitige Anfragen mit demselben Schlüssel auf vier Maschinen ergaben eine Reservierung und vier Ablehnungen, darunter eine aus einer Transaktion, die nichts sperren konnte und deshalb nie zum Einfügen und damit nie zum Wiederholungszweig kam, während in diesem Moment eine Reservierung mit ihrem Schlüssel schlicht existierte. Unmittelbar vor der Ablehnung noch einmal zu fragen behebt das, und es wirkt, weil auf unserer Isolationsstufe jede Anweisung eine frische Momentaufnahme nimmt, ein gerade abgeschlossener Nachbar also sichtbar ist.
Die Zusicherung, die all das überlebt, ist schmal und das Aufschreiben wert: fünf Anfragen mit demselben Schlüssel ergeben eine Reservierung und viermal „jemand war schneller“. Die Verlierer sehen den Gewinner noch nicht, denn er hatte in dem Augenblick noch nicht abgeschlossen. Es gibt keine Fassung davon, in der das anders ginge, ohne aufeinander zu warten, und Warten macht aus jeder betriebsamen Minute eine Schlange.
Aus der Reservierung wird die Bindung
Ist die Bestellung durch, muss die Maschine von reserviert zu verkauft. Die naheliegende Umsetzung gibt die Reservierung frei und schreibt dann die Dienstbindung, und sie enthält ein Fenster von wenigen Millisekunden, in dem die Maschine frei ist. An einem betriebsamen Tag wird dieses Fenster gefunden.
Die Umwandlung schreibt deshalb die Bindung und setzt die Reservierung in einer einzigen Transaktion auf umgewandelt, und die Zeile bleibt gehalten, bis die Bindung existiert. In jedem Augenblick dieser Transaktion sieht eine zweite Bestellung entweder die Reservierung oder die Bindung, und beide schließen sie aus. Es gibt keinen freien Augenblick, weil keine Anweisung einen erzeugt. Dieselbe Form wie das Befördern einer Rackreservierung zum Einbauort, eine Ebene höher.
Der Aufräumlauf ist eine Putzkraft und kein Wächter
Eine Reservierung läuft standardmäßig nach fünfzehn Minuten ab, und ein Arbeiter kehrt die abgelaufenen jede Minute zusammen.
Diese Zahl sind nicht die vier Stunden einer Rackreservierung, und der Unterschied liegt darin, worauf beide warten. Eine Rackreservierung wartet auf einen Menschen mit einem Wagen: jemand muss in den Saal laufen, Schienen setzen, Kabel umstecken. Eine Reservierung wartet auf einen Kaufabschluss: Bezahlseite, Betrugsprüfung, Bestätigung. Währenddessen ist der teuerste Bestand, den ein Hoster hat, unverkäuflich. Fünfzehn Minuten sind großzügig für eine abgebrochene Zahlung, die jemand fortsetzt, und kurz genug, dass ein Sturm abgebrochener Bestellungen das Regal nicht einen halben Tag leer stehen lässt.
Wichtig ist, wo die Frist durchgesetzt wird. Sie wird in der Umwandlung geprüft, nicht vom Aufräumlauf. Wäre der Aufräumlauf die einzige Stelle, ließe sich eine zwischen zwei Läufen abgelaufene Reservierung noch umwandeln, und ein Wächter, der einmal pro Minute vorbeikommt, bewacht nichts. Der Aufräumlauf bringt die Verwaltung in Ordnung und gibt die Maschine an den Bestand zurück; das ist Putzarbeit.
Er hat zudem keinen Funktionsschalter, und das ist eine bewusste Ausnahme. Es gibt einen Ausrollschalter über das gesamte Rechenzentrumsmodul, und ihn hier zu wiederholen hieße, dass das Abschalten des Moduls Maschinen für immer für Bestellungen reserviert lässt, die niemand mehr sehen kann. Ein Schalter, dessen Nebenwirkung eine ausgeschaltete Zeitgrenze ist, ist kein Schalter, sondern ein Leck.
Was wir aufschreiben würden
- Setzen Sie den Schiedsrichter in die Datenbank. Die Anwendung liefert die lesbare Ablehnung; der Index bestimmt den Gewinner.
- Sperren Sie je Kandidat. Eine Menge zu sperren hält Zeilen fest, die Sie nicht brauchen, und das Symptom ist ein entgangener Verkauf und kein Fehler.
- Begrenzen Sie die Suche. Sperren je Kandidat tauscht einen in Geiselhaft genommenen Pool gegen eine lange Schleife, und auch die Schleife braucht eine Grenze.
- Eine Eindeutigkeitsverletzung bricht die Transaktion ab. Ein Wiederholversuch ohne Sicherungspunkt liefert einen Fehler, der das Falsche benennt.
- Schauen Sie auf den Bedingungsnamen. Eine Tabelle hat oft mehrere Eindeutigkeiten, und für den Benutzer bedeuten sie meist Verschiedenes.
- „Nichts verfügbar“ und „jemand war schneller“ sind zwei Mitteilungen. Die Kandidatenliste zweimal zu bauen ist die Art, sie zu unterscheiden.
- Befördern, statt freizugeben und neu zu schreiben. Jede Reihenfolge mit einem freien Augenblick darin wird irgendwann gefunden.
- Setzen Sie die Frist am Ort der Nutzung durch. Ein regelmäßiger Aufräumlauf ist Hauswirtschaft, und Hauswirtschaft ist keine Regel.
Zur Klarheit darüber, was das ist: die Rechenzentrumslogistik liegt hinter einem standardmäßig ausgeschalteten Ausrollschalter mit einer Einstellung je Organisation darüber, davon ist heute also nichts käuflich. Zwei Dinge sind unbewiesen. Der Ablaufaufräumlauf wurde als Funktion gemessen und nicht als laufender Arbeiter. Und eine rohe Anfrage an die Serveraktionen bei geschlossenem Schalter wurde nicht versucht.
Aus der Plattformarbeit von Provibr. Dasselbe Befördern statt Neuschreiben, angewandt auf Höheneinheiten, steht in aus der Reservierung wird der Einbauort, und warum freie Einheiten in einem Rack nicht dasselbe wie verkäufliche Kapazität sind, steht in Rackfläche ist keine Kapazität.