Приоритеты и 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).
# Посмотреть 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 -20 не помогает:
Иногда вы делаете nice -n -20 ./my-job, а оно всё равно медленно. Причины:
- Узкое место — не CPU, а I/O или сеть. nice не влияет.
- У вас mало контента — nice работает только при конкуренции.
- Другие процессы тоже nice -20 (ну или real-time).
- cgroups CPU quota — ограничивает сверху, nice внутри cgroup роли не играет.
ionice — приоритет I/O
CPU не единственный ограниченный ресурс. Диск тоже узкое место. ionice устанавливает класс I/O scheduler:
# Текущий класс 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. Самые важные классы:
# Посмотреть класс процесса:
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. У неё своя шкала:
# Запустить 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 до 39ps -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