Wo mehr Plattenplatz her kommt

Posted by christopher on Tuesday, August 9, 2016

Als wir mit Uberspace angefangen haben, haben wir bekanntermaßen nicht damit gerechnet, dass das so groß wird. Das führte zu ein paar Fehlkalkulationen unsererseits, die verschiedene Probleme aufgeworfen haben, wovon die meisten aber schon lange gelöst sind. Ein hartnäckigeres Problem war das mit dem Plattenplatz. Zu Anfang hatten wir unseren VMs jeweils nur 400 GB Speicher zugewiesen, was sich im Verhältnis zu der Menge an Usern die wir auf die Server gelassen haben als Fehlkalkulation bei der Mischkalkulation herausgestellt hat (Rimshot). Wir lassen keine neuen User mehr auf diese Server, aber auch für die schon vorhandenen User ist der Platz zu knapp und wir müssen ihn vergrößern. Das ist allerdings leider nicht so einfach wie es vielleicht klingt.

Damit das was ich im folgenden erklären will nachvollziehbar wird, muss ich erstmal etwas ausholen und Hintergründe ausleuchten:

So sieht der Speicher unter einer typischen Uberspace-VM aus

Am Anfang gibt es vier Festplatten (bei den ältesten Servern jeweils 1 TB groß, bei den neuesten 4 TB), die von einem dedizierten RAID-Controller zu einem RAID10 zusammengefaßt werden (derzeit arbeiten wir daran, die RAID-Controller los zu werden, darüber werden wir hier zu gegebener Zeit berichten). Von diesem RAID10 bootet der eigentliche Server oder Virtualisierungs-Wirt, von daher kommt da eine Partitionstabelle (bei älteren Servern im MBR-, bei neueren im GPT-Format) drauf und es werden ein paar Partitionen angelegt. Für den Wirt wird aber nur ein winziger Bruchteil (rund 20 GB) des insgesamt verfügbaren Speichers (zwischen 2 TB und 8 TB, je nach Server- bzw. Festplattengeneration) benötigt. Der restliche Speicher wird mit LVM verwaltet und für die Uberspace-VMs in Stücke von 400 bis 600 GB portioniert.

An dieser Stelle verdoppelt sich alles bisher erwähnte, denn es kommt ein zweiter Partner-Wirt ins Spiel, der gleich aufgebaut ist. Die beiden Partner werden später im Failover-Betrieb eingesetzt und damit das klappt, müssen sie den Speicher der VMs zwischen sich replizieren. Dafür verwenden wir derzeit die Software DRBD, die an dieser Stelle lobend erwähnt sei. (Wir denken darüber nach, hier mittelfristig auf eine Software wie Ceph zu wechseln, die andere Topologien ermöglicht und uns daher mehr Flexibilität verschaffen würde. Das ist aber noch weit von Spruchreife entfernt. Nicht zuletzt müsste das ausgiebig getestet werden, inklusive solcher Szenarien wie dem hier beschriebenen.)

Die Uberspace-VM selbst wird dann auf das DRBD Block Device installiert das dabei entsteht und sie legt sich darauf wiederum eine Partitionstabelle an (ältere Server: MBR, neuere GPT) und darin eine bis zwei Partitionen und ein Dateisystem (ältere Server: ext3, neuere ext4, in Zukunft wahrscheinlich xfs).

Das Ganze erinnert ein bisschen an eine Matrjoschka. Zusammengefasst, von außen nach innen, haben wir es also zu tun mit:

  1. Festplatten
  2. RAID10
  3. Partitionstabelle
  4. Partition
  5. LVM physical volume
  6. LVM volume group
  7. LVM logical volume
  8. DRBD
  9. Partitionstabelle
  10. Partition
  11. Dateisystem

Auf den Wirten haben wir in den LVM Volume Groups meist noch genügend Platz-Reserven, von daher müssen wir bei Vergrößerungen eigentlich nur auf die Schichten 8 bis 11 achten. Die haben es aber in sich.

Wo’s bisher klemmte

Konkret bei Schicht 8 hatten wir lange ein Problem. Ich hatte das mehrmals mit Testdaten aus einem Backup unter weitgehend realistischen Bedingungen getestet und dabei war es zu Komplettverlusten der Testdaten gekommen. Die Gründe dafür waren vielschichtig: Mir waren bei einigen Tests Fehler unterlaufen, die eingesetzten Software-Versionen waren ziemlich alt (das war noch zu Zeiten als wir noch Wirte mit CentOS 5 hatten) und beherrschten die Vergrößerung versionsgeschichtlich erst seit sehr kurzer Zeit und hatten daher wohlmöglich noch Bugs in die ich gelaufen bin, obendrein hat auch ext4 (Schicht 11) seine Tücken. Woran auch immer es genau gelegen haben mag (das liegt lange zurück und ich hab damals meine Tests nicht so gründlich protokolliert, wie ich das heute machen würde), das Ende vom Lied war jedenfalls: Vergrößern klappte erstmal nicht.

Der Weg drumherum

Statt dessen haben wir lange Zeit die Uberspace-VMs komplett umgezogen. Dazu haben wir ihnen neuen Speicher auf einem anderen Wirte-Paar angelegt und diesen mit passenden Partitionstabellen und Dateisystemen formatiert. Das haben wir dann mit Daten aus dem letzten Backup befüllt, bevor wir die Uberspace-VM dann runterfuhren, die Daten mit rsync synchronisiert haben, den Boot-Loader neu installiert und die Uberspace-VM auf einem der neuen Wirte wieder hochgefahren haben. Solche Umzüge kosten einiges an Zeit, sind anstrengend und vor allem braucht es dafür freie Ressourcen (nicht nur Platz) auf anderen Wirten, denn es muss ja auch die VM später dort laufen. Trotzdem haben wir das lange so gemacht, nicht zuletzt weil es sich gut damit verbinden ließ, dass wir die Last auf den Wirten bei solchen Gelegenheiten besser verteilen könnten. (Am Anfang hatten wir etwas unterschätzt wie viele Gäste pro Wirt einen guten Mix ergeben, insbesondere einen Mix der auch im Failover-Fall noch flüssig läuft. Also haben wir die Umzüge dafür genutzt, bei den Wirten bei denen wir uns verkalkuliert hatten, die Last um einen Gast zu reduzieren. Auch haben wir die ältesten acht Wirte schlicht abgeschafft und ihre Gäste auf neuere Wirte verteilt.)

Das muss doch…

Mittlerweile sind wir mit dem Entzerren und Ausbalancieren der Wirte aber schon seit einer Weile fertig und diversen VMs fehlt immer noch Platz. Also habe ich das mit dem Vergrößern nochmal ausprobiert und diesmal lief es deutlich besser, in keinem von rund einem Dutzend Tests ging etwas schief. Die früheren Fehlschläge haben mich aber vorsichtig gemacht, von daher ist das Vorgehen zur Vergrößerung das wir derzeit anwenden sehr behutsam. Wer unseren Twitter-Feed verfolgt, hat davon das eine oder andere mitbekommen, aber hier will ich das mal im Ganzen schildern.

Vergrößerung 101
Vorbereitungen

So eine Aktion muss natürlich vorbereitet werden, dazu zählt nicht nur die Ankündigung der Downtime per Twitter, sondern es müssen überhaupt erstmal aus den zu vergrößernden VMs welche ausgesucht werden (mittlerweile Vergrößern wir bis zu drei gleichzeitig). Dann muss geprüft werden, wie viel Platz der VM zugeschlagen werden kann ohne eventuell in Zukunft nötige Vergrößerungen ihrer Nachbar-VMs zu blockieren. Es muss rausgesucht werden, welche Wirte und welcher Backupserver zuständig sind und wo auf letzterem die Backups liegen (jeder Backupserver hat mehrere RAIDs auf denen Backups liegen, korrespondierend mit mehreren ggf. parallel laufenden Backup-Jobs die jeweils eines der RAIDs benutzen).

Da das Vergrößern mit einem Reboot verbunden ist, lohnt es sich im Vorfeld auf der VM einen Rundumschlag zu machen. Updates werden zwar regelmäßig installiert, aber Reboots erfolgen bisher nicht so oft (auch da ist eine Änderung angedacht), also lohnt es sich nachzuschauen ob die Boot-Konfiguration der VM auf dem aktuellen Stand ist. (Wir haben über die Jahre hinweg z.B. verschiedene Kernel-Boot-Parameter verwendet, ein Reboot ist die Gelegenheit um sicherzustellen, dass das aktuelle Set verwendet wird.) Auch lohnt es sich vor der Vergrößerung auf dem System nochmal aufzuräumen. Die Vergrößerungs-Aktion geht umso schneller, desto weniger Daten rsync und fsck durchpflügen müssen.

LVM und DRBD

Mit eins, zwei Tagen Vorlauf werden dann schonmal die LVM Logical Volumes und das DRBD Device vergrößert. Dazu loggt sich jemand auf den beiden zuständigen Wirten ein, prüft sicherheitshalber ob die DRBD-Ressourcen auf beiden Wirten online und im Zustand “connected” sind und stellt sicher, dass das DRBD Device nur auf einem von beiden DRBD-Partnern “primary” ist (wir benutzen Dual-Primary-Mode für VM-Live-Migrationen, daher kann es in seltenen Fällen vorkommen, dass ein DRBD-Device aus Versehen auf beiden Partnern “primary” ist). So sollte der Zustand auf beiden Partnern aussehen (in diesem Fall ging es um die Vergrößerung von norma.uberspace.de, deren DRBD-Device die ID 15 hat):

[root@kriemhild ~]# cat /proc/drbd
version: 8.4.7-1 (api:1/proto:86-101)
GIT-hash: 3a6a769340ef93b1ba2792c6461250790795db49 build by mockbuild@Build64R6, 2016-01-12 13:27:11

15: cs:Connected ro:Primary/Secondary ds:UpToDate/UpToDate C r-----
    ns:1717573588 nr:4597440 dw:1518795260 dr:1871569121 al:8049930 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:d oos:0
[root@brynhild ~]# cat /proc/drbd
version: 8.4.7-1 (api:1/proto:86-101)
GIT-hash: 3a6a769340ef93b1ba2792c6461250790795db49 build by mockbuild@Build64R6, 2016-01-12 13:27:11

15: cs:Connected ro:Secondary/Primary ds:UpToDate/UpToDate C r-----
    ns:0 nr:248131076 dw:248131076 dr:0 al:8 bm:0 lo:0 pe:0 ua:0 ap:0 ep:1 wo:d oos:0

Dann wird auf beiden Partnern zunächst das Logical Volume vergrößert:

[root@brynhild ~]# lvm lvresize -L 600G /dev/images/norma
  Size of logical volume images/norma changed from 400,00 GiB (102400 extents) to 600,00 GiB (153600 extents).
  Logical volume norma successfully resized.
[root@kriemhild ~]# lvm lvresize -L 600G /dev/images/norma
  Size of logical volume images/norma changed from 400,00 GiB (102400 extents) to 600,00 GiB (153600 extents).
  Logical volume norma successfully resized.

Dieser Vorgang dauert nur Sekundenbruchteile bis Sekunden, da LVM hier nur freien Speicher neu zuordnen muss. Dabei werden nur ein paar Verwaltungsdaten von LVM auf das Plattensystem geschrieben.

Als nächstes muss dann das DRBD-Device vergrößert werden. Hierbei erfolgt ein partieller Sync, denn der neue Speicher muss genauso wie der bereits vorhandene auf beiden Partnern identisch aussehen. Der bereits vorhandene Speicher ist schon im Sync, der neu hinzu gekommene Teil muss nun synchronisiert werden. Dieser Befehl wird diesmal nur auf dem DRBD-Partner ausgeführt der das Device gerade als “primary” führt:

drbdadm resize norma.uberspace.de

Der Sync lässt sich mit einem ‘cat /proc/drbd’ auf einem der Partner wiederum beobachten. Wer’s im Auge behalten will, kann noch ein ‘watch’ davor setzen. Der Sync-Vorgang kann ziemlich schnell gehen oder auch lange dauern (zwischen 20 Minuten und 8 Stunden), das hängt davon ab wie viel I/O-Last gerade anfällt, denn DRBD versucht den Sync mit niedriger Priorität zu behandeln, so dass der Betrieb nicht gestört wird. Falls trotzdem zu viel Last entsteht oder andersherum der Sync zu lange dauert, kann die Geschwindigkeit auch justiert werden, z.B. so:

drbdadm disk-options --resync-rate=25M norma.uberspace.de

(Es gäbe die Möglichkeit auf den Sync zu verzichten indem dem Resize-Befehl noch --assume-clean mitgegeben wird. Da wir uns jedoch nicht sicher sind, welche Annahmen für “clean” getroffen werden und wir daher die Folgen nicht abschätzen können, lassen wir davon lieber die Finger.)

Diese ersten zwei Schritte der Vergrößerung werde wie gesagt mit ein paar Tagen Vorlauf erledigt, damit am Stichtag niemand ungeduldig auf den Sync warten muss. Nebenbei sei der Vollständigkeit halber erwähnt, dass der Sync unserem Monitoring auffällt und Warnungen generiert, die dann frühzeitig quittiert werden müssen, damit das Monitoring nicht Alarm schlägt.

Backup muss einmal aussetzen

Am Tag vor der eigentlich Vergrößerung muss dann auf dem zuständigen Backupserver der Backup-Job bei dem die zu vergrößernde VM vorkommt erstmal aussetzen. Wir starten ihn nach der Vergrößerung dann manuell, so dass er nur ein paar Stunden später beginnt als sonst, aber während wir unmittelbar vor der Vergrößerung manuell ein letztes Backup machen, möchten wir auf dem RAID auf dem das jeweilige Backup liegt keine konkurrierenden Zugriffe haben, weil das die Sache zu sehr ausbremst.

VM herunterfahren und aufbocken

Dann geht es um Mitternacht, oder kurz danach los. Zunächst muss im Monitoring eine Downtime für die betreffende VM gesetzt werden. Sobald das erledigt ist, wird die VM sanft heruntergefahren. Damit ist gemeint, dass zunächst alle Services die von Usern gestartet wurden dazu aufgefordert werden, sich zu beenden. Anschließend werden auch die von uns betriebenen Services gestoppt. Der Hintergedanke dabei ist, dass wir vermeiden wollen dass in der Hektik des eigentlichen Shutdowns irgendein Service es nicht mehr schafft alle seine Daten auf das Plattensystem zu schreiben, bevor das System ihn nicht mehr lässt. Außerdem setzen wir alle svscan-Instanzen der User auf dem jeweiligen Server auf “down”, damit sie beim Hochfahren des Systems nicht automatisch starten. Der Grund dafür ist, dass sonst erfahrungsgemäß eine Lastspitze entsteht die sich gewaschen hat (wir haben da schon einen Load von 500 und höher gesehen) und einige Services durch Verkettung widriger Umstände in so einer Lastsituation nicht sauber starten. (Dafür überlegen wir uns noch eine schönere Lösung.)

Ist die VM dann erstmal heruntergefahren, machen wir ihre Dateisysteme auf dem Wirt der ihr DRBD-Device gerade als “primary” hat erreichbar und hängen sie ein (im Regelfall ist es nur ein Dateisystem, bei älteren Systemen gibt es aber auch noch eine /boot-Partition):

[root@kriemhild ~]# kpartx -avp norma /dev/drbd/by-res/norma.uberspace.de/0
add map 0norma1 (253:9): 0 733978624 linear /dev/drbd/by-res/norma.uberspace.de/0 2048
[root@kriemhild ~]# mkdir /mnt/norma
[root@kriemhild ~]# mount /dev/mapper/0norma1 /mnt/norma

An dieser Stelle räumen wir noch kurz in /tmp/, /var/tmp/ und /var/lib/php-sessions/ auf, denn dort liegen Dateien die nicht im Backup gebraucht werden. Teilweise sind das auch Mountpoints für ein tmpfs, in diesen Fällen liegen da entweder gar keine Daten drin, oder es liegen noch Reste aus der Zeit bevor wir dort auf tmpfs gesetzt haben drunter, so oder so: das kann weg. Anschließend wird das Dateisystem auf “read-only” gesetzt:

[root@kriemhild ~]# mount -o remount,ro /mnt/norma
manuelles Backup

Jetzt ziehen wir vom Backup-Server aus ein frisches Backup. Wir arbeiten bei Backups mit Hardlinks, d.h. am Ende eines Backup-Durchlaufs erstellt der Backupserver von allen frisch gemachten Backups eine Hardlink-Kopie die unter den namen “current” abgelegt wird. Diese unterscheidet sich dank Hardlinks erstmal überhaupt nicht vom Original und belegt auch kaum extra Platz im Dateisystem. Der nächste Backup-Durchlauf geht dann mit rsync da drüber und überall wo sich seit dem Vortag Änderungen ereignet haben, wird der Hardlink aufgelöst und die Änderung ins Dateisystem geschrieben. Schließlich wird der Ordner noch von “current” umgenannt in “daily.0” und alle anderen Backups werden ebenfalls entsprechend umbenannt. Zum Schluss beginnt der ganze eben beschriebene Prozess von vorn. Das ist etwa die größte Annäherung an ZFS-Snapshots die mit typischen Dateisystemen und rsync erreicht werden kann. (Bis ZFS erfunden wurde, war das meiner Meinung nach der Goldstandard für inkrementelle Backups.)

Normalerweise können sich unsere Backupserver nicht auf Hosts per SSH einloggen, von denen sie keine Backups machen. Obendrein sind sie normalerweise per ForcedCommand auf einen einzigen Befehl beschränkt, den sie ausführen dürfen, nämlich das Gegenstück zu dem rsync-Befehl den wir auf ihnen starten. Das sah in der /root/.ssh/authorized_keys dann früher so aus (Public Key ist der Übersichtlichkeit halber gekürzt):

command="rsync --server --sender -vlogDtprz --delete-excluded --numeric-ids . /" ssh-rsa AAAA…yei8sssEM= backups@jimbo

Derzeit stellen wir das auf ein noch strengeres Regime um, wobei wir SSH-Zertifikate verwenden, über die der jeweilige Backupserver noch viel stärker eingegrenzt wird:

$ ssh-keygen -L -f ssh_host_rsa_key-cert.pub
ssh_host_rsa_key-cert.pub:
        Type: ssh-rsa-cert-v01@openssh.com user certificate
        Public key: RSA-CERT cc:03:ef:f7:32:6c:67:22:a8:78:20:fe:8c:71:bf:50
        Signing CA: RSA 58:bd:d3:21:f9:ff:42:a8:85:53:a6:8b:3a:cf:21:ea
        Key ID: "Backupserver jimbo"
        Serial: 1431361896
        Valid: from 2015-05-10T18:31:36 to 2020-05-04T18:31:36
        Principals:
                backups@jimbo
        Critical Options:
                force-command rsync --server --sender -vlogDtprz --delete-excluded --numeric-ids . /
                source-address 185.26.156.5/32,2a00:d0c0:200::5/128
        Extensions: (none)

Ein Backupserver muss also

  • sein SSH-Zertifikat vorzeigen und
  • das muss von unserer SSH-CA signiert und
  • gültig sein,
  • der Server muss den Prinzipaliennamen der auf dem Zertifikat des Backupserver steht akzeptieren (d.h. der muss in /root/.ssh/authorized_principals drin stehen),
  • der SSH Key des Servers darf nicht auf der Blacklist in /etc/ssh/not_authorized_keys stehen,
  • die Verbindung darf nur von einer der IP-Adressen auf dem Zertifikat kommen und
  • obendrein steht das ForcedCommand sogar schon im Zertifikat drin.

Mit anderen Worten: Der SSH-Key des Backupsservers nützt uns an dieser Stelle nichts, denn mit dem können wir uns auf dem Wirt nicht einloggen und selbst wenn wir es könnten, könnten wir dort nur ein Backup von / ziehen und nicht von /mnt/norma. Das heißt, derjenige von uns der die Vergrößerung durchführt, muss sich auf dem Backupserver einloggen und dabei per SSH-Agent-Forwarding seinen SSH-Key (bzw. neuerdings sein SSH-Zertifikat, das anders als das des Backupserver weniger Einschränkungen trägt) durchreichen. Das klingt alles sehr kompliziert, läuft aber auf ein simples “ssh -A jimbo” hinaus.

Sodann kann also das Backup gezogen werden:

[root@jimbo ~]# time rsync -vPHAXaxz -e "ssh -c chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" --timeout=7200 --numeric-ids --delete root@kriemhild.jonaspasche.com:/mnt/norma/ /backups2/95.143.172.172/current/ ; echo $?

Um Fehler zu vermeiden testen wir das ganze vorher natürlich nochmal mit dem Parameter --dry-run bzw. -n an, aber das ist nur eine Vorsichtsmaßnahme für den Fall, dass im Eifer des Gefechts mal die Querstricke am Ende der Pfadnamen vergessen wurden (darauf muss bei rsync immer penibel geachtet werden) oder irgendwas anderes nicht passt.

Dieser Befehl unterscheidet sich nur geringfügig von dem Befehl, mit dem wir normalerweise Backups machen, was vor allem fehlt ist die Exclude-Liste, den anders als bei normalen Backups bei denen wir ein paar Dateien auslassen, weil es eigentlich unnötig ist davon ein Backup zu machen (hier wäre vor allem das Swapfile zu erwähnen, aber auch alles unter /proc/, /sys/ und /dev/, denn das wird vom Kernel dynamisch erzeugt und ist für Backups nutzlos), wollen wir diesmal wirklich absolut alles haben. Schließlich ist dieses Backup eine Versicherung gegen den unwahrscheinlichen, aber nicht ganz auszuschließenden Fall eines Totalverlustes aller Daten. Für diesen Fall möchten wir bei der Recovery nicht noch daran denken müssen, das Swapfile neu anzulegen oder darauf zu achten dass für /dev/, /sys/ und /proc/ auch leere Ordner bereitliegen.

Das Anfertigen dieses Backups dauert eine Weile, typischerweise etwa anderthalb bis zwei Stunden. Damit in der Zeit keine Langeweile aufkommt, beschäftigt sich derjenige von uns der das kürzere Streichholz gezogen hat und die Nachtschicht macht mit etwas anderem. Manche erledigen andere Arbeiten, manche spielen Computerspiele (vor allem solche die leicht pausiert werden können falls doch was ist, zum Raiden mit der Gilde eignen sich solche nächtlichen Aktionen weniger), manche gucken Filme oder Serien, wieder andere Lesen. Es kommt auch ein bißchen auf die Stimmung in der jeweiligen Nacht an.

Eine Beobachtung am Rande des Backups: rsync braucht deutlich länger wenn es sich durch viele kleine Dateien arbeiten muss, als wenn es mit großen Dateien konfrontiert ist. Der Engpass an der Stelle sind in der Tat die Syscalls und Dateisystemoperationen, die deutlich mehr Zeit fressen als große Datenmengen von einem Plattensystem zu lesen, oberflächlich zu komprimieren, zu verschlüsseln, durch das Netzwerk zu übertragen, zu entschlüsseln, zu dekomprimieren und schließlich wieder auf ein Plattensystem zu schreiben. Leider gibt es praktisch auf jedem Server ein paar User die (höchstwahrscheinlich ohne das selbst zu ahnen, einfach weil eine Webapplikation das so macht) das Dateisystem als tmpfs missbrachen und z.B. zigtausende Session-Cookies oder Cache-Dateien ablegen, die sich jeden Tag ändern und meist nicht sehr groß sind, so dass rsync ordentlich zu tun bekommt. Wenn das alles in /tmp/ oder /var/tmp/ geschähe wäre es weniger schlimm, denn dort räumen wir vor dem Backup ja sicherheitshalber auf.

Auch ein Spamvorfall am Vortag der Vergrößerungs-Aktion kann rsync beschäftigen, in so einem Fall liegen die aus der Mail-Queue gefischten Spammails als kleine Dateien zu zigtausenden noch im yanked-Ordner. Dort räumen wir sie erst nach ein paar Tagen weg, für den Fall dass wir nochmal darauf zugreifen müssen, weil es Fragen zu dem Spamvorfall gab oder dergleichen. Hin und wieder hat das Backup also mit sowas auch noch seinen Spaß.

Partitionstabelle

Wenn das Backup dann aber endlich durch ist, wird das Dateisystem ausgehängt und auf dem Wirt wieder unzugänglich gemacht (konkret geht es darum, dass der Kernel die Partitionstabelle auf diesem Device vergessen soll):

[root@kriemhild ~]# umount /mnt/norma
[root@kriemhild ~]# kpartx -dvp norma /dev/drbd/by-res/norma.uberspace.de/0

Nun muss die Partitionstabelle im DRBD-Device angepasst werden. Bei älteren Servern muss in der MBR-Partitionstabelle einfach nur das Ende der letzten Partition ans neue Ende des Devices verschoben werden. Bei den meisten Servern muss vorher die GUID-Partitionstabelle selbst vergrößert werden, denn bei GPT gibt es am Anfang und am Ende des Devices jeweils eine Kopie der Partitionstabelle. Netterweise bemerkt das Programm parted direkt selbst, dass sich das Device vergrößert hat und bietet an dies zu reparieren:

[root@kriemhild ~]# parted /dev/drbd/by-res/norma.uberspace.de/0
GNU Parted 2.1
Using /dev/drbd15
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) unit s print
Error: The backup GPT table is not at the end of the disk, as it should be.  This might mean that another operating system believes the disk is smaller.  Fix, by moving
the backup to the end (and removing the old backup)?
Fix/Ignore/Cancel? Fix
Warning: Not all of the space available to /dev/drbd15 appears to be used, you can fix the GPT to use all of the space (an extra 104854400 blocks) or continue with the
current setting?
Fix/Ignore? Fix
Model: Unknown (unknown)
Disk /dev/drbd15: 733980728s
Sector size (logical/physical): 512B/512B
Partition Table: gpt

Number  Start  End         Size        File system  Name     Flags
 1      2048s  629125119s  629123072s  ext4         primary  boot, hidden, legacy_boot

Jetzt muss wiederum das Ende der letzten Partition verschoben werden. Bei parted müssen wir dafür den Eintrag der Partition aus der Partitionstabelle rauswerfen und durch einen neuen mit dem neuen Ende ersetzen:

(parted) rm 1
Warning: WARNING: the kernel failed to re-read the partition table on /dev/drbd15 (Invalid argument).  As a result, it may not reflect all of your changes until after
reboot.
(parted) mkpart primary 2048s 100%
Warning: WARNING: the kernel failed to re-read the partition table on /dev/drbd15 (Invalid argument).  As a result, it may not reflect all of your changes until after
reboot.

Dann müssen noch die Flags an der neuen Partition gesetzt werden:

(parted) toggle 1 boot
Warning: WARNING: the kernel failed to re-read the partition table on /dev/drbd15 (Invalid argument).  As a result, it may not reflect all of your changes until after
reboot.
(parted) toggle 1 hidden
Warning: WARNING: the kernel failed to re-read the partition table on /dev/drbd15 (Invalid argument).  As a result, it may not reflect all of your changes until after
reboot.
(parted) toggle 1 legacy_boot
Warning: WARNING: the kernel failed to re-read the partition table on /dev/drbd15 (Invalid argument).  As a result, it may not reflect all of your changes until after
reboot.

So sollte es zum Schluss (in unserem Setup jedenfalls) aussehen:

(parted) unit s print
Model: Unknown (unknown)
Disk /dev/drbd0: 733980728s
Sector size (logical/physical): 512B/512B
Partition Table: gpt

Number  Start  End         Size        File system  Name     Flags
 1      2048s  733980671s  733978624s  ext4         primary  boot, hidden, legacy_boot

(parted) quit
Information: You may need to update /etc/fstab.
fsck und resize

Nun machen wir die (inzwischen geänderte) Partitionstabelle dem Wirt wieder bekannt:

[root@kriemhild ~]# kpartx -avp norma /dev/drbd/by-res/norma.uberspace.de/0
add map 0norma1 (253:9): 0 733978624 linear /dev/drbd/by-res/norma.uberspace.de/0 2048

Bevor wir das Dateisystem vergrößern, machen wir vorsichtshalber einen fsck:

[root@kriemhild ~]# time e2fsck -fpv -C0 /dev/mapper/0norma1 ; echo $?

Dank -C0 bekommen wir dabei sogar eine Fortschrittsanzeige. Wer schonmal stundenlang auf den Bildschirm gestart und das ominöse Schweigen eines fsck erduldet hat, weiß wie viel weniger nervenaufreibend eine Fortschrittsanzeige sein kann, selbst wenn es gar keinen Grund gibt Probleme im Dateisystem zu befürchten. (Wer bei dem Wort Fortschrittsanzeige sofort an die notorisch ungenauen Zeitvorhersagen der meisten Betreibsysteme beim Kopieren von Dateien denkt: e2fsck liefert wirklich nur eine Fortschrittsanzeige, keine Zeitvorhersage. Allerdings ist unsere Erfahrung, dass der Balken relativ stetig fortschreitet ohne allzulange an bestimmten Punkten zu verweilen. Diese Fortschrittsanzeige ist also in der Tat hilfreich.)

So ein fsck dauert meist zwischen 10 Minuten und 90 Minuten (bei Dateisytemen von 400 GB Größe, auf unseren Plattensystemen), wenn keine Probleme gefunden werden. Bei ext3 dauert das ganze typischerweise deutlich länger, bei ext4 geht es zügiger.

Nach erfolgreichem fsck kann dann endlich das Dateisystem vergrößert werden:

time resize2fs /dev/mapper/0norma1 ; echo $?
fast geschafft

Wenn das ebenfalls ohne Fehlermeldung durchläuft, hängen wir das Dateisystem nochmal ein und schauen nur um ganz sicher zu gehen mal in /etc/, /var/ oder einem anderen geeigneten Ort nach, ob alles normal aussieht. So ein fsck ist unheimlich und wir sind lieber übervorsichtig.

Sodann wird das Dateisystem wieder ausgehängt und die Partition unzugänglich gemacht:

[root@kriemhild ~]# umount /mnt/norma
[root@kriemhild ~]# kpartx -dvp norma /dev/drbd/by-res/norma.uberspace.de/0

Nun kann die VM endlich wieder gestartet werden. Wir beobachten den Boot und zumindest CentOS 6 hat die unschöne Eigenschaft, dabei längere Zeit beim Schritt “enabling local filesystem quotas” zu verweilen. Wir wissen nicht exakt warum. Soweit wir wissen, sieht das System dass ein fsck stattgefunden hat und führt daraufhin einen quotacheck durch. In Tests haben wir jedoch festgestellt, dass dieser quotacheck wenn er z.B. auf dem Wirt vorher händisch ausgeführt wird deutlich schneller läuft, beim Boot dann aber trotzdem wiederholt wird und länger braucht. Es ist uns ein Rätsel was da los ist, aber da das Problem mit CentOS 7 verschwindet, wollen wir da nicht noch mehr Gehirnschmalz drauf vergeuden.

Ist die VM dann endlich gebootet, loggt der zuständige Admin sich ein, kontrolliert kurz ob alle Dienste laufen und stößt dann den Prozess an, der die ganzen svscan-Instanzen der User nach und nach wieder startet, was dann wiederum dafür sorgt dass deren Dienste ebenfalls wieder starten, aber eben nicht alle auf einmal.

Zu guter Letzt fragen wir das Monitoring noch, ob aus seiner Sicht auch wieder alles in Ordnung ist, entfernen die Downtime für die VM und dann ist es geschafft und jemand kann sich endlich ins Bett fallen lassen.

Schlussbemerkungen

Ein paar Dinge gibt es zu diesem ganzen Vorgehen aber noch zu schreiben:

Wir sind hier wirklich übervorsichtig mit Backup und fsck. Das Risiko ist aber auch denkbar hoch. Im ungünstigsten Fall wäre ein komplettes Dateisystem weg und das Backup kann bis zu 24 Stunden alt sein… da schadet Vorsicht nicht.

Es wäre prinzipiell auch möglich, das Dateisystem online zu vergrößern. Da gibt es aber ein paar Fallsticke und handfeste Hindernisse. Wir könnten nach dem DRBD Resize mit ‘‘virsh blockresize $resource $device $size’’ die Größe des Blockdevices das libvirt dem Gast zeigt im laufenden Betrieb anpassen. Das ist in der Praxis aber deutlich komplizierter als es zunächst klingt, denn selbstverständlich arbeiten LVM, DRBD und virsh nicht alle mit der gleichen Basis für ihre Einheiten. Da müsste also erstmal recherchiert werden, wer zur Basis 1000 und wer zur Basis 1024 rechnet und wer eventuell beides kann und wenn ja mit welchen Suffixen. Bisher kennen wir auch noch keinen Weg um von virsh zu erfragen was die maximale Größe für das Device wäre, d.h. es ist etwas kompliziert das Device online zu vergrößern. Möglich wäre es aber.

Das nächste Problem ist dann, dass der Kernel der VM damit auch umgehen können muss. Das kann er in unserem Fall zwar schon allein ob des Treibers virtio, aber natürlich müssten dann als nächstes Paritionstabelle und Partition vergrößert werden und der Linux-Kernel ist (aus durchaus nachvollziehbaren Gründen) nicht geneigt und willens im laufenden Betrieb die Partitionstabelle seines Boot-Devices neu einzulesen. Über diese Hürde kämen wir dann nur mit einem (immerhin kurzen) Reboot der VM hinweg.

Nach einem solchen Reboot wäre dann als nächstes das Dateisystem zu vergrößern. In der Praxis heißt das also, es müsste ein ext4 das im ungünstigsten Fall seit mehreren Jahren kein fsck mehr gesehen hat online vergrößert werden. Schon bei dem Gedanken wird dem erfahrenen Admin mulmig. Aus Gründen. Der fsck ließe sich beim Booten aber auch noch erzwingen, das reduziert das Unwohlsein aber nur geringfügig.

Ich hab den oben beschriebenen Weg durchaus ausprobiert, im Test allerdings mit einem ext4 das am Vortag frisch angelegt und mit Daten aus einem Backup befüllt war. Bekommen habe ich nicht ein größeres Dateisystem, sondern ein leckeres Daten-Haché. So ein Online-Resize mag auf dem heimischen NAS oder einem Backup-Server ganz wunderbar laufen, offenbar wird es aber umso riskanter, je mehr konkurrierende Dateisystemzugriffe dabei stattfinden – und Multiusersysteme dürften der Inbegriff von konkurrierenden Dateisystemzugriffen ohne erkennbare Muster sein.

Und das war die lange Geschichte, warum wir Vergrößerungen bei unseren Servern so vorsichtig machen und warum das immer ein paar Stunden dauert. Danach liefen die Server glücklich bis an das Supportende des Major Releases ihrer Linux-Distribution.