Blog ·
Ein Container wird ersetzt, nicht neu installiert
Ein Container hat keine Template-Disk und kein cloud-init, also heißt neu installieren: zerstören und neu bauen. Das ändert, was Sie dem Kunden schulden.
Von Provibr
Eine virtuelle Maschine auf Proxmox neu zu installieren ist beinahe langweilig. Anhalten, löschen, die Vorlage über dieselbe ID klonen, den Rest erledigt cloud-init. Die Vorlage trägt eine Disk, und die Disk trägt ein Betriebssystem, also heißt „neu installieren“ in Wahrheit „eine neue Kopie von etwas Bekanntem und Gutem anlegen“.
Ein Linux-Container hat beides nicht. Er wird aus einem Tarball ausgepackt, und alles, was er danach ist, die Kerne, der Speicher, der Swap, die Größe des Root-Dateisystems, die Netzwerkkarte, der Resolver, der Hostname, steht in einer Konfigurationsdatei auf dem Hypervisor und sonst nirgends. Es gibt kein Image, das man darüber klonen könnte. Eine Container-Neuinstallation ist also ein Zerstören mit anschließendem Erstellen, und sobald man das laut ausspricht, folgt daraus eine Reihe von Pflichten, die eine VM-Neuinstallation nie hat.
So haben wir es gebaut, und am längsten gedauert hat nicht das Zerstören.
Zwei Phasen, weil die Plattform nicht weiß, was gemountet ist
Der Ablauf ist geteilt. Die erste Phase liest nur und ändert nichts: Der Agent holt die Konfiguration des Containers und gibt sie zurück. Die zweite Phase hält an, zerstört, erstellt und startet, und benutzt dabei das, was die erste Phase gelesen hat, als Vorbedingung.
Die naheliegende Alternative ist eine einzige Phase mit einer Vorschau aus dem, was die Plattform ohnehin weiß. Die steht hier nicht zur Verfügung. Eine Hardware-Inspektion für Container ist nicht umgesetzt, weil der Parser, den sie benutzen würde, QEMU-Konfigurationsschlüssel liest, also gibt es nirgends in unserer Datenbank eine gespeicherte Momentaufnahme der Mountpoints eines Containers. Eine Vorschau, die jeden Mountpoint mit „unknown“ beschriftet, ist keine Vorschau, sondern ein Haftungsausschluss.
Fünf Wörter für das, was mit den Daten geschieht
Jeder Mountpoint in der Konfiguration bekommt genau ein Etikett, und das Etikett handelt von den Daten, nicht vom Mount.
| Etikett | Bedeutung |
|---|---|
erased | das Volume wird zusammen mit dem Container zerstört; das Root-Dateisystem immer |
excluded | das Volume gehört einem anderen Gast, also überspringt Proxmox es und nichts wird angerührt |
externally_managed | ein Bind-Mount, ein Hostpfad; das Verzeichnis überlebt, der Mount kommt nicht zurück |
unknown | die Zeile konnte nicht gelesen werden, und dies ist das einzige Etikett, das den ganzen Vorgang blockiert |
preserved | die Daten bleiben und der Mount kommt zurück |
Die letzte Zeile steht in der Liste und wird nie erzeugt. Ein Volume über ein Zerstören hinweg zu behalten hieße, es abzuhängen, den Container zu löschen, ohne nicht referenzierte Disks einzusammeln, und es danach wieder anzuhängen: drei Schritte, deren mittlerer im Fehlerfall ein Volume hinterlässt, das niemandem mehr gehört. Das ist ein eigenes Stück Arbeit mit einer eigenen Aufräumschleife. Der Wert steht im Wortschatz, damit eine spätere Version ihn nicht still unter einem anderen Namen ablegen muss, eine Funktion sagt ausdrücklich, dass er noch nicht angeboten wird, und ein Test stellt sicher, dass der Klassifizierer ihn nie zurückgibt.
Die wichtige Unterscheidung ist die aus dem ersten Satz. Die Daten eines Mountpoints können überleben und der Mount trotzdem nicht zurückkommen, denn der Erstellungsaufruf, den wir senden, setzt immer nur das Root-Dateisystem. Jeder andere Mount verschwindet aus dem neuen Container, ganz gleich, was mit seinen Bytes geschehen ist. Das zu verdrehen erzeugt den gefährlichsten Satz dieser ganzen Funktion: „preserved“ neben einem Mount, den es danach nicht mehr gibt.
Der Besitz steht im Volume-Bezeichner
Proxmox schreibt ein Volume in einer von zwei Formen, und beide sagen, welchem Gast es gehört. local-lvm:vm-5001-disk-0 und local:5001/vm-5001-disk-0.raw gehören beide zu Container 5001. Ein Bind-Mount ist keines von beiden: Er ist ein absoluter Pfad, und auf den führenden Schrägstrich prüft Proxmox selbst.
Dieses Parsen entscheidet zwischen excluded und erased. Ein Mountpoint in der Konfiguration dieses Containers, der auf das Volume eines anderen Gastes zeigt, wird vom Zerstören übersprungen, weil Proxmox den Besitz prüft, also werden diese Daten nicht gelöscht. Ohne den Vergleich würde die Vorschau „erased“ über Daten sagen, die bleiben. Jemand würde daraufhin ein Backup zurückspielen, das er nicht gebraucht hätte, was ein kleineres Unglück ist als der umgekehrte Fall, aber immer noch eines, das wir verursacht haben.
Die Vorschau ist eine Vorbedingung, kein Bild
Was der Kunde gesehen hat, reist mit dem Ausführungsbefehl als Liste von Bestätigungen zurück, und der Agent liest die Konfiguration unmittelbar vor dem Zerstören erneut. Weicht sie ab, ein hinzugekommener Mountpoint, ein anderes Volume unter demselben Schlüssel, verweigert er.
Zwischen der Vorschau und dem Klick steht ein Mensch, und in dieser Zeit kann ein Administrator in der Oberfläche von Proxmox eine Disk anhängen. Der Satz, dass Bind-Mounts und vom Host verwaltete Mountpoints nicht automatisch Teil einer Neuinstallation sind, ist nur wahr, wenn etwas ihn durchsetzt.
Jede Bestätigung besteht aus einem Schlüssel und einem Volume und ausdrücklich nicht aus dem Etikett. Das Etikett ist das Urteil der Plattform und hat am anderen Ende nichts zu entscheiden; bekäme der Agent es als Teil dessen, womit er vergleicht, gäbe es dort eine Verzweigung, die darauf entscheidet, und dann lägen die Regeln an zwei Stellen. Das Etikett wird trotzdem festgehalten, neben den Bestätigungen im Befehlsdatensatz und in der Absichtszeile, denn das ist die Prüfspur, und dort soll genau stehen, was dem Kunden gezeigt wurde.
Nur wachsen, in Bytes verglichen, in Gibibyte angezeigt
Eine Neuinstallation darf das Root-Dateisystem nicht verkleinern. Der Vergleich läuft auf Bytes, und der Bildschirm rundet auf ganze Gibibyte auf.
Aufrunden ist die vorsichtige Richtung, und das Gegenbeispiel sagt, warum. Rundet man 8,5 GiB auf dem Bildschirm auf „8 GiB“ ab und nimmt daraufhin eine Anforderung über 8 GiB an, dann ist ein halbes Gibibyte weg, um dessen Verlust niemand gebeten hat. Gegen ein nachgebautes Proxmox gemessen wurde ein Container abgelehnt, der 16 GiB meldete, wo der Plan 8 verlangte.
Eine Ebene darunter liegt eine Falle. Proxmox schreibt Größen als 8G, 512M oder als nackte Zahl, und eine nackte Zahl steht für Kibibyte. Als Bytes gelesen sind 8388608 8 MiB; richtig gelesen sind es 8 GiB. Ein Parser, der hier falsch rät, wirft keinen Fehler, er genehmigt einfach jede Schrumpfung, die man ihm je vorlegt.
Neun Gründe für eine Weigerung
Bevor irgendetwas Unumkehrbares geschieht, gibt es neun benannte Blocker: Der Container ist weg, protection=1 ist gesetzt, Proxmox hält eine Sperre auf dem Gast, einer unserer eigenen Backup- oder Wiederherstellungsaufträge läuft, es existiert ein geschützter Wiederherstellungspunkt, das Root-Dateisystem würde schrumpfen, seine aktuelle Größe konnte nicht gelesen werden, ein Mountpoint konnte nicht klassifiziert werden, oder eine Neuinstallation ist bereits unterwegs.
Jeder davon ist ein typisierter Wert mit einem eigenen übersetzten Satz in neun Sprachen, sodass ein Grund, der nicht auf der Liste steht, auch keinen bekommen kann. Sie werden auf unserer Seite zweimal geprüft, einmal beim Zeichnen der Vorschau und noch einmal beim Druck auf die Bestätigungsschaltfläche, und ein drittes Mal vom Agenten gegen die Konfiguration, die er selbst liest. Drei Schließungen, denn das ist der unumkehrbare Schritt dieses Kapitels.
Das Root-Passwort wird in der zweiten Phase abgefragt, nicht in der ersten, und reist als flüchtige Angabe, statt gespeichert zu werden. Ein Geheimnis, das man unmittelbar vor dem unumkehrbaren Schritt eintippt, sitzt keine halbe Stunde in einem Browsertab.
Abgleichbedürftig ist das dritte Ergebnis, und es hält die Sperre
Scheitert etwas zwischen dem Zerstören und dem Erstellen, wird der Lauf nicht „gescheitert“. Er wird „abgleichbedürftig“ und trägt die Container-ID, den Schritt, an dem er gestorben ist, und ob der Container noch existierte, als der Agent aufgab. Dieses letzte Feld kann null sein, und null ist nicht false: „Ich konnte es nicht feststellen“ darf sich nicht lesen wie „Die ID ist frei“.
Ob ein Fehlschlag zerstörerisch ist, wird aus der Position des Schrittes in der geordneten Etappenliste abgeleitet, ab dem Zerstören. Genau deshalb sind die Etappen eine Liste und keine Sammlung loser Zeichenketten: Die Frage „war schon etwas Unumkehrbares passiert“ wird an genau einer Stelle beantwortet.
Ein Lauf in diesem Zustand hält die Sperre auf dem Server geschlossen. Auf einer ID, bei der niemand weiß, ob dort ein Container sitzt, eine zweite Ersetzung zu starten, ist genau das, was dieser Entwurf verbietet, und ein Mensch muss den Fall abschließen.
Der Verlierer eines Compare and Swap muss neu lesen
Es gibt keinen Worker, der diese Zeilen fortschreibt. Der Zustand rückt vor, wenn jemand ihn liest: Das Lesen sieht sich den Befehl an, an dem die Absicht hängt, und schiebt die Zeile weiter, mit einem Compare and Swap auf einem Generationszähler, sodass zwei gleichzeitige Leser zusammen jeden Übergang genau einmal ausführen.
So viel war entworfen. Nicht entworfen war, und genau das hat der Browsertest gefunden, was der Verlierer dieses Tauschs zurückgeben soll.
Das Panel fragt nach einer angeforderten Vorschau einmal pro Sekunde nach, und diese Abfrage führt denselben Übergang aus. Lädt der Kunde die Seite in derselben Sekunde, gewinnt die Abfrage den Tausch, die Seite gibt ihre eigene, jetzt veraltete Zeile zurück, und der Bildschirm sagt weiter „warten auf den Agenten“, während die Vorschau längst in der Datenbank steht. Eine Navigation, eine Anfrage, und ein Bildschirm, der einen Zustand zeigt, den es nicht mehr gab. Erst ein zweites Neuladen hat es behoben.
Die Behebung ist einen Satz lang und lohnt sich einzuprägen: Einen Compare and Swap zu verlieren heißt zwangsläufig, dass gerade jemand anders genau den Übergang gemacht hat, den man wollte. Die richtige Antwort ist deshalb dessen Zeile, und sie kostet ein Select. Die eigene zurückzugeben ist das Naheliegende und immer falsch.
Was wir aufschreiben würden
- Hat das, was Sie ersetzen, kein Image, nennen Sie es eine Ersetzung. Das Wort ändert, was Sie dem Kunden schulden, bevor Sie anfangen.
- Etikettieren Sie die Daten und den Mount getrennt. Das sind zwei Fragen, und die Kombination aus einer wahren Antwort auf die eine und einer falschen auf die andere ist die schlimmste Ausgabe, die das System erzeugen kann.
- Lassen Sie die Vorschau als Bedingung zurückreisen, lesen Sie am anderen Ende neu, und verweigern Sie bei jedem Unterschied. Dazwischen hat ein Mensch geklickt.
- Runden Sie vorsichtig, wenn ein Rundungsfehler nur zerstören kann.
- Geben Sie einem halb fertigen zerstörerischen Vorgang einen eigenen Zustand, halten Sie die Sperre geschlossen, und lassen Sie ihn nie in Erfolg oder Fehlschlag fallen.
- Lesen Sie nach einem verlorenen Compare and Swap neu, immer.
Die Container-Unterstützung auf Proxmox wird in Stufen ausgerollt, und die Neuinstallation sitzt aus den genannten Gründen spät in dieser Reihenfolge. Wenn Sie früher in dieser Abfolge stehen, finden Sie die Abwägungen zwischen den beiden Gasttypen in VM oder Container auf Proxmox, und der Grund, warum ein erfolgreicher API-Aufruf kein fertiger Auftrag ist, steht in warum eine 200 von Proxmox kein Ergebnis ist.
Aus der Proxmox-Arbeit bei Provibr.