Продолжаю играться с TrueNas Scale, которая на момент написания статьи имеет статус «Alpha, почти Beta», благодаря nightly обновлению. Беря во внимание этот факт, а также то, что я с этой сборкой совсем не знаком (чуть-чуть знаком с Debian на примере Ubuntu и Linux Mint), возникло естественное желание проверить скорость работы созданного Raid-z1 массива. Предположу, что обычно для этих целей используют IOzone, поскольку именно эта утилита упомянута на странице Command Line Utilities, вот только мне она недоступна. Скорее всего потому, что это Alpha, ведь iXsystems поддерживают IOzone. Зато мне доступна утилита fio. Но беда в том, что это утилита командной строки, описание параметров которой занимает несколько десятков страниц, а я-то избалован виндой – мне бы кнопочку «сделать все тесты» и наглядные графики, как в CrystalDiskMark. Но вариантов нет и значит нужно разбираться и вспоминать мат. часть…
Для начала рекомендую к прочтению статью Производительность дисковой подсистемы — краткий ликбез – ребятки очень доступно объясняют устройство жёстких дисков и упоминают особенности работы (вообще у ребят много интересных материалов на сайте). Далее обратимся к статье ZFS on Linux: вести с полей 2017, в которой автор акцентирует внимание на некоторых особенностях работы ZFS, в частности что это система использует Копирование при записи, а также упоминается параметр «размер блока» (recordsize). Процитирую автора статьи с хабра: «выставляйте нужный размер блока (recordsize), файлы меньше recordsize будут записываться в уменьшенный блок, файлы больше recordsize будут записывать конец файла в блок с размером recordsize (при recordsize=1M файл размером 1.5мб будет записан как 2 блока по 1мб, в то время как файл 0.5мб будет записан в блок размером 0.5мб).» Значение по умолчанию =128KБ. Далее стоит упомянуть такое понятие как «очередь команд» (NCQ), про которое очень доступно и с картинками рассказано вот тут.
Итак подытожим: жёсткий диск состоит из нескольких дисков, на каждой из сторон которого имеются концентрические дорожки. Совокупность дорожек, равноотстоящих от центра, на всех рабочих поверхностях пластин называют цилиндром. В свою очередь дорожки разбиты на сектора по 4КБ (скорее всего) – это минимальный объём данных, который может физически прочитать или записать сам диск. Т.е. мы можем попытаться записать 512 байт (размер сектора старых дисков), но современный диск при этом считает 4КБ, поменяет 512 байт внутри и запишет на диск всё те же 4КБ. Именно поэтому современные программы тестирования дисков в большинстве случаев пишут блоки по 4КБ. Однако у ZFS есть свой минимальный размер блока (record size), который можно установить как для массива (Pool), так и для набора данных (DataSet). Тут разработчики как бы намекают, что имеет смысл для разных наборов данных использовать разный размер блока, в зависимости от содержимого. Например, для видосиков имеет смысл установить максимальный размер, чтобы лишний раз не считать контрольные суммы, в то время как для файлов баз данных лучше поставить минимальное значение, чтобы иметь возможность записывать\считывать данные небольшими порциями. А в контексте тестирования минимальный объём блока данных записи\чтения должен быть больше или равен размеру минимального блока файловой системы (размер кластера диска или ZFS recordsize).
Ещё одним важным параметром является то, как именно данные будут записаны\считаны: последовательно или случайно. Случайный доступ – это самое проблемное место для жёстких дисков, поскольку нужно физически перемещать головки по диску, а это как раз самая медленная операция во всей цепочке. Представьте на минутку, что на диск идёт запись видосика в FullHD или 3D объёмом 25-50ГБ – как данные последовательно размещаются по кластерам не просто одной дорожки, а всего цилиндра! Или обратная ситуация: чтение данных БД, которые размещены по разным значительно удалённым друг от друга кластерам. Конечно, очередь (NCQ) ситуацию немного выправит, но всё равно подобные операции случайного доступа в десятки, а то в и в сотни раз медленнее. Эти рассуждения натолкнули меня на мысль: разумно было бы использовать жёсткие диски под запись\чтение большого объёма редко изменяющихся данных, например вышеупомянутые видосики и может быть иные файлы мультимедиа, образы дистрибутивов, контейнеры Docker’а, архивы и т.п. А для задач, в которых должна быть высокая скорость случайной записи\чтения использовать твердотельный накопитель. Вот пара статей Хабр ДНС сравнения производительности жёстких дисков и твердотельных накопителей в разных режимах работы. Цифры говорят, что при последовательной записи\чтении жёсткие диски проигрывают твердотельным накопителям в 3-5 раз, в то время как при случайной записи\чтении разница уже в 50-100 раз. В идеале было здорово собрать массив из твердотельных накопителей, но тут есть другая проблема – стоимость. Ценник 1 ГБ объёма твердотельного накопителя для домашнего использования в феврале 2021г стартует от 10 рублей, серверный вариант – 15 руб на TLC и 30 руб на MLC. Тот же объём на жёстком диске для домашнего использования обойдётся в 2 рубля, а диск для NAS – 2,35. И это без использования избыточности, применение которой увеличит стоимость вдвое для зеркала (Mirror) и на треть для Raid-Z1 из трёх дисков. В моём случае как раз используется RAID-Z1 из трёх дисков Seagate 5900 IronWolf [ST4000VN008] приобретённых в ДНС за 9 399 каждый. Путём не хитрых математических вычисления получаем, что хранение одного полуторачасового фильма в FullHD H264 24 кадра/с объёмом 17,6 ГБ обойдётся мне в 62 рубля. Для SSD этот показатель был бы в 6-12 раз больше. Вывод, думаю, уже напрашивается сам собой: в дополнение к дискам нужно брать ещё и небольшой SSD для операций случайной записи\чтения, в то время как массив из жёстких дисков должен использоваться преимущественно для последовательной записи\чтения. И поскольку SSD без избыточности, то необходимо регулярно делать резервные копии данных SSD.
На этом, пожалуй, закончим с вводной частью по жёстким дискам и перейдём к fio. Как работает эта программа? – fio эмулирует обращение к файловой системе в том или ином режиме в зависимости от параметров запуска. Под обращением понимается создание новых файлов и запись в них всякого «мусора», либо чтение ранее созданных файлов. Тестовые файлы должны где-то хранится и по умолчанию они будут созданы в текущем каталоге. Данное поведение также можно изменить с помощью параметров указав отдельный каталог, а также то, как программа будет именовать файлы. Но, конечно, эти и многие другие второстепенные параметры не так важны, как основные: тип нагрузки, размер блока, размер файла, глубина очереди, кол-во потоков и возможно что-то ещё, что я упустил. Попробуем на практике поиграться с параметрами fio для разных типов задач (на всякий случай: документация тут). И если у вас практикум как и у меня – не забудьте перед запуском команд перейти в нужный DataSet.
Начнём с самого простого: запись 1-го файла в 50 ГБ блоками равными 128КБ (размер записи ZFS по умолчанию):
fio —name=seqwrite —rw=write —direct=1 —ioengine=libaio —bs=128k —numjobs=1 —size=64G.
Результат:
seqwrite: (g=0): rw=write, bs=(R) 128KiB-128KiB, (W) 128KiB-128KiB, (T) 128KiB-128KiB, ioengine=libaio, iodepth=1
fio-3.25
Starting 1 process
seqwrite: Laying out IO file (1 file / 65536MiB)
Jobs: 1 (f=1): [W(1)][100.0%][w=238MiB/s][w=1901 IOPS][eta 00m:00s]
seqwrite: (groupid=0, jobs=1): err= 0: pid=35889: Mon Feb 15 16:40:52 2021
write: IOPS=2001, BW=250MiB/s (262MB/s)(64.0GiB/261948msec); 0 zone resets
slat (usec): min=39, max=25020, avg=494.84, stdev=189.76
clat (nsec): min=989, max=905512, avg=2076.73, stdev=4725.70
lat (usec): min=40, max=25027, avg=497.51, stdev=190.11
clat percentiles (nsec):
| 1.00th=[ 1144], 5.00th=[ 1192], 10.00th=[ 1224], 20.00th=[ 1272],
| 30.00th=[ 1304], 40.00th=[ 1352], 50.00th=[ 1416], 60.00th=[ 1560],
| 70.00th=[ 1736], 80.00th=[ 2512], 90.00th=[ 2864], 95.00th=[ 4192],
| 99.00th=[ 10560], 99.50th=[ 10688], 99.90th=[ 14912], 99.95th=[ 21632],
| 99.99th=[127488]
bw ( KiB/s): min=101632, max=1356288, per=100.00%, avg=256289.40, stdev=75823.85, samples=523
iops : min= 794, max=10596, avg=2002.26, stdev=592.37, samples=523
lat (nsec) : 1000=0.01%
lat (usec) : 2=76.21%, 4=18.51%, 10=3.01%, 20=2.21%, 50=0.04%
lat (usec) : 100=0.01%, 250=0.01%, 500=0.01%, 750=0.01%, 1000=0.01%
cpu : usr=1.54%, sys=15.28%, ctx=531413, majf=5, minf=12
IO depths : 1=100.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=0.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
issued rwts: total=0,524288,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=1
Run status group 0 (all jobs):
WRITE: bw=250MiB/s (262MB/s), 250MiB/s-250MiB/s (262MB/s-262MB/s), io=64.0GiB (68.7GB), run=261948-261948msec
262МБ – не плохо. Сразу после покупки я проверял диски с помощью Victoria и там максимальная скорость была от 100 до 200 МБ, в зависимости от того, насколько сектор был близко к центру диска. Отмечу также, что в процессе теста очень прилично нагружался процессор.
Следующий вариант: попробуем увеличить очередь до 32: fio —name=seqwrite —rw=write —direct=1 —ioengine=libaio —bs=128k —numjobs=1 —size=64G —iodepth=32. По идее это никак не должно повлиять на ситуацию, потому что оптимизации NCQ при увеличении глубины очереди могут немного улучшить ситуацию при случайной записи\чтении.
seqwrite: (g=0): rw=write, bs=(R) 128KiB-128KiB, (W) 128KiB-128KiB, (T) 128KiB-128KiB, ioengine=libaio, iodepth=32
fio-3.25
Starting 1 process
Jobs: 1 (f=1): [W(1)][100.0%][w=216MiB/s][w=1726 IOPS][eta 00m:00s]
seqwrite: (groupid=0, jobs=1): err= 0: pid=279832: Mon Feb 15 17:04:09 2021
write: IOPS=1827, BW=228MiB/s (239MB/s)(64.0GiB/286932msec); 0 zone resets
slat (usec): min=42, max=58230, avg=542.87, stdev=213.46
clat (usec): min=11, max=73886, avg=16966.03, stdev=4169.26
lat (usec): min=647, max=74563, avg=17509.49, stdev=4296.47
clat percentiles (usec):
| 1.00th=[ 2245], 5.00th=[13173], 10.00th=[13829], 20.00th=[14484],
| 30.00th=[15139], 40.00th=[15664], 50.00th=[16188], 60.00th=[16909],
| 70.00th=[17957], 80.00th=[19268], 90.00th=[21627], 95.00th=[24249],
| 99.00th=[30540], 99.50th=[33424], 99.90th=[39584], 99.95th=[44827],
| 99.99th=[57934]
bw ( KiB/s): min=114688, max=1617152, per=100.00%, avg=233960.36, stdev=76067.01, samples=573
iops : min= 896, max=12634, avg=1827.82, stdev=594.27, samples=573
lat (usec) : 20=0.01%, 750=0.01%
lat (msec) : 2=0.47%, 4=1.20%, 10=0.50%, 20=81.54%, 50=16.26%
lat (msec) : 100=0.03%
cpu : usr=1.92%, sys=13.71%, ctx=524814, majf=0, minf=13
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=100.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
issued rwts: total=0,524288,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
Run status group 0 (all jobs):
WRITE: bw=228MiB/s (239MB/s), 228MiB/s-228MiB/s (239MB/s-239MB/s), io=64.0GiB (68.7GB), run=286932-286932msec
По факту получилось даже немного медленнее.
Попробую увеличить blocksize до 1M: fio —name=seqwrite —rw=write —direct=1 —ioengine=libaio —bs=1M —numjobs=1 —size=64G. Лучше не стало: WRITE: bw=223MiB/s (234MB/s).
Теперь попробую создать новый набор данных (DataSet) с размером записи (recordsize) 1МБ и выполнить предыдущую команду – лучше не стало WRITE: bw=240MiB/s (252MB/s). Я топчусь на месте…
Хорошо, а что там с чтением? — fio —name=seqwrite —rw=read —direct=1 —ioengine=libaio —bs=1M —numjobs=1 —size=64G даёт результат READ: bw=246MiB/s (258MB/s).
Ещё разок попробую создать новый набор данных с размером записи 1МБ, с выключенной опцией Atime и отключенной компрессией. Результат всё те же WRITE: bw=232MiB/s (244MB/s) и READ: bw=243MiB/s (255MB/s). Что ж – видимо это мой «потолок». Правда есть у меня подозрение, что я уперся в производительность процессора, поскольку во время записи загруженность иной раз подскакивает и до 97%. Во время чтения загрузка в районе 50%, но это для всего процессора, а отдельные потоки всё также 100%. Так что есть куда расти…
Увеличение кол-ва потоков никак не влияет на ситуацию: fio —name=seqwrite —rw=write —direct=1 —ioengine=libaio —bs=1M —numjobs=1 —size=8G —numjobs=8 —group_reporting даёт всё те же ~250МБ как для записи, так и для чтения.
Теперь пример неудачного использования массива: если вдруг какая-то из программ захочет записывать данные блоками по 4КБ: fio —name=seqwrite —rw=write —direct=1 —ioengine=libaio —bs=4k —numjobs=1 —size=1G то получим следующий результат: WRITE: bw=53.3MiB/s (55.9MB/s).
Хотел было уже перейти к тестированию случайного доступа, но команда fio —name=random —rw=randwrite —direct=1 —ioengine=libaio —bs=128k —numjobs=1 —size=16G даёт всё те же 250МБ, видимо за счёт кэширования.
А вот чтение: fio —name=random —rw=randread —direct=1 —ioengine=libaio —bs=128k —numjobs=1 —size=16G показывает реалистичную картину доступа к случайным данным READ: bw=12.5MiB/s (13.1MB/s), что в 200 раз медленнее последовательного доступа. ВУАЛЯ! SSD быть! Любопытно то, что уменьшение размера файла до 1ГБ при условии наличия файла с тем же именем не даёт существенного выигрыша в скорости. А вот если программе придётся создать новый файл для тестирования случайного чтения, то получим вот такую картину: READ: bw=3122MiB/s (3274MB/s).