Stage 3 · Build
Storage Reliability & Performance
fio Workload Modeling
Modeling random reads, sync writes, queue depth, and latency percentiles with fio.
fio Overview
fio (Flexible I/O Tester) is the industry-standard tool for storage benchmarking. It simulates real workloads with precise control over I/O patterns, block sizes, queue depths, and synchronization modes.
# Install fio
sudo apt install fio
sudo dnf install fio
# Basic random read test
fio --name=test --ioengine=libaio --direct=1 --bs=4k --rw=randread --size=1G --numjobs=1 --runtime=30 --filename=/data/testfile
# Output:
# read: IOPS=125K, BW=488MiB/s (512MB/s)
# slat (nsec): min=1234, max=56789, avg=3456.78
# clat (usec): min=5, max=500, avg=25.34
# lat (usec): min=6, max=512, avg=28.90fio output shows IOPS, throughput, and latency statistics (slat, clat, lat). slat is submission latency, clat is completion latency, lat is total latency.
I/O Engines
| Engine | Type | Use Case |
|---|---|---|
| libaio | Async I/O | Direct I/O on Linux (preferred) |
| io_uring | Async I/O | Kernel 5.1+, best performance |
| sync | Synchronous | Simple testing, buffered I/O |
| mmap | Memory-mapped | Memory-mapped file I/O |
| posixaio | POSIX AIO | Cross-platform async I/O |
| null | No I/O | Measuring overhead of fio itself |
# libaio (standard)
fio --name=libaio --ioengine=libaio --direct=1 --bs=4k --rw=randread --size=1G --runtime=30 --filename=/data/testfile
# io_uring (newer, faster)
fio --name=uring --ioengine=io_uring --direct=1 --bs=4k --rw=randread --size=1G --runtime=30 --filename=/data/testfile
# sync (simple)
fio --name=sync --ioengine=sync --bs=4k --rw=randread --size=1G --runtime=30 --filename=/data/testfilelibaio is the standard for Linux benchmarking. io_uring provides slightly better performance on kernel 5.1+.
Workload Patterns
# Random read (database workload)
fio --name=randread --ioengine=libaio --direct=1 --bs=8k --iodepth=64 --rw=randread --size=10G --numjobs=4 --runtime=60 --filename=/data/testfile
# Random write (database workload)
fio --name=randwrite --ioengine=libaio --direct=1 --bs=8k --iodepth=64 --rw=randwrite --size=10G --numjobs=4 --runtime=60 --filename=/data/testfile
# 70/30 read/write mix (typical application)
fio --name=mixed --ioengine=libaio --direct=1 --bs=4k --iodepth=32 --rw=randrw --rwmixread=70 --size=10G --numjobs=4 --runtime=60 --filename=/data/testfile
# Sequential read (backup, streaming)
fio --name=seqread --ioengine=libaio --direct=1 --bs=128k --iodepth=16 --rw=read --size=10G --numjobs=1 --runtime=60 --filename=/data/testfile
# Sequential write (bulk storage)
fio --name=seqwrite --ioengine=libaio --direct=1 --bs=128k --iodepth=16 --rw=write --size=10G --numjobs=1 --runtime=60 --filename=/data/testfileMatch your fio parameters to your actual workload. Use the same block sizes, queue depths, and read/write ratios as your application.
Queue Depth Impact
# Test queue depth impact on IOPS and latency
for qd in 1 4 8 16 32 64 128; do
echo "=== Queue Depth: $qd ==="
fio --name=qd${qd} --ioengine=libaio --direct=1 --bs=4k --iodepth=$qd --rw=randread --size=1G --runtime=10 --filename=/data/testfile --output-format=json | jq '.jobs[0].read | {iops: .iops, lat_avg: .lat_ns.mean}'
done
# Results typically show:
# QD=1: 10K IOPS, 100us latency
# QD=4: 40K IOPS, 100us latency
# QD=32: 125K IOPS, 256us latency
# QD=128: 125K IOPS, 1024us latency (latency increases)Higher queue depth increases IOPS up to a point, then latency increases without IOPS improvement. Find the sweet spot for your workload.
Latency Percentiles
# Show latency percentiles
fio --name=lat --ioengine=libaio --direct=1 --bs=4k --iodepth=32 --rw=randread --size=1G --runtime=30 --filename=/data/testfile --write_bw_log --write_lat_log --write_iops_log --log_avg_msec=1000
# Latency percentiles in output:
# clat percentiles (usec):
# | 1.00th=[ 5], 5.00th=[ 7], 10.00th=[ 8],
# | 50.00th=[ 15], 90.00th=[ 35], 95.00th=[ 50],
# | 99.00th=[ 100], 99.50th=[ 150], 99.90th=[ 300],
# | 99.99th=[ 500]
# Parse with fio2gnuplot for visualization
fio2gnuplot -b bw -i fio-lat.log -o bw.pngFocus on p99 and p999 latencies for SLA compliance. p50 is the median; p99 is the worst 1% of requests.
Real-World Benchmarks
# PostgreSQL-like workload (8KB pages, 70/30, 200 concurrent connections)
fio --name=pgbench --ioengine=libaio --direct=1 --bs=8k --iodepth=200 --rw=randrw --rwmixread=70 --size=50G --numjobs=8 --runtime=120 --filename=/data/pgdata --group_reporting --write_bw_log --write_lat_log --write_iops_log --log_avg_msec=10000
# MySQL-like workload (16KB pages, 50/50)
fio --name=mysql --ioengine=libaio --direct=1 --bs=16k --iodepth=64 --rw=randrw --rwmixread=50 --size=30G --numjobs=4 --runtime=120 --filename=/data/mysql --group_reportingMatch block sizes to your database page size. PostgreSQL uses 8KB pages, MySQL InnoDB uses 16KB pages.
When running multiple jobs, --group_reporting combines statistics into a single summary. Without it, you get per-job output which is harder to interpret.
Without --direct=1, fio measures page cache performance, not disk performance. Always use --direct=1 for storage benchmarking.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.