Storage in Practice — RAID, Snapshots, iSCSI, fio
Attach an iSCSI LUN and Measure It with fio
Goal
Set up a LIO target inside the same VM to provide a LUN, configure discovery, login, a _netdev mount, and automatic login with the initiator, and then measure IOPS and latency by queue depth with fio on that disk, extract the numbers from the JSON, and report them.
Why it matters
Attaching a SAN LUN to a server is everyday work for an infrastructure engineer, but if you do not know the three things that differ from a local disk (a mount that waits for the network, access control by name, and multiple paths), the server stops after a single reboot. And to answer a report that "the disk is slow," you need a measurement with fixed conditions. If you see with your own eyes that changing just the queue depth moves IOPS and latency together, you will be able to explain why the numbers on a spec sheet differ from the numbers an application experiences.
This lab runs as root on an Ubuntu 24.04 VM. The target and the initiator meet in the same VM at 127.0.0.1:3260, so you follow the network storage procedure as it is, but the measured numbers are not the numbers of a real SAN — a fileio backstore goes through the page cache on the target side, so even at queue depth 1 you get tens of thousands of IOPS (measured). The purpose is not the absolute values but to see how the numbers move when you change one condition. The session starts at 60 minutes and can be extended up to 180 minutes, and the VM disappears when the session ends.
Steps
- Create
/var/lib/sto/lun0.img(512MiB) as a fileio backstorelun0, attach it as a LUN totpg1of the targetiqn.2026-09.io.labhub:sto, put this VM's initiator name in the ACL, and save withtargetcli saveconfig. - Run discovery (sendtargets) on the
127.0.0.1portal and save the output to/root/sto/iscsi/discovery.txt. - Log in to the target so that the new disk shows up with TRAN
iscsi, and save the output oflsblk -o NAME,SIZE,TRAN,VENDOR,MODELto/root/sto/iscsi/lsblk.txt. - Format the iSCSI disk as ext4, make it mount at
/mnt/iscsiwith a line in/etc/fstabthat contains UUID= and_netdev, and mount it now. - Change
node.startupin the node record to automatic. - On
/mnt/iscsi/fio.dat(128MiB), measure 4KiBrandreadwithlibaio,direct=1,iodepth=1for 10 seconds, and save the JSON to/root/sto/fio/qd1.json. - Measure under the same conditions changing only
iodepth=32, and save the JSON to/root/sto/fio/qd32.json. - Write five lines,
qd1_iops=,qd1_p99_us=,qd32_iops=,qd32_p99_us=, andqd32_little=, to/root/sto/fio/report.txt, calculated from the JSON.
Notes
- Target:
targetcli ls,/backstores/fileio create,/iscsi create,…/tpg1/luns create,…/tpg1/acls create,saveconfig. - Initiator:
iscsiadm -m discovery -t sendtargets -p 127.0.0.1,iscsiadm -m node -T <IQN> -p 127.0.0.1 --login,iscsiadm -m session. - fio JSON:
jobs[0].read.iops,jobs[0].read.lat_ns.mean,jobs[0].read.clat_ns.percentile["99.000000"](nanoseconds). - Common mistake 1: writing
/dev/sdbin fstab or leaving out_netdev. - Common mistake 2: leaving out
direct=1and reporting page cache speed as disk speed.
Provide a LUN with a LIO target
Create /var/lib/sto/lun0.img (512MiB) as a fileio backstore lun0 and attach it as a LUN to tpg1 of the target iqn.2026-09.io.labhub:sto. Put this VM's initiator name (the InitiatorName= value in /etc/iscsi/initiatorname.iscsi) in the ACL and save the configuration with targetcli saveconfig.
targetcli fills in a tree that looks like a path — /backstores/fileio create, /iscsi create, /iscsi//tpg1/luns create, /iscsi//tpg1/acls create. The portal (0.0.0.0:3260) is created by default when you create the target.
Discover the target
With the initiator, run discovery (sendtargets) on the 127.0.0.1 portal and save its output to /root/sto/iscsi/discovery.txt.
It is iscsiadm -m discovery -t sendtargets -p . Discovery does not establish a session; it only creates node records.
Log in and check the new disk
Log in to the target iqn.2026-09.io.labhub:sto. The newly created disk must show up as iscsi in the TRAN column of lsblk. Save the output of lsblk -o NAME,SIZE,TRAN,VENDOR,MODEL to /root/sto/iscsi/lsblk.txt.
It is iscsiadm -m node -T -p --login. You see the logged-in sessions with iscsiadm -m session.
Register in fstab with _netdev
Format the iSCSI disk as ext4, and make it mount at /mnt/iscsi with a line in /etc/fstab that starts with UUID= and has _netdev in the options. It must be mounted now, attached through the fstab line as in mount /mnt/iscsi.
You can also refer to this disk as /dev/disk/by-path/ip-127.0.0.1:3260-iscsi-iqn.2026-09.io.labhub:sto-lun-0. The /dev/sdX name can change at every boot, so write the UUID in fstab. Without _netdev, it tries to mount before the network at boot.
Turn on automatic login
In the node record of the target iqn.2026-09.io.labhub:sto (portal 127.0.0.1), change node.startup to automatic.
It is iscsiadm -m node -T -p --op update -n node.startup -v automatic. You can find the current value in the output of iscsiadm -m node -T -p .
Measure at queue depth 1
With fio, measure 4KiB random reads on /mnt/iscsi/fio.dat (size 128MiB) with ioengine=libaio, direct=1, and iodepth=1 for 10 seconds (runtime=10, time_based), and save the JSON result to /root/sto/fio/qd1.json.
The shape is fio --name= --filename= --size=128M --rw=randread --bs=4k --ioengine=libaio --direct=1 --iodepth=1 --runtime=10 --time_based --output-format=json --output=. If you leave out direct=1, you end up measuring the page cache.
Measure at queue depth 32
On the same file and under the same conditions as in step 6, change only iodepth=32 and measure again, and save the JSON result to /root/sto/fio/qd32.json.
You have to change only one condition for the comparison to work. Make only the name, the output file, and iodepth different.
Extract numbers from the JSON and Little's law
Extract numbers from the two JSON files and write five lines to /root/sto/fio/report.txt: qd1_iops=, qd1_p99_us=, qd32_iops=, and qd32_p99_us= (IOPS as a rounded integer, p99 as 99.000000 of clat_ns.percentile converted to microseconds and rounded to an integer), and qd32_little= (the qd32 IOPS × the average latency lat_ns.mean converted to seconds, to one decimal place).
Do not copy the values by hand; read the JSON with python3 and calculate. Under jobs[0]['read'] there are iops, lat_ns, and clat_ns. Nanoseconds divided by 1,000 gives microseconds, and divided by one billion gives seconds. If qd32_little is close to the queue depth, Little's law holds.