diff --git a/proxmox-linstor-sds-cluster/content/index.md b/proxmox-linstor-sds-cluster/content/index.md new file mode 100644 index 0000000..87a7c38 --- /dev/null +++ b/proxmox-linstor-sds-cluster/content/index.md @@ -0,0 +1,728 @@ +--- +title: "Chef's special: Proxmox, SDS (linstor) & Cluster" +date: 2025-11-14 +slug: proxmox-linstor-sds-cluster +status: +tags: + - proxmox + - linstor + - sds + - network + - cluster + - homelab + - blog +category: homelab +--- + +

Cooking lessons 101: Proxmox, LINSTOR, and 10G cards

+

Homelab-ing should be fun, right? Right.

+

Until you decided to go down the path of what I call : the not-so-home-but-more-like-prod-lab-ing

+

Well, I recently had the joy (read: chaos) of upgrading from a single Proxmox node to a beautiful, powerful and shiny 3-node cluster. Sounds great on paper until it's 3 AM and i'm wondering why the third node won't do the exact same thing as the first two.

+

So today, let’s unpack the mess together. We’ll go over:

+ +

Current node, peony :

+

Before migrating, i had a single proxmox node that we'll call pve01 or peony, both works.

+

This beauty has been running on :

+ +

I chose this build for a few reasons : I needed 2 nvme Gen4 ports to be able to run a zfs-pool with the 980 pro at full speed. And I also wanted to have an iGPU because I had no needs for an entire GPU and it's power consumption. +I also chose an old Mellanox X-3 for an LACP bond to fully use my USW-Aggregation.

+

The two newborn nodes, petunia and dahlia :

+

Those two nodes, pve02 and pve03 aka petunia and dahlia have the same hardware :

+ +

And I added to all three nodes a dual 1G NIC for a ring-network dedicated to the proxmox cluster & the corosync service.

+

As for what a ring-network is, it's a basic network topology where each nodes is connected to two other nodes, without using a switch.

+

So it should look like this :

+

pve01 :

+
pve01.nic1 <=> pve02.nic1
+pve01.nic2 <=> pve03.nic1
+

pve02 :

+
pve02.nic1 <=> pve01.nic1
+pve02.nic2 <=> pve03.nic2
+

pve03 :

+
pve03.nic1 <=> pve01.nic1
+pve03.nic2 <=> pve02.nic2
+

The SDS rabbit-hole :

+

As this homelab is changing, updating and upgrading into more like a production-ready setup, I decided to go down the HA (High Availability) path since I had 3 nodes now.

+

For simple HA in a Hypervisor-OS cluster (let it be Proxmox, HyperV, ESXI, OpenNebula, etc) you'll need one thing :

+ +

For a more efficient HA in a Hypervisor-OS cluster (let it be Proxmox, HyperV, ESXI, OpenNebula, etc) you'll need two things :

+ +

In PVE (Proxmox Virtual Environnement), there's a few ways to achieve this (there may be more, I only know and used those):

+ +

ZFS Replication :

+

If you are planning to use ZFS for your storage pool, ZFS replication may be a solution. It'll replicate the snapshot of the disk every x period of time (could be every minute, every 15 minutes; whatsoever). So if HA (or yourself) decided to migrate the disk, you'll only lose the data that hasn't been synced by the ZFS replication. This works like a charm, without a lot of configuration, but it wasn't what I wanted. I wanted something more... sophisticated, I'm an homelabber after all, I have no need but greed.

+
     [pve01]                          [pve02]
+   ┌──────────┐                   ┌─────────────┐
+   │  VM Disk │── Snapshot --->   │  VM Disk    │
+   │  Pool A  │                   │  Pool B     │
+   └──────────┘                   └─────────────┘
+         ↑                               ↑
+       Writes                           Replicated every X min
+

CephFS :

+

CephFS is actually full integrated within the PVE ecosystem and could have been a solution. It uses OSD and CephMON (Monitor) that are lightweight nodes that keep track of the cluster's health, metadata and where the OSD are so that it can recover automatically if a node fails. +The hardware requirements are quite heavy :

+ +

This could have been a way for me, easy to setup. But the hardware requirements were too heavy for me and since I already had a 10G switch, I feared it would be very slow and be a nightmare to optimize it.

+
            +----------------+
+            |     MONITOR    |
+            |    (Ceph MON)  |
+            +----------------+
+                    ▲
+      ┌─────────────┼─────────────┐
+      ▼             ▼             ▼
+ +---------+   +---------+   +---------+
+ |  OSD 1  |   |  OSD 2  |   |  OSD 3  |
+ | NVMe+HDD|   | NVMe+HDD|   | NVMe+HDD|
+ +---------+   +---------+   +---------+
+      ▲             ▲             ▲
+      │             │             │
+      └─────────────┴─────────────┘
+                    │
+            Clients (PVE Nodes)
+
+

iSCSI (Internet Small Computer System Iinterface) :

+

In easy term, let's say I have a NAS with disks. I create an iSCSI share, which will be mounted to each PVE nodes of the cluster. I can now create VM/LXC on this storage, and since it'll be shared across the cluster, I could migrate it easily.

+

For the PVE nodes, it would look like they each have the same big disk mounted.

+
       +--------------------+
+       |       NAS          |
+       |   iSCSI Target     |
+       +--------------------+
+         │       │       │
+         │       │       │
+   +-----+       |       +-----+
+   |             |             |
+[pve01]       [pve02]       [pve03]
+   │             │             │
+   └─────────────┴─────────────┘
+            Shared Disk
+(appears as the same big local disk)
+

The chosen one : Linstor :

+

Linstor is kinda a mix of CephFS and iSCSI. It's an open-source storage management solution for block storage (like iSCSI do). +It works off 3 Core Components :

+ +
          +------------------+
+          |LINSTOR Controller|
+          +------------------+
+                    │
+   ┌────────────────┴─────────────────┐
+   ▼                                  ▼
++-------+                         +-------+
+| Satel |                         | Satel |
+| Disk1 |                         | Node2 |
+| Disk1 |       <-- Rep -->       | Disk1 |
++-------+                         +-------+
+   │                                  │
+[pve01]                            [pve02]
+   │                                  │
+   └── Volumes shared across nodes. ──┘
+

Wait, this could work, this could be it. This could be challenging and fun. So I decided to spin up a few VMs, install PVE on it and tried thing. After a few days of trial and error, I finally obtained a virtual cluster with a distributed storage across it, working like a charm !

+

How I actually set up the cluster :

+

As I said before, I used a ring network and Distributed Storage for a better HA.

+

The ring network :

+

First thing first, how to setup the ring-network :

+

Connect each nodes to each other. You'll need 3 networks (pve01 to pve02, pve01 to pve03 and pve02 to pve03), I went with three /29, which gives me plenty of room with 6 IP Adresses.

+ +

You may be wondering, where the hell is the 10.10.1.1/29 and why did he chose the 10.10.3.1/29 network. Great answer.

+

Somehow, against all logic, it just wouldn't work.

+

Trust me. I was there, staring at my computer screen until 3 AM, wondering why the hell 10.10.1.1/29 & 10.10.1.2/29 were not working. Until I simply tried the 10.10.3.1/29 and it worked top notch, so I went with it. Goodbye my 10.10.1.1/29 and 10.10.1.2/29, I truly loved you (f*ck you I hate you).

+

You'll also need to enable routing on each of your proxmox nodes so that pve01 can talk with pve03 via the pve02 link. Quite a headache isn't it ?

+

On pve01 :

+
interlope@pivoine:~$ ip -br -c a | grep 10.10
+enp7s0           UP             10.10.0.1/29 fe80::1efd:8ff:fe7a:e2b8/64
+enp8s0           UP             10.10.3.1/29 fe80::1efd:8ff:fe7a:e08c/64
+
interlope@pivoine:~$ cat /etc/network/interfaces | grep route
+        post-up ip route add 10.10.2.0/29 via 10.10.0.2 dev enp7s0
+
root@pivoine:/home/interlope# traceroute 10.10.2.2
+traceroute to 10.10.2.2 (10.10.2.2), 30 hops max, 60 byte packets
+ 1  10.10.0.2 (10.10.0.2)  0.095 ms  0.076 ms  0.057 ms
+ 2  10.10.2.2 (10.10.2.2)  0.120 ms  0.105 ms  0.119 ms
+

It can join the pve03 while passing by the pve02 node.

+

On pve02 :

+
interlope@petunia:~$ ip -br -c a | grep 10.10
+enp8s0           UP             10.10.0.2/29 fe80::1efd:8ff:fe7a:e239/64
+enp9s0           UP             10.10.2.1/29 fe80::1efd:8ff:fe7a:e23a/64
+
interlope@petunia:~$ cat /etc/network/interfaces | grep route
+        post-up ip route add 10.10.3.0/29 via 10.10.2.2 dev enp9s0
+
root@petunia:/home/interlope# traceroute 10.10.3.1
+traceroute to 10.10.3.1 (10.10.3.1), 30 hops max, 60 byte packets
+ 1  10.10.2.2 (10.10.2.2)  0.091 ms  0.063 ms  0.088 ms
+ 2  10.10.3.1 (10.10.3.1)  0.140 ms  0.130 ms  0.121 ms
+

It can join the pve01 while passing by the pve03 node.

+

On pve03 :

+
interlope@dalia:~$ ip -br -c a | grep 10.10
+enp8s0           UP             10.10.3.2/29 fe80::1efd:8ff:fe7a:e497/64
+enp9s0           UP             10.10.2.2/29 fe80::2e0:4cff:fe68:f52/64
+
interlope@dalia:~$ cat /etc/network/interfaces | grep route
+        post-up ip route add 10.10.0.0/29 via 10.10.3.1 dev enp8s0
+
root@dalia:/home/interlope# traceroute 10.10.0.2
+traceroute to 10.10.0.2 (10.10.0.2), 30 hops max, 60 byte packets
+ 1  10.10.3.1 (10.10.3.1)  0.106 ms * *
+ 2  10.10.0.2 (10.10.0.2)  0.135 ms  0.119 ms  0.152 ms
+

It can join the pve02 while passing by the pve01 node.

+

Now I can simply create my cluster homegarden on pve01, add the two NIC links and join it with pve02 and pve03, wonderful.

+

As for the SDS, that's a whole other ball game.

+

Linstor :

+

This tutorial will be written this way : each step will tell you on which node to execute the commands :

+ +
wget -O /tmp/linbit-keyring.deb https://packages.linbit.com/public/linbit-keyring.deb
+dpkg -i /tmp/linbit-keyring.deb
+PVERS=9 && echo "deb [signed-by=/etc/apt/trusted.gpg.d/linbit-keyring.gpg] http://packages.linbit.com/public/ proxmox-$PVERS drbd-9" > /etc/apt/sources.list.d/linbit.list
+
+apt update
+apt -y install drbd-dkms drbd-utils drbd-reactor proxmox-default-headers linstor-proxmox linstor-controller linstor-satellite linstor-client linstor-proxmox linstor-gui vim
+ +
modprobe drbd
+echo "drbd" >> /etc/modules
+reboot
+ +
systemctl enable --now linstor-controller
+ +
cat <<EOF > /etc/linstor/linstor-client.conf
+[global]
+controllers=ipnode1,ipnode2,ipnode3
+EOF
+ +
systemctl edit linstor-satellite.service 
+[Service]
+Type=notify
+TimeoutStartSec=infinity
+Environment=LS_KEEP_RES=linstor_db
+ +
linstor node create hostnamenode01 ipnode1
+linstor node create hostnamenode02 ipnode2 
+linstor node create hostnamenode03 ipnode3
+linstor node set-property hostnamenode01 DrbdOptions/AutoEvictAllowEviction false
+linstor node set-property hostnamenode02 DrbdOptions/AutoEvictAllowEviction false
+linstor node set-property hostnamenode03 DrbdOptions/AutoEvictAllowEviction false
+

Note : the DrbdOptions/AutoEvictAllowEviction false is to avoid your nodes to be auto-removed from the linstor cluster during temporary issues and to also avoid split-brain scenarios (data still being written on a node that has lost communication with the cluster), creating data corruption and divergent data.

+

Which should looks like this :

+
interlope@pivoine:~$ linstor node list
+╭──────────────────────────────────────────────────────────╮
+┊ Node    ┊ NodeType  ┊ Addresses                 ┊ State  ┊
+╞══════════════════════════════════════════════════════════╡
+┊ dalia   ┊ SATELLITE ┊ 10.100.4.103:3366 (PLAIN) ┊ Online ┊
+┊ petunia ┊ SATELLITE ┊ 10.100.4.102:3366 (PLAIN) ┊ Online ┊
+┊ pivoine ┊ SATELLITE ┊ 10.100.4.101:3366 (PLAIN) ┊ Online ┊
+╰──────────────────────────────────────────────────────────╯
+

I decided to go down the path of a ZFS pool on each node with raid1 (mirroring) between the two nvmes, do what suits yourself and your cluster :

+
zpool create pool-nvme-01 mirror /dev/nvme0n1 /dev/nvme1n1
+ +
linstor storage-pool create zfsthin pve01 sto-pool_zfs-01 pool-nvme-01
+linstor storage-pool create zfsthin pve02 sto-pool_zfs-01 pool-nvme-01
+linstor storage-pool create zfsthin pve03 sto-pool_zfs-01 pool-nvme-01
+ +
linstor resource-group create res-gr_pve --storage-pool sto-pool_zfs-01 --place-count 2 --diskless-on-remaining true
+ +
linstor resource-group create --storage-pool sto-pool_zfs-01 --place-count 3 --diskless-on-remaining true linstor-db-grp
+linstor resource-group drbd-options --auto-promote=no --quorum=majority --on-suspended-primary-outdated=force-secondary --on-no-quorum=io-error --on-no-data-accessible=io-error linstor-db-grp
+linstor resource-group spawn-resources linstor-db-grp linstor_db 200M
+ +
systemctl disable --now linstor-controller
+ +
systemctl disable linstor-controller
+ +

This is needed to achieve HA-controller since normally, the linstor-controller stores all the metadata storage on a local disk. But if another node becomes the linstor-controller instead, it'll need to have those metadata, so we share it via a resource-group we created earlier.

+
cat > /etc/systemd/system/var-lib-linstor.mount << 'EOF'
+[Unit]
+Description=Filesystem for the LINSTOR controller
+
+[Mount]
+What=/dev/drbd/by-res/linstor_db/0
+Where=/var/lib/linstor
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl daemon-reload
+ +
mv /var/lib/linstor{,.orig} 
+mkdir /var/lib/linstor 
+chattr +i /var/lib/linstor 
+drbdadm primary linstor_db 
+mkfs.ext4 /dev/drbd/by-res/linstor_db/0 
+systemctl start var-lib-linstor.mount 
+cp -r /var/lib/linstor.orig/* /var/lib/linstor 
+systemctl start linstor-controller
+ +
systemctl stop linstor-controller
+chattr +i /var/lib/linstor
+ +

This will allow the drdb-reactor to chose who's the linstor-controller.

+
cat <<EOF > /etc/drbd-reactor.d/linstor_db.toml
+[[promoter]]
+[promoter.resources.linstor_db]
+start = ["var-lib-linstor.mount", "linstor-controller.service"]
+EOF
+
systemctl restart drbd-reactor
+systemctl enable drbd-reactor
+systemctl restart linstor-satellite
+

Check the logs with drbd-reactorctl status linstor_db and systemctl status drbd-reactor, it should look like this :

+
root@pivoine:/home/interlope# drbd-reactorctl status linstor_db
+/etc/drbd-reactor.d/linstor_db.toml:
+Promoter: Currently active on node 'petunia'
+○ drbd-services@linstor_db.target
+○ ├─ drbd-promote@linstor_db.service
+○ ├─ var-lib-linstor.mount 
+○ └─ linstor-controller.service 
+

pve02 aka petunia is currently the linstor-controller.

+

Now, i've tuned this a little bit since i've created a VIP (Virtual IP) via keepalived to access :

+ +

But how does this works ? Each node has a keepalived configuration that basically says :

+

If you have the linstor-controller running, TAKE THAT IP and don't asks questions

+

We saw earlier that pve02 was the current controller. If we look at his IP adresses, the VIP should be there :

+
interlope@petunia:~$ ip -br -c a | grep .100
+enp1s0f1np1      UP             10.100.4.102/24 10.100.4.100/24 fe80::3efd:feff:fee5:5535/64 
+vmbr1.3@vmbr1    UP             10.100.3.102/24 10.100.3.100/24 fe80::3efd:feff:fee5:5534/64
+

We see that on the linstor NIC and the proxmox NIC, it got a second ip, the .100, which is shared between all three nodes.

+

This is also needed for our linstor-controller-HA goal.

+ +
apt install keepalived -y
+ +
cat <<'EOF' > /usr/local/bin/check_linstor.sh
+#!/bin/bash
+# Checks if LINSTOR controller service is active
+if systemctl is-active --quiet linstor-controller.service; then
+  exit 0
+else
+  exit 1
+fi
+EOF
+
+chmod +x /usr/local/bin/check_linstor.sh
+ +
cat <<EOF > /etc/keepalived/keepalived.conf
+vrrp_script chk_linstor {
+    script "/usr/local/bin/check_linstor.sh"
+    interval 2
+    timeout 2
+    fall 2
+    rise 1
+}
+
+vrrp_instance VI_1 {
+    state BACKUP
+    interface vmbr0
+    virtual_router_id 51
+    priority 150
+    advert_int 1
+    authentication {
+        auth_type PASS
+        auth_pass mySecret123
+    }
+    virtual_ipaddress {
+        10.100.4.100/24 dev ens19 # LINSTOR NIC
+        10.100.3.100/24 dev vmbr1.3 #PROXMOX PUBLIC NIC
+    }
+    track_script {
+        chk_linstor
+    }
+}
+EOF
+
+systemctl enable keepalived
+systemctl start keepalived
+

On the node who has the linstor-controller service up and running, once you've started the keepalived service, you should see a secondary ip on it. +Now that the linstor VIP is configured, that the linstor-controller-HA is working, let's use our storage !

+ +

+drbd: linstor
+    content images,rootdir
+    controller VIPADRESS
+    resourcegroup res-gr_pve
+
+systemctl restart pve-cluster pvedaemon pvestatd pveproxy pve-ha-lrm
+

How I use the VIP :

+

I've DNS entries poiting the 3.100 and 4.100 to my nginx reverse-proxy which then redirect the 3.100 to pve.local.ndd and 4.100 to linstor.local.ndd so that I can access both the cluster and the lisntor-gui on what-ever node it is running on.

+

Monitoring via prometheus :

+

If you ever want to do some dashboards via grafana and prometheus, on all nodes:

+
cat << EOF > /etc/drbd-reactor.d/prometheus.toml
+[[prometheus]]
+enums = true
+address = "0.0.0.0:9942"
+EOF
+
+systemctl restart drbd-reactor.service
+

Benchmarks :

+

Now that everything is done, joined, clustered and linstored, let's see what we have there : +Image description

+

Each node having a ZFS pool created with default options, it should be on standard.

+
interlope@pivoine:~$ zfs get sync pool-nvme-01
+NAME          PROPERTY  VALUE     SOURCE
+pool-nvme-01  sync      standard  local
+

There's 3 possibles values :

+ +

Let's benchmark it on a VM to get some results :

+ +
root@guinea-pig:/mnt/fio# fio --name=sync-write --filename=/mnt/fio/testfile --size=1G --bs=4k \
+    --rw=write --iodepth=1 --direct=1 --sync=1 --runtime=60 --time_based --group_reporting
+sync-write: (g=0): rw=write, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=1
+fio-3.39
+Starting 1 process
+sync-write: Laying out IO file (1 file / 1024MiB)
+Jobs: 1 (f=1): [W(1)][100.0%][w=604KiB/s][w=151 IOPS][eta 00m:00s]
+sync-write: (groupid=0, jobs=1): err= 0: pid=2112: Fri Nov 14 20:20:26 2025
+  write: IOPS=137, BW=552KiB/s (565kB/s)(32.3MiB/60005msec); 0 zone resets
+    clat (msec): min=5, max=277, avg= 7.24, stdev= 3.55
+     lat (msec): min=5, max=277, avg= 7.24, stdev= 3.55
+    clat percentiles (msec):
+     |  1.00th=[    6],  5.00th=[    6], 10.00th=[    6], 20.00th=[    6],
+     | 30.00th=[    7], 40.00th=[    7], 50.00th=[    7], 60.00th=[    7],
+     | 70.00th=[    8], 80.00th=[    9], 90.00th=[   10], 95.00th=[   11],
+     | 99.00th=[   16], 99.50th=[   18], 99.90th=[   23], 99.95th=[   26],
+     | 99.99th=[  279]
+   bw (  KiB/s): min=  192, max=  656, per=99.83%, avg=551.92, stdev=95.14, samples=119
+   iops        : min=   48, max=  164, avg=137.97, stdev=23.78, samples=119
+  lat (msec)   : 10=94.14%, 20=5.69%, 50=0.16%, 500=0.01%
+  cpu          : usr=0.08%, sys=0.85%, ctx=16563, majf=0, minf=8
+  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
+     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     issued rwts: total=0,8280,0,0 short=0,0,0,0 dropped=0,0,0,0
+     latency   : target=0, window=0, percentile=100.00%, depth=1
+
+Run status group 0 (all jobs):
+  WRITE: bw=552KiB/s (565kB/s), 552KiB/s-552KiB/s (565kB/s-565kB/s), io=32.3MiB (33.9MB), run=60005-60005msec
+
+Disk stats (read/write):
+    dm-1: ios=3/50255, sectors=24/430144, merge=0/0, ticks=0/94740, in_queue=94740, util=97.46%, aggrios=3/33377, aggsectors=24/430816, aggrmerge=0/17008, aggrticks=1/63005, aggrin_queue=81082, aggrutil=97.32%
+  sda: ios=3/33377, sectors=24/430816, merge=0/17008, ticks=1/63005, in_queue=81082, util=97.32%
+
root@guinea-pig:/mnt/fio# fio --name=async-write --filename=/mnt/fio/testfile --size=1G --bs=4k \
+    --rw=write --iodepth=16 --direct=1 --runtime=60 --time_based --group_reporting
+async-write: (g=0): rw=write, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=16
+fio-3.39
+Starting 1 process
+note: both iodepth >= 1 and synchronous I/O engine are selected, queue depth will be capped at 1
+Jobs: 1 (f=1): [W(1)][100.0%][w=2242KiB/s][w=560 IOPS][eta 00m:00s]
+async-write: (groupid=0, jobs=1): err= 0: pid=2115: Fri Nov 14 20:21:28 2025
+  write: IOPS=569, BW=2280KiB/s (2334kB/s)(134MiB/60002msec); 0 zone resets
+    clat (usec): min=603, max=16360, avg=1752.90, stdev=679.44
+     lat (usec): min=604, max=16360, avg=1753.14, stdev=679.43
+    clat percentiles (usec):
+     |  1.00th=[ 1156],  5.00th=[ 1188], 10.00th=[ 1205], 20.00th=[ 1237],
+     | 30.00th=[ 1270], 40.00th=[ 1303], 50.00th=[ 1401], 60.00th=[ 2114],
+     | 70.00th=[ 2180], 80.00th=[ 2212], 90.00th=[ 2311], 95.00th=[ 2442],
+     | 99.00th=[ 3589], 99.50th=[ 4146], 99.90th=[ 8717], 99.95th=[11731],
+     | 99.99th=[15664]
+   bw (  KiB/s): min= 1632, max= 2664, per=100.00%, avg=2281.08, stdev=181.23, samples=119
+   iops        : min=  408, max=  666, avg=570.27, stdev=45.31, samples=119
+  lat (usec)   : 750=0.01%, 1000=0.02%
+  lat (msec)   : 2=55.05%, 4=44.28%, 10=0.57%, 20=0.07%
+  cpu          : usr=0.27%, sys=1.97%, ctx=34203, majf=0, minf=8
+  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
+     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     issued rwts: total=0,34195,0,0 short=0,0,0,0 dropped=0,0,0,0
+     latency   : target=0, window=0, percentile=100.00%, depth=16
+
+Run status group 0 (all jobs):
+  WRITE: bw=2280KiB/s (2334kB/s), 2280KiB/s-2280KiB/s (2334kB/s-2334kB/s), io=134MiB (140MB), run=60002-60002msec
+
+Disk stats (read/write):
+    dm-1: ios=3/34301, sectors=24/274320, merge=0/0, ticks=0/58832, in_queue=58832, util=97.84%, aggrios=3/34256, aggsectors=24/274800, aggrmerge=0/105, aggrticks=0/59357, aggrin_queue=59414, aggrutil=97.70%
+  sda: ios=3/34256, sectors=24/274800, merge=0/105, ticks=0/59357, in_queue=59414, util=97.70%
+ +
root@guinea-pig:/mnt/fio# fio --name=sync-write --filename=/mnt/fio/testfile --size=1G --bs=4k     --rw=write --iodepth=1 --direct=1 --sync=1 --runtime=60 --time_based --group_reporting
+sync-write: (g=0): rw=write, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=1
+fio-3.39
+Starting 1 process
+Jobs: 1 (f=1): [W(1)][100.0%][w=4580KiB/s][w=1145 IOPS][eta 00m:00s]
+sync-write: (groupid=0, jobs=1): err= 0: pid=2129: Fri Nov 14 20:24:56 2025
+  write: IOPS=1527, BW=6112KiB/s (6259kB/s)(358MiB/60001msec); 0 zone resets
+    clat (usec): min=131, max=9992, avg=653.09, stdev=344.29
+     lat (usec): min=132, max=9992, avg=653.29, stdev=344.30
+    clat percentiles (usec):
+     |  1.00th=[  182],  5.00th=[  210], 10.00th=[  237], 20.00th=[  285],
+     | 30.00th=[  359], 40.00th=[  660], 50.00th=[  725], 60.00th=[  783],
+     | 70.00th=[  832], 80.00th=[  889], 90.00th=[  979], 95.00th=[ 1045],
+     | 99.00th=[ 1369], 99.50th=[ 1942], 99.90th=[ 3130], 99.95th=[ 4228],
+     | 99.99th=[ 8455]
+   bw (  KiB/s): min= 4128, max=12736, per=100.00%, avg=6129.87, stdev=2924.21, samples=119
+   iops        : min= 1032, max= 3184, avg=1532.46, stdev=731.06, samples=119
+  lat (usec)   : 250=13.32%, 500=20.58%, 750=20.45%, 1000=37.72%
+  lat (msec)   : 2=7.46%, 4=0.40%, 10=0.06%
+  cpu          : usr=0.66%, sys=5.32%, ctx=211682, majf=0, minf=9
+  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
+     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     issued rwts: total=0,91681,0,0 short=0,0,0,0 dropped=0,0,0,0
+     latency   : target=0, window=0, percentile=100.00%, depth=1
+
+Run status group 0 (all jobs):
+  WRITE: bw=6112KiB/s (6259kB/s), 6112KiB/s-6112KiB/s (6259kB/s-6259kB/s), io=358MiB (376MB), run=60001-60001msec
+
+Disk stats (read/write):
+    dm-1: ios=0/421324, sectors=0/2637864, merge=0/0, ticks=0/81948, in_queue=81948, util=88.37%, aggrios=0/304112, aggsectors=0/2642352, aggrmerge=0/117885, aggrticks=0/54648, aggrin_queue=58947, aggrutil=87.88%
+  sda: ios=0/304112, sectors=0/2642352, merge=0/117885, ticks=0/54648, in_queue=58947, util=87.88%
+
root@guinea-pig:/mnt/fio# fio --name=async-write --filename=/mnt/fio/testfile --size=1G --bs=4k \
+    --rw=write --iodepth=16 --direct=1 --runtime=60 --time_based --group_reporting
+async-write: (g=0): rw=write, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=psync, iodepth=16
+fio-3.39
+Starting 1 process
+note: both iodepth >= 1 and synchronous I/O engine are selected, queue depth will be capped at 1
+Jobs: 1 (f=1): [W(1)][100.0%][w=16.5MiB/s][w=4230 IOPS][eta 00m:00s]
+async-write: (groupid=0, jobs=1): err= 0: pid=2132: Fri Nov 14 20:25:58 2025
+  write: IOPS=4169, BW=16.3MiB/s (17.1MB/s)(977MiB/60001msec); 0 zone resets
+    clat (usec): min=95, max=11141, avg=238.70, stdev=128.18
+     lat (usec): min=95, max=11142, avg=238.87, stdev=128.19
+    clat percentiles (usec):
+     |  1.00th=[  135],  5.00th=[  155], 10.00th=[  174], 20.00th=[  186],
+     | 30.00th=[  196], 40.00th=[  206], 50.00th=[  225], 60.00th=[  241],
+     | 70.00th=[  255], 80.00th=[  277], 90.00th=[  310], 95.00th=[  347],
+     | 99.00th=[  474], 99.50th=[  693], 99.90th=[ 1762], 99.95th=[ 2245],
+     | 99.99th=[ 4883]
+   bw (  KiB/s): min=14352, max=18576, per=100.00%, avg=16689.14, stdev=751.16, samples=119
+   iops        : min= 3588, max= 4644, avg=4172.29, stdev=187.79, samples=119
+  lat (usec)   : 100=0.01%, 250=66.85%, 500=32.25%, 750=0.46%, 1000=0.14%
+  lat (msec)   : 2=0.23%, 4=0.06%, 10=0.02%, 20=0.01%
+  cpu          : usr=1.02%, sys=9.82%, ctx=250210, majf=0, minf=11
+  IO depths    : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
+     submit    : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     complete  : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
+     issued rwts: total=0,250199,0,0 short=0,0,0,0 dropped=0,0,0,0
+     latency   : target=0, window=0, percentile=100.00%, depth=16
+
+Run status group 0 (all jobs):
+  WRITE: bw=16.3MiB/s (17.1MB/s), 16.3MiB/s-16.3MiB/s (17.1MB/s-17.1MB/s), io=977MiB (1025MB), run=60001-60001msec
+
+Disk stats (read/write):
+    dm-1: ios=0/249989, sectors=0/1999824, merge=0/0, ticks=0/54784, in_queue=54784, util=91.31%, aggrios=0/250291, aggsectors=0/2003080, aggrmerge=0/105, aggrticks=0/56227, aggrin_queue=56228, aggrutil=91.21%
+  sda: ios=0/250291, sectors=0/2003080, merge=0/105, ticks=0/56227, in_queue=56228, util=91.21%
+

Those speeds are kinda slow, I'm not gonna lie to you. However, it can be explained because of a few factors : Linstor Sync, ZFS Sync + VM overhead. But it works like a charm, i've no issues with speed at all nor needs.

+ +

pve01 to pve03 :

+
root@pivoine:/home/interlope# iperf3 -c 10.100.3.103
+Connecting to host 10.100.3.103, port 5201
+[  5] local 10.100.3.101 port 54678 connected to 10.100.3.103 port 5201
+[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
+[  5]   0.00-1.00   sec  1.09 GBytes  9.38 Gbits/sec   33   1.21 MBytes
+[  5]   1.00-2.00   sec  1.09 GBytes  9.38 Gbits/sec    0   1.22 MBytes
+[  5]   2.00-3.00   sec  1.09 GBytes  9.37 Gbits/sec    0   1.22 MBytes
+[  5]   3.00-4.00   sec  1.09 GBytes  9.37 Gbits/sec    0   1.23 MBytes
+[  5]   4.00-5.00   sec  1.09 GBytes  9.38 Gbits/sec    1   1.23 MBytes
+[  5]   5.00-6.00   sec  1.09 GBytes  9.37 Gbits/sec    0   1.23 MBytes
+[  5]   6.00-7.00   sec  1.09 GBytes  9.37 Gbits/sec    0   1.24 MBytes
+[  5]   7.00-8.00   sec  1.09 GBytes  9.37 Gbits/sec    0   1.24 MBytes
+[  5]   8.00-9.00   sec  1.09 GBytes  9.36 Gbits/sec    0   1.27 MBytes
+[  5]   9.00-10.00  sec  1.09 GBytes  9.38 Gbits/sec    0   1.29 MBytes
+- - - - - - - - - - - - - - - - - - - - - - - - -
+[ ID] Interval           Transfer     Bitrate         Retr
+[  5]   0.00-10.00  sec  10.9 GBytes  9.37 Gbits/sec   34            sender
+[  5]   0.00-10.00  sec  10.9 GBytes  9.37 Gbits/sec                  receiver
+
+iperf Done.
+

pve02 to pve01 :

+
root@petunia:/home/interlope# iperf3 -c 10.100.3.101
+Connecting to host 10.100.3.101, port 5201
+[  5] local 10.100.3.102 port 54198 connected to 10.100.3.101 port 5201
+[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
+[  5]   0.00-1.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.89 MBytes
+[  5]   1.00-2.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.89 MBytes
+[  5]   2.00-3.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.89 MBytes
+[  5]   3.00-4.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.89 MBytes
+[  5]   4.00-5.00   sec  1.09 GBytes  9.38 Gbits/sec    0   1.98 MBytes
+[  5]   5.00-6.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.98 MBytes
+[  5]   6.00-7.00   sec  1.09 GBytes  9.40 Gbits/sec    0   1.98 MBytes
+[  5]   7.00-8.00   sec  1.09 GBytes  9.38 Gbits/sec    0   1.98 MBytes
+[  5]   8.00-9.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.98 MBytes
+[  5]   9.00-10.00  sec  1.09 GBytes  9.40 Gbits/sec    0   1.98 MBytes
+- - - - - - - - - - - - - - - - - - - - - - - - -
+[ ID] Interval           Transfer     Bitrate         Retr
+[  5]   0.00-10.00  sec  10.9 GBytes  9.39 Gbits/sec    0            sender
+[  5]   0.00-10.00  sec  10.9 GBytes  9.39 Gbits/sec                  receiver
+
+iperf Done.
+

pve02 to pve03 :

+
root@petunia:/home/interlope# iperf3 -c 10.100.3.103
+Connecting to host 10.100.3.103, port 5201
+[  5] local 10.100.3.102 port 49102 connected to 10.100.3.103 port 5201
+[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
+[  5]   0.00-1.00   sec  1.07 GBytes  9.17 Gbits/sec   34   1.21 MBytes
+[  5]   1.00-2.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.31 MBytes
+[  5]   2.00-3.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.31 MBytes
+[  5]   3.00-4.00   sec  1.09 GBytes  9.38 Gbits/sec    0   1.31 MBytes
+[  5]   4.00-5.00   sec  1.09 GBytes  9.40 Gbits/sec    0   1.32 MBytes
+[  5]   5.00-6.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.32 MBytes
+[  5]   6.00-7.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.32 MBytes
+[  5]   7.00-8.00   sec  1.09 GBytes  9.38 Gbits/sec    0   1.32 MBytes
+[  5]   8.00-9.00   sec  1.09 GBytes  9.39 Gbits/sec    0   1.35 MBytes
+[  5]   9.00-10.00  sec  1.09 GBytes  9.38 Gbits/sec    0   1.35 MBytes
+- - - - - - - - - - - - - - - - - - - - - - - - -
+[ ID] Interval           Transfer     Bitrate         Retr
+[  5]   0.00-10.00  sec  10.9 GBytes  9.37 Gbits/sec   34            sender
+[  5]   0.00-10.00  sec  10.9 GBytes  9.36 Gbits/sec                  receiver
+
+iperf Done.
+ +
task started by HA resource agent
+2025-11-14 21:47:40 conntrack state migration not supported or disabled, active connections might get dropped
+2025-11-14 21:47:40 use dedicated network address for sending migration traffic (10.100.3.103)
+2025-11-14 21:47:40 starting migration of VM 20057 to node 'dalia' (10.100.3.103)
+2025-11-14 21:47:40 starting VM 20057 on remote node 'dalia'
+2025-11-14 21:47:41 start remote tunnel
+2025-11-14 21:47:42 ssh tunnel ver 1
+2025-11-14 21:47:42 starting online/live migration on unix:/run/qemu-server/20057.migrate
+2025-11-14 21:47:42 set migration capabilities
+2025-11-14 21:47:42 migration downtime limit: 100 ms
+2025-11-14 21:47:42 migration cachesize: 2.0 GiB
+2025-11-14 21:47:42 set migration parameters
+2025-11-14 21:47:42 start migrate command to unix:/run/qemu-server/20057.migrate
+2025-11-14 21:47:43 migration active, transferred 515.4 MiB of 12.6 GiB VM-state, 428.3 MiB/s
+2025-11-14 21:47:44 migration active, transferred 1.1 GiB of 12.6 GiB VM-state, 1014.6 MiB/s
+2025-11-14 21:47:45 migration active, transferred 2.0 GiB of 12.6 GiB VM-state, 1.1 GiB/s
+2025-11-14 21:47:46 migration active, transferred 2.8 GiB of 12.6 GiB VM-state, 812.0 MiB/s
+2025-11-14 21:47:47 migration active, transferred 3.3 GiB of 12.6 GiB VM-state, 823.0 MiB/s
+2025-11-14 21:47:48 migration active, transferred 3.9 GiB of 12.6 GiB VM-state, 669.9 MiB/s
+2025-11-14 21:47:49 migration active, transferred 4.5 GiB of 12.6 GiB VM-state, 564.6 MiB/s
+2025-11-14 21:47:50 migration active, transferred 5.0 GiB of 12.6 GiB VM-state, 507.2 MiB/s
+2025-11-14 21:47:51 migration active, transferred 5.5 GiB of 12.6 GiB VM-state, 484.9 MiB/s
+2025-11-14 21:47:52 migration active, transferred 6.0 GiB of 12.6 GiB VM-state, 727.3 MiB/s
+2025-11-14 21:47:53 migration active, transferred 6.9 GiB of 12.6 GiB VM-state, 949.1 MiB/s
+2025-11-14 21:47:54 migration active, transferred 7.8 GiB of 12.6 GiB VM-state, 1.0 GiB/s
+2025-11-14 21:47:55 migration active, transferred 8.8 GiB of 12.6 GiB VM-state, 1.0 GiB/s
+2025-11-14 21:47:56 migration active, transferred 9.7 GiB of 12.6 GiB VM-state, 966.1 MiB/s
+2025-11-14 21:47:57 migration active, transferred 10.6 GiB of 12.6 GiB VM-state, 1.0 GiB/s
+2025-11-14 21:47:58 average migration speed: 806.6 MiB/s - downtime 59 ms
+2025-11-14 21:47:58 migration completed, transferred 11.7 GiB VM-state
+2025-11-14 21:47:58 migration status: completed
+
+NOTICE
+  Intentionally removing diskless assignment (pm-299b3873) on (pivoine).
+  It will be re-created when the resource is actually used on this node.
+2025-11-14 21:48:00 migration finished successfully (duration 00:00:20)
+TASK OK
+

The VM is migrated in about 20 seconds with 59 freaking millisecond downtime. And since it's a live migration, there's absolutely no down time. The ping still goes on, and I'm still ssh'd in. This is wonderful. ( 1.0 GiB/s = roughly 8.59Gbps)

+

Conclusion :

+

To end this article/tutorial, i'll simply say that it works. It migrate quickly, i have no IO delay. It may not be the best setup nor the most money/performance ratio I could get, but I like it. That's what a production-ready homelab is : having fun trying things.

+

Thanks for reading me,

+

spleenftw

\ No newline at end of file