Добавить в корзинуПозвонить
Найти в Дзене
File Energy

Kyber и BFQ - два подхода к управлению дисковой очередью под разные нагрузки в Linux

Среди всего многообразия настроек Linux-системы I/O-планировщик - одна из тех тонких ручек, которую большинство администраторов никогда не трогает. И напрасно. Разница в задержках между правильно подобранным планировщиком и дефолтным значением может составлять от 20 до 200 процентов - в зависимости от железа и характера нагрузки. Это не теоретическая цифра: именно такой разрыв зафиксировали исследователи из Amsterdam VU в работе, опубликованной на ICPE 2024 после испытаний на современных Samsung 980 PRO. Чтобы понять, почему выбор планировщика вообще имеет значение, нужно разобраться, где именно он живёт. Планировщик I/O находится между файловой системой и драйвером блочного устройства. Он принимает запросы от ядра и решает, в каком порядке отправить их накопителю. На механических дисках это было вопросом выживания: неупорядоченные запросы заставляли головку мотаться по пластине и убивали производительность. На NVMe картина другая - устройство само умеет переупорядочивать запросы во вн
Оглавление

Среди всего многообразия настроек Linux-системы I/O-планировщик - одна из тех тонких ручек, которую большинство администраторов никогда не трогает. И напрасно. Разница в задержках между правильно подобранным планировщиком и дефолтным значением может составлять от 20 до 200 процентов - в зависимости от железа и характера нагрузки. Это не теоретическая цифра: именно такой разрыв зафиксировали исследователи из Amsterdam VU в работе, опубликованной на ICPE 2024 после испытаний на современных Samsung 980 PRO.

Чтобы понять, почему выбор планировщика вообще имеет значение, нужно разобраться, где именно он живёт. Планировщик I/O находится между файловой системой и драйвером блочного устройства. Он принимает запросы от ядра и решает, в каком порядке отправить их накопителю. На механических дисках это было вопросом выживания: неупорядоченные запросы заставляли головку мотаться по пластине и убивали производительность. На NVMe картина другая - устройство само умеет переупорядочивать запросы во внутренней очереди, и грубое вмешательство планировщика здесь может навредить больше, чем помочь.

Современный Linux начиная с ядра 5.0 использует архитектуру blk-mq (multi-queue block layer), и именно в ней живут три актуальных планировщика: none, mq-deadline, bfq. Kyber - четвёртый, модульный, и по умолчанию не загружен. Каждый из них исповедует свою философию, и понимание этой философии - ключ к грамотной настройке.

Kyber - минималистичный токен-бакет для быстрых устройств

Kyber появился в ядре 4.12 и с самого начала проектировался под одну задачу: управлять латентностью на быстрых многоочередных устройствах с минимальными накладными расходами. Его кодовая база - около 1000 строк, тогда как BFQ написан примерно в 10 раз объёмнее. Это не случайность, а выбор архитектуры.

Механизм работы Kyber строится на токен-баккетах. Для каждого типа запросов - чтение, запись, discard - существует отдельный пул токенов. Запрос уходит на устройство только при наличии доступного токена. Количество токенов в пуле - не фиксированное: планировщик постоянно сравнивает достигнутую P99-латентность с целевыми значениями и регулирует глубину очереди в реальном времени. Если реальная задержка чтения ниже цели - пул токенов сжимается, снижая нагрузку на контроллер. Если задержка растёт выше целевого порога - пул расширяется.

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

/* Default latency targets (linux/block/kyber-iosched.c) */
static const u64 kyber_latency_targets[] = {
[KYBER_READ] = 2ULL * NSEC_PER_MSEC, /* 2 ms */
[KYBER_WRITE] = 10ULL * NSEC_PER_MSEC, /* 10 ms */
[KYBER_DISCARD] = 5ULL * NSEC_PER_SEC, /* 5 s */
};

Эти значения разумны для большинства NVMe-дисков среднего класса, но не оптимальны. Высокопроизводительный NVMe вполне способен обслуживать чтение за 300-500 мкс, и тогда цель в 2 мс выглядит слабой.

Прежде чем менять параметры, нужно убедиться, что Kyber вообще загружен и назначен нужному устройству:

# Проверяем текущий планировщик для всех блочных устройств
for dev in /sys/block/*/queue/scheduler; do
echo "$(dirname $(dirname $dev) | xargs basename): $(cat $dev)"
done

# Пример вывода:
# nvme0n1: [none]
# sda: [mq-deadline] kyber bfq

# Загружаем модуль Kyber если он не в системе
sudo modprobe kyber-iosched

# Назначаем Kyber NVMe-устройству
echo kyber | sudo tee /sys/block/nvme0n1/queue/scheduler

# Проверяем параметры - они появятся только после назначения
ls /sys/block/nvme0n1/queue/iosched/
# read_lat_nsec write_lat_nsec

Теперь можно работать с целевыми задержками. Kyber управляет количеством токенов, непрерывно отслеживая текущую латентность завершённых запросов и сравнивая её с целевыми значениями read_lat_nsec и write_lat_nsec. Снижение read_lat_nsec заставляет планировщик агрессивнее сжимать очередь чтения, жертвуя пропускной способностью ради задержки - это нужно базам данных. Повышение, напротив, позволяет накопить больше запросов в очереди и выиграть в throughput при потоковой нагрузке:

# Профиль для базы данных (PostgreSQL, MySQL) - приоритет низкой латентности чтения
echo 500000 | sudo tee /sys/block/nvme0n1/queue/iosched/read_lat_nsec # 0.5 мс
echo 5000000 | sudo tee /sys/block/nvme0n1/queue/iosched/write_lat_nsec # 5 мс

# Профиль для потоковой записи (логи, бэкапы) - приоритет пропускной способности
echo 2000000 | sudo tee /sys/block/nvme0n1/queue/iosched/read_lat_nsec # 2 мс (дефолт)
echo 20000000 | sudo tee /sys/block/nvme0n1/queue/iosched/write_lat_nsec # 20 мс

# Проверяем текущие значения
cat /sys/block/nvme0n1/queue/iosched/read_lat_nsec
cat /sys/block/nvme0n1/queue/iosched/write_lat_nsec

Важно понимать границы применимости Kyber. Kyber бесполезен для HDD и дешёвых или устаревших SSD. Его токен-баккетная логика рассчитана на устройства, способные держать сотни параллельных операций с предсказуемой латентностью. SATA SSD справляется с задачей, но медленный USB-накопитель или eMMC-модуль превратят Kyber в источник проблем, а не в решение.

BFQ - пропорциональное распределение полосы пропускания между процессами

BFQ (Budget Fair Queueing) строится на совершенно другой идее. Если Kyber думает о том, насколько быстро выполняются запросы, то BFQ думает о том, кто именно их отправляет. BFQ создаёт отдельную очередь для каждого процесса и назначает ей бюджет, измеряемый не во временных слотах, а в количестве секторов. Пока у процесса есть бюджет - он работает. Когда бюджет исчерпан - в очередь встаёт следующий.

Это делает BFQ идеальным для ситуаций, где несколько процессов конкурируют за один диск с разными приоритетами: фоновый backup не должен мешать интерактивному приложению, компиляция не должна тормозить отклик рабочего стола. BFQ в конфигурации по умолчанию ориентирован на минимальную латентность, а не на максимальный throughput, и гарантирует, что ни одно приложение не монополизирует весь bandwidth накопителя.

Назначить BFQ и осмотреть его параметры несложно:

# Назначаем BFQ для SATA SSD
echo bfq | sudo tee /sys/block/sda/queue/scheduler

# Смотрим все доступные параметры
ls /sys/block/sda/queue/iosched/
# back_seek_max back_seek_penalty fifo_expire_async
# fifo_expire_sync low_latency max_budget
# slice_idle slice_idle_us strict_guarantees
# timeout_async timeout_sync

# Проверяем текущие значения ключевых параметров
cat /sys/block/sda/queue/iosched/low_latency # 1 = включён режим низкой латентности
cat /sys/block/sda/queue/iosched/slice_idle # в миллисекундах (обычно 8)
cat /sys/block/sda/queue/iosched/slice_idle_us # то же самое в микросекундах

Самый влиятельный параметр BFQ - slice_idle. Он задаёт, сколько времени BFQ будет ждать нового запроса от текущего процесса, когда его очередь опустела. Ожидание преследует двойную цель: увеличить throughput за счёт последовательности операций и гарантировать справедливое распределение полосы пропускания. На вращающихся дисках это ожидание окупается с лихвой - последовательные операции несравнимо эффективнее случайных. На SSD и NVMe ситуация иная.

# Профиль для рабочего стола (интерактивная отзывчивость, SATA SSD)
echo 1 | sudo tee /sys/block/sda/queue/iosched/low_latency
echo 8000 | sudo tee /sys/block/sda/queue/iosched/slice_idle_us # 8 мс - дефолт, хорош для десктопа

# Профиль для сервера с SATA SSD (throughput важнее fairness)
echo 0 | sudo tee /sys/block/sda/queue/iosched/slice_idle # отключаем ожидание
echo 0 | sudo tee /sys/block/sda/queue/iosched/slice_idle_us

# Профиль для NVMe с BFQ (если Kyber по какой-то причине не подходит)
echo 0 | sudo tee /sys/block/nvme0n1/queue/iosched/slice_idle
echo 0 | sudo tee /sys/block/nvme0n1/queue/iosched/strict_guarantees

Для конфигураций с несколькими шпинделями за одним LUN, аппаратными RAID-контроллерами или быстрыми flash-накопителями установка slice_idle=0 может дать лучший throughput при сохранении приемлемых задержек.

Параметр low_latency - отдельная история. Когда он включён, BFQ автоматически повышает приоритет очередей интерактивных приложений и процессов, работающих в режиме soft real-time. Это та самая магия, из-за которой рабочий стол остаётся отзывчивым при активной фоновой копировании файлов. Но для серверного окружения, где все процессы равнозначны, этот режим иногда стоит отключить - он мешает ручному управлению весами через ionice.

Матрица выбора планировщика под разные сценарии нагрузки

Вопрос "какой планировщик лучше" не имеет универсального ответа. Правильная постановка звучит иначе: какой планировщик лучше для конкретного устройства под конкретную нагрузку. Практика и замеры дают следующую картину:

  • NVMe + база данных (PostgreSQL, MySQL, Redis) - Kyber с read_lat_nsec=500000. Низкая латентность чтения критична, устройство справляется само с упорядочиванием запросов.
  • NVMe + стриминг видео или журналирование - none или Kyber с повышенными write_lat_nsec. Throughput важнее задержки, планировщик не должен дросселировать запись.
  • SATA SSD + рабочий стол - BFQ с low_latency=1 и дефолтным slice_idle. Честное распределение между приложениями, UI остаётся живым при любой фоновой нагрузке.
  • SATA SSD + сервер приложений - BFQ с slice_idle=0 или mq-deadline. Throughput и предсказуемость важнее fairness.
  • HDD - BFQ безоговорочно. Только он умеет правильно группировать запросы для сокращения seek-time.

Проверить текущее состояние всех устройств одной командой удобно через lsblk:

lsblk -d -o NAME,ROTA,SCHED
# NAME ROTA SCHED
# sda 1 bfq ← вращающийся диск
# sdb 0 mq-deadline ← SATA SSD
# nvme0n1 0 none ← NVMe

Как закрепить настройки через udev и не потерять их после перезагрузки

Все изменения через /sys/block/ живут только до следующей перезагрузки. Правильный способ сделать их постоянными - udev-правила, которые применяются при каждом обнаружении устройства:

# Создаём файл правил
sudo nano /etc/udev/rules.d/60-iosched.rules

# NVMe - Kyber для низкой латентности
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", \
ATTR{queue/scheduler}="kyber"

# SATA SSD (не вращающийся) - BFQ с отключённым slice_idle
ACTION=="add|change", KERNEL=="sd*[!0-9]", \
ATTR{queue/rotational}=="0", \
ATTR{queue/scheduler}="bfq", \
ATTR{queue/iosched/slice_idle}="0"

# HDD (вращающийся) - BFQ с low_latency
ACTION=="add|change", KERNEL=="sd*[!0-9]", \
ATTR{queue/rotational}=="1", \
ATTR{queue/scheduler}="bfq"

# Применяем правила без перезагрузки
sudo udevadm control --reload-rules
sudo udevadm trigger

Для тонкой настройки параметров Kyber после назначения udev-правила недостаточно - атрибуты /sys/block/.../queue/iosched/ появляются только после назначения планировщика. Здесь выручает systemd-сервис или скрипт в /etc/rc.local:

# /etc/systemd/system/iosched-tune.service
[Unit]
Description=I/O scheduler tuning
After=systemd-udev-settle.service

[Service]
Type=oneshot
ExecStart=/bin/bash -c '\
echo 500000 > /sys/block/nvme0n1/queue/iosched/read_lat_nsec; \
echo 5000000 > /sys/block/nvme0n1/queue/iosched/write_lat_nsec'

[Install]
WantedBy=multi-user.target

sudo systemctl enable --now iosched-tune.service

Как измерить эффект от изменений с помощью fio

Любая настройка без замеров - это гадание. fio позволяет за несколько минут получить объективную картину того, что дал конкретный планировщик на конкретном железе:

# Базовый тест случайного чтения 4K (типичная нагрузка БД)
fio --name=randread \
--filename=/dev/nvme0n1 \
--rw=randread \
--bs=4k \
--direct=1 \
--numjobs=4 \
--iodepth=32 \
--runtime=30 \
--time_based \
--group_reporting

# Сравнительный тест всех планировщиков в цикле
for SCHED in none kyber bfq mq-deadline; do
echo "$SCHED" | sudo tee /sys/block/nvme0n1/queue/scheduler > /dev/null
echo "=== Scheduler: $SCHED ==="
fio --name=test \
--filename=/tmp/fio_test \
--size=1G \
--rw=randrw \
--bs=4k \
--direct=1 \
--numjobs=4 \
--runtime=20 \
--time_based \
--group_reporting \
--output-format=terse | \
awk -F';' '{print "READ IOPS:", $8, "| WRITE IOPS:", $49, "| READ lat P99:", $28, "us"}'
done

При анализе результатов стоит смотреть не только на средние значения, но и на P99-латентность. Именно хвостовая латентность на уровне 99-го процентиля - наиболее честный показатель поведения планировщика под нагрузкой, поскольку именно она определяет пользовательский опыт в реальных приложениях.

Числа, которые дают замеры, нередко удивляют. Разница между none и Kyber на NVMe при высококонкурентной нагрузке часто укладывается в 5-10% - и это хороший результат для того количества работы, что Kyber добавляет поверх нулевого планировщика. BFQ на той же NVMe может показать провал в raw throughput, зато P99-латентность для привилегированных процессов окажется вдвое лучше, чем у конкурентов - именно потому, что BFQ защищает их от соседей по очереди.

Выбор планировщика - это, по сути, выбор того, что система считает важным: абсолютную скорость, честность между процессами или гарантированную латентность. Kyber отвечает на первый и третий вопрос для быстрых устройств. BFQ отвечает на второй - для любых. Понимание этого различия переводит настройку из разряда ритуалов в разряд инженерных решений.

https://fileenergy.com/linux