Этот урок - карта. Не команда-волшебная-палочка, а именно карта местности: по каким слоям проходит запрос на чтение или запись, прежде чем превратиться в реальное движение данных, и где на этом пути возникают задержки. Когда карта в голове есть, выбор инструмента становится очевидным. Болит на уровне приложения - берешь strace или eBPF. Болит на уровне устройства - берешь iostat. Без карты любой инструмент кажется одинаково непонятным набором цифр.
Как устроена io подсистема linux: слои сверху вниз
Представь себе многоэтажку, где запрос на чтение байтов спускается с верхнего этажа в подвал, к железу. Вот эти этажи по порядку.
- Приложение. Твой код вызывает read(), write(), pread(), pwrite() или open(). Для программы это просто функция, но под капотом это вход в ядро.
- Системный вызов (syscall). Граница между пользовательским кодом и ядром. Именно здесь strace перехватывает и показывает, что именно просит приложение.
- VFS (Virtual File System). Слой-переводчик. Он прячет различия между файловыми системами: для VFS и ext4, и xfs, и сетевая NFS выглядят одинаково. Благодаря ему read() работает с любой ФС без переписывания кода.
- Файловая система - ext4, xfs, btrfs. Знает, в каких блоках на диске лежит твой файл, ведет журнал, отвечает за целостность.
- Page cache - кэш страниц в оперативной памяти. Критически важный слой. Большая часть чтений сюда и попадает, не доходя до диска вообще.
- Block layer (блочный слой) - сердце дискового стека. Собирает запросы в очередь, объединяет соседние (merge), упорядочивает их планировщиком io. Здесь работает blk-mq - многоочередная архитектура для современных дисков.
- Драйвер устройства. Превращает абстрактный block-запрос в команды конкретного контроллера (SATA, SAS, NVMe).
- Устройство - физический накопитель HDD, SSD или NVMe.

Page cache, синхронный и буферизованный io: где прячется задержка
Главная развилка, без понимания которой дисковый io в Linux выглядит магией - это page cache.
Когда ты пишешь обычным write(), данные по умолчанию НЕ летят сразу на диск. Они оседают в page cache (это так называемые "грязные", dirty, страницы), а ядро сбрасывает их на устройство потом, фоновым потоком writeback. Такая запись называется буферизованной (buffered). Она быстрая для приложения, потому что фактическая работа диска отложена. Минус: если выдернуть питание до сброса, эти данные потеряются.
Сколько "грязи" ядро готово копить, регулируют параметры vm.dirty_ratio и vm.dirty_background_ratio (в /proc/sys/vm/). Когда грязных страниц набирается до фонового порога, просыпается writeback и начинает сброс. Когда грязи становится слишком много (верхний порог), ядро притормаживает само приложение, заставляя его писать синхронно - это явление называют writeback throttling, и со стороны оно выглядит как внезапные "залипания" записи. Полезно знать, если видишь рваный профиль записи в iostat.
Чтение тоже идет через кэш. Первый раз файл читается с диска и кладется в page cache. Второй раз он прилетает уже из оперативки - в сотни раз быстрее. Поэтому повторный запуск той же команды часто "ускоряется" - это не магия, это кэш. Объем кэша видно в free -h (колонка buff/cache) и в /proc/meminfo (поля Cached и Dirty).
А вот когда программе нужна гарантия, что данные на диске, она зовет fsync() (целиком файл) или fdatasync() (только данные, без лишних метаданных - чуть дешевле), либо открывает файл с флагом O_SYNC или O_DIRECT. fsync блокирует приложение до тех пор, пока устройство не подтвердит запись. Базы данных делают это на каждый коммит. Вот почему медленный fsync на дешевом SSD без конденсатора (power loss protection) убивает производительность Postgres, хотя "диск вроде не загружен". Синхронный io - это плата за надежность, и часто именно он, а не пропускная способность, оказывается узким местом.
io-bound - это про задачу, которая упирается в скорость диска, а не процессора. Если процесс почти все время ждет данные с накопителя, он io-bound. Противоположность - cpu-bound, где узкое место в вычислениях. iowait в выводе top как раз показывает долю времени, когда CPU простаивал в ожидании io. Высокий iowait - сигнал "ищи проблему в дисковом стеке", но сам по себе он не диагноз: иногда iowait высокий просто потому, что нагрузки на CPU нет, и ему нечем заняться, кроме ожидания одного-единственного потока.
Где мерить блочный слой и устройство: практика и чтение вывода
Карта нужна, чтобы выбрать точку измерения. Начнем снизу - с устройства. Главный инструмент тут iostat из пакета sysstat.
Код: Выделить всё
sudo iostat -xz 1 3
Код: Выделить всё
Device r/s w/s rkB/s wkB/s rareq-sz wareq-sz r_await w_await aqu-sz %util
nvme0n1 12.0 340.0 480.0 21760.0 40.0 64.0 0.18 8.40 2.95 62.30
- r/s, w/s - операций чтения и записи в секунду (это и есть IOPS). Здесь явный перекос в запись.
- rkB/s, wkB/s - килобайты в секунду, то есть пропускная способность (throughput).
- rareq-sz, wareq-sz - средний размер одного запроса в килобайтах. Маленький размер при большом числе операций - это random io (много мелких разбросанных запросов), большой размер - последовательный (sequential). Эта пара колонок мгновенно отвечает на вопрос "это случайная нагрузка или потоковая", что критично для HDD.
- r_await, w_await - среднее время обслуживания запроса в миллисекундах, включая ожидание в очереди планировщика плюс время самого устройства. Это твой главный индикатор задержки (latency). Для NVMe норма - доли миллисекунды, для SSD - единицы мс, для HDD - десятки мс. Здесь w_await=8.4 мс для NVMe - это уже подозрительно много, диск захлебывается записью или ждет flush.
- aqu-sz - средняя длина очереди запросов (раньше колонка называлась avgqu-sz). Это суммарно очередь планировщика плюс запросы, уже отданные в устройство. Близко к нулю - диск скучает. Стабильно больше единицы - запросы выстраиваются, устройство не успевает.
- %util - процент времени, когда устройство было занято хоть одним запросом. У старых HDD близость к 100% означала насыщение. ВНИМАНИЕ, типичная ловушка 2026 года: для NVMe и SSD %util врет. Эти диски обрабатывают много запросов параллельно (несколько аппаратных очередей), поэтому %util считает занятость, как будто очередь одна, и быстро упирается в 100%, хотя диск выжат на 20 процентов. Смотри на await, aqu-sz и реальные IOPS, а не на util.
Теперь посмотрим на блочный слой - какой планировщик io работает:
Код: Выделить всё
cat /sys/block/nvme0n1/queue/scheduler
Код: Выделить всё
[none] mq-deadline kyber bfq- none - без переупорядочивания, запросы идут в порядке поступления. Дефолт и правильный выбор для NVMe.
- mq-deadline - следит за дедлайнами запросов, не дает чтению голодать из-за записи. Хорош для HDD, SATA SSD и виртуалок.
- kyber - легкий латентностный планировщик для быстрых дисков, держит целевые задержки чтения/записи.
- bfq - честное деление полосы между процессами, дает отзывчивость на интерактивном десктопе, но имеет накладные расходы и не любит очень высокий IOPS.
Код: Выделить всё
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
iostat показывает, что диск занят, но не объясняет, КТО и ЗАЧЕМ его грузит. Чтобы понять, что делает само приложение на уровне syscall, поднимаемся на верх стека.
Классика - strace:
Код: Выделить всё
sudo strace -T -e trace=read,write,fsync,fdatasync -p 1234
Код: Выделить всё
fsync(7) = 0 <0.412019>В 2026 году правильный инструмент для верха стека под нагрузкой - это eBPF. Он давно зрелый: на ядрах 5.x и 6.x пакеты bcc и bpftrace есть в репозиториях всех серьезных дистрибутивов, и для многих задач они уже вытеснили strace и старый blktrace, потому что почти не дают накладных расходов и видят то, что снаружи невидимо. Вот рабочий минимум, который стоит знать.
Код: Выделить всё
sudo biolatency.bt
Код: Выделить всё
sudo biosnoop.bt
Типы накопителей и их профили задержек держи в голове как реперные точки. HDD: механика, головка физически едет к дорожке, случайное чтение 5-15 мс, ненавидит random io, любит sequential. SATA SSD: нет механики, около 0.1-0.5 мс, держит десятки тысяч IOPS. NVMe: подключен прямо в PCI Express, сотни тысяч IOPS, задержки в десятки микросекунд. Если на NVMe ты видишь await в миллисекундах - что-то не так: либо диск дешевый без аппаратной защиты кэша, либо перегружен, либо тормозит fsync/flush.
cgroup v2 и io: кто на самом деле делит диск
Еще один пласт, актуальный на 2026: на современных системах с systemd по умолчанию работает cgroup v2 (единая иерархия). Через нее ядро умеет ограничивать и взвешивать дисковый io между сервисами контроллером io. Это значит, что "тормоза диска" у конкретного сервиса могут быть не свойством железа, а заданным лимитом. Посмотреть статистику io по cgroup:
Код: Выделить всё
cat /sys/fs/cgroup/system.slice/<имя>.service/io.stat
Типичные грабли и заблуждения
- "Высокий iowait = виноват диск". Не всегда. iowait - это просто простой CPU в ожидании io. На почти простаивающей машине он может быть высоким без всякой проблемы. Смотри в комплексе: await в iostat плюс iowait плюс гистограмма biolatency.
- "%util 100% = диск на пределе". Для HDD близко к правде, для SSD/NVMe - нет. Эти устройства параллельны, util их недооценивает и упирается в 100% раньше времени. Верь await, aqu-sz и абсолютным IOPS.
- "Записал в файл - значит, на диске". Нет. Без fsync данные в page cache и при сбое питания теряются. И наоборот: кажущиеся "тормоза диска" - это часто честный fsync, а не медленное железо.
- "Повторный тест быстрее, диск разогрелся". Нет, это page cache отдает данные из памяти. Для честного бенчмарка чтения кэш сбрасывают:
Код: Выделить всё
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches - "Сменю планировщик на bfq и станет быстрее". На NVMe чаще наоборот: none быстрее, а bfq добавляет накладные расходы. Планировщик меняют под конкретный профиль нагрузки и проверяют бенчмарком, а не "на всякий случай".
- "strace покажет проблему на проде". Покажет, но затормозит горячий процесс. Под нагрузкой используй eBPF (biosnoop, biolatency), а strace - точечно.
- Запусти в одном терминале и оставь работать.
Код: Выделить всё
iostat -xz 1 - Во втором создай нагрузку на запись: (флаг oflag=direct - это O_DIRECT, обход page cache, чтобы увидеть реальный диск). Смотри, как растут w/s, wkB/s и w_await.
Код: Выделить всё
dd if=/dev/zero of=/tmp/testfile bs=1M count=2000 oflag=direct - Повтори dd БЕЗ oflag=direct и сравни: с буферизацией приложение "отстреляется" мгновенно, а реальная запись растянется во времени - это и есть работа page cache и writeback.
- Если доступен bpftrace: в третьем терминале запусти , повтори dd и прерви биолатенси по Ctrl-C - получишь гистограмму задержек блочного io.
Код: Выделить всё
sudo biolatency.bt - Посмотри планировщик:
Код: Выделить всё
cat /sys/block/*/queue/scheduler - Сбрось кэш и убедись, что повторное чтение снова идет с диска:
Код: Выделить всё
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches - Удали тестовый файл:
Код: Выделить всё
rm /tmp/testfile
Контрольные вопросы
- По каким слоям проходит запрос write() от приложения до пластины диска? Назови их по порядку.
- Чем буферизованная запись отличается от вызова fsync() и почему fsync может стать узким местом даже на быстром NVMe?
- Почему на NVMe нельзя верить колонке %util, и на какие колонки iostat смотреть вместо нее?
- Чем biolatency полезнее средней колонки await, и когда вместо strace на проде стоит брать biosnoop?