MAATRIX / Блог / Как устроен приоритет ввода-вывода и почему ночной бэкап душит сайт

Как устроен приоритет ввода-вывода и почему ночной бэкап душит сайт

MAATRIX

Ночью на сервере стартует бэкап: pg_dump базы, архивация логов, снапшот тома. Загрузка процессора почти не меняется — top показывает свободные ядра, но сайт в эти часы отвечает на секунды дольше обычного, база копит блокировки, очередь запросов растёт. Дело не в CPU: диск занят одним большим потоком чтения-записи, а у ядра по умолчанию нет понятия «это фоновая задача, пропусти вперёд настоящего пользователя». Разберём, как устроен приоритет ввода-вывода в Linux, что реально делает ionice и как защитить сайт от собственного бэкапа.

Почему все процессы конкурируют за диск на равных

У процессора приоритеты понятны почти всем: nice понижает вес процесса в планировщике, и при конкуренции за ядро CPU достаётся более приоритетному соседу первым. С диском по умолчанию всё иначе — там нет аналога nice, который применяется автоматически.

Когда приложение хочет прочитать или записать данные, запрос попадает не сразу на устройство, а в очередь блочного слоя ядра (block layer). Дальше запросы от веб-сервера, базы данных и утилиты бэкапа лежат в этой очереди рядом друг с другом без какой-либо метки «это важнее» или «это может подождать». Если явно не назначить приоритет, все процессы попадают в один и тот же класс обслуживания — так называемый best-effort с нормальным (средним) уровнем. Диск не различает, чей это запрос: важный SELECT к базе для живого пользователя или последовательное чтение файла для архивации.

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

Отсюда и картина, которая сбивает с толку при диагностике: htop показывает низкую загрузку CPU, ядра свободны, а сайт тормозит. Смотреть в такой момент нужно не на CPU, а на iowait в выводе vmstat 1 или top, и на состояние процессов — если веб-воркер или процесс базы данных завис в состоянии D (uninterruptible sleep), он ждёт именно диск, а не процессор. Про это состояние и про то, почему такой процесс не убивает даже kill -9, стоит разбираться отдельно — здесь важно просто зафиксировать: без диска CPU бессилен, сколько бы ядер ни простаивало.

Как ядро всё-таки различает приоритет: классы ionice

Начиная с ранних версий Linux 2.6 в ядро добавили механизм приоритезации ввода-вывода — интерфейс ioprio_set(), с которым работает утилита ionice. Он вводит три класса, и внутри одного из них — ещё и восемь уровней приоритета.

Класс 1 — real-time. Самый высокий приоритет: планировщик обслуживает такие запросы практически без задержек, отодвигая всех остальных. Внутри класса тоже есть уровни от 0 (максимум) до 7. На практике этот класс стоит использовать крайне осторожно и почти никогда не для фоновых задач — он может настолько сильно оголодать остальные процессы, что сервер станет недоступен для всего, что не входит в real-time. Назначение real-time-приоритета процессу требует прав root не просто так: это инструмент для действительно критичных по задержке операций (например, для системы реального времени), а не способ «ускорить бэкап».

Класс 2 — best-effort. Класс по умолчанию для всех обычных процессов. Внутри — восемь уровней приоритета, от 0 (наивысший в рамках класса) до 7 (наинизший). Если процесс не задал явно ничего, он получает best-effort с уровнем, производным от его nice-значения для CPU: планировщик переиспользует ту же шкалу, так что процесс с более высоким nice (менее приоритетный для CPU) по умолчанию получает и более низкий приоритет ввода-вывода. Это единственная связь между nice и диском без явной настройки ionice — и она довольно слабая: диапазон, который выводится из nice, узкий и не решает задачу «явно отодвинуть фоновую задачу».

Класс 3 — idle. Процесс с этим классом получает доступ к диску только тогда, когда никто другой прямо сейчас не использует устройство. Как только у любого другого процесса появляется запрос, idle-процесс уступает. Это именно то, что нужно для честного фонового бэкапа: он выполнится за то время, что диск свободен, и практически не потревожит остальные процессы — ценой того, что при постоянно занятом диске idle-задача может выполняться заметно дольше, чем без ограничения.

Сама идея близка к тому, как nice работает для CPU, но реализация другая и физически завязана на планировщик блочного уровня, который активен для конкретного устройства — а вот здесь начинается важная оговорка, без которой вся схема с классами ionice работает не всегда.

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

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

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

Ключевая оговорка: ionice работает не на любом планировщике

Классы и уровни ionice — это не универсальный флаг, который действует «сам по себе» на любом диске. Реально их учитывает конкретный I/O-планировщик (I/O scheduler) блочного устройства, и таких планировщиков в современном Linux несколько: mq-deadline, bfq, kyber, none. Посмотреть активный планировщик для конкретного устройства можно так:

cat /sys/block/sda/queue/scheduler
none [mq-deadline] kyber bfq

Значение в квадратных скобках — активный планировщик. Приоритеты и веса ionice полноценно понимает BFQ (Budget Fair Queueing) — он изначально спроектирован с учётом классов и уровней ionice и честно распределяет пропускную способность между процессами согласно им, включая класс idle. Частично приоритеты учитывает mq-deadline — но там логика проще и не даёт такой же гибкой градации по восьми уровням.

А вот планировщик none (часто используется по умолчанию на NVMe, потому что переупорядочивание запросов там не даёт выигрыша — подробнее об этом в статье про настройку планировщика ввода-вывода) фактически не различает приоритеты вообще: запросы уходят к устройству в порядке поступления, без сортировки по классам и уровням. Это значит, что на многих современных серверах с NVMe и планировщиком none команда ionice -c3 для фонового процесса ничего не изменит на уровне блочного слоя — она просто не с кем работать.

Из этого следует практический вывод: прежде чем полагаться на ionice, проверьте, какой планировщик реально активен на устройстве. Если это none, а результат нужен именно от классов ionice — стоит переключиться на BFQ хотя бы для тех устройств, где идут фоновые задачи вроде бэкапа:

echo bfq > /sys/block/sda/queue/scheduler

Это меняется на лету и не переживает перезагрузку без отдельного правила udev — если решение рабочее, его нужно закрепить постоянно, а не проверять раз и забыть.

Как посмотреть текущий приоритет и назначить свой

Узнать класс и уровень приоритета конкретного процесса:

ionice -p 1234
best-effort: prio 4

prio 4 — это стандартный best-effort-уровень, тот самый «на равных со всеми», о котором шла речь выше.

Назначить приоритет уже запущенному процессу по PID:

# понизить до idle-класса — процесс будет ждать, пока диск свободен
ionice -c3 -p 1234

# best-effort с явно низким приоритетом (уровень 7 из 8, но не idle)
ionice -c2 -n7 -p 1234

Запустить новый процесс сразу с нужным приоритетом:

ionice -c3 nice -n19 rsync -a /var/lib/data/ /mnt/backup/

Обратите внимание на связку ionice и nice в одной команде — это не тавтология. ionice -c3 управляет доступом к диску, nice -n19 понижает приоритет по CPU. Бэкап обычно нагружает и то и другое: чтение больших файлов — это диск, а упаковка в архив или сжатие (gzip, zstd) — это заметная нагрузка на процессор. Если ограничить только одно, вторая часть системы всё равно может мешать боевым процессам.

Проверить, кто прямо сейчас реально грузит диск, удобно через iotop (нужны права root, модуль ядра CONFIG_TASK_IO_ACCOUNTING):

iotop -oPa

Флаг -o показывает только процессы с реальной активностью ввода-вывода, -P — по процессам, а не по потокам, -a — накопленные значения, а не мгновенные: так виднее, кто суммарно нагрузил диск, а не кто просто попал в кадр. Если общая картина неясна, есть смысл начать с более широкой диагностики — этому посвящена отдельная статья про то, как узнать, кто нагружает сервер.

Контейнеры и cgroups: более надёжный инструмент, чем ionice

У классического ionice есть ограничение: он назначает приоритет процессу, а не группе процессов или контейнеру. Если бэкап запускается из контейнера или через systemd-юнит с несколькими дочерними процессами, придётся либо расставлять ionice на каждый из них, либо взять контроллер io в cgroups v2 — он управляет ресурсами диска на уровне всей группы процессов сразу, независимо от того, применяют ли сами процессы ionice.

Через cgroups можно задать не абстрактный приоритет, а конкретные ограничения: максимальную скорость чтения/записи в байтах в секунду или операциях в секунду, либо относительный вес при конкуренции нескольких групп за одно устройство:

# ограничить группу backup.slice по чтению максимум условными 50 MB/s с устройства 8:0 (sda)
echo "8:0 rbps=52428800" > /sys/fs/cgroup/backup.slice/io.max

# задать относительный вес группы при конкуренции за диск (диапазон 1–10000, по умолчанию 100)
echo "default 50" > /sys/fs/cgroup/backup.slice/io.weight

Конкретные цифры лимитов здесь — не рекомендация, а иллюстрация синтаксиса: правильный порог зависит от реальной пропускной способности вашего диска и от того, сколько запаса вы готовы оставить бэкапу. Подобрать его нужно на своём железе, ориентируясь на показатели iostat -x 1 в момент выполнения бэкапа.

Для тех, кто арендует VPS, важно понимать ещё один слой: провайдер обычно сам ограничивает дисковые IOPS и пропускную способность на уровне гипервизора для конкретной виртуальной машины — независимо от настроек внутри гостевой ОС. Подробнее об этом — лимиты дисковых операций для виртуалок. Если вы упираетесь именно в этот внешний лимит, никакой ionice или io.weight внутри гостевой системы не помогут — задачи конкурируют за одну и ту же урезанную квоту. А если вообще непонятно, диск ли тут узкое место, стоит начать с базовой диагностики — как проверить, что диск на VPS медленный.

Практическая схема для ночного бэкапа

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

Если бэкап запускается через cron, самый простой вариант — обернуть команду:

0 3 * * * ionice -c3 nice -n19 /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Если бэкап управляется через systemd (timer + service) — приоритеты лучше задавать декларативно прямо в юните, это надёжнее, чем полагаться на то, что сам скрипт вызовет ionice:

# /etc/systemd/system/nightly-backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Nice=19
IOSchedulingClass=idle
IOSchedulingPriority=7
CPUSchedulingPolicy=other

IOSchedulingClass=idle здесь эквивалентен ionice -c3, а параметр IOSchedulingPriority имеет смысл только для класса best-effort (класс 2) — для idle его можно не указывать, но systemd не считает это ошибкой.

Практическая последовательность действий на сервере:

  1. Убедитесь, что для устройства с базой данных и файлами сайта активен планировщик, который вообще учитывает приоритеты (bfq, при необходимости — переключить с none/mq-deadline).
  2. Заверните команду бэкапа в ionice -c3 (idle) как базовый вариант. Если бэкап растягивается на неприемлемое время, а сайт всё ещё чувствует нагрузку — переходите на ionice -c2 -n7 (низкий best-effort вместо полного idle) как компромисс.
  3. Добавьте nice для CPU-части бэкапа (сжатие, шифрование) — отдельно от диска.
  4. В контейнере или отдельном systemd-slice продублируйте ограничение через cgroups io.weight или io.max, не полагаясь только на ionice внутри процесса.
  5. Проверьте в момент бэкапа: vmstat 1 (столбец wa — iowait), iotop -oPa и отклик самого сайта. Хорошая настройка — та, при которой iowait и время ответа почти не отличаются от обычных ночных значений.

Отдельно стоит сказать честно: даже идеально настроенный ionice не сделает бэкап бесплатным для диска. Он меняет порядок обслуживания запросов, но не увеличивает физическую пропускную способность накопителя. Если диску объективно не хватает производительности для продакшн-нагрузки и бэкапа одновременно, приоритеты просто растянут бэкап дольше — а решать проблему нужно быстрым хранилищем, отдельным диском под бэкапы, переносом на менее нагруженное окно или снапшотом на уровне файловой системы вместо прямого копирования горячих данных.

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

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

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

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

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

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

Помогает ли повышение nice (понижение приоритета CPU) само по себе снизить нагрузку на диск?

Слабо. Связь между nice и приоритетом ввода-вывода best-effort есть, но она узкая и неявная — не заменяет прямое назначение через ionice или cgroups. Для диска нужен отдельный инструмент.

Почему ionice -c3 не дал никакого эффекта на моём сервере?

Скорее всего активный I/O-планировщик устройства — none, который не учитывает классы и уровни приоритета вообще. Проверьте cat /sys/block/<устройство>/queue/scheduler и рассмотрите переключение на bfq для этого диска, если приоритеты вам действительно нужны.

Можно ли назначить real-time-класс бэкапу, чтобы он выполнился быстрее?

Формально можно, но смысл обратный — real-time даёт максимальный приоритет, а не защиту от него. Для фоновой задачи, которая не должна мешать остальным, нужен класс idle, а не real-time.

Ionice и лимиты провайдера VPS на IOPS — это одно и то же?

Нет. ionice управляет порядком обслуживания запросов внутри вашей гостевой ОС. Лимит IOPS/пропускной способности, который задаёт провайдер на уровне гипервизора, — это отдельный внешний потолок, который действует независимо и может обесценить любую внутреннюю приоритезацию, если вы в него упираетесь.

Что делать, если у меня несколько разных фоновых задач (бэкап, антивирусное сканирование, переиндексация) и их тоже нужно развести между собой по приоритету?

Используйте разные уровни внутри best-effort (-n0...-n7) для градации между собой, а класс idle оставьте для самой некритичной по срокам задачи — той, которую действительно не жалко отложить, пока диск занят чем-то ещё.

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

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

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