Приложение тормозит, процессор загружен на четверть, свободной памяти хватает, а пользователи всё равно смотрят на крутящиеся индикаторы загрузки. Виновником в таких историях чаще всего оказывается диск, и самый быстрый способ это доказать живёт в пакете sysstat. Утилита iostat показывает, сколько запросов в секунду получает каждый накопитель, как долго они стоят в очереди и какую долю времени устройство занято работой. Пара минут с ней отвечает на вопрос, который без цифр растягивается на недели гаданий: система упёрлась в физику вращающегося диска или проблема прячется где-то ещё.

Откуда iostat берёт показатели и как правильно запускать его на сервере

Утилита входит в пакет sysstat и ставится одной командой:

apt install sysstat

или, на семействе RHEL:

dnf install sysstat

В отличие от sar, которому нужна включённая служба сбора статистики, iostat работает сразу после установки. Он читает счётчики напрямую из файлов /proc/diskstats и /sys, поэтому никакой предварительной настройки не требует. Условие только одно: смонтированная файловая система /proc, без неё утилита откажется стартовать.

Первая ловушка поджидает сразу после запуска. Команда iostat без параметров печатает один отчёт, усреднённый за всё время с момента загрузки. Такой средний по больнице показатель для диагностики малопригоден: за полгода аптайма на диске успело случиться всякое. Рабочая схема всегда использует интервал:

iostat -x 2

Ключ -x включает расширенный отчёт, двойка задаёт период в секундах, и утилита печатает свежие срезы непрерывно, пока её не прервут. Первый срез по-прежнему охватывает период с загрузки, дальше идут актуальные показания. Ключ -y выбрасывает этот первый срез совсем, -z прячет устройства без активности, -k и -m переключают единицы с блоков на кибибайты и мегабайты, -t добавляет метку времени к каждому срезу. Ключи -d и -c оставляют только отчёт по устройствам или только по процессору. Собранный вместе вариант для дежурного диагностирования выглядит так:

iostat -x -y -m -t 2

Точность дробной части настраивается параметром --dec, по умолчанию вывод идёт с двумя знаками после запятой. Если в системе есть разделы и LVM, пригодятся ключи -p с именем диска, -N для имён device mapper и -j UUID для постоянных имён из каталога /dev/disk, чтобы искать нужный накопитель по метке, а не по сгенерированному имени sda.

Расшифровка столбцов расширенного отчёта от r/s до %util

Фрагмент реального вывода со скромной виртуальной машины, строка по диску sda под фоновой нагрузкой:

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   wrqm/s  %wrqm w_await wareq-sz     d/s     dkB/s   drqm/s  %drqm d_await dareq-sz     f/s f_await  aqu-sz  %util
sda              0.47     23.26     0.24  34.13    0.25    49.95    4.12     57.49     3.03  42.43    0.45    13.96    0.33    177.10     0.00   0.00    0.13   529.36    0.83    0.18    0.00   0.06

r/s и w/s представляют собой те самые IOPS: число завершённых запросов чтения и записи в секунду после склейки соседних обращений. rkB/s и wkB/s описывают пропускную способность. Пара rrqm/s и %rrqm рассказывает о работе планировщика, который объединяет идущие подряд запросы: на примере выше склеено 34% операций чтения, и до устройства дошло заметно меньше обращений, чем породили приложения. Высокий %wrqm у механического диска или массива считается нормой, потому что соседние записи в последовательный поток выгодно сливать в одну крупную операцию.

Размер запроса виден в rareq-sz и wareq-sz, среднее значение в кибибайтах. Мелкие запросы по 4 КиБ выдают случайную нагрузку, характерную для баз данных и почтовых систем, крупные по 512 КиБ и больше говорят о линейном копировании. В старых выпусках эти поля назывались avgrq-sz и avgqu-sz, а размер считался в секторах, поэтому на сборках вроде CentOS 7 цифры и порядок столбцов другие, и скрипты с жёсткими номерами полей между версиями не переносятся. Колонки svctm в свежих версиях вообще нет: от неё отказались как от ненадёжной.

Время отклика разложено по типам операций: r_await для чтения, w_await для записи, d_await для обнуления блоков, f_await для принудительного сброса кэша на пластину. Общая очередь живёт в aqu-sz. Столбцы d/s и f/s считают операции обнуления и сброса в секунду, и в примере выше видно, что Discard-операции уносят 177 КиБ в секунду без единой склейки, это фоновая работа файловой системы. Из тонкостей ещё две заслуживают запоминания: буквы kB в столбцах означают кибибайты, то есть 1024 байта, а базовой единицей служит 1024-байтный блок, который меняется на 512-байтный только при выставленной переменной окружения POSIXLY_CORRECT.

Очередь запросов и время отклика как главные индикаторы затыка

Метрика await складывается из двух отрезков: ожидания в очереди и самого обслуживания. Разделить их iostat не умеет, но для вывода это и не требуется, потому что страдает именно сумма. Ориентиры выглядят так: механический диск на случайных операциях отвечает за 5-12 мс, SATA SSD за 0,1-1 мс, NVMe за десятые и сотые доли миллисекунды. Когда w_await уползает за 30-50 мс, приложения начинают чувствовать это без всякого мониторинга, и жалобы пользователей приходят раньше графиков.

Для HDD цифры выводятся из физики. Пластина со скоростью 7200 оборотов в минуту совершает оборот за 60000/7200 = 8,33 мс, в среднем головка ждёт половину оборота, то есть 4,17 мс, плюс перемещение между дорожками добавляет 4-9 мс. Один случайный запрос обходится в 8-13 мс, что даёт жёсткий потолок порядка сотни случайных IOPS. Линейное же чтение с той же пластины идёт со скоростью 150-250 МБ/с. Контраст разительный: случайные 4-килобайтные запросы при 100 IOPS дают всего 0,4 МБ/с, почти в пятьсот раз меньше, чем способна отдать сама поверхность диска. Формулировка "диск медленный" поэтому всегда неполна: медленный он только для беспорядочных мелких обращений.

Очередь связана с задержкой правилом из теории массового обслуживания: средняя длина очереди равна интенсивности потока, умноженной на среднее время пребывания заявки в системе. Для iostat это выглядит как aqu-sz, примерно равный произведению (r/s + w/s) на await с делением на 1000. Проверка на числах: 250 IOPS при 12 мс дают ровно 3,0. Если оба значения растут одновременно, а %util прижимается к сотне, диск стал затыком с гарантией. Одиночный же всплеск на минуту, например при запуске задачи резервного копирования, ещё ни о чём не говорит, и это самая частая ошибка чтения отчёта.

Заполненный индикатор %util на быстрых накопителях обманчив

Самая неверно трактуемая метрика отчёта. Насыщение при значениях, близких к 100%, наступает только у устройств, обслуживающих запросы строго по очереди, то есть у одиночных механических дисков. RAID-массивы и современные SSD обрабатывают несколько запросов параллельно, и для них высокий %util не отражает предела производительности. Читая отчёт по SSD со значением 98%, делать вывод о перегрузке рано: контроллер может оставаться загруженным на все сто и при этом отвечать за полмиллисекунды.

У NVMe эффект выражен ярче всего. Протокол позволяет держать до 65535 очередей по 65536 команд в каждой, и контроллер выполняет их пачками, не дожидаясь завершения предыдущей операции. На живом NVMe-накопителе %util под 100% при w_await в десятые доли миллисекунды означает не затык, а штатную работу под нагрузкой. Практический вывод: тройка await, aqu-sz и %util надёжно работает вместе на HDD, а для SSD и NVMe решают только задержки и длина очереди, а столбец %util показывает лишь, что устройство чем-то занято.

Есть и обратная сторона: перегруженный по глубине очереди NVMe сначала почти не меняет %util, он и так держится около максимума, зато быстро растут aqu-sz и await. Наблюдательный администратор заметит деградацию по этим двум столбцам на час или два раньше, чем она станет заметна пользователям.

Пороги, при которых серверу пора переезжать с HDD на NVMe

Решение о замене принимается по цифрам, а не по ощущениям. Смотрите на устойчивые значения за час наблюдений в типичный рабочий день, а не на одиночные пики после запуска задачи резервного копирования. Переход на быстрые накопители оправдан, когда выполняется хотя бы одно из условий:

  1. r_await или w_await стабильно выше 20 мс, хотя здоровый HDD укладывается в 5-12;
  2. aqu-sz держится выше 3-5 минутами подряд, а не секундными всплесками;
  3. %util стоит выше 90% большую часть рабочих часов;
  4. суммарный поток r/s + w/s упёрся в 150-250 случайных запросов в секунду, то есть в физический потолок механики;
  5. ночные копии растягивают задержки так, что к утру на них жалуются первые пользователи.

Для сравнения потенциала: SATA SSD обычно выдаёт десятки тысяч IOPS с задержкой 0,1-1 мс, а NVMe уходит в сотни тысяч IOPS и в десятые доли миллисекунды. На нагруженной базе данных переезд с механики на NVMe в реальности даёт ускорение ответов в десятки раз без единой правки в коде, потому что каждое обращение вместо 10 мс обходится в 0,05. Деньги на такой апгрейд обычно окупаются быстрее, чем часы, потраченные на оптимизацию запросов вслепую.

Нагрузочная проверка до переезда и контроль после него

Перед закупкой железа стоит убедиться, что затык действительно в диске, а не в сетевом пути или в планировщике CPU. Быстрый тест записи с обходом страничного кэша выполняется в терминале сервера:

dd if=/dev/zero of=/root/loadtest.bin bs=1M count=4096 oflag=direct

Параллельно в соседнем терминале снимается отчёт командой iostat -x 1, и по нему видно, как под нагрузкой растут w_await и aqu-sz. Файл после теста удаляется командой rm /root/loadtest.bin. Точнее покажет fio, если пакет установлен, потому что он умеет случайный профиль и заданную глубину очереди:

fio --name=hddcheck --filename=/root/fio.tmp --size=1G --rw=randread --bs=4k --ioengine=libaio --iodepth=32 --direct=1 --runtime=30 --time_based

Глубина очереди 32 при случайном чтении блоками по 4 КиБ имитирует типичную нагрузку занятой базы данных, и результат такого теста честно показывает, где заканчиваются возможности накопителя. После переезда сравниваются те же столбцы. Типичная картина: w_await падает с 25 мс до 0,3, aqu-sz с 5 до 0,1, и жалобы на тормоза уходят сами собой. Отдельного взгляда заслуживает блок avg-cpu в шапке отчёта: столбец %iowait показывает долю времени, когда процессор простаивал при наличии незавершённых дисковых запросов. Высокий iowait при тихом остальном процессоре почти всегда означает, что у замедления один автор, и это накопитель.

Автоматический контроль латентности силами одной строки awk

В терминал постоянно смотреть никто не будет, поэтому отчёт отдаётся планировщику и фильтру. Однострочник печатает момент, когда чтение на любом физическом диске задержалось дольше 20 мс:

iostat -x 10 | awk '$1 ~ /^sd|^nvme/ && $6+0 > 20 {print strftime("%F %T"), $1, "r_await", $6}'

Проверено на sysstat 12.x: шестое поле совпадает с r_await, двенадцатое с w_await, поэтому для наблюдения за записью условие меняется на $12+0 > 20. Вызов ставится в cron с конечным числом отчётов, например iostat -x 60 2 -y, чтобы процесс завершался сам, а знак процента в формате времени экранируется обратным слэшем, иначе crontab обрежет команду на первом проценте.

Для скриптов удобнее машиночитаемый JSON в паре с jq:

iostat -o JSON -x 2 | jq -c '.sysstat.hosts[0].statistics[-1].disk[] | select(."w_await" > 20) | {device: .disk_device, w_await: ."w_await"}'

Такой поток удобно складывать в лог, строить по нему график или отправлять в общую систему мониторинга: счётчики из /proc/diskstats читают и экспортёры Prometheus, и агент Zabbix, так что цифры из iostat совпадут с картинкой в дашборде. Разница только в глубине детализации, у iostat она ближе всего к ядру и не теряется на промежуточных преобразованиях.

iostat не заменяет полноценную систему мониторинга, но как инструмент первой реакции у него мало конкурентов. Он стоит на любой машине, запускается без подготовки и за минуту показывает, крутится ли головка вхолостую или действительно пора менять накопитель. Хорошая привычка: перед любым запросом "сервер тормозит" сначала открывать iostat, а уже потом браться за код приложений. Чаще всего окажется, что лечить нужно не программу, а физику под ней.