Контроль полосы пропускания на сервере — это не только «урезать скорость». Чаще задача звучит так: убрать bufferbloat на исходящем канале, гарантировать минимум для критичных сервисов, ограничить «шумных» клиентов, не дать бэкапам забить uplink и при этом сохранить отзывчивость системы.
В Linux для этого обычно комбинируют tc (Traffic Control) и nftables: tc формирует очереди и делает shaping, а nftables удобно классифицирует трафик (mark/ct mark) и ставит «предохранители» по частоте событий (rate limit на новые соединения, пакеты).
Базовая рабочая схема такая: egress shaping делаем tc на исходящем интерфейсе; ingress shaping — через ifb (потому что входящий поток на самом интерфейсе «по-настоящему» шейпить нельзя); а rate limiting в nftables используем точечно, чтобы сгладить пики и защитить чувствительные порты.
Ключевая теория: ingress, egress и почему «входящий шейпинг» не прямой
Egress — пакеты, которые ваш сервер отправляет наружу. Здесь tc работает лучше всего: вы управляете очередью перед физическим выходом, значит реально контролируете скорость и задержку.
Ingress — пакеты, которые прилетают на сервер. Они уже пришли «по проводу», и на входе вы можете в основном только дропнуть/пометить. Поэтому классический трюк для контроля ingress egress bandwidth: перенаправить входящий поток на виртуальный интерфейс ifb и дальше шейпить его как egress уже на ifb.
Практическое правило: чтобы задержка была предсказуемой, шейпьте egress на 1–5% ниже реальной скорости канала. Для ingress через IFB — тоже задавайте лимит чуть ниже ожидаемого входящего, чтобы очередь формировалась у вас, а не «где-то в сети».
Инструменты tc: qdisc, классы, фильтры и что выбрать (fq_codel, CAKE, HTB)
В tc всё крутится вокруг qdisc (очередей):
- classless qdisc — проще, но без иерархии классов (например,
fq_codel,cake). - classful qdisc — можно делить полосу между классами/сервисами (например,
htb), а внутри классов ставить «умные» очереди вродеfq_codel.
Что чаще всего нужно админу/DevOps на практике:
- fq_codel — хорошая «очередь по умолчанию» против bufferbloat, но сама по себе не задаёт лимит скорости (она про качество очередей, а не про shaping).
- CAKE — «комбайн»: шейпер + справедливое распределение + борьба с bufferbloat; удобен, когда не нужна сложная иерархия классов.
- HTB — когда нужна гарантированная/максимальная скорость по классам (per-service bandwidth limit), например: ограничить бэкапы, выделить полосу для API, остальное отдать вебу.

Быстрый старт: egress shaping на одном интерфейсе (CAKE) и базовые проверки
Если задача — просто сделать «ровный» egress и убрать пики задержек, начните с простого. Допустим, интерфейс eth0, реальная скорость выхода около 1 Гбит/с, а шейпить хотите на 950 Мбит/с:
tc qdisc replace dev eth0 root cake bandwidth 950Mbit
Проверка счётчиков и дропов:
tc -s qdisc show dev eth0
Если CAKE недоступен в вашем ядре/сборке, обычно выбирают связку HTB (ограничение скорости и классы) + fq_codel (качество очереди внутри класса).
Вариант: HTB (лимит) + fq_codel (очередь) для одного класса
Пример — общий egress 200 Мбит/с. Один корневой класс и дефолтный дочерний класс:
tc qdisc replace dev eth0 root handle 1: htb default 10
tc class replace dev eth0 parent 1: classid 1:1 htb rate 200Mbit ceil 200Mbit
tc class replace dev eth0 parent 1:1 classid 1:10 htb rate 200Mbit ceil 200Mbit
tc qdisc replace dev eth0 parent 1:10 handle 10: fq_codel
Это уже реальный tc egress shaping: скорость ограничена, а очередь на выходе становится «умной».
Per-service bandwidth limit: делим полосу HTB-классами через маркировку nftables
Самый практичный паттерн: nftables помечает трафик (meta mark или ct mark), а tc по этим меткам раскладывает пакеты по классам HTB. Так вы получаете per-service bandwidth limit без «зоопарка» фильтров по адресам и портам в tc: меняете классификацию в nft — и не трогаете структуру HTB.
Подход особенно удобен на VDS, где важно быстро и предсказуемо отделить веб/SSH/бэкапы и не получить скачки задержки под нагрузкой.
Шаг 1. Размечаем трафик в nftables (пример по портам сервиса)
Пример: веб (80/443) — mark 10, SSH — mark 20, rsync (873) — mark 30. Для egress ответов сервера логичнее ставить метку в цепочке output и матчить sport:
nft add table inet mangle
nft 'add chain inet mangle output { type route hook output priority mangle; policy accept; }'
nft add rule inet mangle output tcp sport { 80, 443 } meta mark set 10
nft add rule inet mangle output tcp sport 22 meta mark set 20
nft add rule inet mangle output tcp sport 873 meta mark set 30
Важно: если вы ограничиваете не «ответы сервиса», а исходящие подключения сервера к внешним ресурсам (например, бэкап в стороннее хранилище по 443), то сервисный порт будет в dport. Это самая частая причина, почему трафик «не попадает» в нужный класс.
Шаг 2. HTB классы и фильтры по метке (fw)
Создадим общий корень 200 Мбит/с и классы: web до 160, ssh до 20, backup до 40. Остальное уходит в дефолтный класс.
tc qdisc replace dev eth0 root handle 1: htb default 40
tc class replace dev eth0 parent 1: classid 1:1 htb rate 200Mbit ceil 200Mbit
tc class replace dev eth0 parent 1:1 classid 1:10 htb rate 120Mbit ceil 160Mbit prio 0
tc class replace dev eth0 parent 1:1 classid 1:20 htb rate 5Mbit ceil 20Mbit prio 1
tc class replace dev eth0 parent 1:1 classid 1:30 htb rate 10Mbit ceil 40Mbit prio 2
tc class replace dev eth0 parent 1:1 classid 1:40 htb rate 1Mbit ceil 200Mbit prio 3
tc qdisc replace dev eth0 parent 1:10 handle 10: fq_codel
tc qdisc replace dev eth0 parent 1:20 handle 20: fq_codel
tc qdisc replace dev eth0 parent 1:30 handle 30: fq_codel
tc qdisc replace dev eth0 parent 1:40 handle 40: fq_codel
tc filter replace dev eth0 parent 1: protocol ip prio 10 handle 10 fw flowid 1:10
tc filter replace dev eth0 parent 1: protocol ip prio 10 handle 20 fw flowid 1:20
tc filter replace dev eth0 parent 1: protocol ip prio 10 handle 30 fw flowid 1:30
Проверяем счётчики по классам и фильтрам:
tc -s class show dev eth0
tc -s filter show dev eth0 parent 1:
Если трафик не попадает в нужный класс, почти всегда виновато одно из трёх: (1) метка ставится не в той точке (output vs postrouting), (2) перепутан sport/dport, (3) трафик идёт через другой интерфейс (например, ens3 вместо eth0).
По теме мониторинга и аккуратной «обвязки» таких правил можно подсмотреть в материале про мониторинг и шейпинг трафика на VDS.
Ingress shaping через IFB: как ограничить входящий трафик
Для ingress делаем так: создаём ifb0, цепляем ingress-qdisc на eth0, перенаправляем (redirect) поток на ifb0, и уже на ifb0 ставим shaping (CAKE или HTB+fq_codel).
Создаём IFB и включаем интерфейс
modprobe ifb
ip link add ifb0 type ifb
ip link set ifb0 up
Перенаправляем ingress с eth0 на ifb0
tc qdisc replace dev eth0 handle ffff: ingress
tc filter replace dev eth0 parent ffff: protocol ip prio 10 u32 match u32 0 0 action mirred egress redirect dev ifb0
Шейпим «входящий» как egress на ifb0
Например, ограничим входящий до 300 Мбит/с и включим CAKE:
tc qdisc replace dev ifb0 root cake bandwidth 300Mbit
Проверка:
tc -s qdisc show dev ifb0
tc -s qdisc show dev eth0
IFB — мощный инструмент, но добавляет точку сложности. Начинайте с общего лимита ingress и только потом усложняйте классификацию, если она действительно нужна.
nftables rate limit: где уместно и почему не заменяет tc
nftables rate limit хорош как «предохранитель»: ограничить частоту новых соединений к сервису, снизить эффект от бурстов, не дать забить лог и ресурсы слишком частыми попытками. Но это не shaping: nftables не формирует очередь и не делает поток «ровным» — он чаще дропает/не пропускает выше порога.
Типичный безопасный кейс — ограничить новые TCP SYN к SSH (и не завалить себя логами):
nft add table inet filter
nft 'add chain inet filter input { type filter hook input priority filter; policy drop; }'
nft add rule inet filter input iifname "lo" accept
nft add rule inet filter input ct state established,related accept
nft add rule inet filter input tcp dport 22 ct state new limit rate 10/minute accept
nft add rule inet filter input tcp dport 22 ct state new drop
Если вы строите более строгий сетевой периметр на VDS, полезно также посмотреть подходы с SYN-proxy: SYNPROXY в nftables для защиты входящих TCP.
Systemd slice и tc: как привязать лимиты к сервисам (и где границы)
Запрос «systemd slice tc» обычно означает «хочу ограничить сеть конкретного сервиса через systemd». Важно понимать границы: systemd отлично ограничивает CPU/память/IO, но сетевые лимиты напрямую не даёт так универсально, как хотелось бы.
Практичный путь выглядит так:
- сервис запускается в отдельном cgroup (slice);
- пакеты помечаются по cgroup (механизм зависит от ядра и окружения);
- дальше
tcфильтрует по метке и применяет нужные классы.
Если вы не готовы поддерживать привязку по cgroup, то для большинства веб-серверных задач проще и стабильнее маркировать по портам/адресам, как в примере выше. Это грубее, но предсказуемо и легче обслуживается.

Диагностика и типовые ошибки
Понимаем, где узкое место
ip -s link show dev eth0— ошибки/дропы на интерфейсе.ss -s— общая картина по сокетам.tc -s qdisc show dev eth0иtc -s class show dev eth0— счётчики, дропы, backlog.nft list ruleset— порядок правил и где именно ставится метка.
Частые проблемы
- Перепутан интерфейс: в VDS часто
ens3/enp1s0, а неeth0. - Mark ставится не там: для локально генерируемого трафика точка
outputчасто удобнее, но для проходящего может понадобитьсяprerouting/postrouting. - Перепутан sport/dport: для ответов сервера на 443 «порт сервиса» будет source; для исходящих подключений сервера к чужому 443 — destination.
- Шейпинг выше реальной скорости: эффекта на задержку почти не будет. Ставьте чуть ниже, чтобы очередь формировалась у вас.
- Ingress без IFB: попытки «ограничить входящий» без IFB обычно заканчиваются дропами, но не управляемой скоростью.
Рекомендованный минимальный «боевой» профиль для VDS
Если вы настраиваете shaping на VDS под сайты/API и вам важны задержки:
- на egress: CAKE с bandwidth чуть ниже реального аплинка или HTB+fq_codel, если нужно деление по сервисам;
- на ingress: IFB + CAKE с лимитом по входящему каналу (если входящая полоса тоже должна быть предсказуемой);
- в nftables: rate limit только как предохранитель (SSH/админки), а не как «шейпер скорости».
Как откатить изменения и не потерять доступ
Сеть легко «перерезать» неправильным правилом. Перед экспериментами держите вторую SSH-сессию и план отката.
Сброс tc на интерфейсе:
tc qdisc del dev eth0 root
tc qdisc del dev eth0 ingress
Если использовали IFB:
tc qdisc del dev ifb0 root
ip link set ifb0 down
ip link del ifb0
Для nftables заранее держите сохранённый ruleset в файле и понятный rollback (зависит от того, как у вас загружаются правила: unit systemd, nftables.conf и т. п.).
Чек-лист: что считать успехом
- На
tc -sрастут счётчики в нужных классах/очередях. - Под нагрузкой задержка не «раздувается» (особенно на исходящем канале).
- Критичные сервисы сохраняют полосу даже при фоновых задачах.
- Rate limit в nftables не ломает легитимный трафик (нет всплеска ошибок/ретраев на уровне приложения).
Если хотите собрать конфигурацию под ваш сценарий, подготовьте исходные данные: имя интерфейса, реальную скорость канала (в обе стороны), какие сервисы делим (по портам или по назначениям), нужен ли ingress, и где важнее «гарантия» (rate) vs «потолок» (ceil).


