Kubernetes Local Persistent Volumes mit virtio-Disks

In Kubernetes ermöglichen Persistent Volumes (PVs) den Betrieb von Workloads mit persistentem State. Der Inhalt dieser Volumes existiert unabhängig von der Lebensdauer der Pods, die sie verwenden.

Es gibt viele verschiedene Implementierungen/Provider für PVs. Wenn man Kubernetes bei einem Cloud-Anbieter betreibt, kann man deren Blockdevice- oder Netzwerkdateisystem-Service verwenden, z.B. Elastic Block Store (EBS) bei AWS

Auch in einer on-prem-Umgebung wie unserer gibt es Möglichkeiten: die meisten unserer Persistent Volumes liegen auf NetApps. Mit dem offiziellen NetApp-Provisioner Trident werden diese bei Bedarf automatisch angelegt.

In manchen Fällen möchte man aber nicht Volumes per Netzwerk einbinden, sondern diese direkt auf der Node haben, d.h. Local Storage nutzen. Das kann z.B. schneller oder kostengünstiger sein, und hat weniger Abhängigkeiten. Es passt insbesondere dann, wenn die Redundanz der Daten auf Anwendungsebene sichergestellt ist, so dass ein Verlust eines einzelnen PVs keinen Datenverlust bedeutet.

Pods verwenden die PVs nicht direkt, sondern sie referenzieren einen Persistent Volume Claim (PVC). Ein PVC ist ein „Wunsch“ nach einem Volume einer bestimmter Art und Größe. Eine Controller-Loop in Kubernetes sorgt dafür, dass ein PVC mit einem passenden PV verknüpft wird. Erst dann kann das PVC von Pods verwendet werden.

Für die meisten Arten von Volumes gibt es dynamisches Provisioning: wenn jemand ein PVC erstellt, wird automatisch vom Hersteller-spezifischen Provisioner (wie NetApp Trident) ein passendes PV erstellt. Da das neue PV genau auf das PVC passt, werden diese dann von Kubernetes verknüpft. 

Bei den local Volumes gibt es stattdessen ein statisches Provisioning: der local-static-provisioner scannt nur auf der jeweiligen Node, wo er läuft, nach schon bestehenden Mountpoints oder Blockdevices, und erstellt dafür die PV-Objekte. Wenn man also ein PVC anlegt, und es gar kein passendes PV gibt, dann wird das PVC pending bleiben, bis man selbst ein Volume erstellt.

Wir benutzen die local PVs in Kubernetes seit sie mit 1.10 als Beta-Feature eingeführt wurden. Unser Provisioner ist so konfiguriert, dass er alle Mountpoints unterhalb von /mnt/disks als Persistent Volumes bereitstellt. 

Die alte Lösung

In der Vergangenheit hatten wir jede Kubernetes-Worker-Node mit LVM installiert, mit 10 leeren LVM Logical Volumes je 5 GB, die wir unter /mnt/disks/1, /mnt/disks/2 usw gemountet haben. Dadurch hatten wir automatisch viele Volumes im Cluster verfügbar, ohne dass wir irgendwas tun mussten. 

Der Ansatz hatte aber auch einige Nachteile:

  • wir haben auf jeder Node 50 GB Platz freigehalten, egal ob die Volumes gebraucht wurden oder nicht
  • Die Volumes waren immer initial 5 GB groß, unabhängig davon was wir wirklich belegen wollten. 
  • Die Volumes zu vergrößern war nervig
    • wir mussten zuerst die Disk der Worker-VM vergrößern, dann die Partition mit dem LVM Physical Volume, und dann das jeweilige LVM Logical Volume
    • der local-volume-provisioner ändert die Größenangabe des PV-Objekts nicht. Wenn man ein Volume vergrößert hat, damit es für ein PVC mit größerer Anforderung passte, dann musste man das PV-Objekt einmal löschen, damit der Provisioner es neu erstellt mit der richtigen Größe
  • Wenn man ein Volume nicht mehr brauchte, konnte man zwar das LVM Logical Volume löschen, aber dadurch wurde die VM-Disk nicht kleiner

Durch die Einführung der NetApp ist unser Bedarf an local Volumes stark gesunken. Wir haben also zunehmend Platz verschwendet.

Die neue Lösung

Im Zuge der Migration unserer Kubernetes-Plattform von CentOS 7 auf Debian 12 haben wir den Mechanismus überarbeitet.  Der wichtigste Unterschied ist, dass wir keine Volumes mehr auf Vorrat anlegen. Es wird also kein Platz verschwendet. Stattdessen haben wir das Anlegen neuer Volumes vereinfacht, so dass man es leicht bei Bedarf durchführen kann: 

Der einfachste Weg um einer VM zusätzlichen Storage zu geben ist, eine Virtio-Disk hinzuzufügen. Diese taucht in der VM als eigenes Blockdevice auf, und ihre Größe ist unabhängig von allen anderen Volumes. 

So wird eine Disk zu einer libvirt-VM hinzugefügt, auf dem VM-Host: 

sudo virsh vol-create-as vg1 worker23-disk2 5GB
sudo virsh attach-disk --persistent worker23 /dev/vg1/worker23-disk2 vdb

Anschließend muss man noch in der VM die Disk formatieren:

sudo mkfs.xfs /dev/vdb

Das Weitere übernimmt eine udev-Regel, die das Dateisystem automatisch mountet:

ACTION=="add|change", KERNEL=="vd[b-z]", ENV{ID_FS_USAGE}=="filesystem", ENV{ID_FS_UUID_ENC}=="?*", RUN{program}+="/usr/bin/systemd-mount --no-block --collect $devnode /mnt/disks/$env{ID_FS_UUID_ENC}"

Die Regel bedeutet: jedes Dateisystem auf den Block-Devices /dev/vdb usw. bis /dev/vdz wird automatisch unter /mnt/disks/$UUID gemountet. systemd-mount legt das Verzeichnis für den Mountpoints an, falls es noch nicht existiert.

Wir verwenden hier die UUID statt dem Geräte-Namen. Wenn man die Disk neu formatiert, z.B. weil die VM neu installiert wird, dann ändert sich der Mountpoint. Dadurch werden alte PV-Objekte ungültig und zeigen nicht fälschlicherweise auf eine leere Disk.

Sobald der Mount unter /mnt/disks/$UUID existiert, wird das vom Provisioner erkannt und ein PV-Objekt erstellt:

$ kubectl describe pv local-pv-bb1049dc 
Name:              local-pv-bb1049dc
Labels:            kubernetes.io/hostname=worker23
Annotations:       pv.kubernetes.io/provisioned-by: local-volume-provisioner-worker23-217e0fb5-8bf6-4489-90c5-b7829e7adcea
Finalizers:        [kubernetes.io/pv-protection]
StorageClass:      local-storage
Status:            Available
Claim:             
Reclaim Policy:    Delete
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          5000Mi
Node Affinity:     
  Required Terms:  
    Term 0:        kubernetes.io/hostname in [worker23]
Message:           
Source:
    Type:  LocalVolume (a persistent volume backed by local storage on a node)
    Path:  /mnt/disks/1c7d8705-d9e0-4982-b404-45a900f511d1
Events:    <none>

So können wir mit wenig Aufwand Volumes erstellen. Zusätzlich sind die Volumes unabhängig von der Haupt-Disk (vda) der VM. Mit noch etwas Installer-Konfiguration wäre es also sogar möglich, dass wir Node-VMs neu installieren können, ohne dass die wir Disks neu formatieren, d.h. die Daten würden erhalten bleiben.

Falls du auch Interesse hast solche Lösungen zu finden und an diesen zu arbeiten, bewirb dich doch bei uns.

(Header-Bild: avaragado from Cambridge, CC BY 2.0, via Wikimedia Commons)