QoS в Linux: tbf (token bucket filter)

TBF является классовой дисциплиной, предназначенной для шейпинга(shaping) трафика, т.е. подразумевается наличие буфера пакетов, в отличии от полисинга(policing). В Linux дисциплина tbf имеет следующие ключевые возможности: ограничение средней и максимальной скоростей, возможность задания другой дисциплины для управления очередью(буфером пакетов), что позволяет распределить трафик внутри заданной полосы, например с помощью абсолютной приоритезации или честного(fair) распределения потоков.

В самом простейшем случае, алгоритм работы tbf можно представить таким образом:

tbf token bucket

Ограничение скорости работает следующим образом: ведро наполняется маркерами с заданной скоростью(например, 40Мбит/с). Пакеты, которые передаются алготриму tbf помещаются в очередь(на рисунке обозначена как “очередь пакетов”). В случае, если в ведре есть необходимое количество маркеров(байт) для отправки пакетов(т.е. не меньше, чем размер пакета), то этому пакету разрешается быть отправленным. Если необходимого количества маркеров нет, то он(пакет) ожидает в очереди их появления, при этом другие пакеты, желающие быть отправленными встают в очередь за ним(fifo). Если очередь переполняется(из-за того, что пакеты поступают быстрее, чем маркеры), то новые пакеты отбрасываются(tail drop), тем самым происходит “жёсткое” ограничение скорости. “Мягкое” ограничение происходит за счёт помещения в очередь и ожидания появления маркеров, разрешающих отправку. В силу свойств tcp такой способ ограничения(путём задержки пакетов очереди) хорошо работает. В идеальном случае(который легко продемонстрировать на практике), скорость ограничивается, при этом дропов(удаления трафика) нет, трафик просто немного задерживается.

tbf является, пожалуй, самой удобной дисциплиной для ограничения скорости абоненту, если абонент представлен на маршрутизаторе(далее – BRAS) в виде отдельного интерфейса – это ppp-интерфейс в случае pppoe/l2tp/pptp, tun/tap в случае openvpn, dot1Q/QinQ-сабынтерфейс для IPoE vlan-per-user или ipoeX(модуль ядра ipoe accel-pppd) для IPoE L2 shared или L3 connected.

Как и в предыдущих заметках, используется дистрибутив Linux Ubuntu 14.04(ядро 3.13.0-24.46). Для демонстрации работы tbf будет использована следующая схема:

tbf token bucket testing scheme

netns C – клиент, netns G – bras. Базовая конфигурация осуществляется следующим образом:

# ip netns add G //создание netns G
# ip netns add C //создание netns C
# ip link add vethG type veth peer name vethC //создание двух виртуальных eth-интерфейсов и линка между ними
# ip link set dev vethG netns G //привязка интерфейса vethG к netns G
# ip link set dev vethC netns C //привязка интерфейса vethC к netns C
# hostname C  ; ip netns exec C bash //(в терминале 1)
# hostname G  ; ip netns exec G bash //(в терминале 2)
G:~# ifconfig vethG 192.0.2.1/30 up //задание IP для vethG
C:~# ifconfig vethC 192.0.2.2/30 up //задание IP для vethC

Для ограничения скорости от оператора к абонента(для абонента это скорость скачивания) применим дисциплину tbf:

G:~# tc qdisc add dev vethG root tbf latency 100ms burst 16K rate 40mbit

У tbf есть 3 обязательных параметра – rate(с этим вопросов нет), burst и limit или latency. Limit и latency, в соответствии с q_tbf.c связаны между собой следующим соотношением:

Limit = max{rate*latency + burst, 
            peakrate*latency + minburst}

Что такое peakrate и minburst будет сказано позже, но пока считаем что они не заданы. Такая формула вычисления limit фактически означает, что реальная максимальная задержка(когда ведро постоянно опустошается) увеличивается на burst/rate. Кроме того, путём задания latency>0 нельзя задать limit<burst. А в случае задания peakrate много больше, чем rate, получим реальный latency много больше заданного. Для того, чтобы посмотреть limit нужно выполнить tc с опцией '-r'(raw) (tc -r qdisc show)

Возникает вопрос – зачем вообще нужно ведро(точнее, зачем его объём больше одного MTU) и какой размер очереди нужно устанавливать. Burst задаёт объём трафика, который может быть отправлен со скоростью интерфейса. К чему приводит выкручивание burst-а можно почитать в статье Burst vs буфер коммутации. Для низких скоростей(например 256Кбит/с) задав burst=256K (Кбайт), абонент сможет скачать первые 256Кбайт на скорости интерфейса, что составляло бы 8 секунд при скорости 256Кбит/с. Во времена, когда совокупные размеры страниц сайта исчилялись килобайтами, такой трюк позволял обеспечить комфортный веб-сёрфинг при условии, что физическая скорость много больше тарифной(например adsl на 8Мбит/с или eth на 100Мбит/с). В случае с dsl на 8Мбит/c, 256Кбайт скачается примерно за 0.3 секунды, а не за 8 секунд по тарифной скорости, т.е. сайты открывались практически мгновенно. После исчерпания ведра маркеров(burst-а), скорость установится на уровне rate, поэтому эту скорость иногда называют средней(average).

Однако, если физическая скорость низкая(dial-up, gprs-соединение, прочие виды низкоскоростной связи), то большой burst ничем не поможет, только наоборот, может ухудшить ситуацию.

Даже в современных реалиях, когда 100Мбит/с интернета дома это уже обыденность, до сих пор на Земле остаются места где нет нормального алпинка и в его качестве используются такие ужасные вещи как спутниковый интернет и низкоскоростные РРЛ. Если вам придётся раздавать интернет в таких местах, то линии к абонентам у вас будут, скорее всего, быстрее аплинка, однако скоростные ограничения(ввиду низкой скорости аплинка) придётся вводить довольно суровые. В таких условиях есть смысл поиграться с burst(и размером очереди, но об этом чуть позже). Для того, чтобы не нарваться на проблемы, описанные в статье “Burst vs буфер коммутации” нужно установить параметр peakrate равный(чуть меньше) физической скорости подключения абонента. При задании peakrate запускается ещё одна корзина маркеров, работающая по аналогичному принципу. Кроме peakrate нужно задать ещё и minburst – это размер второй корзины маркеров. Его значение должно быть не меньше MTU, но и не больше, чем буферы коммутации на транзитных узлах – от bras до оборудования доступа. Это значение можно установить, например в 8Кбайт. Итого, для низкоскоростного тарифа на хорошей adsl-ной линии(8Мбит/с) получим:

# tc qdisc add dev vethG root tbf limit 64K \
burst 256K rate 256kbit \
peakrate 8mbit minburst 8K
C:~# iperf -s  -i 1 //приём трафика
[  4] local 192.0.2.2 port 5001 connected with 192.0.2.1 port 54549
[ ID] Interval       Transfer     Bandwidth
[  4]  0.0- 1.0 sec   273 KBytes  2.24 Mbits/sec
[  4]  1.0- 2.0 sec  31.1 KBytes   255 Kbits/sec
[  4]  2.0- 3.0 sec  28.3 KBytes   232 Kbits/sec
[  4]  3.0- 4.0 sec  31.1 KBytes   255 Kbits/sec
...
G:~# iperf -c 192.0.2.2 -t 1000 //отправка трафика

Из вывода iperf-а видно, сначала трафик отправлялся быстро, затем на скорости 256Кбит/с. Откуда взялась цифра 2.24Мбит/с? Дело в том, что со скоростью ~8Мбит/с(peakrate) было отправлено лишь первые 256Кбайт(размер burst), а затем трафик отправлялся на скорости 256Кбит/с, поэтому за 1 секунду успело произойти усреднение скорости. И если говорить про adsl, то нужно помнить, что из 8Мбит/с прилично съедает ATM-инкапсуляция ethernet-фреймов, а ещё физическая скорость у разных абонентов может быть разная, но обсуждать dsl и тем более ATM сейчас неуместно и мало кому интересно. Нужно просто знать, что этот нюанс существует.

Относительно размера limit/latency, здесь существуют следующие соображения. Отправной точкой может служить размер tcp-окна и когда-то давно его максимальный размер был 64Кбайта. Но ввиду RFC1323(1992 год) сейчас размер tcp-окна может быть очень большим. Кроме того, в действительности, tcp чаще всего подстраивается под характеристики канала передачи данных.
Второе соображение, исходя из которого можно определять размер буфера пакетов это таймауты приложений, ожидающих трафик. Например, трафик, который ожидал в буфере 10 секунд, уже может быть никому не интересен. Третий фактор – размер памяти на BRAS тоже не бесконечен и делать limit в размере 1s*rate для тарифа 100Мбит/с это значит, что абонент может занимать до ~12.5Мбайт памяти. Для большинства задач, размер буфера можно задавать примерно так(в основном, исходя из критерия минимизации дропов): для скоростей >10Мбит/с limit=50-200ms*rate(чем больше скорость, тем меньше задержку можно задавать), для низкоскоростных подключений(<1Мбит/с) limit=500-2000ms*rate(или даже больше, если скорости совсем низкие). Это очень приблизительные ориентиры.

В самом деле, существует ещё один метод подбора limit/latency(и burst) – подгон параметров под speedtest.net. Способ конечно плохой, но имеет право на существование.

И ещё одно замечание относительно burst. В “старых”(not-tickless) ядрах минимально необходимый burst нужно было высчитывать исходя из CONFIG_HZ. Об этом можно прочесть в man 8 tc-tbf.

tbf inner qdisc

Не смотря на то, что ещё с 2003 года tbf является классовой дисциплиной, многие HOWTO, которые вы найдёте будут утверждать, что она является бесклассовой. В действительности, при применении tbf, создаётся один класс(с заранее заданным номером :1), к которому можно применить другую дисциплину с целью того, чтобы вместо обычного fifo для управления очередью использовать более сложные алгоритмы, например prio или sfq. Но возникает вопрос – зачем оно нужно и чем же плох используемый по умолчанию bfifo?

Запустим два потока трафика через tbf:

G:~# tc -r qdisc show dev vethG
qdisc tbf 8022: root refcnt 2 rate 256000bit burst 256Kb [07a12000] peakrate 8000Kbit minburst 8Kb [0001f400] limit 64Kb lat 57.3ms
G:~# iperf -c 192.0.2.2 -t 1000 &
G:~# ping 192.0.2.2 -c 3
PING 192.0.2.2 (192.0.2.2) 56(84) bytes of data.
64 bytes from 192.0.2.2: icmp_seq=1 ttl=64 time=1877 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=1819 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=64 time=1958 ms

Как видно, в момент времени, когда абонента что-то качает(это эмулируется iperf-ом), задержки для icmp-потока стали порядка 2 секунд(что соответствует limit/rate(64КБайт/32Кбит/с = 2 секунды), это также отразится на веб-сёрфинге, реал-тайм приложениях(например, непошаговые игры). При том в случае, если два приложения используют по одному tcp-потоку, то они ещё как-то будут делить полосу, а если же одно приложение агрессивное(много tcp/udp-потоков, например torrent), то в этом случае все остальные будут получить трафик с солидной задержкой.

Для решения описанной проблемы, у tbf можно заменить bfifo на дисциплину, которая распределяет трафик на несколько очередей, тем самым можно разделить полосу “справедливо” между приложениями или же попытаться задать низкий приоритет мусорному трафику(p2p и прочее). В самом деле, в (ванильном)ядре Linux нет полноценных L7-классификаторов, поэтому выделить p2p и прочий bulk-трафик крайне затруднительно. Кроме того, не совсем ясно как отличить скачивание большого файла по http от просмотра видеоролика по http. Если понизить приоритет скачивания можно, то случай с видеороликом можно считать, наоборот, приоритетной задачей. Так или иначе, существует, как минимум, 3 подхода к решения этой задачи – замена bfifo на prio и выделение “хорошего” трафика в более приоритетные очереди, использование дисциплины sfq(автоматическое разбиение трафика на потоки, исходя из хеш-функции от L4-информации пакета). 3 вариант – комбинация prio+sfq (внутри prio очереди(ей) использовать sfq). Начнём с sfq:

G:~# tc qdisc add dev vethG root handle 10: tbf limit 64K burst 256K rate 256kbit peakrate 8mbit minburst 8K
G:~# tc qdisc add dev vethG parent 10:1 handle 20: sfq
G:~# iperf -c 192.0.2.2 -t 1000 &                                                                          
G:~# ping 192.0.2.2 -c 3
PING 192.0.2.2 (192.0.2.2) 56(84) bytes of data.
64 bytes from 192.0.2.2: icmp_seq=1 ttl=64 time=67.6 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=15.2 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=64 time=57.8 ms

Это куда лучше, чем задержка в ~2 секунды.

При замене inner qdisc на другую, параметры limit/latency дисциплины tbf перестают иметь значение. Управление очередью передаётся другой дисциплине, поэтому размер очереди(ей) нужно устанавливать именно в них.

Вариант с prio:

G:~# tc qdisc add dev vethG root handle 10: tbf limit 64K burst 256K rate 256kbit peakrate 8mbit minburst 8K
G:~# tc qdisc add dev vethG parent 10:1 handle 20: prio
G:~# iperf -c 192.0.2.2 -t 1000 &                                                                          
G:~# iptables -t mangle -I POSTROUTING -p icmp -j CLASSIFY --set-class 0:6 // трафик с LP=6 попадёт в самую приоритетную очередь prio
G:~# ping 192.0.2.2 -c 4
PING 192.0.2.2 (192.0.2.2) 56(84) bytes of data.
64 bytes from 192.0.2.2: icmp_seq=1 ttl=64 time=0.042 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=0.039 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=64 time=0.032 ms
64 bytes from 192.0.2.2: icmp_seq=4 ttl=64 time=0.040 ms

Можно также посмотреть статистику по классам prio и убедиться что эти 4 пакета прошли в нужную очередь:

G:~# tc -s class show dev vethG
class tbf 10:1 parent 10: leaf 20: 

class prio 20:1 parent 20: 
 Sent 392 bytes 4 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 
class prio 20:2 parent 20: 
 Sent 6108064 bytes 4048 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 105980b 35p requeues 0 
class prio 20:3 parent 20: 
 Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0

Для распределения трафика по потокам для prio очередей 0 и 1 (т.е классы 20:1 и 20:2), можно применить к ним sfq:

G:~# tc qdisc add dev vethG parent 20:1 handle 30: sfq
G:~# tc qdisc add dev vethG parent 20:2 handle 31: sfq

Такая конфигурация позволяет использовать абсолютную приоритезацию(например, для реал-тайм приложений), а если приоритет потоков трафика одинаковый, то делить полосу между ними честно(если не случится коллизии хеш-функции потоков sfq(см. man 8 tc-sfq)).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s