Blog ·
Warum eine 200 von Proxmox kein Ergebnis ist
Proxmox beantwortet eine Klonanfrage mit einer UPID und macht weiter. Wie man diese Kennung liest, ehrlich abfragt und beendet von gelungen unterscheidet.
Von Provibr
Sie schicken eine Klonanfrage per POST an einen Proxmox-Knoten. Zurück kommen HTTP 200 und eine Zeichenkette, die aussieht wie Leitungsrauschen. In diesem Augenblick ist genau eines wahr: Die Anfrage wurde angenommen. Keine Platte wurde kopiert, keine Maschine existiert, und fehlgeschlagen ist auch noch nichts.
Fast jeder verändernde Aufruf in der Proxmox-API arbeitet so, und für Vorgänge, die Minuten dauern, ist das ein völlig vernünftiger Entwurf. Zum Problem wird es erst, wenn der aufrufende Code die 200 als Ergebnis behandelt, und das ist das Standardverhalten jedes jemals geschriebenen HTTP-Clients.
So lesen wir diese Aufgaben, und diese Teile erwiesen sich als schärfer, als sie aussehen.
Die Zeichenkette ist eine Kennung, und sie ist strukturiert
Was zurückkommt, ist eine UPID: eine durch Doppelpunkte getrennte Kennung, die genau eine laufende Aufgabe benennt. Sie trägt unter anderem den Knoten, auf dem die Aufgabe läuft, die Art der Aufgabe, das Ding, auf das sie wirkt, und den Benutzer, der sie gestartet hat.
Drei davon sind tragend.
Der Knoten ist derjenige, den Sie nach dem Status fragen müssen. Eine Aufgabe gehört zu dem Knoten, der sie ausführt, und eine clusterweite Frage ist nicht dieselbe Frage. Nehmen Sie den Knoten aus der Kennung und nicht aus irgendeiner Variablen, die Sie zufällig zur Hand hatten, denn die beiden können sich unterscheiden, und der Fall, in dem sie es tun, ist genau der, den Sie nicht getestet haben.
Die Art sagt Ihnen, was tatsächlich gestartet wurde, und das ist nicht immer das, was Sie dem Namen nach verlangt haben. Ein Backup, das über den Knoten-Endpunkt gestartet wird, kommt mit der Aufgabenart vzdump zurück und nicht mit etwas Gastförmigem.
Das Ziel ist bei Gastvorgängen die Gastkennung, und es kann leer sein. Das ist keine Beschädigung. Eine Aufgabe darf berechtigterweise einen Knoten statt eines Gastes betreffen, also muss Code, der dieses Feld liest, „kein Gast“ zurückgeben, statt zu werfen oder auf null zu verfallen.
Unser Parser ist bewusst eng darin, was er ablehnt: Er verlangt das Präfix, er verlangt mindestens acht durch Doppelpunkte getrennte Felder, und er verlangt, dass Knoten und Art nicht leer sind. Alles andere darf fehlen, denn das Format muss für Aufgabenarten weiterfunktionieren, die wir nie gesehen haben.
Beendet ist nicht dasselbe wie gelungen
Fragen Sie einen Knoten nach dem Status einer Aufgabe, bekommen Sie zwei Felder, die zwei verschiedene Fragen beantworten. Das eine sagt, ob die Aufgabe noch läuft oder angehalten hat. Das andere, nur vorhanden, sobald sie angehalten hat, sagt OK oder trägt eine Fehlerzeichenkette.
Das als zwei getrennte Prädikate zu schreiben statt als eines ist keine Pedanterie. Ein einziges isDone(), das beide vermengt, erzeugt den Fehler, bei dem ein gescheiterter Klon als fertiger Klon gemeldet wird und die Maschine, die es nicht gibt, eine Zeile in Ihrer Datenbank bekommt. Wir haben zwei Funktionen: Die eine fragt, ob sie angehalten hat, die andere fragt, ob sie angehalten hat und der Beendigungsstatus genau OK lautet. Alles andere ist ein Fehlschlag, einschließlich Beendigungsstatus, die wir nie gesehen haben, denn ein Status, den wir nicht kennen, ist keine Erlaubnis, Erfolg anzunehmen.
Wo die Aufgabe lebt, ist Teil der API und kein Detail
Das haben uns Backups beigebracht. Es liegt nahe zu erwarten, dass ein Gastvorgang am Gast hängt: etwas unterhalb des eigenen Pfades der virtuellen Maschine. Einen solchen Endpunkt gibt es nicht. Ein Backup wird am Knoten gestartet, mit der Gastkennung als Parameter.
Das ist keine Marotte, um die man herumroutet. Es verändert, was Sie wissen müssen, bevor Sie starten können: welcher Knoten, welcher Speicher auf diesem Knoten, und ob dieser Speicher überhaupt Backups halten darf. Ein Speicher, der nur Plattenabbilder annimmt, nimmt Ihre Anfrage bereitwillig an und scheitert innerhalb der Aufgabe, was beim Kunden als Fehlermeldung eines Programms auftaucht, von dem er nie gehört hat. Also filtern wir die Zielliste nach dem, was der Speicher zu enthalten erklärt, und lassen jeden Speicher weg, der aufgeführt ist, aber nicht antwortet, bevor wir überhaupt jemandem eine Wahl anbieten.
Dieselbe Regel gilt umgekehrt für das Ablegen einer Platte. Dieselbe Idee, entgegengesetzte Inhaltsart, und es lohnt sich, das als zwei ausdrückliche Filter zu halten statt als einen cleveren gemeinsamen.
Abfragen ist ein Rhythmus, kein Intervall
Während die Aufgabe läuft, müssen Sie wiederholt fragen. Die naiven Möglichkeiten sind beide schlecht: schnell und für immer abfragen, und Sie hämmern eine Stunde lang auf die Verwaltungs-API von jemandem ein; langsam abfragen, und jeder schnelle Vorgang fühlt sich zäh an.
Unser Kompromiss sind drei schnelle Versuche im Abstand von einer Sekunde und danach alle drei Sekunden. Eine Maschine zu starten wird innerhalb des schnellen Fensters fertig und wirkt augenblicklich. Ein Backup von einer Stunde kostet eine Anfrage alle drei Sekunden, was nichts ist.
Die Schleife endet auf genau drei Weisen, und alle drei sind ausdrücklich: Die Aufgabe meldet, dass sie angehalten hat, die Frist des Befehls läuft ab, oder der ganze Befehl wird abgebrochen. Es gibt keinen vierten Zweig und keinen Weg, auf dem die Schleife einfach weiterläuft, und das ist eine Eigenschaft, die man absichtlich herstellen und nicht erhoffen sollte. Was eine Frist anhält und was nicht, ist ein eigenes Thema; hier genügt zu sagen, dass die Abfrageschleife eine Möglichkeit zu verlieren haben muss.
Die meisten Aufgaben verraten nicht, wie weit sie sind
Das ist der Teil, der jeden enttäuscht, der einen Fortschrittsbalken baut. Der Aufgabenstatus gibt Ihnen laufend oder angehalten. Er gibt Ihnen keinen Prozentsatz.
Manche Aufgaben schreiben den Fortschritt in ihr Protokoll, als Text. Ein Klon gibt Zeilen mit einer übertragenen Menge und einem Prozentsatz in Klammern aus. Für die lesen wir also das Ende des Protokolls und nehmen den letzten Prozentsatz, den wir darin finden. Einen Prozentsatz aus einer Protokollzeile zu parsen ist nicht elegant, und es ist der einzige Ort, an dem die Zahl existiert.
Für alles andere zeigen wir eine Schätzung, und hier zählt die Entwurfsentscheidung mehr als die Formel. Unsere nähert sich aus einer erwarteten Dauer asymptotisch neunzig Prozent, der Balken bewegt sich also immer und erreicht nie das Ende. Ein Balken, der bei 100 Prozent steht, während nichts geschieht, ist schlimmer als kein Balken, denn er sagt der zusehenden Person, dass Sie fertig sind, und widerspricht sich dann selbst.
Die erwartete Dauer, die diese Kurve speist, ist ein Formparameter, keine Frist. Unsere sagt, ein Backup dauert üblicherweise etwa zehn Minuten und eine Wiederherstellung etwa zwanzig, mit der Begründung, dass eine Wiederherstellung das Archiv liest und die Platten schreibt und damit ungefähr doppelt so viele Bytes bewegt. Läuft ein Backup länger als die Schätzung, geht nichts kaputt; die Kurve bleibt einfach höflich unter der Linie.
Der Fehler steht im Protokoll, nicht im Status
Wenn eine Aufgabe scheitert, ist der Beendigungsstatus oft nahezu nutzlos. „unable to create VM“ ist eine Kategorie, keine Ursache. Die Ursache steht in den letzten Zeilen des Aufgabenprotokolls.
Also wird ein Fehlerbericht zusammengesetzt: der Beendigungsstatus, dazu das Ende des Protokolls mit herausgefilterten Fortschrittszeilen, begrenzt auf eine Handvoll Zeilen und einige hundert Zeichen. Das Filtern der Fortschrittszeilen zählt mehr, als es klingt, denn ein scheiternder Klon kann mit zwanzig Zeilen Übertragungsprozenten enden und dem eigentlichen Grund darüber.
Eine Regel über diesen Text, an die wir uns streng halten: Er ist ein Zitat. Es ist das, was der Hypervisor gesagt hat, und es geht unverändert in unsere Aufzeichnungen und auf den Bildschirm. Wir übersetzen es nicht, wir räumen es nicht auf, und wir formulieren es nicht in etwas Freundlicheres um. Der Satz darum herum gehört uns und wird für die lesende Person übersetzt; der zitierte Teil bleibt genau so, wie die andere Seite es gesagt hat, denn er ist die einzige Zeichenkette im ganzen Austausch, nach der eine Fachkraft im Support suchen kann.
Falten Sie keine eigenen Worte in den Satz der Maschine
Die letzte Lektion ist die, die wir von einem Aufgabenabfrager nicht erwartet hätten.
Unsere Warteschleife nahm früher eine frei geschriebene Beschriftung für den Vorgang und faltete sie in drei Sätze: irgendetwas läuft, irgendetwas ist fertig, irgendetwas ist fehlgeschlagen. Es funktionierte, bis jemand bemerkte, dass mehrere dieser Beschriftungen in unserer eigenen Bürosprache statt auf Englisch geschrieben waren. Kunden lasen halb übersetzte Sätze in ihren Fortschrittsbalken, und wenn ein Vorgang scheiterte, wurde dieser halb übersetzte Satz dauerhaft im Fehlerfeld gespeichert.
Die Reparatur bestand nicht darin, die Beschriftungen zu korrigieren. Sie bestand darin, die Möglichkeit zu entfernen: Der Vorgang ist jetzt eine Aufzählung, und jeder Fall trägt Kennungen für „läuft“, „fertig“ und „fehlgeschlagen“ statt Wörter. Es ist kein Text mehr übrig, den man falsch machen könnte, und der Satz wird ganz am Ende in der Sprache der lesenden Person gebaut. Diese Kataloge ehrlich zu halten erweist sich als eigene Disziplin.
Die Prüfliste
Für jede API, die eine Veränderung mit einer Kennung statt mit einem Ergebnis beantwortet:
- Zerlegen Sie die Kennung, speichern Sie sie nicht nur. Sie sagt Ihnen, wo Sie fragen müssen, und wo ist nicht immer dort, wo Sie gefragt haben.
- Trennen Sie beendet von gelungen, im Typsystem, wenn es geht.
- Behandeln Sie einen unbekannten Endzustand als Fehlschlag, nie als Erfolg.
- Geben Sie der Abfrageschleife einen Rhythmus und einen garantierten Weg zu enden.
- Zeigen Sie den Fortschritt, den Sie wirklich haben, und eine ehrliche Schätzung, die Sie nie mit vorhandenem Fortschritt verwechseln können.
- Zitieren Sie die andere Seite wörtlich in Fehlern und bauen Sie Ihren eigenen Satz um das Zitat herum statt hinein.
Nichts davon ist schwierig. Alle sechs sind Dinge, die wir zuerst falsch gemacht haben.