Канал, занятый одной большой передачей, перестаёт обслуживать всё остальное: интерактивная сессия отвечает рывками, страницы открываются с задержкой. Причина не в нехватке скорости, а в том, что очередь интерфейса заполняется одинаково важными и неважными пакетами. Механизм управления трафиком в ядре Linux задаёт порядок отправки и ограничивает скорость, а настраивается он утилитой tc. Разбираем базовые схемы, разделение на классы и проверку результата.
Как устроено управление трафиком и где оно применяется
Ядро отправляет пакеты в очередь сетевого устройства, и правила этой очереди описываются объектом qdisc. По умолчанию применяется простая очередь, которая обслуживает пакеты в порядке поступления и не различает потоки. Именно поэтому одна длительная передача занимает весь канал.
Управление возможно только на отправке. Для приёма есть лишь два инструмента: полицейское ограничение, которое отбрасывает лишнее без накопления, и перенаправление входящего трафика на виртуальное устройство, где к нему можно применить полноценную очередь.
Первое правило настройки состоит в том, чтобы ограничивать скорость ниже фактической возможности канала. Если задать предел равным номиналу, очередь будет накапливаться в оборудовании провайдера, куда нет доступа, и задержка вырастет. Практический запас составляет десять-пятнадцать процентов.
tc qdisc show dev eth0
tc -s qdisc show dev eth0
ip -s link show dev eth0
Вывод с ключом -s показывает счётчики отправленного и отброшенного, по ним видно, работает ли правило и есть ли потери.
Простая очередь с борьбой против накопленной задержки
Первый шаг, который стоит сделать на любой машине с быстрым каналом, заменить простую очередь на вариант с управлением задержкой. Дисциплина fq_codel разделяет потоки по отдельным очередям и отбрасывает пакеты из потоков, которые долго удерживают место, благодаря чему одна передача перестаёт мешать остальным.
sudo tc qdisc replace dev eth0 root fq_codel
tc -s qdisc show dev eth0
Команда replace заменяет существующую дисциплину, и изменения применяются немедленно. Для большинства сценариев этого достаточно, чтобы интерактивный трафик перестал зависеть от идущей загрузки.
Если нужно ограничить скорость без разделения на классы, применяют очередь на основе токенов, которая накапливает единицы разрешения и отправляет пакет при их наличии.
sudo tc qdisc replace dev eth0 root tbf rate 100mbit burst 1540 latency 50ms
Параметр rate задаёт среднюю скорость, burst объём данных, отправляемый одним всплеском, latency допустимое время пребывания пакета в очереди. Слишком малое значение burst приводит к частым отбрасываниям, слишком большое не даёт ограничения на коротких интервалах.
Обе схемы не различают виды трафика и подходят, когда нужно уложиться в заданную скорость либо убрать влияние одного потока на остальные.
Иерархическое распределение по классам через htb
Когда разные виды трафика обслуживают по-разному, применяют иерархическую дисциплину: она задаёт корневое ограничение и набор классов с гарантированной скоростью и потолком.
sudo tc qdisc replace dev eth0 root handle 1: htb default 30
sudo tc class add dev eth0 parent 1: classid 1:1 htb rate 900mbit ceil 900mbit
sudo tc class add dev eth0 parent 1:1 classid 1:10 htb rate 100mbit ceil 900mbit prio 0
sudo tc class add dev eth0 parent 1:1 classid 1:20 htb rate 200mbit ceil 600mbit prio 1
sudo tc class add dev eth0 parent 1:1 classid 1:30 htb rate 600mbit ceil 900mbit prio 2
Параметр default указывает класс для пакетов, не отнесённых ни к одному правилу. Параметр rate задаёт гарантированную долю, ceil потолок, до которого класс занимает свободную полосу соседей. Значение prio определяет порядок при нехватке полосы: меньшее число означает высокий приоритет.
Внутрь каждого класса стоит добавить дисциплину, разделяющую потоки, иначе класс с большим числом соединений снова начнёт вести себя нечестно.
sudo tc qdisc add dev eth0 parent 1:10 handle 10: fq_codel
sudo tc qdisc add dev eth0 parent 1:20 handle 20: fq_codel
sudo tc qdisc add dev eth0 parent 1:30 handle 30: fq_codel
Распределение скорости между классами подбирают по характеру нагрузки. Для машины, которая обслуживает посетителей и выполняет резервное копирование, копию помещают в класс с низкой гарантированной долей и высоким потолком: пока канал свободен, копирование идёт быстро, при появлении других задач уступает место.
Отнесение пакетов к классам через фильтры и метки
Классы не знают, какой пакет куда относить, это делают фильтры. Простейший вариант отбор по порту или диапазону адресов.
sudo tc filter add dev eth0 parent 1: protocol ip u32 \
match ip dport 22 0xffff flowid 1:10
sudo tc filter add dev eth0 parent 1: protocol ip u32 \
match ip dport 53 0xffff flowid 1:10
sudo tc filter add dev eth0 parent 1: protocol ip u32 \
match ip dst 192.168.10.0/24 flowid 1:20
Для сложных условий удобнее метка, которую присваивает сетевой экран, а фильтр её читает: правила отбора живут в одном месте вместе с остальной фильтрацией.
sudo nft add table inet qos
sudo nft add chain inet qos output '{ type route hook output priority mangle; }'
sudo nft add rule inet qos output tcp dport 22 meta mark set 0x10
sudo nft add rule inet qos output udp dport 53 meta mark set 0x10
sudo tc filter add dev eth0 parent 1: protocol ip handle 0x10 fw flowid 1:10
Обратный путь: сетевой экран выставляет поле DSCP, а фильтр отбирает пакеты по нему. Подход полезен, когда классификация сохраняется на всём пути, а не только на одной машине.
sudo tc qdisc replace dev eth0 root handle 1: htb default 30
sudo tc filter add dev eth0 parent 1: protocol ip u32 \
match ip tos 0x10 0xfc flowid 1:10
При большом числе правил перебор выполняется последовательно, и частые условия ставят первыми. Альтернатива фильтр flower, который быстрее при множестве записей.
Приоритет интерактивного трафика и типичная схема сервера
Интерактивные сессии чувствительны не к полосе, а к задержке. Пакетам удалённого терминала нужно несколько килобит в секунду, но идти они должны без ожидания в очереди за мегабайтами чужих данных. Служебный трафик хорошо переносит задержку и занимает только свободную полосу.
Практичная схема для сервера состоит из трёх классов: приоритетный для терминальных сессий, службы имён и небольших ответов, основной для обычных соединений и фоновый для резервного копирования и обновлений. Гарантированные доли подбирают так, чтобы их сумма не превышала общий предел, иначе при одновременной нагрузке классы начнут конкурировать, и приоритеты перестанут соблюдаться.
#!/bin/sh
DEV=eth0
RATE=900mbit
tc qdisc replace dev $DEV root handle 1: htb default 30
tc class add dev $DEV parent 1: classid 1:1 htb rate $RATE ceil $RATE
tc class add dev $DEV parent 1:1 classid 1:10 htb rate 20mbit ceil $RATE prio 0
tc class add dev $DEV parent 1:1 classid 1:20 htb rate 600mbit ceil $RATE prio 1
tc class add dev $DEV parent 1:1 classid 1:30 htb rate 100mbit ceil 800mbit prio 2
tc qdisc add dev $DEV parent 1:10 handle 10: fq_codel
tc qdisc add dev $DEV parent 1:20 handle 20: fq_codel
tc qdisc add dev $DEV parent 1:30 handle 30: fq_codel
tc filter add dev $DEV parent 1: protocol ip u32 match ip dport 22 0xffff flowid 1:10
tc filter add dev $DEV parent 1: protocol ip u32 match ip dport 53 0xffff flowid 1:10
Проверить эффект можно простым опытом: запустить передачу крупного файла и одновременно измерить задержку до удалённого узла. Передачу создают любой службой копирования либо загрузкой большого файла на соседнюю машину. До настройки значения вырастают до сотен миллисекунд, после остаются в пределах единиц.
ping -c 20 -i 0.2 remote.example.net | tail -n 2
Передачу, чувствительную к задержке, вроде голосовых потоков, помещают в приоритетный класс: полоса ей нужна небольшая, а колебания времени доставки губительны.
Ограничение входящего трафика и готовые решения
Входящее направление управляется иначе. Полицейское ограничение отбрасывает пакеты сверх заданной скорости, и для TCP это работает приемлемо, поскольку протокол сам снижает темп в ответ на потери.
sudo tc qdisc add dev eth0 handle ffff: ingress
sudo tc filter add dev eth0 parent ffff: protocol ip u32 \
match u32 0 0 police rate 200mbit burst 1mbit drop flowid :1
Более корректный способ перенаправление входящего трафика на промежуточное устройство с применением к нему обычной очереди.
sudo modprobe ifb numifbs=1
sudo ip link set dev ifb0 up
sudo tc qdisc add dev eth0 handle ffff: ingress
sudo tc filter add dev eth0 parent ffff: protocol ip u32 match u32 0 0 \
action mirred egress redirect dev ifb0
sudo tc qdisc add dev ifb0 root handle 2: htb default 20
Для домашней сети подходит дисциплина cake: она сама делит полосу по числу активных потоков и учитывает накладные расходы, а указать ей нужно только доступную скорость.
sudo tc qdisc replace dev eth0 root cake bandwidth 900mbit besteffort
tc -s qdisc show dev eth0
На маршрутизаторах с OpenWrt эта дисциплина доступна в виде отдельного пакета и настраивается через интерфейс, что делает её самым простым способом убрать влияние загрузки на отзывчивость сети.
Стоит помнить, что правила применяются к конкретному устройству и не наследуются другими интерфейсами. При нескольких внешних каналах сценарий выполняют для каждого, а при наличии виртуальных интерфейсов с инкапсуляцией очередь настраивают на том устройстве, через которое пакеты фактически уходят, иначе ограничение сработает не там, где ожидается.
Выбор между cake и ручной схемой на htb определяется задачей. Для одной машины с разнородным трафиком и явными приоритетами удобнее htb с фильтрами, для канала целиком cake требует меньше настройки и хорошо справляется с накопленной задержкой.
Проверка результата и сохранение настроек
После применения правил проверяют состояние очереди и счётчики.
tc -s -d qdisc show dev eth0
tc -s class show dev eth0
tc filter show dev eth0
В выводе видны отправленные байты и пакеты по классам, число превышений и отброшенных пакетов. Класс, у которого счётчики не растут, трафика не получает, и причину ищут в фильтрах.
Изменения, внесённые утилитой tc, действуют до перезагрузки, поэтому рабочую схему оформляют сценарием и запускают его службой.
# /etc/systemd/system/tc-shaping.service
[Unit]
Description=Настройка очередей на eth0
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/tc-shaping.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
sudo chmod +x /usr/local/sbin/tc-shaping.sh
sudo systemctl daemon-reload
sudo systemctl enable --now tc-shaping.service
Поле DSCP удобно тем, что его значение задаёт приложение или пограничный маршрутизатор, а очереди на всех машинах пути читают одно и то же поле. Выставлять его можно и на самом сервере через сетевой экран, после чего фильтр относит помеченные пакеты к нужному классу без разбора адресов и портов.
Отдельно проверяют, что промежуточное устройство для входящего трафика создаётся при каждой загрузке, поскольку оно существует только в памяти. Команды загрузки модуля и включения устройства входят в тот же сценарий, который настраивает очереди, иначе после перезагрузки ограничение входящего направления перестанет действовать, а исходящее продолжит работать, и картина будет выглядеть необъяснимой.
Сценарий стоит писать так, чтобы его можно было выполнить повторно: команда replace заменяет существующую дисциплину, а при удалении правил их отсутствие не должно приводить к остановке сценария.
tc qdisc del dev eth0 root 2>/dev/null || true
Отдельно проверяют, что настройки не конфликтуют с другими инструментами: среды управления контейнерами добавляют собственные правила на создаваемых интерфейсах. Ограничение задают на внешнем интерфейсе машины, а не на внутренних устройствах.
Завершает работу измерение до и после. Канал может стать медленнее по скорости передачи, но заметно отзывчивее по времени реакции, и это ожидаемый результат правильной настройки очередей.