Das Schöne an Proxmox VE (und generell an Open Source Software) ist, dass man nicht auf die Anwendungsmöglichkeiten beschränkt ist, die ein Hersteller für mich vorsieht.

Proxmox VE bindet FC- oder iSCSI-Storages normalerweise als Block-Storage ein. Das ist recht unkompliziert, performant und einfach zu erledigen. Allerdings lassen sich dann keine Snapshots von einzelnen Images oder VMs erzeugen. Storage-seitig können zwar Snapshots erzeugt werden (sofern die Storage das anbietet), aber dann wird eben die gesamte LUN einem Snapshot unterzogen.

Möchte man FC-/iSCSI-Storage und Proxmox-seitige Snapshots von einzelnen VMs, kann man mit Cluster-Filesystemen arbeiten, die bereits im Linux Kernel integriert sind wie beispielsweise GFS2.

Dieser Blog Artikel beschreibt die Vorgehensweise, wenn ein bestehender FC-/iSCSI-/GFS2-/Proxmox-Cluster um zusätzliche Server erweitert werden soll.

Ich setze voraus, das es bereits einen fertigen Proxmox VE Cluster mit funktionierendem Corosync gibt und iSCSI-LUNs bereits als Device auf dem Proxmox VE Server erscheinen.

Wie immer gilt: Vorher ein Backup erstellen für den Fall, dass etwas nicht nach Plan läuft oder völlig schief geht.

Schema der Erweiterung

%%{init: {'theme': 'neutral', "flowchart" : { "curve" : "basis" } } }%%
flowchart BT
    subgraph Storage
      SAN["FC-/iSCSI SAN (z.B. LUN0)"]
    end
    subgraph alt[Bestehende Server]
      style alt fill:lightgreen
      SAN -- FC / iSCSI --> PVE1
      SAN -- FC / iSCSI --> PVE2
      SAN -- FC / iSCSI --> PVE3
      PVE1["Proxmox VE"]
      PVE2["Proxmox VE"]
      PVE3["Proxmox VE"]
    end
    subgraph neu[zusätzlicher Server]
      style neu fill:yellow
      SAN -- FC / iSCSI --> PVE4
      PVE4["Proxmox VE"]
      linkStyle 3 stroke-dasharray: 3 3
    end
    subgraph LVM
      lvmlockd <==> LVM_VG1
      style lvmlockd fill:orange
      dlm <==> lvmlockd
      style dlm fill:orange
      PVE1 --> LVM_PV
      PVE2 --> LVM_PV
      PVE3 --> LVM_PV
      PVE4 -.-> LVM_PV
      LVM_PV["LVM PV (z.B. /dev/sdf)"] ---> LVM_VG1
      LVM_VG1["LVM VG1 (z.B. /dev/vg1)"] --> LVM_LV1
    end
    LVM_LV1["LVM LV1 (z.B. /dev/vg1/lv1)"] --BlockDevice--> GFS2_FS1["GFS2 FS1 (z.B. ClusterName:fs1)"] --FileSystem--> DIR_FS1["Mountpoint für FS1 (z.B. /mnt/gfs2-fs1)"]
    subgraph fs1[File Storage]
        DIR_FS1["Mountpoint (z.B. /mnt/gfs2-fs1)"] --/mnt/gfs2-fs1/images/100/--> VM_100["VM-Image (z.B. vm-100-disk-0.qcow2)"]
        DIR_FS1["Mountpoint (z.B. /mnt/gfs2-fs1)"] --/mnt/gfs2-fs1/images/101/--> VM_101["VM-Image (z.B. vm-101-disk-0.qcow2)"]
    end

Benötigte Pakete installieren

Zusätzlich zum bereits durch Proxmox VE verwendeten corosync benötigen wir einen Lockmanager für LVM sowie für später noch die Tools für das Management des GFS2:

apt install lvm2-lockd dlm-controld gfs2-utils

Nach der Installation der Pakete werden die Services bereits automatisch gestartet. Mit dmesg können wir das Ergebnis betrachten:

[ 2284.071591] DLM installed
[ 2285.994440] dlm: Using TCP for communications
[ 2285.994869] dlm: lvm_global: joining the lockspace group...
[ 2286.013531] dlm: lvm_global: dlm_recover 1
[ 2286.013554] dlm: lvm_global: group event done 0
[ 2286.013651] dlm: lvm_global: add member 4
[ 2286.013688] dlm: lvm_global: add member 3
[ 2286.013698] dlm: lvm_global: add member 2
[ 2286.013703] dlm: lvm_global: add member 1
[ 2286.013751] dlm: connecting to 3
[ 2286.014196] dlm: got connection from 2
[ 2286.014241] dlm: connecting to 2
[ 2286.014451] dlm: connecting to 1
[ 2286.014494] dlm: successful connected to node 3
[ 2286.014729] dlm: successful connected to node 2
[ 2286.014741] dlm: got connection from 1
[ 2286.014878] dlm: successful connected to node 1
[ 2286.014951] dlm: got connection from 3
[ 2286.015169] dlm: version 0x00030002 for node 2 detected
[ 2286.015472] dlm: version 0x00030002 for node 3 detected
[ 2286.015598] dlm: version 0x00030002 for node 1 detected
[ 2286.016968] dlm: lvm_global: dlm_recover_members 4 nodes
[ 2286.016985] dlm: lvm_global: join complete
[ 2286.042853] dlm: lvm_global: generation 3 slots 4 1:1 2:2 3:3 4:4
[ 2286.042860] dlm: lvm_global: dlm_recover_directory
[ 2286.044671] dlm: lvm_global: dlm_recover_directory 0 in 0 new
[ 2286.119042] dlm: lvm_global: dlm_recover_directory 0 out 3 messages
[ 2286.271171] dlm: lvm_global: dlm_recover 1 generation 3 done: 228 ms
[ 2287.048460] dlm: lvm_vg1: joining the lockspace group...
[ 2287.065781] dlm: lvm_vg1: dlm_recover 1
[ 2287.065821] dlm: lvm_vg1: group event done 0
[ 2287.065882] dlm: lvm_vg1: add member 4
[ 2287.065900] dlm: lvm_vg1: add member 3
[ 2287.065903] dlm: lvm_vg1: add member 2
[ 2287.065905] dlm: lvm_vg1: add member 1
[ 2287.067360] dlm: lvm_vg1: dlm_recover_members 4 nodes
[ 2287.067387] dlm: lvm_vg1: join complete
[ 2287.143024] dlm: lvm_vg1: generation 3 slots 4 1:1 2:2 3:3 4:4
[ 2287.143032] dlm: lvm_vg1: dlm_recover_directory
[ 2287.145123] dlm: lvm_vg1: dlm_recover_directory 1 in 1 new
[ 2287.170910] dlm: lvm_vg1: dlm_recover_directory 0 out 3 messages
[ 2287.226828] dlm: lvm_vg1: dlm_recover 1 generation 3 done: 84 ms

Mit dlm_tool status und dlm_tool ls können wir uns das Ergebnis danach auch noch mal anschauen:

root@pve4:~# dlm_tool status
cluster nodeid 4 quorate 1 ring seq 253 253
daemon now 2505 fence_pid 0 
node 1 M add 2283 rem 0 fail 0 fence 0 at 0 0
node 2 M add 2283 rem 0 fail 0 fence 0 at 0 0
node 3 M add 2283 rem 0 fail 0 fence 0 at 0 0
node 4 M add 2283 rem 0 fail 0 fence 0 at 0 0
root@pve4:~# dlm_tool ls
dlm lockspaces
name          lvm_vg1
id            0x31931822
flags         0x00000000 
change        member 4 joined 1 remove 0 failed 0 seq 1,1
members       1 2 3 4 

name          lvm_global
id            0x12aabd2d
flags         0x00000000 
change        member 4 joined 1 remove 0 failed 0 seq 1,1
members       1 2 3 4 

Das LVM Locking und DLM sind nun also betriebsbereit. Schauen wir uns die PV/VG/LV des LVM an:

root@pve4:~# pvs
  PV         VG  Fmt  Attr PSize   PFree  
  /dev/sda3  pve lvm2 a--  <99.50g <12.38g
  /dev/sdf   vg1 lvm2 a--  <99.97g 992.00m

Das PV (Physical Volume) sieht aus wie erwartet. Werfen wir einen Blick auf die Volume Group:

root@pve4:~# vgs
  VG  #PV #LV #SN Attr   VSize   VFree  
  pve   1   3   0 wz--n- <99.50g <12.38g
  vg1   1   2   0 wz--ns <99.97g 992.00m

Die Volume Group ist da und mit s = Shared gekennzeichnet. So soll das sein. Und das LV?

root@pve4:~# lvs
  LV       VG  Attr       LSize  Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  data     pve twi-a-tz-- 52.37g             0.00   1.59                            
  root     pve -wi-ao---- 24.75g                                                    
  swap     pve -wi-ao----  8.00g                                                    
  gfs2-lv1 vg1 -wi------- 50.00g                                                    

Das Logical Volume ist noch nicht aktiv, darum kümmern wir uns gleich. Zunächst müssen wir dafür sorgen, dass auch weitere Nodes in das GFS2 Filesystem aufgenommen werden können, in dem wir die Anzahl der GFS2-Journals prüfen und ggf. erweitern.

GFS2 Journals prüfen und ggf. erweitern

Pro Node im Cluster benötigt jedes GFS2-Filesystem ein eigenes Journal. Haben wir drei Nodes im Cluster, benötigen wir also mindestens 3 Journals. Soll eine hinzukommen wie bei uns, sollten wir die Anzahl prüfen und ggf. erhöhen. Es schadet auch nicht, die Anzahl der Journals vorsorglich höher auszulegen. Laut Dokumentation hat das keinen negativen Effekt, sorgt aber dafür dass wir später einfacher neue Nodes in den Cluster aufnehmen können.

Auf einer bereits bestehenden Node im Cluster können wir das mit gfs2_edit -p jindex <device> | grep journal herausfinden:

root@pve1:~# gfs2_edit -p jindex /dev/vg1/gfs2-lv1 | grep journal
   3/3 [fc7745eb] 1/20 (0x1/0x14) +0: File    journal0
   4/4 [8b70757d] 2/32859 (0x2/0x805b) +0: File    journal1
   5/5 [127924c7] 3/65698 (0x3/0x100a2) +0: File    journal2

Um eine vierte Node in den GFS2-Cluster aufnehmen zu können müssen wir also mindestens ein weiteres Journal erzeugen. Da etwas mehr Journale nicht schaden, erzeugen wir direkt zwei weitere für den Fall dass noch mal eine neue Node hinzugefügt werden soll. Dazu verwenden wir gfs2_jadd -j <anzahl zusätzliche logs> -J <log size> <device>.

Nicht ungeduldig werden - das kann einige Minuten oder länger dauern und man erhält in der Zwischenzeit keine Ausgabe.

root@pve1:~# gfs2_jadd -j 2 -J 128 /mnt/gfs2-lv1/
Filesystem: /mnt/gfs2-lv1/
Old journals: 3
New journals: 5

Diesen Vorgang müssen wir für jedes GFS2-Filesystem wiederholen, das auf die weiteren Nodes ausgerollt werden soll.

Logical Volumes aktivieren im Shared Mode

Die LVs auf denen die GFS2-Filesysteme liegen sind auf den anderen Nodes ja bereits gemounted. Wir müssen LVM nun also mitteilen, dass wir diese auf dem neuen Host ebenfalls shared mounten möchten, da sie sonst aus Sicherheitsgründen exklusiv gemounted werden.

Mit lvchange -asy können wir das Logical Volume aktivieren. Das “s” muss angegeben werden um das LV shared zu mounten. Geben wir das nicht an, wird versucht das LV im “e” = exclusive Mode zu mounten und stünde nur auf einer Node zur Verfügung. Damit wäre es nicht als Grundlage für unser GFS2 geeignet.

root@pve4:~# lvchange -asy /dev/vg1/gfs2-lv1 
root@pve4:~# lvs
  LV       VG  Attr       LSize  Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  data     pve twi-a-tz-- 52.37g             0.00   1.59                            
  root     pve -wi-ao---- 24.75g                                                    
  swap     pve -wi-ao----  8.00g                                                    
  gfs2-lv1 vg1 -wi-a----- 50.00g                                                    

Dies müssen wir für jedes gewünschte GFS2-Filesystem wiederholen.

GFS2 mounten

Nun können wir das GFS2-Filesystem unter dem gewünschten Mountpoint einhängen:

root@pve4:~# mkdir /mnt/gfs2-lv1
root@pve4:~# mount /dev/vg1/gfs2-lv1 /mnt/gfs2-lv1/
root@pve4:~# df -h
Filesystem                             Size  Used Avail Use% Mounted on
[...]
/dev/mapper/vg1-gfs2--lv1               50G   11G   40G  22% /mnt/gfs2-lv1
[...]

Directory Storage in Proxmox hinzufügen

Damit wir dort qcow2-Images für VMs hinlegen können, müssen wir die Storage als shared Directory-Storage in Proxmox noch anlegen. Das lässt sich entweder über die Web-GUI oder direkt über die Shell erledigen:

pvesm add dir gfs2-lv1 --path /mnt/gfs2-lv1 --content images,rootdir --shared 1

Soll das so?

Normalerweise sollte sich ein Cluster Manager wie Pacemaker um das automatische Starten, Monitoren und Stoppen der benötigten Komponenten und Dienste kümmern. Da Proxmox einen eigenen Cluster Manager verwendet, der sich allerdings ausschließlich um VMs und Container kümmert, müssen wir die einzelnen Komponenten unmanaged verwenden (Notiz an mich: Vielleicht gibt’s da noch bessere Varianten - mal darüber nachdenken).

In besagter Umgebung habe ich zwar das iSCSI-Login auf automatisch gesetzt, lvmlockd und dlm habe ich allerdings auf disabled gesetzt um sie nach dem Systemstart dann manuell hochzufahren und das GFS anschließend einzubinden.

systemctl disable lvmlockd dlm

lvmlockd und dlm starte ich also nach einem Host Neustart manuell:

systemctl start lvmlockd dlm

Anschließend mounte ich das GFS2 dann als Filesystem:

mount -t gfs2 /dev/vg1/lv1 /mnt/gfs2-lv1

Mir persönlich ist das zu viel Handarbeit für ein Cluster Filesystem. Zumal die Lösung oben nicht einfach zu supporten ist. Ich würde sie daher auch nur in Ausnahmefällen einsetzen und regulär eher darauf verzichten.