Last active 5 days ago

Hard drive speed test using fio or dd

Revision 3f5fba94fefe0f0c0161a53ff31545327d911bad

speedtest-hd.sh Raw
1#!/usr/bin/env bash
2
3# Robust HD/SDD/NVMe performance CLI utility
4# Utilizing FIO for sequential/random writes/writes
5# Dependencies: fio (apt install fio)
6# See: https://cloud.google.com/compute/docs/disks/benchmarking-pd-performance
7# See: https://arstechnica.com/gadgets/2020/02/how-fast-are-your-disks-find-out-the-open-source-way-with-fio/
8# mReschke 2024-01-18
9
10# CLI Parameters
11path="$1"
12option=""
13simple=false
14for arg in "${@:2}"; do
15 case "$arg" in
16 --simple) simple=true ;;
17 --dd) option="--dd" ;;
18 --fio) option="--fio" ;;
19 esac
20done
21
22# Main application flow
23function main {
24
25 # Show usage if no params
26 if [ ! "$path" ]; then
27 usage
28 fi
29
30 # Understand . path
31 if [ "$path" == '.' ]; then
32 path=$(pwd)
33 fi
34
35 # Check if path exists
36 if [ ! -e "$path" ]; then
37 echo "Path $path does not exist"
38 exit 1
39 fi
40
41 # Must type y or n THEN press enter (which I like better)
42 echo "NOTICE: 1GB free space on '$path' is required to perform the benchmark."
43 echo -n "Are you ready to start a robust IO benchmark against '$path' ?"; read answer
44 if [ "$answer" != "${answer#[Yy]}" ]; then
45 echo "Great! Starting benchmark now!"
46 echo "------------------------------"
47 else
48 echo "Ok, cancelled!"
49 exit 0
50 fi
51
52 # Use dd of fio based on param or defaults
53 if [ "$option" == "--dd" ]; then
54 dd_speedtest
55 elif [ "$option" == "--fio" ]; then
56 fio_speedtest
57 elif [ "$option" == "" ]; then
58 # If fio is installed, use it, else use dd
59 [ "$simple" != true ] && echo ""
60 if ! command -v fio &> /dev/null; then
61 dd_speedtest
62 else
63 fio_speedtest
64 fi
65 fi
66}
67
68function print_result {
69 # Print a single fio test result, either as the full "Run status group"
70 # line or, when --simple is given, as a compact aligned "Label value" row.
71 # The simple value is taken from the parenthetical (MB/s) figure that
72 # follows the first bw= value in fio's output.
73 local label="$1"
74 local output="$2"
75 local status
76 status=$(echo " - $output " | /usr/bin/grep -A1 'Run status group' | tail -n1)
77 if [ "$simple" == true ]; then
78 local value
79 # Grab the parenthetical (MB/s) figure after the first bw=, then put a
80 # space between the number and the unit, eg "98.4MB/s" -> "98.4 MB/s".
81 value=$(echo "$status" | /usr/bin/grep -oP 'bw=\S+\s+\(\K[^)]+' | sed -E 's/([0-9.]+)([A-Za-z])/\1 \2/')
82 printf "%-28s%s\n" "$label" "$value"
83 else
84 echo "$status"
85 fi
86}
87
88function fio_write_single_random_4k {
89 # Single 4k Random Writes
90
91 # This is a single process doing random 4K writes. This is where the pain
92 # really, really lives; it's basically the worst possible thing you can ask a
93 # disk to do. Where this happens most frequently in real life: copying home
94 # directories and dotfiles, manipulating email stuff, some database operations,
95 # source code trees.
96
97 # When I ran this test against the high-performance SSDs in my Ubuntu
98 # workstation, they pushed 127MiB/sec. The server just beneath it in the rack
99 # only managed 33MiB/sec on its "high-performance" 7200RPM rust disks... but
100 # even then, the vast majority of that speed is because the data is being
101 # written asynchronously, allowing the operating system to batch it up into
102 # larger, more efficient write operations.
103
104 # If we add the argument --fsync=1, forcing the operating system to perform
105 # synchronous writes (calling fsync after each block of data is written) the
106 # picture gets much more grim: 2.6MiB/sec on the high-performance SSDs but
107 # only 184KiB/sec on the "high-performance" rust. The SSDs were about four
108 # times faster than the rust when data was written asynchronously but a
109 # whopping fourteen times faster when
110
111 # --name= is a required argument, but it's basically human-friendly fluff—fio will create files based on that name to test with, inside the working directory you're currently in.
112 # --ioengine=posixaio sets the mode fio interacts with the filesystem. POSIX is a standard Windows, Macs, Linux, and BSD all understand, so it's great for portability—although inside fio itself, Windows users need to invoke --ioengine=windowsaio, not --ioengine=posixaio, unfortunately. AIO stands for Asynchronous Input Output and means that we can queue up multiple operations to be completed in whatever order the OS decides to complete them. (In this particular example, later arguments effectively nullify this.)
113 # --rw=randwrite means exactly what it looks like it means: we're going to do random write operations to our test files in the current working directory. Other options include seqread, seqwrite, randread, and randrw, all of which should hopefully be fairly self-explanatory.
114 # --bs=4k blocksize 4K. These are very small individual operations. This is where the pain lives; it's hard on the disk, and it also means a ton of extra overhead in the SATA, USB, SAS, SMB, or whatever other command channel lies between us and the disks, since a separate operation has to be commanded for each 4K of data.
115 # --size=1g our test file(s) will be 1GB in size apiece. (We're only creating one, see next argument.)
116 # --numjobs=1 we're only creating a single file, and running a single process commanding operations within that file. If we wanted to simulate multiple parallel processes, we'd do, eg, --numjobs=16, which would create 16 separate test files of --size size, and 16 separate processes operating on them at the same time.
117 # --iodepth=1 this is how deep we're willing to try to stack commands in the OS's queue. Since we set this to 1, this is effectively pretty much the same thing as the sync IO engine—we're only asking for a single operation at a time, and the OS has to acknowledge receipt of every operation we ask for before we can ask for another. (It does not have to satisfy the request itself before we ask it to do more operations, it just has to acknowledge that we actually asked for it.)
118 # --runtime=15 --time_based Run and even if we complete sooner, just start over again and keep going until 60 seconds is up.
119 # --end_fsync=1 After all operations have been queued, keep the timer going until the OS reports that the very last one of them has been successfully completed—ie, actually written to disk.
120 [ "$simple" != true ] && { echo ""; echo "Single 4K Random Writes (size=1G, time=15sec, jobs=1, iodepth=1)"; }
121 x=`sudo fio \
122 --name=fio-write-random-4k \
123 --directory=$path \
124 --ioengine=posixaio \
125 --rw=randwrite \
126 --bs=4k \
127 --size=1g \
128 --numjobs=1 \
129 --iodepth=1 \
130 --time_based --runtime=15 \
131 --end_fsync=1`
132 print_result "Single 4K Random Writes" "$x"
133
134 # Cleanup my test files
135 rm -rf $path/fio-write-random-4k*
136}
137
138function fio_write_parallel_random_64k {
139 # Parallel 64k Random Writes
140
141 # This time, we're creating 16 separate 64MB files (still totaling 1GB, when
142 # all put together) and we're issuing 64KB blocksized random write operations.
143 # We're doing it with sixteen separate processes running in parallel, and
144 # we're queuing up to 16 simultaneous asynchronous ops before we pause and wait
145 # for the OS to start acknowledging their receipt.
146
147 # This is a pretty decent approximation of a significantly busy system. It's
148 # not doing any one particularly nasty thing—like running a database engine or
149 # copying tons of dotfiles from a user's home directory—but it is coping with
150 # a bunch of applications doing moderately demanding stuff all at once.
151
152 # This is also a pretty good, slightly pessimistic approximation of a busy,
153 # multi-user system like a NAS, which needs to handle multiple 1MB operations
154 # simultaneously for different users. If several people or processes are trying
155 # to read or write big files (photos, movies, whatever) at once, the OS tries
156 # to feed them all data simultaneously. This pretty quickly devolves down to a
157 # pattern of multiple random small block access. So in addition to "busy desktop
158 # with lots of apps," think "busy fileserver with several people actively using it."
159
160 # You will see a lot more variation in speed as you watch this operation play
161 # out on the console. For example, the 4K single process test we tried first
162 # wrote a pretty consistent 11MiB/sec on my MacBook Air's internal drive—but
163 # this 16-process job fluctuated between about 10MiB/sec and 300MiB/sec during
164 # the run, finishing with an average of 126MiB/sec.
165
166 # Most of the variation you're seeing here is due to the operating system and
167 # SSD firmware sometimes being able to aggregate multiple writes. When it
168 # manages to aggregate them helpfully, it can write them in a way that allows
169 # parallel writes to all the individual physical media stripes inside the SSD.
170 # Sometimes, it still ends up having to give up and write to only a single
171 # physical media stripe at a time—or a garbage collection or other maintenance
172 # operation at the SSD firmware level needs to run briefly in the background,
173 # slowing things down.
174 [ "$simple" != true ] && { echo ""; echo "Parallel 64K Random Writes (size=1G, time=15sec, jobs=16, iodepth=16)"; }
175 x=`sudo fio \
176 --name=fio-write-random-64k \
177 --directory=$path \
178 --ioengine=posixaio \
179 --rw=randwrite \
180 --bs=64k \
181 --size=64m \
182 --numjobs=16 \
183 --iodepth=16 \
184 --time_based --runtime=15 \
185 --end_fsync=1`
186 print_result "Parallel 64K Random Writes" "$x"
187
188 # Cleanup my test files
189 rm -rf $path/fio-write-random-64k*
190}
191
192function fio_write_single_sequential_1m {
193 # Single 1M Random Writes
194
195 # This is pretty close to the best-case scenario for a real-world system
196 # doing real-world things. No, it's not quite as fast as a single, truly
197 # contiguous write... but the 1MiB blocksize is large enough that it's quite
198 # close. Besides, if literally any other disk activity is requested simultaneously
199 # with a contiguous write, the "contiguous" write devolves to this level of
200 # performance pretty much instantly, so this is a much more realistic test of
201 # the upper end of storage performance on a typical system.
202
203 # You'll see some kooky fluctuations on SSDs when doing this test. This is largely
204 # due to the SSD's firmware having better luck or worse luck at any given time,
205 # when it's trying to queue operations so that it can write across all physical
206 # media stripes cleanly at once. Rust disks will tend to provide a much more
207 # consistent, though typically lower, throughput across the run.
208
209 # You can also see SSD performance fall off a cliff here if you exhaust an
210 # onboard write cache—TLC and QLC drives tend to have small write cache areas
211 # made of much faster MLC or SLC media. Once those get exhausted, the disk has
212 # to drop to writing directly to the much slower TLC/QLC media where the data
213 # eventually lands. This is the major difference between, for example, Samsung
214 # EVO and Pro SSDs—the EVOs have slow TLC media with a fast MLC cache, where
215 # the Pros use the higher-performance, higher-longevity MLC media throughout
216 # the entire SSD.
217
218 # If you have any doubt at all about a TLC or QLC disk's ability to sustain
219 # heavy writes, you may want to experimentally extend your time duration here.
220 # If you watch the throughput live as the job progresses, you'll see the impact
221 # immediately when you run out of cache—what had been a fairly steady,
222 # several-hundred-MiB/sec throughput will suddenly plummet to half the speed
223 # or less and get considerably less stable as well.
224
225 # However, you might choose to take the opposite position—you might not
226 # expect to do sustained heavy writes very frequently, in which case you
227 # actually are more interested in the on-cache behavior. What's important
228 # here is that you understand both what you want to test, and how to test
229 # it accurately.
230
231 [ "$simple" != true ] && { echo ""; echo "Single 1M Sequential Writes (size=1G, time=15sec, jobs=1, iodepth=1)"; }
232 x=`sudo fio \
233 --name=fio-write-random-1m \
234 --directory=$path \
235 --ioengine=posixaio \
236 --rw=write \
237 --bs=1m \
238 --size=1g \
239 --numjobs=1 \
240 --iodepth=1 \
241 --time_based --runtime=15 \
242 --end_fsync=1`
243 print_result "Single 1M Sequential Writes" "$x"
244
245 # Cleanup my test files
246 rm -rf $path/fio-write-random-1m*
247}
248
249function fio_read_sequential_1m {
250 # Sequential Parallel Reads
251
252 [ "$simple" != true ] && { echo ""; echo "Sequential 4x 1M Reads"; }
253 x=`sudo fio \
254 --name=fio-read-sequential-1m \
255 --directory=$path \
256 --ioengine=posixaio \
257 --bs=1M \
258 --numjobs=4 \
259 --size=256M \
260 --time_based --runtime=30s \
261 --ramp_time=2s \
262 --direct=1 \
263 --verify=0 \
264 --iodepth=64 \
265 --rw=read \
266 --group_reporting=1 \
267 --iodepth_batch_submit=64 \
268 --iodepth_batch_complete_max=64`
269 print_result "Sequential 4x 1M Reads" "$x"
270 rm -rf $path/fio-read-sequential-1m*
271}
272
273function fio_read_random_4k {
274 # Random 4k Reads
275
276 [ "$simple" != true ] && { echo ""; echo "Random 4k Reads"; }
277 x=`sudo fio \
278 --name=fio-read-random-4k \
279 --directory=$path \
280 --ioengine=posixaio \
281 --rw=randread \
282 --bs=4k \
283 --size=1g \
284 --time_based --runtime=30s \
285 --ramp_time=2s \
286 --direct=1 \
287 --verify=0 \
288 --iodepth=256 \
289 --rw=read \
290 --group_reporting=1 \
291 --iodepth_batch_submit=256 \
292 --iodepth_batch_complete_max=256`
293 print_result "Random 4k Reads" "$x"
294 rm -rf $path/fio-read-random-4k*
295}
296
297function fio_speedtest {
298 # Write tests
299 fio_write_single_random_4k
300 fio_write_parallel_random_64k
301 fio_write_single_sequential_1m
302
303 # Read Tests
304 fio_read_sequential_1m
305 fio_read_random_4k
306}
307
308function dd_speedtest {
309 # Basic HD speed test using DD
310 # mReschke 2017-07-11
311
312 file=$path/bigfile
313 size=1024
314
315 echo "Running dd based HD/SSD/NVMe Benchmarks"
316 echo "---------------------------------------"
317
318 printf "Cached write speed...\n"
319 dd if=/dev/zero of=$file bs=1M count=$size
320
321 printf "\nUncached write speed...\n"
322 dd if=/dev/zero of=$file bs=1M count=$size conv=fdatasync,notrunc
323
324 printf "\nUncached read speed...\n"
325 echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null
326 dd if=$file of=/dev/null bs=1M count=$size
327
328 printf "\nCached read speed...\n"
329 dd if=$file of=/dev/null bs=1M count=$size
330
331 rm $file
332 printf "\nDone\n"
333}
334
335# Show help and usage information
336function usage {
337 echo "Robust Flexible Input/Output HD Speedtest"
338 echo " If FIO is installed, we use FIO for more detailed performance analysis."
339 echo " If FIO is not installed, we use basic DD analysis."
340 echo " You should apt install fio (pacman -S fio) for detailed analysis."
341 echo "mReschke 2024-01-18"
342 echo ""
343 echo "NOTICE, this creates a 1GB file on the desired destination disk."
344 echo "Please ensure you have write access with 1GB free space on destination."
345 echo ""
346 echo "Usage:"
347 echo " This will use FIO if installed, else DD"
348 echo " ./speedtest-hd /mnt/somedisk"
349 echo " ./speedtest-hd ."
350 echo ""
351 echo " This will force FIO"
352 echo " ./speedtest-hd /mnt/somedisk --fio"
353 echo " ./speedtest-hd . --fio"
354 echo ""
355 echo " This will force DD"
356 echo " ./speedtest-hd /mnt/somedisk --dd"
357 echo " ./speedtest-hd . --dd"
358 echo ""
359 echo " Add --simple (FIO only) for a compact, aligned summary of MB/s values"
360 echo " ./speedtest-hd . --simple"
361 echo " ./speedtest-hd . --fio --simple"
362 exit 0
363}
364
365# Go
366main
367