Zum Inhalt springen

Special vdev: 113 TB an zwei Dateien

Das Special vdev eines 113-TB-Pools lag auf zwei qcow2-Dateien: beide auf derselben Consumer-SSD mit 11 % Restlebensdauer. Der Umzug war eine Operation am laufenden Mirror ohne Spielraum für Fehler. Und er hat zwei Checks aufgedeckt, die die ganze Zeit auf das Falsche gezeigt haben.

Patrick Lehmann
8 Min. Lesezeit
Links der Grenze die Medien, die der Hypervisor besitzt. Rechts davon die zwei aktiven Member des Metadaten-Mirrors, und die Lücke, wo das alte Paar lag.

Ein ZFS Special vdev trägt die Metadaten eines Pools: die Block-Pointer, die Verzeichnisstruktur, die Karte, auf der steht, wo alles liegt. Man muss es nicht anlegen. Hat man es einmal angelegt, ist es nicht mehr optional. Fällt ein Special vdev aus, verliert man nicht die Metadaten. Man verliert den Pool.

Unseres hielt die Metadaten eines raidz2-Pools mit 113 TB. Auf diesem Pool steht bei uns praktisch alles: sämtliche SMB-Freigaben, der Share, den jeder Swarm-Service einhängt, die Fotobibliothek, der Cache der GitLab-Runner und das Ziel, auf das jedes Backup geschrieben wird.

Es bestand aus zwei qcow2-Dateien auf einer Consumer-SSD mit 11 % Restlebensdauer.

Nicht auf dem Array also, sondern auf einer einzelnen Samsung 990 PRO mit 89 % Abnutzung: derselben Disk, von der der Hypervisor bootete. Beide Hälften des Mirrors lagen auf demselben Stück Hardware. Ein Mirror über eine einzige Disk ist Redundanz nur dem Namen nach.

Dazu kommt ein Kreis, den wir uns selbst gebaut haben: Unsere Sicherungen landen auf einer Freigabe, und diese Freigabe kommt von genau der Maschine, um die es hier geht. Vom Special vdev gab es damit keine einzige Kopie ausserhalb des Rechners, der es selbst trug.

Und während das so lag, steckten in derselben Maschine vier gesunde NVMe mit 0–1 % Abnutzung. Durchgereicht an genau diesen Guest, für einen Pool, den es seit Monaten nicht mehr gab. Die Hardware, die das Problem löst, lag die ganze Zeit im Gehäuse und tat nichts.

Erst anhängen, dann abhängen

Naheliegend wäre gewesen, dem Guest die vier NVMe zu lassen und dort einen neuen Pool zu bauen. Dagegen sprach eine Erfahrung, die fünf Tage zurücklag: Dieser Guest liefert Storage an seinen eigenen Hypervisor. Wenn er hängt, wartet ausgerechnet die Schicht auf ihn, die ihn wieder flottmachen könnte. Passthrough hätte die Redundanz unseres kritischsten vdev vollständig in diesen Guest hinein verlegt.

Also bekam der Hypervisor die Disks. Ein Pool namens zpool_nvme, daraus zwei Zvols zu je 400 GB, die dem Guest als ganz gewöhnliche SCSI-Disks gereicht werden. Die Medien unter den Metadaten gehören jetzt der Schicht, die einen Ausfall des Guests übersteht.

hl15: der Hypervisor
  pool: zpool_nvme
 state: ONLINE
config:

	NAME                                      STATE     READ WRITE CKSUM
	zpool_nvme                                ONLINE       0     0     0
	  mirror-0                                ONLINE       0     0     0
	    nvme-Samsung_SSD_990_PRO_2TB_…4475V   ONLINE       0     0     0
	    nvme-Samsung_SSD_990_PRO_2TB_…4352W   ONLINE       0     0     0
	  mirror-1                                ONLINE       0     0     0
	    nvme-Samsung_SSD_990_PRO_2TB_…4463B   ONLINE       0     0     0
	    nvme-Samsung_SSD_990_PRO_2TB_…4490Z   ONLINE       0     0     0

errors: No known data errors

Nachmachen sollte man vor allem die Reihenfolge. ZFS kennt keinen Befehl, der ein Special vdev ersetzt. Es kennt zpool attach und zpool detach, und die einzige Frage ist, welcher von beiden zuerst läuft.

Wer zuerst abhängt, steht ein paar Minuten lang ohne zweite Hälfte da: auf genau dem Gerät, dessen Ausfall 113 TB kostet. Wer zuerst anhängt, steht dieselben Minuten besser da als vorher. Wir sind deshalb vom 2-Wege- über den 3-Wege- auf einen 4-Wege-Mirror gegangen und haben rund 124 GB resilvern lassen, bis null Fehler gemeldet wurden. Danach haben wir den verbreiterten Mirror zusätzlich im TrueNAS-UI gegengeprüft, und erst dann die beiden qcow2-Hälften abgehängt.

Die Redundanz ist zu keinem Zeitpunkt unter den Stand vom Anfang gefallen. Auch nicht in den Minuten, die niemand eingeplant hatte.

Das kostet nichts ausser Geduld, verändert aber die Natur des heiklen Teils. Aus einem Fenster, das man überstehen muss, wird ein Zustand, in dem man beliebig lange sitzen bleiben kann. Wir hätten nach dem vierten Anhängen aufhören, schlafen gehen und eine Woche später weitermachen können. Der Punkt ohne Wiederkehr war ein einziger Befehl, abgesetzt, als alle Belege auf dem Tisch lagen.

Die alten Volumes haben wir danach gelöscht statt liegen gelassen. Eine abgehängte Special-Hälfte veraltet in dem Moment, in dem der Pool ohne sie weiterläuft, und lässt sich danach nie wieder gefahrlos einhängen; einhängen lässt sie sich aber sehr wohl. Ein Gerät, das nutzlos und trotzdem erreichbar ist, ist eine geladene Waffe, die auf 113 TB zeigt. Nebenbei kamen 288 GB zurück, und der Pool auf der Boot-Disk fiel von 39 % auf 8 %. Eines der vier Volumes hatte gar keinen Eintrag mehr in der Konfiguration: Ein früherer Versuch mit einer Einzeldisk hatte die Referenz entfernt und die Daten liegen lassen.

qm set verschiebt gar nichts

Wir beschreiben die Disks unserer VMs in Ansible, samt der Angabe, auf welchem Storage jede Disk liegen soll. Der naheliegende nächste Schritt wäre, diese Angabe konvergieren zu lassen: Liegt eine Disk falsch, verschiebt Ansible sie.

Genau das haben wir bewusst nicht gebaut.

qm set 4110 --scsi0 zpool_nvme:vm-4110-disk-0 sieht aus, als würde es eine Disk umziehen. Tut es nicht. Es tauscht den kompletten Drive-Eintrag in der VM-Konfiguration aus und zeigt danach auf ein Volume, das auf dem Ziel nie angelegt wurde. Die Daten bleiben, wo sie sind. Bewegt werden sie einzig von qm move-disk, einem Mirror im laufenden Betrieb.

Deshalb wird storage nur deklariert und gemeldet, nie angewendet. Die Auswertung überspringt das Feld, bevor der Code überhaupt in die Nähe eines Drive-Eintrags kommt, und markiert eine Maschine nie als geändert: ein konvergierender Lauf kann eine VM also gar nicht erst aus diesem Grund auswählen. Weicht die Angabe von der Realität ab, meldet das Audit die Abweichung und schreibt den qm move-disk-Befehl hin, den ein Mensch ausführen soll.

Das ist keine Lücke, die wir irgendwann schliessen wollen. Manche Angaben lohnen sich als Aussage, aber nicht als Automatismus. Am ehrlichsten hält man das fest, indem man die Automatik handlungsunfähig macht, und sich nicht darauf verlässt, dass alle daran denken. Unit-Tests halten es fest, und ein Integrationstest prüft, dass selbst bei ausdrücklich eingeschaltetem Apply-Modus kein qm set herauskommt.

Ein Detail haben wir zuerst aus Versehen richtig gemacht und dann absichtlich behalten: Die Angabe nennt den Storage, nicht den Namen des Volumes. Proxmox vergibt bei jedem Umzug neue Nummern: aus scsi0 wurde bei uns disk-4. Auf welcher Disk die Daten liegen, ist eine Aussage wert. Wie die Datei am Ende heisst, nicht.

Der Check war da. Er zeigte auf das Falsche.

Im Wartungsfenster ist zweimal etwas schiefgegangen, und beide Male hatte es dieselbe Form.

Das erste Mal: Wir haben über die Proxmox-API zwei PCI-Mappings angelegt und geprüft, ob sie da sind. Sie waren da. qm start hat die VM trotzdem verweigert: missing expected property 'iommugroup'. Der Endpunkt, der ein Mapping anlegt, validiert es nicht. Die Validierung sitzt in einer Funktion, die erst beim Start einer VM aufgerufen wird. Der Abstand zwischen «angelegt» und «brauchbar» bleibt damit bis zum denkbar schlechtesten Moment unsichtbar. Unser eigener Kommentar im Code behauptete, der Umweg über die API bringe uns Validierung. Bringt er nicht.

Das zweite Mal: Vier von fünf Disks sind auf den neuen Pool gezogen. scsi0 blieb auf der zu 89 % abgenutzten SSD liegen, und das nächtliche Audit hat dazu geschwiegen. Das war kein Fehler im Code. Eine Disk, für die niemand einen Storage angibt, wird mit nichts verglichen: eine halb fertige Migration sieht deshalb aus wie eine fertige. Das Audit hat die Frage beantwortet, die wir ihm gestellt haben. Gestellt hatten wir die falsche.

Zu prüfen, ob etwas existiert, heisst nicht zu prüfen, ob es gültig ist. Und ein Check, der Deklariertes mit Vorhandenem vergleicht, kann nicht melden, was man zu deklarieren vergessen hat.

In beiden Fällen hatte ein Test den Irrtum mit festgeschrieben. Die Suite war grün auf Ausgaben, die Proxmox ablehnt. Das ist der Punkt, den man mitnehmen sollte: Ein Test prüft nicht, ob man ein System verstanden hat. Er hält fest, wie man es verstanden hat. Ist dieses Verständnis falsch, verteidigt ausgerechnet der Test den Fehler.

Beides ist inzwischen strukturell abgestellt und nicht bloss notiert. iommugroup ist schon bei der Deklaration Pflicht, erzwungen dort, wo das Mapping gebaut wird, statt beim nächsten Start einer VM aufzufallen. Und ob eine Maschine vollständig deklariert sein muss, lässt sich einzeln einschalten: Auf diesem Guest fällt jede angehängte Disk ohne Eintrag laut auf. Auf diesem und auf keinem anderen: der Schalter ist seinen Lärm nur dort wert, wo im Fehlerfall genug auf dem Spiel steht.

Was die Zahlen wirklich sagen

Gemessen haben wir direkt nach der Migration, unter laufender Produktionslast, auf einem Dataset, das den Cache umgeht und danach wieder gelöscht wurde.

Profil IOPS Durchsatz Latenz
4k sync randwrite, QD1 637 2.5 MiB/s Ø 1.55 ms, p99 3.9 ms, p99.9 11.6 ms
16k randwrite, 4 Jobs QD16 60 800 950 MiB/s Ø 64 µs, p99 734 µs
1M sequenziell schreiben 1 507 1 507 MiB/s

Der Vergleich, den alle sehen wollen, ist der mit den 688 ms Flush-Latenz, die wir auf dem abgelösten SATA-Storage gemessen hatten. Das erste Profil ist die vergleichbare Grösse, und 1.55 ms gegen 688 ms ist eine Zahl, die man gerne aufschreibt.

Ein Verhältnis ist es trotzdem nicht, und wir verkaufen es auch nicht als eines. Die 688 ms stammen aus einem echten CI-Workload unter echter Nebenläufigkeit, das hier ist synthetisches fio auf einem einzelnen Thread. Die beiden Werte stimmen in Vorzeichen und Grössenordnung überein. In einer Zahl stimmen sie nicht überein: wer den einen durch den anderen teilt, hat eine Pressemitteilung geschrieben und keine Messung.

637 IOPS sind für NVMe eher wenig, und der Grund steht offen in der Konfiguration: Dieser Pool hat kein eigenes Log-Device, 4k-Sync-Writes landen deshalb im pool-internen Intent-Log. Gut zu wissen, bevor jemand die erste Zeile für ein Urteil über die Hardware hält.

Was offen bleibt

Das Issue, aus dem diese Arbeit entstanden ist, wollte zwei voneinander unabhängige Fehlerdomänen für den Metadaten-Mirror herstellen. Ganz erreicht hat es das nicht. Und das gehört in den Artikel und nicht in ein Ticket.

zpool_nvme besteht aus zwei gespiegelten Paaren, über die verteilt geschrieben wird. Beide 400-GB-Zvols liegen deshalb quer über beiden Paaren. Fällt eines der Paare aus, sind beide Hälften des Special vdev weg und mit ihnen der Pool mit 113 TB. Das ist um Längen besser als zwei Dateien auf einer sterbenden SSD. Die Eigenschaft, die wir eigentlich herstellen wollten, ist es nicht.

Dazu kommt: Alle vier Disks hängen hinter einem einzigen PCIe-Switch, und auf dieser Hardware gibt es keine Topologie, die daran etwas ändert. Das Wurzeldateisystem des Hypervisors liegt weiterhin auf der abgenutzten SSD, die diese ganze Übung ausgelöst hat: wir haben die Metadaten heruntergeholt, nicht das Betriebssystem. Und der neue Pool trägt jetzt die Metadaten dieses Guests und mittelfristig weitere Workloads; aus einem isolierten Thema ist ein geteiltes geworden. Der Benchmark selbst hat den Load Average des Hosts von 16.7 auf 40.1 getrieben und sich innerhalb von zwei Minuten wieder beruhigt.

Nichts davon spricht dafür, länger gewartet zu haben. Es ist der Unterschied zwischen einer Migration, die abgeschlossen ist, und einer, die erledigt ist, und nur eines der beiden Wörter heisst, dass man aufhören darf, daran zu denken.

Was ich behalten würde

  • Immer erst anhängen, dann abhängen. Lässt sich ein Austausch als «erst verbreitern, dann verschmälern» formulieren, muss man den heiklen Teil nicht mehr überstehen. Man darf darin sitzen bleiben, so lange man will.
  • Was ersetzt wurde, gehört gelöscht. Eine veraltete Mirror-Hälfte, die sich noch einhängen lässt, ist keine Reserve. Sie ist ein Fehler, der auf einen müden Menschen wartet.
  • Gefährliche Automatik gehört strukturell verhindert, nicht abgeschaltet. Einen Schalter, den man umlegen kann, legt irgendwann jemand um. Code, der die Operation gar nicht erreichen kann, ist ein anderes Versprechen.
  • Man sollte fragen, ob der eigene Check Abwesenheit sieht. Die meisten Checks vergleichen Deklariertes mit Vorhandenem. Kaum einer merkt, was nie deklariert wurde, und genau dort versteckt sich halb fertige Arbeit.
  • Die Grenze, die man nicht erreicht hat, gehört veröffentlicht. Der Satz über die verteilten Mirrors ist der nützlichste in diesem Artikel, und er beschreibt das, was wir nicht hinbekommen haben.
Patrick Lehmann

Architecture & Governance Lead

Squibble GmbH

Bringt seit über zwanzig Jahren Struktur in gewachsene IT-Landschaften — als Architekt, Entwickler und Betreiber. Schreibt hier über die Systeme, die bei Squibble tatsächlich laufen, und über die Entscheide dahinter.

Weiterlesen