Наверно любой, кто настраивал приоритезацию трафика в Linux сталкивался с тем, что возможности ifb для управления входящим трафиком довольно скудны. Далее я расскажу о ряде типичных проблем при построении QoS, и о том, как эти проблемы удалось решить с помощью контейнеров.
Так получилось, что управлять скоростью сетевого потока мы можем только в направление «на выход». То, что уже пришло к нам на интерфейс, уже прошло множество узких мест, и казалось бы, нет смысла отбрасывать или задерживать этот трафик. Но, так как большинство протоколов (TCP и те, что построены поверх UDP, или имеют полностью свою реализацию) имеют механизмы управления исходящим потоком, который может учитывать актуальную пропускную способность клиента, и изменять скорость отправки. В такой ситуации имеет смысл управлять потоком, который идет к клиенту на нашей стороне. Механизмов это реализующих много, я рассмотрю часть из них.
Самая простая и беспроблемная ситуация. Здесь и далее, мы примем как данность (для упрощения): сам шлюз трафик не генерирует, он просто его передает.
Итак, тут все просто, навешиваем очереди на оба интерфейса, и управляем скоростью к клиентам (входящая), на интерфейсе, который и смотрит в локальную сеть (на его исходящем потоке). Исходящая, соответственно, на внешнем интерфейсе. Все работает, ничего интересного, идем дальше.
Теперь не все так хорошо. Как поделить «входящий» поток между внутренними сетями? Очереди соседних интерфейсов ничего не знают о реальной нагрузке на канал. Тут можно поделить всю скорость пополам, и отдать по половине на каждую сеть. В итоге если вторая сеть не пользуется каналом, а вторая хочет максимум, то получит всего половину от него… Печаль, но нам поможет ifb (или его почти умерший конкурент imq).
Идея ifb заключается в том, что мы размещаем псевдоинтерфейс перед нашим реальным, и начинаем таким образом управлять «входящим» потоком реального интерфейса. К сожалению, в этой бочке слишком много дёгтя. Оказывается, ifb на столько особенный, что механизмы маркировки и фильтрации утилиты iptables применить на нем нельзя, можно только с помощью tc. Чем плох же tc? А тем, что в плане фильтрации и маркировки он умеет крайне мало и синтаксис уже совсем другой.
Как бы там не было плохо с tc, поставленную задачу он решить сможет вполне неплохо, поставим его на входящий интерфейс, а правила с внутренних мы уберем, они там уже не нужны.
Вот теперь все становится совсем плохо. У нас появляется новый интерфейс, который работает как обычный, но кроме всего прочего, он «незаметно» использует пропускную способность другого интерфейса.
Теперь мы не можем эффективно распределить трафик, ifb нам тут тоже не поможет. Остается решение в «лоб»: выделить для туннеля фиксированную полосу, и управлять содержимым туннеля как обычным интерфейсом (навесив на него ifb и обычные очереди).
Все «не плохо», только впустую можем терять скорость отданную в туннель или в обычную сеть (как посмотреть).
Вообще, главная мысль в применении netns, но применить контейнер проще (хоть он и расходует больше ресурсов, в основном дискового пространства).
Итак: нам нужна промежуточная цепочка интерфейсов между «внешними» и «внутренними» интерфейсами. Именно на этой цепочке можно с легкостью сделать QoS всего трафика, и максимум, что мы от него потеряем, это 4,5% (оверхед посчитайте сами, тут я учел IPSec\GRE) на туннелировании и шифрование (для гарантии, что все получат заданную полосу, примем, что весь трафик который приходит\уходит к нам, приходит из туннеля).
Думаю, что создать контейнер на LXC могут все, я рассмотрю только некоторые частности, которые нам могут понадобится.
Итак, в конфигурации контейнера нам понадобится:
Добавить наш реальный внешний интерфейс в контейнер (мы «продавим» его в контейнер, таким образом он «исчезнет» из основного пространства имен):
Или аналогично для vlan:
Добавим интерфейс, через который будем связываться с основной системой (скрипт поднимет интерфейс в основной системе, конфигурацию надо задать зарание):
Добавим автоматический запуск и остановку интерфейсов основной системы, при запуске\остановке контейнера:
Добавим возможность использовать tun\tap интерфейсов:
Разрешим управлением некоторыми параметрами маршрутизации (кто знает как сделать более «узкие» разрешения, прошу подсказку):
При необходимости, сделайте контейнер автозапускаемым.
В основной системе нам понадобится профиль для интерфейса route0, который появится при старте контейнера, я предполагаю, что у вас используется NetworkManager.
Что нужно настроить ещё:
Теперь мы просто настроим очереди на исходящих потоках интерфейсов, связывающих основную систему с контенером. Теперь, проблемы с определением назначения трафика нет, и мы можем задействовать всю доступную полосу от нашего провайдера (с учетом % на оверхед от туннелей).
P.S. Эпоха «тазиков», которые под Linux раздают интернет стремительно уходит, стоимость аппаратных маршрутизаторов становится все привлекательней, а их мощь и гибкость уже мало чем уступает «большим» братьям, то же Mikrotik решит эту «проблему» без особых заморочек.
Почему такие проблемы?
Так получилось, что управлять скоростью сетевого потока мы можем только в направление «на выход». То, что уже пришло к нам на интерфейс, уже прошло множество узких мест, и казалось бы, нет смысла отбрасывать или задерживать этот трафик. Но, так как большинство протоколов (TCP и те, что построены поверх UDP, или имеют полностью свою реализацию) имеют механизмы управления исходящим потоком, который может учитывать актуальную пропускную способность клиента, и изменять скорость отправки. В такой ситуации имеет смысл управлять потоком, который идет к клиенту на нашей стороне. Механизмов это реализующих много, я рассмотрю часть из них.
Типичные ситуации и проблемы
По одному интерфейсу в интернет и в локальную сеть, никаких туннелей
Самая простая и беспроблемная ситуация. Здесь и далее, мы примем как данность (для упрощения): сам шлюз трафик не генерирует, он просто его передает.
Итак, тут все просто, навешиваем очереди на оба интерфейса, и управляем скоростью к клиентам (входящая), на интерфейсе, который и смотрит в локальную сеть (на его исходящем потоке). Исходящая, соответственно, на внешнем интерфейсе. Все работает, ничего интересного, идем дальше.
Добавим ещё один внутренний интерфейс
Теперь не все так хорошо. Как поделить «входящий» поток между внутренними сетями? Очереди соседних интерфейсов ничего не знают о реальной нагрузке на канал. Тут можно поделить всю скорость пополам, и отдать по половине на каждую сеть. В итоге если вторая сеть не пользуется каналом, а вторая хочет максимум, то получит всего половину от него… Печаль, но нам поможет ifb (или его почти умерший конкурент imq).
Идея ifb заключается в том, что мы размещаем псевдоинтерфейс перед нашим реальным, и начинаем таким образом управлять «входящим» потоком реального интерфейса. К сожалению, в этой бочке слишком много дёгтя. Оказывается, ifb на столько особенный, что механизмы маркировки и фильтрации утилиты iptables применить на нем нельзя, можно только с помощью tc. Чем плох же tc? А тем, что в плане фильтрации и маркировки он умеет крайне мало и синтаксис уже совсем другой.
Как бы там не было плохо с tc, поставленную задачу он решить сможет вполне неплохо, поставим его на входящий интерфейс, а правила с внутренних мы уберем, они там уже не нужны.
Теперь свяжем два офиса с помощью туннеля
Вот теперь все становится совсем плохо. У нас появляется новый интерфейс, который работает как обычный, но кроме всего прочего, он «незаметно» использует пропускную способность другого интерфейса.
Теперь мы не можем эффективно распределить трафик, ifb нам тут тоже не поможет. Остается решение в «лоб»: выделить для туннеля фиксированную полосу, и управлять содержимым туннеля как обычным интерфейсом (навесив на него ifb и обычные очереди).
Все «не плохо», только впустую можем терять скорость отданную в туннель или в обычную сеть (как посмотреть).
LXC нам поможет!
Вообще, главная мысль в применении netns, но применить контейнер проще (хоть он и расходует больше ресурсов, в основном дискового пространства).
Итак: нам нужна промежуточная цепочка интерфейсов между «внешними» и «внутренними» интерфейсами. Именно на этой цепочке можно с легкостью сделать QoS всего трафика, и максимум, что мы от него потеряем, это 4,5% (оверхед посчитайте сами, тут я учел IPSec\GRE) на туннелировании и шифрование (для гарантии, что все получат заданную полосу, примем, что весь трафик который приходит\уходит к нам, приходит из туннеля).
Думаю, что создать контейнер на LXC могут все, я рассмотрю только некоторые частности, которые нам могут понадобится.
Итак, в конфигурации контейнера нам понадобится:
Добавить наш реальный внешний интерфейс в контейнер (мы «продавим» его в контейнер, таким образом он «исчезнет» из основного пространства имен):
lxc.network.type = phys
lxc.network.flags = up
lxc.network.link = enp3s6
Или аналогично для vlan:
lxc.network.type = vlan
lxc.network.flags = up
lxc.network.link = enp2s0
lxc.network.vlan.id = 603
Добавим интерфейс, через который будем связываться с основной системой (скрипт поднимет интерфейс в основной системе, конфигурацию надо задать зарание):
lxc.network.type = veth
lxc.network.flags = up
lxc.network.veth.pair = route0
lxc.network.name = eth1
lxc.network.hwaddr = 02:b2:30:41:30:25
lxc.network.script.up = /usr/bin/nmcli connection up route0
Добавим автоматический запуск и остановку интерфейсов основной системы, при запуске\остановке контейнера:
lxc.hook.pre-start = /var/lib/lxc/route0/pre-start.sh
lxc.hook.post-stop = /var/lib/lxc/route0/post-stop.sh
/var/lib/lxc/route0/pre-start.sh
#!/bin/sh
/usr/bin/nmcli connection down vlan603 >/dev/null 2>&1
exit 0
/var/lib/lxc/route0/post-stop.sh
#!/bin/sh
/usr/bin/nmcli connection up vlan603 >/dev/null 2>&1
exit 0
Добавим возможность использовать tun\tap интерфейсов:
lxc.hook.autodev = /var/lib/lxc/route0/autodev.sh
/var/lib/lxc/route0/autodev.sh
#!/bin/bash
cd ${LXC_ROOTFS_MOUNT}/dev
mkdir net
mknod net/tun c 10 200
chmod 0666 net/tun
Разрешим управлением некоторыми параметрами маршрутизации (кто знает как сделать более «узкие» разрешения, прошу подсказку):
lxc.mount.auto = proc:rw sys:ro
При необходимости, сделайте контейнер автозапускаемым.
В основной системе нам понадобится профиль для интерфейса route0, который появится при старте контейнера, я предполагаю, что у вас используется NetworkManager.
Что нужно настроить ещё:
- Настроить в основной системе ip адрес интерфейса route0, к примеру: 192.168.20.22/30 и маршрут по умолчанию 192.168.20.21
- В контейнере надо настроить ip адрес интерфейса eth1, к примеру: 192.168.20.21/30 и маршрут к внутренней сети 192.168.21.0/24 via 192.168.20.22
- В контейнере надо настроить «внешний» интерфейс, маскардинг и форвардинг.
- В основной системе нам понадобится скорее всего только форвардинг.
- Все, что должно ходить во внешней сети (включая туннели), размещаем в контейнере.
Теперь мы просто настроим очереди на исходящих потоках интерфейсов, связывающих основную систему с контенером. Теперь, проблемы с определением назначения трафика нет, и мы можем задействовать всю доступную полосу от нашего провайдера (с учетом % на оверхед от туннелей).
P.S. Эпоха «тазиков», которые под Linux раздают интернет стремительно уходит, стоимость аппаратных маршрутизаторов становится все привлекательней, а их мощь и гибкость уже мало чем уступает «большим» братьям, то же Mikrotik решит эту «проблему» без особых заморочек.
Поделиться с друзьями
ValdikSS
А зачем вы применяли LXC? Просто в качестве парсилки конфигурационного файла?
MagicGTS
По сути да, как я и писал, можно все через netns сделать.