Meine bisherigen Messungen der Performance habe ich immer mit sysbench durchgeführt. Mit Flexible IO (fio) gibt es aber ein Stück Software, das sehr viel umfangreichere Tests durchführen kann

fio
fio ist in C geschrieben und steht unter GPL V2 Lizenz. Es ist für Linux, Mac OS und Windows verfügbar.
Installation
In den Repositories von Armbian und Raspberry Pi OS ist fio bereits enthalten und kann daher einfach mit
sudo apt install fio -y
installiert werden. Auch die zum Hauptprogramm gehörigen Tools sind darin enthalten.
Falls nötig, kannst du den Quellcode mit
git clone https://git.kernel.dk/cgit/fio/
Vom git Repository herunterladen und selber bauen.
Der erste Test
Ich verwende in diesem Post den Orange Pi 5 Pro, da er mit einer NVME und einem eMMC Modul ausgestattet ist.
Unser erster Testaufruf sieht so aus:
fio --name=random-write --ioengine=posixaio --rw=randwrite --bs=4k --numjobs=1 --size=4g --iodepth=1 --runtime=60 --time_based --end_fsync=1
Das sieht kompliziert aus, wenn man es erstmal verstanden hat, bemerkt man aber, welche Möglichkeiten fio hat. Außerdem werden wir den Aufruf weiter unten noch vereinfachen.
--name=random-write
Dieser Parameter definiert einen frei wählbaren Namen für den Test.
--ioengine=posixaio
Hiermit wird die verwendete I/O Engine festgelegt. posixaio steht für Posix Async I/O. Unter Windows benutzt du besser windowsaio
. Der Mac unterstützt posixaio nur eingeschränkt, vielleicht ist da psync
besser geeignet.
--rw=randwrite
Jetzt legen wir den Testtyp fest: Mit randwrite
werden zufällige Schreibzugriffe getestet. Weitere Werte sind read, write, randread, randrw
--bs=4k
Damit definieren wir die Blockgröße (block size) für die I/O Operationen und legen sie auf 4KB fest. Alternativ ist auch eine Blockgröße von 8k möglich, aber die meisten Dateisysteme arbeiten mit einer Blockgröße von 4KB.
--numjobs=1
Wir setzen die Anzahl der parallelen Threads, die verwendet werden sollen auf 1. Mit einem Wert von 4 oder 8 kannst du gut parallele Zugriffe testen.
--size=4g
Die Datenmenge, die pro Job geschrieben werden kann, legen wir auf 4GB fest.
--iodepth=1
Dieser Parameter legt die Anzahl der gleichzeitig laufenden I/O-Anfragen pro Job fest. Mit einem Wert von 1 wartet jeder Job mit einer neuen Anfrage so lang, bis die vorherige abgeschlossen ist.
--runtime=60
Dieser Test soll 60s lang laufen. Dieser Parameter „beißt“ sich etwas mit --size
, da der Test evtl. nicht lang genug läuft, bis die definierte Datenmenge voll ausgeschöpft wurde. Mit –runtime lassen sich sehr gut Langzeittest initiieren.
--time_based
Hiermit legen wir fest, dass der Test zeitbasiert beendet werden soll, sonst würde er laufen, bis die Datenmenge aus –size vollständig verwendet wurde.
--end_fsync=1
Der letzte Parameter bewirkt, dass zum Abschluss des Tests fsync()
aufgerufen wird, um sicher zu stellen, dass alle Daten wirklich ins Dateisystem geschrieben werden. Dadurch steigt der Realitätsbezug des Tests.
Wenn der Test dann vollständig durchgelaufen ist, gibt er das Resultat auf der Console aus.

Das Ergebnis interpretieren
Die Menge an Informationen, die fio ausgibt, kann einen schon erschlagen, aber schauen wir uns die wichtigsten Ergebnisse mal im Detail an:
IOPS=29.8k
Es wurden 29800 Schreibvorgänge pro Sekunde durchgeführt, mit lediglich iodepth=1
ist das ein gutes Ergebnis.
BW=116MiB/s (122MB/s)
Dies ist der Schreibdurchsatz, einmal mit in 116 Mebibytes per second und in 122 Megabyte pro Sekunde. Mebibytes ist der in der Digitaltechnik übliche Binärpräfix mit Basis 1024, Megabytes orientiert sich an der Basis 1000, die im SI-Einheitensystem üblich ist. Aus Marketinggründen steht häufig der höhere Wert auf der Packung.
lat (usec): min=10, max=10781, avg=26.12, stdev=15.04
In dieser Zeile werden die Latenzen angeben, Als minimaler, maximaler Wert, sowie Durchschnitt und Standardabweichung. Daraus ergibt sich, dass 99.9% der Schreibzugriffe unter 81 µs lagen. Der Ausreißer von max=10,7ms ist vermutlich auf ein kurzes Thermal Throtteling der NVME zurückzuführen. Dies wirkt sich natürlich negativ auf die Standardabweichung aus. Je kleiner dieser Wert ist, desto gleichmäßiger war die Performance während des Tests.
iops : min=18334, max=60766, avg=35487.98, stdev=10256.14, samples=106
Ähnlich der Latenzzeile werden hier die Statistikwerte für die Schreibvorgänge pro Messintervall anzeigt. Der Wert samples=106
deutet darauf hin, dass alle 0,5s eine Messung durchgeführt wurde.
Testdatei
Nach dem Test steht eint 4GB große Datei namens random-write.0.0 im aktuellen Verzeichnis. Diese kann gelöscht werden, sie ist nicht weiter wichtig, bei einem weiteren Testlauf würde eine Datei random-write-1.0
erzeugt werden.

Logfiles
fio kann alle Ergebnisse auch in Logdateien speichern. Dazu gibt es mehrere Programmoptionen:
--write_bw_log=mytest
– schreibt die Durchsatzergebnisse in das Logfile mytest_bw.1.log--write_lat_log=mytest
– schreibt die Latenzergebnisse in die Logfiles mytest_lat.1.log,mytest_clat.1.log und mytest_slat.1.log--write_iops_log=mytest
– schreibt die IOPS-Ergebnisse in das Logfile mytest_iops.1.log
Wenn ich den obigen Lauf wiederhole, sieht das mit allen drei gesetzten Optionen so aus:

In den Logfiles werden die Werte pro Messintervall zeilenweise festgehalten
grafische Auswertung
Die Daten der Logfiles können auch visualisiert werden. dazu wird das Programm fio_generate_plots
mit installiert. Damit es arbeiten kann, musste ich in Armbian noch gnuplot
nachinstallieren.
sudo apt install gnuplot
Um ein paar Messdaten mehr zu bekommen, mache ich einen längeren Testlauf
fio --name=test --rw=read --size=1G --time_based --runtime=60s --group_reporting --log_avg_msec=100 --write_bw_log=mytest --write_lat_log=mytest --write_iops_log=mytest
Dieser reine Lesetest läuft 60s und erzeugt alle o.a. Logfiles. Der Parameter --group_reporting
sorgt dafür, dass die Ergebnisse mehrere Jobs zu einer Gruppe zusammengefasst werden und nicht jeder Job einzeln behandelt wird. Das ist in diesem Beispiel nicht ganz so wichtig, ich wollte es nur mal erwähnt haben.
Dann kann ich mit
fio_generate_plots mytest
Die Charts zu jedem Log erzeugen lassen, fio_generate_plots sucht sich im aktuellen Verzeichnis selbstständig die Logdateien zusammen und erzeugt aus deren Inhalt die dazu gehörenden Graphen als SVG-Grafik. Diese konvertiere ich im Anschluss in ein PNG Bild und erhalte für die Durchsatz diese Kennlinie

Man erkennt, dass der Durchsatz immer wieder kurz rapide absinkt. Dies liegt vermutlich daran, dass die NVME unter der Platine des Orange Pi im Gehäuse sitzt, wo der Luftstrom des Lüfters nicht gut kühlen kann. Dadurch kommt es immer kurz zum Thermal Throtteling.
Jobfiles
Die Menge an Argumenten immer wieder einzutippen, ist natürlich lästig. Daher bietet fio die Möglichkeit, Jobfiles anzulegen, die alle Parameter für einen Testlauf definiert. Ich lege eine Datei fio_4k_read an und schreibe dies hinein:
#Lesetest mit 4K Blocksize, Laufzeit 60s
[4k_read]
rw=read
size=1G
blocksize=4k
time_based
runtime=60s
group_reporting
log_avg_msec=100
write_bw_log=4k_read
write_lat_log=4k_read
write_iops_log=4k_read
Der Test „4k_read“ ist ein reiner Lesetest rw=read
, mit einer Dateigröße von 1GB size=1G
und einem Blocksize blocksize=4k
von 4k, er wird time_based
für 60s runtime=60s
durchgeführt. Das group_reporting
ist eingeschaltet und die Logfiles werden unter dem Namen 4k_read
abgelegt.
Ich kann jetzt den Testlauf einfach mit
fio fio_4k_read
starten und muss mir nicht jedes mal die korrekten Parameter überlegen.

Testsuite
Häufig ist es sinnvoll, auf einem Datenträger mehrere verschiedene Tests laufen zu lassen. Für diesen Zweck kannst du in einem Jobfile mehrere Testläufe definieren. Dazu kopiere ich fio_4k_read nah fio_suite und erweitere die Kopie um einen einen weiteren Testlauf für einen 1h Langzeittest.
#fio_suite: Testsuite mit Lesetest und 1h Langzeittest
[4k_read]
rw=read
size=1G
blocksize=4k
time_based
runtime=60s
group_reporting
log_avg_msec=100
write_bw_log=4k_read
write_lat_log=4k_read
write_iops_log=4k_read
[longrun]
ioengine=libaio
direct=1
time_based
runtime=1h
size=4G
numjobs=4
blocksize=4k
readwrite=randwrite
write_bw_log=longrun
write_lat_log=longrun
write_iops_log=longrun
Beide Tests werden mit dem Aufruf fio fio_suite hintereinander ab gespult und schreiben ihre Daten in separate Logfiles.
Unschön ist allerdings, dass Einstellungen wie blocksize oder time_based doppelt vorkommen.Auch dafür hat fio eine Lösung. Wir definieren zu Beginn eine [global] Sektion, in der alle Werte, die für alle Testläufe gelten sollen, gemeinsam deklariert werden.
#fio_suite: Testsuite mit Lesetest und 1h Langzeittest
[global]
ioengine=libaio
time_based
blocksize=4k
[4k_read]
rw=read
size=1G
runtime=60s
group_reporting
log_avg_msec=100
write_bw_log=4k_read
write_lat_log=4k_read
write_iops_log=4k_read
[longrun]
direct=1
runtime=1h
size=4G
numjobs=4
readwrite=randwrite
write_bw_log=longrun
write_lat_log=longrun
write_iops_log=longrun
den longrun Test lasse ich nochmal über die NVME laufen. Dabei zeigt sich, dass die Konstruktion der NVME auf der Unterseite im Asixsixx Gehäuse ungünstig ist. In den ersten Sekunden werden noch über 90.000 IOPS gemessen, kurz darauf riegelt der Controller aber wegen der Hitze auf irgendwas unterhalb von 10 IOPS ab. Vielleicht sollte ich doch noch einen Heatsink auf die NVME kleben.

Zum Abschluss lasse ich die Testsuite nochmal über das eMMC Modul laufen. Da direkt neben der NVME auf der Unterseite der Platine sitzt, zeigt sich ein ähnliches Bild.

Fazit
Der Name Flexible I/O verspricht nicht zu viel. fio ist ein Testprogramm mit vielen Möglichkeiten für den Test von Datenträgern. Deshalb ist der Blog-Post auch etwas länger als üblich geworden.