User guide: NVMe-oF Machine storage
Kairon's own CSI driver (csi.kairon.zyvor.dev, see
machine-storage-csi.md) can serve volumes over
NVMe over Fabrics as well as iSCSI. The storage node exports each volume
through the kernel NVMe target (nvmet). Machine nodes connect over
nvme-tcp or nvme-rdma. With several portals, native NVMe multipath
merges the paths into one /dev/nvmeXnY. A volumeMode: Block volume
reaches FluxVM as that raw device: a Machine boots from it in place, or
attaches it as a data disk with the bus and I/O path you choose.
No nvme-cli or nvmetcli is needed. The controller writes nvmet's
configfs directly. The node plugin writes connect strings to
/dev/nvme-fabrics and reads path state from sysfs.
Setup
On the storage node, load nvmet plus nvmet_tcp and/or nvmet_rdma. On
every Machine node, load nvme_tcp and/or nvme_rdma, and keep native
multipath on (nvme_core.multipath=Y, the default on most distributions).
A volume with several paths is refused on a node with multipath off.
helm upgrade --install kairon ./charts/kairon -n kairon-system \
--set csiNode.enabled=true \
--set csiController.enabled=true \
--set csiController.portal=10.0.0.50:3260 \
--set csiController.nvmePortals='tcp:10.0.0.50:4420\,tcp:10.0.1.50:4420' \
--set csiController.storageClass.create=true \
--set csiController.storageClass.name=kairon-nvme \
--set csiController.storageClass.parameters.transport=nvme-tcp
csiController.nvmePortals (--nvme-portals on kairon-csi-controller)
lists tcp:IP:PORT and rdma:IP:PORT addresses. Use IP addresses, not
host names: the kernel target binds to them. Every volume is exported on
every portal of its transport. Two tcp: portals on two storage NICs
therefore give every node two paths. A portal's nvmet port id is 7400
plus its position in the list, so keep the order stable. A port whose
address changes while volumes are exported on it is refused rather than
rewritten.
The StorageClass parameter transport picks the protocol per class:
nvme-tcp, nvme-rdma, or iscsi (the default when unset). One
controller serves all three. The chart creates one StorageClass; add
another by hand (provisioner csi.kairon.zyvor.dev) for each extra
transport.
The node plugin derives the host NQN from /etc/nvme/hostnqn, or else
from /etc/machine-id. It derives the host id from /etc/nvme/hostid,
or else from the NQN. The chart mounts both paths read-only, so the
identity survives plugin restarts.
Using it
A Block-mode claim as the boot disk:
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: db-root}
spec:
storageClassName: kairon-nvme
volumeMode: Block
accessModes: [ReadWriteOnce]
resources: {requests: {storage: 20Gi}}
---
apiVersion: kairon.zyvor.dev/v1
kind: Machine
metadata: {name: db}
spec:
image:
digest: sha256:...
source: {httpURL: https://images.example/noble.raw, format: raw}
io: {aio: io_uring, cache: none, iothread: true, queues: 4}
volumes: [{name: root, claimName: db-root}]
disks:
- {name: data, claimName: db-data, io: {bus: virtio-blk, aio: io_uring, cache: none, iothread: true, queues: 4}}
- {name: logs, claimName: db-logs, io: {bus: vhost-user-blk, queues: 2}}
resources: {cpu: "4", memory: "8Gi"}
runtime: {backend: qemu}
kairon-node stages and publishes the volume with a Block capability.
The publish target is a bind mount of the device. On the first boot, the
image named by spec.image.source is written onto the device, but only
while its first MiB is still zero. A device that already holds data is
never overwritten. A raw image is copied as is. A qcow2 image is
converted with qemu-img, which kairon-node must find on its PATH. The
kairon-node container image doesn't have it, so use raw images there. A
kairon-node installed on the host can use either. FluxVM then boots the
device in place
(storage: shared), so the guest writes straight to the volume and a
node-local overlay is never involved. A Filesystem-mode claim still boots
from disk.img on the mounted filesystem, as before.
spec.image.io tunes the root disk, which is always virtio-blk.
spec.disks[].io picks each data disk's bus: scsi (the default),
virtio-blk, or vhost-user-blk. It also sets aio (io_uring,
native, threads), cache (none, writeback, writethrough,
directsync, unsafe), iothread and queues (1-64). Unset fields keep
FluxVM's defaults. The options apply on the qemu backend only and are
passed to FluxVM unchanged. Its API guide covers what each one does in
QEMU. io.throttle caps a disk's IOPS and bandwidth and can be changed on
a running Machine; see disk QoS.
A vhost-user-blk disk is served by its own qemu-storage-daemon rather
than by QEMU. When a Machine lists one, Kairon creates the VM with
shareable memory so the disk can be hot-added. Inside the guest,
virtio-blk disks appear as /dev/disk/by-id/virtio-<name>.
vhost-user-blk disks have no serial number, so find them by size or
order.
Static PVs work too. volumeHandle is
nvme|<nvme-tcp|nvme-rdma>|<subsystem NQN>|<host:port>,<host:port>. When
volumeAttributes omits transport, nqn and portals, they are taken
from the handle.
Path health
kairon-node sets a StoragePaths condition on every Machine with an
NVMe-oF boot volume or data disk. The condition is True/AllPathsLive
while every portal has a live, usable path, and
False/PathsDegraded otherwise. A usable path is ANA optimized or
non-optimized; inaccessible or lost paths don't count. The message lists
each path, for example
2 of 2 paths usable: nvme0 tcp 10.0.0.50:4420 live optimized; ....
Connections use ctrl_loss_tmo=-1, so a lost path keeps reconnecting.
When it comes back it rejoins on its own, and while no path is up I/O
queues instead of failing.
ANA states are nvmet's defaults (group 1, optimized on every port).
Kairon reads them and does not set them. To steer I/O away from a portal,
set that port's ANA state on the target yourself.
Lab results
scripts/lab-nvmeof.sh runs the whole
path on one node, which acts as both storage node and Machine host. It
uses two tcp portals on a dummy interface and an rdma portal over
soft-RoCE (rdma_rxe). See the NVMe-oF rows in
COMPATIBILITY.md for what ran and what didn't.
Real limits today
- No access control. Subsystems allow any host, and there is no DH-CHAP or in-band authentication. Anyone who can reach a portal can connect to any volume. Put the portals on a storage network only your nodes can reach. A StorageClass with CHAP secrets is refused for NVMe-oF.
- One storage node, file-backed. Volumes are sparse files in
csiController.volumeDir, as for iSCSI. Expansion grows the file and revalidates the namespace, and nodes rescan it; this ran in the lab. Snapshots copy the file as for iSCSI; they are unit-tested only.nvmetlogsWRITE_ZEROESas unsupported for file-backed namespaces; this is harmless. - RDMA has run only over soft-RoCE. Real RDMA NICs (RoCE, iWARP, InfiniBand) have not been tested.
- Block mode means one Machine per volume. The boot volume is written by the guest directly, with no overlay, so it can't be shared or cloned through FluxVM. Use CSI snapshots instead.
- vhost-user-blk disks: FluxVM refuses live resize and hibernation, and leaves them out of live and incremental (CBT) backups. Adding one to a Machine that already runs without one needs a restart, because the shareable memory is set at boot.
- Device allowlist. On FluxVM's eBPF dataplane, each VM's device cgroup has 8 entries: three fixed devices, one per VFIO group, and one per block device. That leaves room for five NVMe-oF disks (boot included) on a VM without passthrough.
- Migration. Detach CSI data disks before migrating, as for iSCSI. Migrating a Machine that boots from a Block volume has not been tested.