Learning Platform
Глоссарий Troubleshooting
Урок 06.03 · 18 мин
Начальный
SchedulerPriorityniceionicechrtLinux

Приоритеты и nice — как влиять на scheduler из user-space

Когда вы запускаете тяжёлый расчёт, который не должен мешать вашему IDE — что вы делаете? Если хорошо знаете Linux — запускаете nice -n 19 ./heavy-job. Если не знаете — ругаетесь, что система тормозит.

В этом уроке разберём, как scheduler-приоритеты в Linux выглядят на user-space уровне. Три главных инструмента: nice (CPU приоритет), ionice (I/O приоритет), chrt (real-time класс). Также разберём разницу между static и dynamic priority, и что показывают колонки PR и NI в top / ps.

Для junior data engineer это базовый survival kit. ETL’ы, ML-training, индексация БД — всё это процессы, которые надо уметь приоритизировать, чтобы не клали production-сервис.


Static vs dynamic priority

В Linux есть две независимые системы приоритетов:

Static priority — для real-time классов. Задаётся явно (1-99 для SCHED_FIFO/RR). Не меняется ядром.

Dynamic priority — для обычных процессов (SCHED_OTHER, SCHED_BATCH). Базируется на nice value (-20 до +19), но может корректироваться ядром (raньше — через эвристики O(1) scheduler’а, сейчас — через CFS weights).

Приоритеты в Linux -- две системы
Real-time priority1-99. Задаётся через chrt -f или chrt -r. Static -- kernel не меняет. Real-time процессы ВСЕГДА имеют приоритет над обычными
Nice value-20 до +19. Задаётся через nice или renice. От него зависит вес процесса в CFS. -20 = высокий приоритет, +19 = низкий
ps PR columnВнутренний приоритет ядра. Для обычных процессов: PR = 20 + nice. Для real-time: PR = -1 - rt_priority (отрицательное число)
ps NI columnТот же nice value, что вы задавали через nice command
Nice rangesnice -20: max boost. nice 0: default. nice 19: minimum. nice +19 ≈ 68 раз меньше CPU чем nice 0, мультипликативно
# Посмотреть priorities всех процессов:
ps -eo pid,pri,ni,stat,comm | head -10
# pid  pri  ni  stat  comm
# 1    19   0   Ss    systemd
# 2    19   0   S     kthreadd
# Колонка pri -- внутренний приоритет (выше = лучше для нормальных, специальная шкала для RT)
# Колонка ni -- nice

# В top нажать r -- renice выбранный процесс
# В top нажать R -- сортировать по PRI

nice — приоритет CPU для обычных процессов

nice — классическая утилита. Значения от -20 (highest priority, requires root) до +19 (lowest). Default 0.

Этимология: «be nice» — «будь вежливым». Большой positive nice = «я готов уступить CPU другим». Negative nice = «я наглый, мне CPU надо».

# Запустить процесс с nice:
nice -n 10 ./my-script.py   # nice 10 (вежливый)
nice -n -5 ./important     # nice -5 (наглый, требует root)
nice ./default              # nice 10 без явного значения

# Изменить nice уже запущенному процессу:
renice -n 15 -p 1234       # PID 1234 -> nice 15

# Только root может уменьшить nice (повысить priority):
renice -n -5 -p 1234       # error: Operation not permitted (если не root)
renice -n 10 -p 1234       # OK -- увеличить nice (понизить priority) можно

Лимиты nice:

Что может и не может nice
Может: snižить CPU shareПри наличии других процессов nice 19 будет получать 1-2% CPU. Эффективно для batch jobs
Может: повысить (root only)root может задать negative nice (-1 до -20), увеличивая CPU share для важных процессов
Не может: гарантия latencynice -- relative priority при контентном CPU. Если нагрузки нет -- ваш low-priority получит 100%. Никаких гарантий хвоста latency
Не может: I/O prioritynice влияет только на CPU. Для I/O есть отдельный ionice -- nice не помогает приоритизировать дисковые операции

Когда nice -20 не помогает:

Иногда вы делаете nice -n -20 ./my-job, а оно всё равно медленно. Причины:

  1. Узкое место — не CPU, а I/O или сеть. nice не влияет.
  2. У вас mало контента — nice работает только при конкуренции.
  3. Другие процессы тоже nice -20 (ну или real-time).
  4. cgroups CPU quota — ограничивает сверху, nice внутри cgroup роли не играет.

ionice — приоритет I/O

CPU не единственный ограниченный ресурс. Диск тоже узкое место. ionice устанавливает класс I/O scheduler:

ionice classes
Class 1: realtimeReal-time I/O. Самый высокий приоритет, отбирает у всех. 0-7 levels внутри. Только root. Опасно: может задушить системные процессы
Class 2: best-effortDefault. 0-7 priority levels внутри. Большинство процессов здесь. Distribution через CFQ или mq-deadline scheduler
Class 3: idleПолучает I/O только когда никто другой не хочет. Идеально для бэкапов и индексации без влияния на основной workflow
ionice -c 3 ./backupЗапустить backup в idle классе. Не помешает основной нагрузке
ionice -c 2 -n 7 ./batchBest-effort с самым низким уровнем 7 -- ниже default (4), выше idle
# Текущий класс I/O процесса:
ionice -p 1234
# best-effort: prio 4    (default)

# Изменить класс:
ionice -c 3 -p 1234           # idle класс
ionice -c 2 -n 7 -p 1234      # best-effort, lowest level

# Запустить новый процесс:
ionice -c 3 tar czf backup.tar.gz /data    # backup в idle классе
nice -n 19 ionice -c 3 ./batch-etl         # и CPU, и I/O в idle

# Текущий I/O scheduler ядра:
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none
# Текущий -- mq-deadline (default современный)

ionice работает только с некоторыми schedulers. На современном Linux это bfq (Budget Fair Queueing) или cfq (Completely Fair Queuing, deprecated). Если у вас mq-deadline или none — ionice влияет ограниченно или не влияет.

# Сменить I/O scheduler:
echo bfq | sudo tee /sys/block/sda/queue/scheduler
# Теперь ionice работает в полную силу

chrt — real-time классы

chrt — утилита для смены scheduling class. Самые важные классы:

Scheduling classes в Linux
SCHED_FIFOReal-time, FIFO порядок. Процесс работает до выхода или блокировки. ВСЕГДА вытесняет normal процессы. Опасно: бесконечный цикл в SCHED_FIFO замораживает систему
SCHED_RRReal-time round-robin. Как FIFO, но с time slice -- равноприоритетные RR процессы переключаются по очереди
SCHED_OTHERDefault class. Обычные процессы, используют CFS/EEVDF. nice value влияет на share. Большинство процессов здесь
SCHED_BATCHOptimized для batch workload. Меньше переключений, дольше держит CPU. Для long-running CPU-bound -- ETL, compile, render
SCHED_IDLEСамый низкий приоритет. Получает CPU только когда никто другой не runnable. Ниже nice 19
SCHED_DEADLINEReal-time с deadlines. Каждой задаче задаётся (runtime, deadline, period). Используется для предсказуемой латентности. Обзор в уроке 04
# Посмотреть класс процесса:
chrt -p 1234
# pid 1234's current scheduling policy: SCHED_OTHER
# pid 1234's current scheduling priority: 0

# Перевести в SCHED_BATCH (без RT, но оптимизирован под throughput):
chrt -b -p 0 1234

# Перевести в SCHED_IDLE (самый низкий приоритет):
chrt -i -p 0 1234

# Запустить процесс в SCHED_BATCH:
chrt -b 0 ./heavy-etl.sh
# 0 -- приоритет (для SCHED_BATCH/IDLE/OTHER всегда 0)

# Запустить процесс в SCHED_FIFO (требует root):
sudo chrt -f 50 ./real-time-audio
# 50 -- RT priority 1-99

# Запустить в SCHED_RR:
sudo chrt -r 50 ./worker

# ВНИМАНИЕ: SCHED_FIFO + бесконечный цикл = повисшая система
# Защита через RLIMIT_RTTIME, но осторожнее

Когда что выбирать (для DE):

  • SCHED_OTHER + nice 0 — default, не трогать для обычных задач.
  • SCHED_OTHER + nice -10 — важные процессы (БД, prod-сервис), но без RT.
  • SCHED_OTHER + nice 10-19 — фоновые задачи (метрики, мониторинг).
  • SCHED_BATCH — ETL, ML-обучение, тяжёлые batch.
  • SCHED_IDLE — бэкапы, индексация, что угодно без срочности.
  • SCHED_FIFO/RR — НЕ для DE задач. Используется в звуке, видео, контроллерах, real-time embedded.

Что показывают PR и NI в top/ps

Колонка PR (priority) в top — это kernel internal priority. У неё своя шкала:

Колонка PR в top -- расшифровка
PR = -100 to -1Real-time процессы. PR = -1 - rt_priority. То есть SCHED_FIFO prio 99 -> PR = -100 (max), SCHED_FIFO prio 1 -> PR = -2
PR = 0 to 39Normal процессы (SCHED_OTHER). PR = 20 + nice. nice -20 -> PR = 0 (max for normal), nice 19 -> PR = 39 (min)
PR = rtИногда top показывает 'rt' вместо числа -- это значит real-time priority -100 (макс)
Меньше = выше приоритетВ kernel internal scale -- меньше PR = выше приоритет. Обратная интуитивная шкала
# Запустить SCHED_FIFO процесс:
sudo chrt -f 50 sleep 100 &
PID=$!
ps -p $PID -o pid,pri,ni,cls,rtprio
#   PID PRI  NI CLS RTPRIO
#  12345  90   - FF      50
# PRI=90 -- это в шкале ps. RTPRIO=50 -- real-time priority
# CLS=FF означает SCHED_FIFO
kill $PID

# Запустить с nice 10:
nice -n 10 sleep 100 &
PID=$!
ps -p $PID -o pid,pri,ni,cls
#   PID PRI  NI CLS
#  12346  10  10  TS
# PRI=10 (=20-10), NI=10, CLS=TS (TS = SCHED_OTHER aka 'time sharing')
kill $PID

В разных утилитах разные шкалы PR:

  • top: -100 до 39
  • ps -o pri: 0 (low) до 99 (high)
  • Они показывают одно и то же, но в инвертированных шкалах

Реальные сценарии

Сценарий 1: ETL не должен мешать API

# Запуск ETL с пониженным CPU и I/O приоритетом:
nice -n 19 ionice -c 3 ./etl-pipeline.sh

# Или даже с SCHED_BATCH:
chrt -b 0 nice -n 19 ionice -c 3 ./etl-pipeline.sh
# CPU: batch class + nice 19 -- получит CPU только при отсутствии других
# I/O: idle class -- получит диск только при отсутствии других

Сценарий 2: критичный сервис должен иметь приоритет

# Понизить приоритет всем, кроме API:
ps -eo pid,comm | grep -v 'api-server' | awk '{print $1}' | xargs -I{} sudo renice -n 5 -p {} 2>/dev/null
# Или повысить только API:
sudo renice -n -5 -p $(pgrep api-server)

Сценарий 3: real-time — НЕ для production без подготовки

# НЕ ДЕЛАТЬ в production без чёткого понимания:
# sudo chrt -f 80 ./worker
# Бесконечный цикл в SCHED_FIFO повесит систему. Используйте RLIMIT_RTTIME и timeouts.

Сценарий 4: cgroups — более правильный путь для гарантий

# systemd-run с cgroup лимитом:
systemd-run --user --slice=background.slice -p CPUWeight=10 ./batch-job
# CPUWeight 10 (vs default 100) = 10% от обычного share

# С жестким лимитом по CPU:
systemd-run --slice=etl.slice -p CPUQuota=50% ./etl-pipeline
# 50% от одного ядра в среднем. Не зависит от nice других процессов.

cgroups мы подробно не разбираем (не входит в этот курс), но запомните: для production-уровня изоляции — cgroups, не nice.

Requests и limits в Kubernetes: cgroups для контейнеров

Попробуй сам

# 1. Эксперимент с nice:
# Запустите 2 процесса с разным nice и сравните CPU usage:
yes > /dev/null &
A=$!
nice -n 10 yes > /dev/null &
B=$!
sleep 5
ps -p $A,$B -o pid,ni,%cpu
# nice 0 получит ~90%, nice 10 ~10%
kill $A $B

# 2. ionice -- сравните I/O share:
# Терминал 1: фоновая большая запись
dd if=/dev/zero of=/tmp/big bs=1M count=10000 &
DD1=$!

# Терминал 2: запись в idle класс
ionice -c 3 dd if=/dev/zero of=/tmp/big2 bs=1M count=10000 &
DD2=$!

# Сравнить скорость (нужно посмотреть iotop или iostat)
iostat -x 1
# DD1 будет получать много I/O, DD2 (idle) только когда DD1 не активен

kill $DD1 $DD2
rm /tmp/big /tmp/big2

# 3. Посмотреть scheduling class для своего shell:
chrt -p $$
# pid X's current scheduling policy: SCHED_OTHER
# pid X's current scheduling priority: 0

# 4. Замерить, сколько nice -19 даст реальной разницы:
time (nice -n 0 awk 'BEGIN{for(i=0;i<1e8;i++) sum+=i; print sum}')
time (nice -n 19 awk 'BEGIN{for(i=0;i<1e8;i++) sum+=i; print sum}')
# При наличии других процессов -- nice 19 в 5-10 раз медленнее

# 5. PR и NI в реальных процессах:
ps -eo pid,pri,ni,cls,comm | head -20
# Большинство: CLS=TS (SCHED_OTHER), PRI=20, NI=0
# Иногда CLS=IDL (SCHED_IDLE)
# kworker'ы могут быть RT

Проверка знанийKnowledge check
Junior спрашивает: 'Я сделал nice -n -20 для своего скрипта, но он всё равно работает с такой же скоростью. Почему? Что я делаю не так?'
ОтветAnswer
Несколько возможных причин: 1. Узкое место -- не CPU. Если ваш скрипт ждёт I/O, сеть, базу данных или другие процессы -- nice вообще не помогает. nice влияет ТОЛЬКО на распределение CPU времени между runnable процессами. Если процесс blocked на read() из медленного диска, nice не ускорит чтение. Диагностика: top или ps -- посмотрите state процесса. Если он часто в S (sleep) или D (disk wait) -- проблема не CPU. Если в R (running) и CPU% низкий -- тоже не CPU. 2. Нет конкуренции. nice работает только когда несколько процессов борются за CPU. Если у вас 8-core machine и только ваш скрипт CPU-bound -- он получит 100% CPU независимо от nice. nice влияет на ОТНОСИТЕЛЬНОЕ распределение, не на абсолютное. Диагностика: cat /proc/loadavg -- если load низкий, нет конкуренции -- nice бесполезен. 3. cgroups перебивают nice. Если ваш скрипт запущен в systemd unit или Docker контейнере, могут быть cgroup лимиты (CPUQuota, CPUWeight). Они работают независимо от nice -- если есть cap 50% от ядра, ваш скрипт никогда не получит больше, какой бы nice ни был. Диагностика: systemctl status -p MainPID, либо cat /sys/fs/cgroup/cpu.max или /proc/PID/cgroup -- посмотрите cgroup и его лимиты. 4. Параллелизация. Если ваш скрипт однопоточный, он использует 1 ядро. На 8-core машине это ~12.5% системного CPU. Чтобы скрипт стал реально быстрее -- нужно распараллелить, не nice'ить. Диагностика: top -- посмотрите %CPU процесса. Если ~100% -- использует 1 ядро. Если 700%+ -- использует много ядер. 5. nice -20 vs nice 0 = 1024 / 88761 ≈ 12x. Это вес в CFS. Это значит ваш скрипт получит примерно в 12 раз больше CPU чем normal процессы при конкуренции. Это много, но не безграничная разница -- если normal процесс ест 50% CPU, ваш с nice -20 не получит 1000% (это даже технически невозможно на 1 ядре). Правильная диагностика: - ps -eo pid,stat,%cpu,ni,comm | grep myscript -- состояние, CPU usage, nice - vmstat 1 -- если 'wa' высокий, проблема в I/O - pidstat -d -p PID 1 -- I/O нагрузка процесса - strace -c -p PID -- какие syscalls сжирают время - perf top -- профилирование, что реально выполняется Возможно, ваше решение: вместо nice использовать taskset для прибивания к незанятым ядрам, или параллелизовать через multiprocessing/Go, или оптимизировать алгоритм.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Какой диапазон значений у команды nice в Linux и что они означают?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4