Резервное копирование, запущенное в час ночи, к утру превратило сервер в кисель. База данных отвечает с задержками в секунды, веб-приложение таймаутится, пользователи злятся. Знакомая картина. Проблема в том, что никто не объяснил ядру, что rsync и PostgreSQL должны получать ресурсы в разной пропорции. Ядро не умеет угадывать намерения администратора: оно распределяет CPU и I/O по своим правилам, и если эти правила не скорректировать явно, все процессы считаются равными. Равенство в условиях нехватки ресурсов означает, что тот, кто требует больше, получает больше, независимо от того, насколько он критичен.
nice и ionice, инструменты, которые позволяют это скорректировать. Они работают на разных уровнях: nice влияет на планировщик CPU, ionice на планировщик дискового ввода-вывода. Понять, как именно они влияют, а не просто выучить синтаксис, важно потому что оба инструмента имеют ненулевые ограничения, и слепое применение даёт результат хуже ожидаемого.
Что такое nice value и как CFS его интерпретирует
Nice value, число от -20 до 19. Ноль, значение по умолчанию для обычного процесса. Отрицательные значения дают процессу больший приоритет, положительные, меньший. Диапазон асимметричный: шаг от 0 до 19 снижает приоритет постепенно, тогда как шаг от 0 до -20 поднимает его значительно резче. Минус двадцать, это не "в двадцать раз больше CPU", это нечто качественно иное.
Полностью правильного понимания nice value не даст даже man-страница без знания того, как CFS (Completely Fair Scheduler) его использует. CFS не работает с приоритетами напрямую: он работает с весами. Nice value конвертируется в вес по таблице, где каждый шаг на единицу меняет вес примерно на 10%. Nice 0 имеет вес 1024. Nice 1, вес 820. Nice -1, вес 1277. Разница между 0 и 1 составляет около 20%, разница между -20 и 19, 9 раз. CFS использует эти веса для расчёта виртуального времени выполнения: процесс с бо́льшим весом накапливает виртуальное время медленнее, а значит дольше остаётся "должником" планировщика и чаще получает реальное CPU-время.
На практике это означает следующее. Если на машине два процесса, один с nice 0 и один с nice 19, и оба постоянно хотят CPU, первый получит примерно 9/10 процессорного времени, второй примерно 1/10. Но если первый спит или ждёт I/O, второй получит 100% CPU без каких-либо ограничений. Nice value, это механизм распределения при конкуренции, а не ограничитель потолка.
# Запустить процесс с пониженным приоритетом
nice -n 19 rsync -av /data/ /backup/
# Запустить с повышенным приоритетом (требует root или CAP_SYS_NICE)
nice -n -10 /usr/bin/critical-daemon
# Изменить nice value уже запущенного процесса
renice -n 15 -p 12345
# Изменить nice value всех процессов пользователя
renice -n 10 -u backup-user
# Изменить nice value всей группы процессов
renice -n 5 -g 12345
# Проверить текущие nice values
ps -eo pid,ni,comm | sort -k2 -n | head -20
Попытка поднять nice value ниже текущего без прав root завершится ошибкой. Обычный пользователь может только повышать nice value своих процессов, но не понижать. Это ограничение намеренное: иначе любой пользователь мог бы поднять приоритет своих процессов до максимума.
Интересная деталь: дочерние процессы наследуют nice value родителя. Если shell-скрипт запущен через nice -n 15, все команды внутри него тоже получат nice 15. Это удобно для batch-задач, где весь граф дочерних процессов должен работать с пониженным приоритетом:
# Весь конвейер получит nice 19
nice -n 19 bash -c 'find /data -type f | xargs md5sum > /tmp/checksums.txt'
# Проверить nice value в systemd-сервисе
systemctl show myservice.service -p Nice
Как nice value отражается в реальном времени отклика
Разрыв между теорией и практикой здесь особенно велик. Многие администраторы выставляют nice 19 для резервного копирования и удивляются, что сервер всё равно тормозит. Причина почти всегда одна: процесс тормозит не из-за CPU, а из-за I/O, а nice на I/O не влияет вообще. Rsync с nice 19 читает диск ровно с той же скоростью, что и rsync с nice 0, если планировщик I/O не настроен отдельно.
Второй источник иллюзий, нагрузка на однопроцессорную очередь. CFS справедливо делит время между потоками, но если за доступ к одному ресурсу (мьютекс, сокет, строка в базе данных) конкурируют несколько потоков, nice value не помогает: планировщик не знает, что конкретный поток базы данных важнее конкретного потока бэкапа.
# Измерить фактическое влияние nice value на CPU-интенсивный процесс
# Запустить два параллельных вычислительных процесса с разными приоритетами
time nice -n 0 openssl speed rsa2048 &
time nice -n 19 openssl speed rsa2048 &
wait
# Посмотреть реальное распределение CPU через top
# Колонка NI показывает nice value, %CPU - фактическое потребление
top -b -n 1 | head -30
# Или через ps с сортировкой по CPU
ps aux --sort=-%cpu | head -15
ionice и три класса планирования I/O
Для дискового ввода-вывода ядро использует отдельный планировщик, и на него влияет ionice. Здесь модель принципиально другая: вместо непрерывной шкалы от -20 до 19 используются три класса с разной логикой.
Класс 1, Real-time (rt): процесс получает гарантированный доступ к диску первым, перед всеми остальными. Внутри класса восемь уровней (0-7): чем ниже число, тем выше приоритет. Злоупотребление этим классом лишает другие процессы дискового доступа полностью. Назначить этот класс может только root.
Класс 2, Best-effort (be): класс по умолчанию для большинства процессов. Тоже восемь уровней (0-7), чем ниже число, тем выше приоритет. Если класс явно не задан, процессы получают уровень, вычисляемый из nice value по формуле (nice + 20) / 5, что даёт значения от 0 до 7.
Класс 3, Idle: процесс получает дисковое время только тогда, когда больше никто не обращается к диску. Без уровней, просто факт: есть другие желающие, ждёшь. Именно этот класс нужен для резервного копирования, дефрагментации, фоновой индексации. Idle-процесс никогда не будет причиной деградации дискового I/O для других.
# Запустить rsync в idle-классе (никогда не мешает другим)
ionice -c 3 rsync -av /data/ /backup/
# Запустить с best-effort классом, уровень 7 (самый низкий в классе)
ionice -c 2 -n 7 find /var/log -name "*.log" -exec gzip {} \;
# Повысить I/O-приоритет уже запущенного процесса (нужен root для rt)
ionice -c 2 -n 0 -p 12345
# Назначить realtime класс критичному сервису (только root)
ionice -c 1 -n 4 -p $(pgrep postgres)
# Проверить текущий I/O-класс процесса
ionice -p 12345
# Посмотреть I/O-классы всех процессов
for pid in $(ps -eo pid --no-header); do
ionice -p $pid 2>/dev/null | grep -v "none" | \
awk -v pid=$pid '{print pid, $0}'
done | head -20
Важное ограничение: ionice эффективен только с планировщиками I/O, которые поддерживают приоритеты. Исторически это был CFQ (Completely Fair Queuing). Начиная с ядра 5.0 CFQ удалён, и дефолтным планировщиком стал mq-deadline или BFQ в зависимости от дистрибутива. BFQ (Budget Fair Queueing) поддерживает классы ionice полноценно. mq-deadline поддерживает только realtime-класс. none и kyber вообще игнорируют приоритеты ionice.
# Проверить текущий планировщик I/O для каждого устройства
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none <- активный в скобках
# Сменить планировщик для nvme-диска на BFQ
echo bfq > /sys/block/nvme0n1/queue/scheduler
# Сделать изменение постоянным через udev
cat > /etc/udev/rules.d/60-ioschedulers.rules << 'EOF'
# Для вращающихся дисков
ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="1", \
ATTR{queue/scheduler}="bfq"
# Для NVMe
ACTION=="add|change", KERNEL=="nvme[0-9]*", \
ATTR{queue/scheduler}="bfq"
EOF
udevadm control --reload-rules && udevadm trigger
Совместное применение nice и ionice для реальных задач
Правильный ответ на большинство задач управления приоритетами, комбинация обоих инструментов, потому что реальные процессы потребляют и CPU, и I/O. Rsync с nice 19 и ionice idle станет по-настоящему прозрачным для остальной системы:
# Полностью "тихий" rsync: низкий CPU и I/O только когда диск свободен
nice -n 19 ionice -c 3 rsync -av --progress /data/ /backup/ &
# Тихая компрессия логов в фоне
nice -n 19 ionice -c 3 find /var/log -name "*.log" -mtime +7 \
-exec gzip -9 {} \;
# Фоновое обслуживание базы данных
nice -n 10 ionice -c 2 -n 6 vacuumdb --all --analyze
# Критичный процесс с высоким I/O-приоритетом
ionice -c 1 -n 2 nice -n -5 /usr/local/bin/realtime-processor
Systemd позволяет задать оба приоритета в unit-файле напрямую, что надёжнее скриптовых обёрток: параметры применяются ядром при запуске процесса, а не через дочернюю команду, и они не потеряются при перезапуске сервиса:
[Service]
ExecStart=/usr/bin/backup-agent --config /etc/backup.conf
# Nice value для CPU
Nice=15
# I/O scheduling class и уровень
IOSchedulingClass=idle
# IOSchedulingClass=best-effort
# IOSchedulingPriority=7 # для best-effort, 0-7
# Проверить что параметры применились
systemctl show backup.service -p Nice -p IOSchedulingClass -p IOSchedulingPriority
Мониторинг и диагностика приоритетов в реальном времени
Измерить фактическое влияние приоритетов на производительность помогают несколько инструментов. iotop показывает дисковый I/O в реальном времени с разбивкой по процессам: именно здесь видно, кто на самом деле пожирает пропускную способность диска, а не просто выглядит подозрительно по нагрузке на CPU:
# Топ по дисковому I/O в реальном времени
iotop -o # показывать только процессы с ненулевым I/O
# Накопленная статистика I/O по процессам
iotop -b -n 5 -d 2 | grep -v "^Total\|^Actual\|0.00 B"
# Подробная статистика I/O конкретного процесса через procfs
cat /proc/12345/io
# rchar: байт прочитано через read()
# wchar: байт записано через write()
# syscr: количество read-системных вызовов
# syscw: количество write-системных вызовов
# read_bytes: байт реально прочитано с диска (не из page cache)
# write_bytes: байт реально записано на диск
# Статистика планировщика для процесса (CFS-веса и время)
cat /proc/12345/sched | head -20
Разница между rchar и read_bytes в /proc/PID/io часто открывает глаза: процесс может казаться I/O-интенсивным по числу системных вызовов, но большая часть операций обслуживается page cache без обращения к диску. В таком случае ionice idle вообще не нужен, потому что реального дискового I/O почти нет.
# Быстрый скрипт для ранжирования процессов по реальному дисковому I/O
for pid in /proc/[0-9]*/io; do
name=$(cat ${pid%/io}/comm 2>/dev/null)
read_b=$(awk '/^read_bytes/{print $2}' $pid 2>/dev/null)
write_b=$(awk '/^write_bytes/{print $2}' $pid 2>/dev/null)
total=$((${read_b:-0} + ${write_b:-0}))
echo "$total $name"
done | sort -rn | head -10
Nice values и ionice решают разные задачи и работают на разных уровнях стека, но их логика одинакова: они не ограничивают потолок потребления, а управляют распределением ресурса при конкуренции. Процесс с nice 19 получит весь CPU, если больше никто не претендует. Процесс с ionice idle получит весь диск, если очередь I/O пуста. Именно поэтому правильно настроенные приоритеты дают нулевое влияние на производительность в спокойное время и реальную защиту критичных сервисов в момент нагрузки. Это не компромисс, это и есть цель.