NVMe-oF Setup Guide for Proxmox VE

AI-generated picture

NVMe storage (Generated by AI)

NVMe over TCP

By default, NVMe-oF communicates via TCP on port 4420.

 

Requirements

The following package must be installed: nvme-cli

apt install nvme-cli -y

You can then load the nvme_tcp module manually (this will not persist across reboots):

modprobe nvme_tcp

To ensure it loads by default at boot:

echo “nvme_tcp” > /etc/modules-load.d/nvme_tcp.conf

 

Adjustments to the hostnqn

Like iSCSI (IQN), NVMe-oF has a unique host address, the hostnqn.

 

The NVMe Base Specification defines two valid formats for NVMe Qualified Names (NQN). By default, the UUID-based format is used when creating the /etc/nvme/hostnqn file:

nqn.2014-08.org.nvmexpress:uuid:8e5e052f-8c2a-86d1-ed11-f3f8bac1a80e

 

The prefix nqn.2014-08.org.nvmexpress is reserved exclusively for this UUID format and must not be combined with custom identifiers. To use a descriptive NQN with the hostname instead, the domain-based format must be used:

nqn.<yyyy-mm>.<reverse domain>:<unique identifier>

 

The following conditions apply:

 

  • <yyyy-mm>: The year and month during which the organization owned the domain.
  • The domain is specified in reverse order (e.g., de.example for example.de).
  • The identifier after the colon can be chosen freely but must be unique within the NVMe-oF environment—the hostname is suitable for this purpose.
  • The total length must not exceed 223 bytes; lowercase letters, digits, -, ., and : are recommended.

 

Example – Conversion from UUID-based to hostname-based NQN for the host pve-01:

Before: nqn.2014-08.org.nvmexpress:uuid:8e5e052f-8c2a-86d1-ed11-f3f8bac1a80e
After: nqn.2005-03.de.example:pve-01

 

The new NQN is entered in /etc/nvme/hostnqn. The file /etc/nvme/hostid remains unchanged. After the change, the new NQN must be stored in the host mapping of the storage target, since existing mappings are based on the old NQN.

Discovery

NVMe-oF TCP targets can now be discovered as follows:

nvme discover --transport=tcp --traddr=<IP-STORAGE>

Depending on the implementation of the NVMe-oF protocol on the SAN, this command returns all available controllers and IP addresses on the SAN. In some cases, however, you will only receive a log entry from the IP address on which the request was made. In most cases, though, additional IP addresses (e.g., in high-availability multi-controller systems) are available for connection.

 

An output listing all endpoints looks like this, for example:

root@pve-01:~# nvme discover --transport=tcp --traddr=192.168.50.10 --trsvcid=4420

Discovery Log Number of Records 2, Generation counter 7
=====Discovery Log Entry 0======
trtype:  tcp
adrfam:  ipv4
subtype: nvme subsystem
treq:    not specified, sq flow control disable supported
portid:  1
trsvcid: 4420
subnqn:  [FAKE]
traddr:  192.168.50.10
eflags:  none
sectype: none
=====Discovery Log Entry 1======
trtype:  tcp
adrfam:  ipv4
subtype: nvme subsystem
treq:    not specified, sq flow control disable supported
portid:  2
trsvcid: 4420
subnqn:  [FAKE]
traddr:  192.168.51.10
eflags:  none
sectype: none
=====Discovery Log Entry 2======
trtype:  tcp
adrfam:  ipv4
subtype: nvme subsystem
treq:    not specified, sq flow control disable supported
portid:  2
trsvcid: 4420
subnqn:  [FAKE]
traddr:  192.168.50.11
eflags:  none
sectype: none
=====Discovery Log Entry 3======
trtype:  tcp
adrfam:  ipv4
subtype: nvme subsystem
treq:    not specified, sq flow control disable supported
portid:  2
trsvcid: 4420
subnqn:  [FAKE]
traddr:  192.168.51.11
eflags:  none
sectype: none

Connecting Targets

The subsystems returned by the discovery process can be connected either individually per portal or collectively.

Connecting a single target:

nvme connect --transport=tcp --traddr=192.168.50.10 --trsvcid=4420 -n <subnqn>

The <subnqn> corresponds to the value from the respective Discovery log entry. The command is repeated for each portal (each traddr)—this is what creates multiple paths, which the native NVMe multipath then aggregates into a single subsystem.

Connect all targets of a discovery portal:

nvme connect-all --transport=tcp --traddr=192.168.50.10 --trsvcid=4420

`connect-all` performs the discovery and automatically connects all returned entries. If the SAN returns all controller IPs in the discovery log (see Discovery above), a single call is sufficient for all paths. If, however, the target returns only entries for the requested IP, `connect` must be executed for each portal IP.

If a subsystem is already connected, a new attempt to connect it will result in an error message (which can be suppressed with --quiet); this does not result in duplicate connections.

The result can then be verified using `nvme list` (namespaces) and `nvme list-subsys` (paths); see Checking Targets.

Note: Connections established in this way do not survive a reboot; see Automatic Setup at Startup. Recommended parameters such as --ctrl-loss-tmo=-1 should already be specified when connecting manually; see Optimized Multipathing Settings.

Checking Targets

 

The targets/paths can be listed using nvme list-subsys -v, along with the I/O policy.

However, the ANA states (comparable to ALUA) are not displayed.

These can be viewed using nvme list-subsys -o json /dev/nvme0n1 (e.g., optimized / non-optimized).

Automatic Setup at Startup

Connecting Targets

Connections established using nvme connect or nvme connect-all do not survive a reboot. To automatically establish connections at boot time, the discovery parameters are specified in the file /etc/nvme/discovery.conf, with one entry per discovery portal:

 

--transport=tcp --traddr=192.168.50.10 --trsvcid=4420 [--ctrl-loss-tmo=-1 --keep-alive-tmo=10] 
--transport=tcp --traddr=192.168.51.10 --trsvcid=4420 [--ctrl-loss-tmo=-1 --keep-alive-tmo=10] 
--transport=tcp --traddr=192.168.50.11 --trsvcid=4420 [--ctrl-loss-tmo=-1 --keep-alive-tmo=10] 
--transport=tcp --traddr=192.168.51.11 --trsvcid=4420 [--ctrl-loss-tmo=-1 --keep-alive-tmo=10]

 

If necessary, please verify that the configuration in [] matches the optimized multipathing settings.

At system startup, the systemd service nvmf-autoconnect.service effectively executes an nvme connect-all command against all entries stored there, thereby automatically re-establishing all paths.

The service must be enabled:

systemctl enable nvmf-autoconnect.service

You can check the status with:

systemctl is-enabled nvmf-autoconnect.service

Change the I/O Policy

By default, the NVMe-oF targets are set to the numa I/O policy. To automatically change this to, for example, Round-Robin at startup, a udev rule is required.

The following I/O policies are available:

 

 

Policy Description
numa Default. Path with the shortest NUMA distance to the requesting CPU; no load balancing.
=> Not very useful if multiple optimized paths are available
   
round-robin Distributes I/O evenly across all optimized paths (bandwidth aggregation).
   
queue-depth Selects the path with the lowest current queue depth.

 

At runtime (not persistent across reboots):

echo “round-robin” > /sys/class/nvme-subsystem/nvme-subsys0/iopolicy #Adjust subsys0 accordingly

To ensure the policy is set automatically at startup, the udev rule /etc/udev/rules.d/71-nvme-io-policy.rules is required:

ACTION==“add|change”, SUBSYSTEM==“nvme-subsystem”, ATTR{iopolicy}=“round-robin”

 

Enable and verify at runtime:

udevadm control --reload-rules
udevadm trigger --subsystem-match=nvme-subsystem
cat /sys/class/nvme-subsystem/nvme-subsys*/iopolicy

Optional: Limiting to a Specific Subsystem

The rule above applies to all NVMe subsystems, including local NVMe SSDs. For single-path devices, this has no effect and is therefore not critical. If, however, you want to apply the policy specifically to a particular target, you can narrow down the rule using the subsystem’s NQN or model.

Determine the subsystem’s NQN:

cat /sys/class/nvme-subsystem/nvme-subsys*/subsysnqn

Rule with NQN match:

ACTION=="add|change", SUBSYSTEM=="nvme-subsystem", ATTR{subsysnqn}=="nqn.2002-10.com.infortrend:raid.sn1234567.001", ATTR{iopolicy}="round-robin"

 

Rule with Model-Match:

ACTION=="add", SUBSYSTEM=="nvme-subsystem", ATTR{subsystype}=="nvm", ATTR{model}=="GS 4000 Series", ATTR{iopolicy}="round-robin"

 

Multiple targets can be covered using additional lines or a wildcard match (e.g., ATTR{subsysnqn}==“nqn.2002-10.com.infortrend:*”).


Additional Recommendations

The use of jumbo frames (MTU 9000) is recommended for higher throughput, provided this can be configured for all participants.

Configure LVM

Now that Multipath has been successfully configured, we must use the /dev/mapper assignment going forward.

For example, for 3600144f028f88a0000005037a95d0001, the WWID would be /dev/mapper/3600144f028f88a0000005037a95d0001

If you have set up an optional alias assignment as described above, use that assignment.

The following commands create a PV and a VG on the LUN (replace DEVICE_NAME with the name of the multipath device and VG_NAME with the desired name for the VG):

  1. pvcreate /dev/mapper/DEVICE_NAME
  2. vgcreate VG_NAME /dev/mapper/DEVICE_NAME

You can then create an LVM volume for this VG in the Proxmox GUI.

VG in der Proxmox-GUI2

Step 5: Enter the desired ID for the storage.

Step 6: Select the VG you created earlier.

Step 7: If multiple Proxmox VE nodes can access the LUN, enable “Shared Storage.”

In a Proxmox VE cluster, the new PVs and VGs may not be immediately visible on all nodes. To make them visible on all nodes, you can either restart all nodes or run the following command on all nodes:

pvscan --cache

Additional Recommended Configurations

Optimized Multipathing Settings

In the default configuration, paths are deleted after approximately 10 minutes of downtime. These paths are not automatically re-added afterward and require manual intervention.

To prevent this, certain parameters must also be set in the discovery.conf file:

--transport=tcp --traddr=192.168.50.10 --trsvcid=4420 --ctrl-loss-tmo=-1 --keep-alive-tmo=10
--transport=tcp --traddr=192.168.51.10 --trsvcid=4420 --ctrl-loss-tmo=-1 --keep-alive-tmo=10

 

Parameter Default Value Description
--ctrl-loss-tmo=-1 600 s Time after which an interrupted path is permanently removed. If set to -1, the path remains in the “connecting” state indefinitely and automatically reestablishes the connection once the disruption is resolved, rather than being deleted.
     
--keep-alive-tmo=10 30 s Keep-Alive Timeout (KATO) for detecting silent path failures in which the connection goes silent without a TCP error (e.g., switch failure in the path). Reduces the worst-case detection time from 30 to 10 seconds. Values below 5 seconds are not recommended (risk of unnecessary connection drops under load).

Problems Observed

Missing Targets After Boot

Persistent configuration is missing

Please take “Automatic Setup at Startup” (see above) into account

Network connection became available too late

Although the nvmf-autoconnect.service does not start until after network-online.target, this target is considered reached as soon as ifupdown2 has configured the interfaces—not when the network actually begins transmitting packets.

LACP negotiation on the bond or STP listening/learning on the switch port (up to 30 seconds without Edge Port/PortFast configuration) may still be in progress at this point. The one-time nvme connect-all command then runs during this time window: TCP connects to portals that are not yet reachable fail and are not retried.

Typical symptom: After boot, individual paths are missing, but a manual connect subsequently works without any problems. --ctrl-loss-tmo=-1 does not help here, since the parameter only protects existing connections, and a controller that never established a connection cannot reconnect.

Diagnosis

Check the timing relationship between the network startup and connect errors:

journalctl -b -u networking.service -u nvmf-autoconnect.service -o short-precise

If the connect failed messages occur only milliseconds to a few seconds after the completion of networking.service, the race condition is confirmed.

 

Possible Solution (Static): Start Delay via systemd Override

Instead of editing the unit file directly (which would be overwritten during package updates), create a drop-in file:

systemctl edit nvmf-autoconnect.service

Content:

[Service]
ExecStartPre=/bin/sleep 15

This causes the service to wait 15 seconds before running nvme connect-all, which is sufficient for LACP connections; for STP without an edge port, increase this to 30–45 seconds if necessary. (A more permanent solution is to resolve the issue on the switch side by configuring the host ports as RSTP edge ports (PortFast), which completely eliminates the forwarding delay window.) The drop-in file is then located at /etc/systemd/system/nvmf-autoconnect.service.d/override.conf.

Verification

Check the override and perform a reboot test:

systemd-delta --type=extended | grep nvmf
systemctl cat nvmf-autoconnect.service

 

After the restart, all paths must be present:

nvme list-subsys


Also check the log to verify that the service was executed only after the wait time had elapsed and that no Connect errors are occurring anymore:

journalctl -b -u nvmf-autoconnect.service

Any questions?

starline_computer_gmbh_logo
Kevin Fietz
Technical Support

Our expert on Ceph, GRAID, PetaSAN, Proxmox, and more