MAATRIX / Блог / fio для честного замера диска: методика и типичные ошибки

fio для честного замера диска: методика и типичные ошибки

MAATRIX

Вы запустили fio разок с параметрами по умолчанию, получили число мегабайт в секунду, обрадовались или расстроились — и оказалось, что оно не имеет отношения к тому, как реально работает ваша база данных или бэкап на этом диске. Так происходит почти всегда: один прогон fio с дефолтными настройками измеряет не «скорость диска», а скорость диска в одном конкретном, обычно нерепрезентативном сценарии. Разберём, как задавать fio так, чтобы результат отвечал на реальный вопрос — «как поведёт себя мой диск под моей нагрузкой», а не на абстрактный «какое самое большое число можно выжать».

Почему один запуск fio не даёт ответа

fio (Flexible I/O Tester) — стандартный инструмент для нагрузочного тестирования дисковой подсистемы: он умеет эмулировать практически любой профиль ввода-вывода, от одного потока с синхронной записью до сотен параллельных случайных запросов. Именно эта гибкость и есть источник главной методической ошибки: инструмент, способный смоделировать что угодно, требует, чтобы вы явно указали, что именно моделировать. Запуск без продуманных параметров моделирует то, что разработчики fio выбрали значениями по умолчанию — и это почти никогда не совпадает с профилем вашего приложения.

Типичная картина: администратор гоняет fio --name=test --filename=testfile --size=1G без дополнительных флагов, получает цифру пропускной способности и записывает её в акт приёмки сервера как «скорость диска». Проблема в том, что у диска нет одной скорости — есть множество характеристик, каждая для своего паттерна доступа, и они могут отличаться в разы и на порядки. Диск, который выдаёт впечатляющие цифры на последовательной записи крупными блоками, может показывать заметно более скромный результат на случайном чтении мелкими блоками — и наоборот бывает редко, но встречается на некоторых типах накопителей. Если ваше приложение — это база данных со случайными чтениями по 8 КБ, а вы тестировали последовательную запись мегабайтными блоками, ваш «замер» рассказал вам о диске то, что вам не нужно знать, и промолчал о том, что нужно.

Правильная методика — это не один универсальный тест, а набор из нескольких целевых прогонов, каждый из которых сознательно эмулирует конкретный сценарий, релевантный именно для вашей нагрузки. Один прогон — случайное чтение мелкими блоками, как у типичной СУБД. Отдельный прогон — последовательная запись крупными блоками, как при резервном копировании. При необходимости — ещё прогон под смешанную нагрузку чтения и записи, если у вас она реально смешанная. У вас получится не одна цифра «производительности диска», а несколько цифр, каждая привязана к сценарию, и вместе они дают честную картину.

`--rw`: паттерн доступа решает всё

Первый и самый важный параметр — --rw, тип паттерна ввода-вывода. Он определяет не просто «читаем или пишем», а гораздо более критичную вещь — идут ли запросы последовательно (одно смещение на диске логически продолжает предыдущее) или случайно (каждый запрос обращается к произвольному, не связанному с предыдущим адресу).

Основные значения:

  • read — последовательное чтение;
  • write — последовательная запись;
  • randread — случайное чтение;
  • randwrite — случайная запись;
  • readwrite (или rw) — смешанное последовательное чтение и запись, доля задаётся --rwmixread;
  • randrw — смешанное случайное чтение и запись, та же опция --rwmixread регулирует пропорцию.

Разница между последовательным и случайным паттерном для одного и того же физического диска может быть кардинальной. На вращающихся HDD она огромна — механике диска приходится физически перемещать головку между случайными секторами, и случайный доступ проседает в разы и на порядки относительно последовательного. На SSD и NVMe разрыв меньше (нет движущихся частей), но он всё равно есть и часто существен: случайный доступ хуже утилизирует внутренний параллелизм контроллера и чаще упирается в накладные расходы на трансляцию адресов (FTL). Поэтому первое, что нужно решить перед запуском fio, — какой паттерн доступа реально порождает ваше приложение.

Веб-сервер, раздающий статику большими файлами последовательно, — это read/write. Система резервного копирования, пишущая один большой архив подряд, — тоже последовательный паттерн. А вот СУБД с индексами и разрозненными строками, почтовый сервер с тысячами мелких файлов, виртуализация с несколькими гостевыми ОС, конкурирующими за один диск, — это randread/randwrite, и часто смешанные. Если вы тестируете диск под будущую базу данных паттерном write (последовательная запись), вы получите красивое число, которое не будет иметь никакого отношения к тому, как диск поведёт себя под реальными случайными запросами СУБД.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

`--iodepth`: очередь запросов и параллелизм одного потока

--iodepth задаёт глубину очереди — сколько запросов ввода-вывода может быть отправлено накопителю одновременно, не дожидаясь ответа на предыдущий, в рамках одного потока (job). Это второй по значимости параметр после --rw, и почти так же часто игнорируется или выбирается неосознанно.

Тест с --iodepth=1 эмулирует приложение, которое отправляет один запрос, дожидается ответа, и только потом отправляет следующий — строго последовательное, синхронное ожидание. Это даёт консервативную, но честную оценку задержки одного отдельного запроса (эффективно — «время отклика диска на один запрос без конкуренции»). Такой профиль реалистичен для однопоточных утилит, простых скриптовых операций, некоторых легаси-приложений.

Тест с более высоким --iodepth (16, 32, 64 и выше) эмулирует ситуацию, когда в полёте одновременно находится множество запросов — именно так работает большинство современных серверных нагрузок: пул соединений СУБД, множество параллельных клиентов, гипервизор с несколькими виртуальными машинами. Современные SSD и особенно NVMe-накопители спроектированы для параллельной обработки множества команд одновременно (у NVMe архитектурно поддерживаются тысячи очередей команд), и их полный потенциал раскрывается только при достаточной глубине очереди. Если тестировать NVMe-диск с --iodepth=1, вы увидите задержку одного запроса, но не увидите, на что диск способен под параллельной нагрузкой — а разница между этими двумя числами может быть очень значительной, подробнее о том, почему паспортные цифры IOPS требуют высокой глубины очереди и почему единственного «правильного» числа тут не существует, разобрано в статье про IOPS и почему заявленная цифра часто недостижима.

Практический вывод: тестируйте на нескольких значениях --iodepth, а не на одном. Низкое значение (1–4) покажет задержку отклика для нагрузки, близкой к однопоточной. Высокое (32 и более) покажет предельную пропускную способность под параллельной нагрузкой. Разница между этими прогонами — не шум и не ошибка измерения, а содержательный ответ на вопрос, сколько именно параллелизма вашему диску нужно, чтобы раскрыться, и сколько параллелизма реально создаёт ваше приложение.

`--numjobs`: параллелизм на уровне процессов, а не очереди

--numjobs — отдельный параметр параллелизма, который часто путают с --iodepth, хотя они работают на разных уровнях. --iodepth — это глубина очереди внутри одного потока (сколько запросов не дожидаются ответа одновременно в рамках одного job). --numjobs — это количество отдельных параллельных потоков (или процессов) fio, каждый из которых independently генерирует свою нагрузку, в том числе со своей собственной очередью заданной глубины.

Разница практически значима. Один поток с --iodepth=32 — это одна очередь запросов от одного «клиента». Четыре потока с --iodepth=8 каждый (--numjobs=4 --iodepth=8) — это уже четыре независимых источника нагрузки, суммарно дающих ту же формальную глубину 32, но эмулирующих ситуацию, когда к диску одновременно обращаются несколько независимых процессов или клиентов — что гораздо ближе к реальности сервера, на котором крутится не одно приложение, а несколько сервисов, или СУБД с несколькими воркерами.

Если вы тестируете диск под нагрузку от единственного однопоточного процесса, --numjobs=1 с нужной вам глубиной очереди — корректный выбор. Если вы моделируете сервер с СУБД, у которой десятки одновременных подключений, каждое из которых время от времени делает запрос к диску, комбинация умеренного --iodepth с несколькими --numjobs часто ближе к реальности, чем один поток с очень большой глубиной очереди. Оба параметра стоит варьировать осознанно, а не полагаться на значение по умолчанию (обычно --numjobs=1).

`--bs`: размер блока и его влияние на результат

--bs (block size) задаёт размер одной операции ввода-вывода — сколько данных передаётся за один запрос. Это четвёртый параметр, который так же сильно смещает результат, как и три предыдущих, и так же часто выбирается «по умолчанию» без связи с реальным приложением.

Мелкие блоки (типично 4К, иногда 8К или 16К) характерны для случайного доступа баз данных — большинство СУБД читают и пишут страницами именно такого порядка величины. На мелких блоках результат в мегабайтах в секунду будет казаться скромным даже на быстром накопителе — просто потому, что накладные расходы на каждую отдельную операцию (а не на передачу самих данных) начинают доминировать. Здесь показательнее не мегабайты в секунду, а IOPS (число операций в секунду) и задержка одной операции.

Крупные блоки (256К, 1М и больше) типичны для последовательной передачи больших файлов — резервное копирование, потоковое видео, массовое копирование данных. Здесь, наоборот, показательна именно пропускная способность в мегабайтах или гигабайтах в секунду, а не IOPS: накладные расходы на операцию размываются на большой объём данных внутри неё.

Если протестировать диск под будущую СУБД с --bs=1m (крупный блок), вы получите красивую цифру мегабайт в секунду, которая ничего не скажет о том, как диск справится с реальным паттерном мелких случайных запросов базы. И наоборот: если тестировать диск под задачу резервного копирования крупными последовательными потоками с --bs=4k, вы намеренно занизите себе оценку и, возможно, откажетесь от подходящего варианта из-за нерелевантного теста.

Практический пример: тест случайного чтения СУБД

Соберём всё вместе на конкретном сценарии — эмуляция случайного чтения, характерного для нагруженной базы данных с активным случайным доступом к страницам на диске:

fio --name=db_randread \
    --filename=/data/testfile \
    --size=4G \
    --direct=1 \
    --rw=randread \
    --bs=8k \
    --iodepth=32 \
    --numjobs=4 \
    --runtime=60 \
    --time_based \
    --group_reporting

Разбор параметров:

  • --filename=/data/testfile — тестовый файл создаётся на том же разделе/диске, который вы хотите проверить (убедитесь, что это реально целевой том, а не системный раздел);
  • --size=4G — размер тестового файла; важно, чтобы он был заметно больше объёма кеша (RAM, кеш контроллера), иначе тест частично измерит кеш, а не диск;
  • --direct=1 — обход буферного кеша операционной системы, чтения и записи идут напрямую к устройству, без искажения результата кешированием на уровне ОС;
  • --rw=randread — случайное чтение, паттерн, характерный для СУБД со случайным доступом к строкам/индексам;
  • --bs=8k — размер блока, типичный для страниц многих реляционных СУБД (у части баз данных размер страницы 4К, у некоторых конфигураций — 8К или 16К, сверьтесь с настройками своей СУБД);
  • --iodepth=32 — достаточная глубина очереди, чтобы раскрыть параллельные возможности SSD/NVMe и увидеть, а не задержку одиночного запроса, а параллельную пропускную способность под нагрузкой пула соединений;
  • --numjobs=4 — четыре параллельных потока, эмулирующих несколько одновременных клиентов/воркеров СУБД;
  • --runtime=60 и --time_based — тест длится ровно 60 секунд с повторным проходом по данным при необходимости, а не завершается сразу после первого прохода по файлу; так результат меньше зависит от кратковременных всплесков и провалов;
  • --group_reporting — агрегированная сводка по всем --numjobs, а не отдельный отчёт на каждый поток (удобнее читать итоговые IOPS и задержку).

Для сравнения — прогон под сценарий резервного копирования (последовательная запись крупными блоками) выглядел бы иначе: --rw=write --bs=1m --iodepth=4 --numjobs=1 — тут высокая глубина очереди и множество потоков не так критичны, важнее непрерывность последовательной записи. Именно наличие двух (или более) целевых прогонов вместо одного универсального — и есть честная методика.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Обязательно ли использовать --direct=1?

Настоятельно рекомендуется для тестов, которые должны отражать поведение самого диска. Без --direct=1 часть операций (особенно повторные чтения одного и того же блока) может обслуживаться из кеша операционной системы в оперативной памяти, и вы измерите скорость RAM, а не диска.

Почему на моём NVMe тест с --iodepth=1 показывает намного более скромный результат, чем в характеристиках производителя?

Потому что паспортные цифры производителя обычно сняты на высокой глубине очереди (32 и выше) с несколькими потоками — то есть под нагрузкой, которую редко создаёт одно реальное приложение. Тест с --iodepth=1 показывает задержку одиночного запроса, что тоже полезная, но другая метрика.

Нужно ли тестировать на файле или можно сразу на устройстве (/dev/sdX, /dev/nvme0n1)?

Тестирование на файле внутри файловой системы безопаснее и обычно достаточно репрезентативно для оценки диска под приложение. Прямое тестирование блочного устройства (--filename=/dev/nvme0n1) точнее отражает «сырую» производительность диска, но перезаписывает данные на устройстве — делайте это только на пустых дисках или явно предназначенных для теста разделах, никогда на разделе с данными.

Сколько раз нужно повторить один и тот же тест fio?

Минимум дважды, а лучше три раза подряд, особенно для дисков в облаке/на арендованном сервере — единичный прогон может попасть на кратковременный всплеск соседской нагрузки на общем накопителе. Если результаты заметно расходятся между прогонами, ориентируйтесь на медиану, а не на лучший результат.

Есть ли готовые «профили» fio под типовые задачи?

Прямых универсальных пресетов, подходящих для любого железа, не существует именно потому, что паттерн должен соответствовать вашему приложению — но структуру команды из примера в статье (случайное чтение мелкими блоками для СУБД, последовательная запись крупными блоками для бэкапов) можно использовать как отправную точку и адаптировать под конкретный размер блока и глубину очереди вашей нагрузки.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →