MAATRIX / Блог / Как ядро решает, кому отдать процессор следующим, и почему nice работает не так, как вы думаете

Как ядро решает, кому отдать процессор следующим, и почему nice работает не так, как вы думаете

MAATRIX

Рано или поздно каждый, кто администрирует сервер, ставит nice -n 19 перед тяжёлой задачей и ждёт чуда — а чуда не происходит. Или наоборот: видит, что фоновый бэкап «съедает» весь CPU, ставит ему nice -n -19 для «ускорения», и через минуту сервер начинает тормозить целиком. Дело не в том, что nice сломан — дело в том, что почти никто не читал, как на самом деле работает планировщик, которым nice управляет. Разберёмся, что реально происходит внутри ядра, когда на сервере одновременно хотят работать десять процессов, а ядро всего четыре.

Что вообще решает планировщик и почему тут не может быть простой очереди

На сервере с 4 vCPU и сотней процессов (веб-сервер, база, cron-задачи, systemd-сервисы, ssh-сессии) в любой момент времени реально исполняться могут только 4 процесса — по одному на ядро. Все остальные, кто готов работать прямо сейчас (не спит, не ждёт диска или сети), стоят в очереди на CPU. Именно это число готовых-к-работе процессов и показывает load average — но само по себе оно не говорит, кому из очереди достанется ядро первым. Это решает планировщик: код, который каждые несколько миллисекунд решает, кого снять с ядра, кого на него поставить.

Наивная идея — обычная очередь FIFO — не работает по двум причинам. Во-первых, процессы не равны: одни критичны к задержке (обработчик HTTP-запроса должен отвечать за миллисекунды), другие нет (архивация логов может подождать). Во-вторых, если пускать процессы по кругу на фиксированный квант, тяжёлый процесс, который никогда не спит (ffmpeg, gzip большого файла), будет отжимать себе то же время, что и процесс, просыпающийся на 2 мс и снова засыпающий, — хотя первый потребляет CPU почти всё время, а второй — доли процента.

Ядро Linux с 2007 года (начиная с версии 2.6.23) решает это через CFS — Completely Fair Scheduler, «полностью честный планировщик». С ядра 6.6 (2023 год) в мейнлайне появился его преемник EEVDF с той же идеологией, но точнее учитывающий задержки — для практики с nice разницы почти нет: веса и понятие «виртуального времени» работают одинаково что на CFS, что на EEVDF, поэтому дальше говорим про CFS как про общую модель.

CFS изнутри: виртуальное время вместо тактов

Ключевая идея CFS в том, что планировщик не делит физическое время поровну — он делит виртуальное время, vruntime. У каждого процесса есть счётчик: сколько CPU-времени он «на самом деле» использовал, с поправкой на его вес. Планировщик всегда выбирает на исполнение процесс с наименьшим vruntime — того, кто «меньше всех поработал» относительно своего веса.

Все готовые к исполнению процессы хранятся в структуре, отсортированной по vruntime (в классическом CFS — красно-чёрное дерево, в EEVDF — похожая структура с учётом виртуального дедлайна). Планировщик каждый раз берёт самый «левый» элемент — с минимальным vruntime. Когда процесс поработал, его vruntime вырос, и он сдвинулся вправо, уступив место другим. Никакого фиксированного «тик-так» по кругу, никакой жёсткой квоты в стиле «каждому по 10 мс» — только постоянный пересчёт, кто сейчас «меньше всех получил».

Практическое следствие: если на ядре одновременно готовы работать два одинаковых по весу процесса, каждый в среднем получит около половины времени этого ядра — не потому что кто-то это гарантировал заранее, а потому что как только один вырывается вперёд по vruntime, планировщик тут же переключается на второго. Это и называется «честностью» — не идеальное деление по секундомеру, а постоянное выравнивание по накопленному виртуальному времени.

Это же объясняет, почему интерактивные процессы (например, обработчик запроса, который спит между запросами) обычно откликаются быстро: после сна vruntime не «замораживается» на месте — CFS слегка подтягивает его к текущему минимуму в дереве, но не сбрасывает в ноль. Процессы, которые почти не грузят CPU, а в основном ждут, регулярно оказываются близко к «голове очереди», когда просыпаются, — и получают ядро почти сразу, без долгого ожидания за тяжёлыми CPU-bound задачами.

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

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

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

Что такое nice на самом деле: вес, а не приоритет

Вот тут и разваливается интуиция большинства админов. Слово «priority» в описании nice в man-странице наводит на мысль об абсолютной шкале важности — «чем меньше число, тем ты важнее для системы». На деле nice — это множитель, влияющий только на то, как быстро растёт vruntime относительно реального времени, ничего больше.

У каждого значения nice (от -20 до +19, по умолчанию 0) есть соответствующий вес в таблице ядра (kernel/sched/core.c, массив sched_prio_to_weight). Значения зафиксированы в исходниках и не меняются от сервера к серверу:

niceвес (примерно)что это значит
-20~88761vruntime растёт крайне медленно относительно реального времени
-10~9548заметно медленнее среднего
01024базовое значение, «стандартный» темп
10~110заметно быстрее среднего
19~15vruntime растёт почти в 70 раз быстрее, чем у nice 0

Формула прироста примерно такая: vruntime += реально_потраченное_время × (1024 / вес_процесса). Чем выше вес (ниже nice), тем меньше растёт vruntime за ту же секунду реальной работы — процесс дольше остаётся «слева» в дереве и получает ядро чаще. Чем ниже вес (выше nice), тем быстрее vruntime улетает вперёд — процесс быстро «наедается» виртуального времени и уступает очередь остальным.

Важно: это распределение доли CPU между процессами, которые реально конкурируют за одно и то же ядро в один и тот же момент. Nice не резервирует процент CPU, не гарантирует минимум и не выставляет абсолютный приоритет — это относительный вес в дележе, который применяется только тогда, когда есть с кем делить.

Почему «nice -19 — это супер-приоритет» — миф, который дорого стоит

Отсюда вытекает самая частая ошибка. Админ ставит nice -n -19 python train.py на сервере, где кроме этого скрипта больше ничего тяжёлого не крутится, видит, что скрипт отработал быстро, и делает вывод: «nice -19 ускоряет процесс». На самом деле если процессор был свободен, скрипт и так получил бы 100% ядра при nice 0 — конкурировать было не с кем, и вес роли не сыграл вообще никакой. Nice -19 в этой ситуации не сделал ровным счётом ничего, кроме одного: если бы в этот момент на сервер зашёл ssh или веб-сервер начал бы отвечать на запрос, они конкурировали бы за то же ядро уже в заведомо проигрышной позиции.

Вторая сторона той же ошибки опаснее: если на машине много одновременно активных CPU-bound процессов (несколько воркеров очереди задач плюс веб-сервер плюс фоновая индексация), процесс с nice -20 при плотной конкуренции забирает себе радикально больше времени ядра — вплоть до того, что веб-сервер с nice 0 начинает ощутимо «подвисать» на каждом запросе, потому что после своего маленького кванта снова оказывается позади в дереве по vruntime. Разница в весах между nice 0 и nice -20 — почти в 87 раз, и при реальной конкуренции это ощущается как настоящий отжим CPU, а не «немного быстрее». Именно поэтому отрицательные значения nice требуют root или CAP_SYS_NICE — случайно выставленный на бесконечный цикл nice -20 на общем сервере способен реально задушить всё остальное.

Третий вариант мифа: «поставил nice, а процесс всё равно тормозит». Это почти всегда значит, что процесс не CPU-bound, а I/O-bound — он не стоит в очереди за процессором, а спит, ожидая ответа от диска, сети или блокировки. Nice управляет только очередью на CPU. Процесс, который 95% времени проводит в состоянии D (uninterruptible sleep, ждёт диск) или блокируется на сетевом сокете, почти никогда не оказывается тем самым «конкурентом за ядро», которого нужно обгонять по vruntime, — там, где он реально теряет время, nice не участвует вообще.

Когда renice реально работает, а когда это пустая трата времени

renice -n <значение> -p <PID> меняет вес уже запущенного процесса на лету, без перезапуска. Но чтобы это дало эффект, должны совпасть два условия: процесс CPU-bound (реально грузит ядро вычислениями, а не ждёт диск/сеть) и на сервере в этот момент есть настоящая конкуренция за то же ядро — суммарная нагрузка от других процессов приближается к числу ядер или превышает его.

Nice помогает:

  • Фоновая пересборка индекса, конвертация видео (ffmpeg), архивация (tar, gzip, zstd) на сервере, где параллельно крутится веб-приложение или база — типичный случай nice -n 15 tar czf /backup/data.tar.gz /var/www.
  • Компиляция большого проекта (make -j$(nproc)) на сервере с активными пользовательскими сервисами — nice -n 10 make -j$(nproc).
  • Пакетная обработка данных (ETL, батч-скрипты), которая не критична к времени завершения, но конкурирует за CPU с онлайн-сервисом.
  • Massive parallel-задачи cron (updatedb, обход файловой системы, генерация отчётов).

Nice не поможет (нужен другой инструмент или подход):

  • Задачи, упирающиеся в диск или сеть, а не в CPU, — здесь работает ionice, а не nice (ionice управляет отдельным планировщиком ввода-вывода для блочных устройств, это принципиально другая подсистема ядра).
  • Сервер с одним активным тяжёлым процессом и свободными ядрами — конкурировать не с кем, вес роли не играет.
  • Задачи, которым нужна гарантированная низкая задержка независимо от нагрузки, — для этого в Linux есть отдельный класс планирования реального времени (SCHED_FIFO/SCHED_RR, управляется через chrt), который находится выше всех обычных SCHED_OTHER-процессов (тех самых, для которых работает CFS/EEVDF и nice) и полностью их вытесняет. Nice внутри SCHED_OTHER никогда не поднимет процесс на уровень реального времени — это разные лиги, а не разные места в одной таблице.
  • Попытка «выжать больше 100% CPU» — nice лишь перераспределяет уже существующую долю между конкурентами, он не увеличивает суммарную вычислительную мощность сервера.

Практика на сервере: nice, ionice, chrt и cgroup-лимиты вместе

На реальном VPS или выделенном сервере эти механизмы стоит комбинировать осознанно, а не наугад крутить одно число.

Посмотреть текущий nice запущенных процессов:

ps -eo pid,ni,pcpu,comm --sort=-pcpu | head -20
# или в реальном времени
top   # колонка NI
htop  # колонка NI, можно менять клавишами F7/F8

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

nice -n 15 tar czf /backup/site-$(date +%F).tar.gz /var/www

Изменить вес уже работающего процесса:

renice -n 10 -p 48213

Для I/O-тяжёлых фоновых задач (например, ночной бэкап, который душит диск, а не CPU) нужен именно ionice, а не nice — это отдельная настройка планировщика ввода-вывода:

ionice -c3 -p 48213         # class 3 = idle, работать только когда диск свободен
# или сразу при запуске
ionice -c2 -n7 nice -n 19 rsync -a /data/ /backup/

nice и ionice здесь работают вместе, но независимо: nice снижает шанс rsync отжать CPU у веб-сервера, ionice — шанс, что тот же rsync забьёт очередь диска и увеличит задержки чтения для базы. Это разные подсистемы ядра, и для по-настоящему фонового процесса обычно нужны обе настройки сразу.

На сервере с systemd (что сегодня почти любой современный дистрибутив) для сервисов удобнее задавать вес не через nice вручную, а через юнит-файл — тогда настройка переживёт перезапуск:

[Service]
Nice=10
IOSchedulingClass=idle
CPUWeight=50

Здесь стоит понимать важный нюанс: CFS сначала честно делит CPU между группами (cgroups — например, между слайсом user.slice, system.slice и вашим сервисом), а уже потом внутри группы — между процессами по их nice. Параметр CPUWeight в systemd — это вес именно на уровне cgroup, а Nice внутри юнита — это вес процесса внутри этой группы. Если задать сервису жёсткий CPUQuota=50%, то никакой отрицательный nice внутри этого сервиса не позволит ему выйти за пределы выделенной квоты — cgroup-лимит стоит на уровень выше и режет сверху вниз, вне зависимости от весов внутри.

Кстати, похожий принцип «взвешенного выбора вместо жёсткого правила» ядро использует не только для CPU: когда памяти не хватает катастрофически, за выбор жертвы отвечает отдельный механизм — OOM killer, и там тоже работает не приоритет в бытовом смысле, а числовой скоринг конкретных процессов.

Отдельно стоит сказать про chrt — если у вас на сервере есть по-настоящему времякритичный процесс (например, обработчик очереди с жёстким SLA по задержке), можно перевести его в класс реального времени:

chrt -f -p 10 48213   # SCHED_FIFO, приоритет 10 (1-99)

Это не «nice покруче», а выход из очереди CFS вообще — в класс с полным вытеснением обычных процессов. Пользоваться нужно осторожно: неправильно написанный процесс в SCHED_FIFO с приоритетом выше системных демонов способен подвесить весь сервер, ведь он не обязан уступать ядро никому из SCHED_OTHER.

Чек-лист для арендованного сервера:

  • Фоновые CPU-задачи (архивация, компиляция, конвертация) — nice -n 10..19.
  • Фоновые I/O-задачи (rsync, дедупликация, сканирование диска) — ionice -c3 или -c2 -n7, отдельно от nice.
  • Сервисы с гарантированным лимитом на общем сервере — systemd CPUWeight/CPUQuota, а не только nice отдельных процессов.
  • Времякритичные обработчики — архитектурное решение (taskset/CPUAffinity, при крайней нужде — chrt), не nice.
  • Сомневаетесь, поможет ли nice, — смотрите top/htop под нагрузкой: процесс в R и конкурирует за CPU — nice сработает; в D или S с низким %CPU — дело не в планировщике CPU.

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

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

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

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

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

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

Можно ли поставить nice выше 19 или ниже -20?

Нет, диапазон жёстко ограничен ядром: от -20 (максимальный вес) до +19 (минимальный вес). Значения за пределами диапазона nice/renice просто обрежет до ближайшей границы.

Нужен ли root, чтобы понизить nice (сделать значение более отрицательным)?

Да, для отрицательных значений нужен root или CAP_SYS_NICE. Повысить nice (сделать процесс более «уступчивым») обычный пользователь может и своему процессу без root.

Влияет ли nice на потоки (threads) внутри процесса или только на процесс целиком?

Планировщик Linux работает на уровне задач ядра (task), а поток — тоже task с общей памятью. renice -p PID меняет вес именно указанного PID; чтобы поменять вес отдельного потока, применяют renice к TID потока (ps -eLo tid,pid,ni,comm), а не к главному PID.

Почему у VPS с гипервизором nice может ощущаться слабее, чем на железном сервере?

Гипервизор снаружи тоже планирует, каким гостевым VM отдать физическое ядро — это ещё один уровень планирования поверх вашего. Nice честно работает внутри вашей ОС, но если гипервизор в пиковой нагрузке урезает ваши vCPU-такты, это уже вне досягаемости nice — здесь помогает тариф с гарантированными, а не overcommit-ядрами.

Есть ли разница между CFS и EEVDF для практики с nice?

Для повседневной работы с nice/renice — нет, интерфейс и веса те же. EEVDF точнее держит задержки при высокой нагрузке, но модель «вес определяет долю CPU при конкуренции» не меняется.

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

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

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