В этой заметке будут рассмотрены две дисциплины – бесклассовая pfifo(самая простая дисциплина в Linux) и классовая prio(абсолютная приоритезация). В отличии от pfifo_fast, дисциплина prio имеет возможность параметризации – задание количества очередей(bands) и priomap(маппинг Linux Priority(LP) в очередь), а также (ввиду того, что prio является классовой дисциплиной) позволяет применить какую-либо другую дисциплину к каждой своей очереди, например для различных очередей можно задать разные значения размера буфера(длины очереди), ограничить по полосе или сделать “справедливое”(fair-queue) распределение трафика между потоками(tcp/udp/icmp-flows) внутри очереди.
С помощью tc filter будет продемонстировано каким образом можно поместить трафик(например, по критерию l4protocol=icmp) в определённый класс(в случае prio, номер класса однозначно соответствует номеру очереди). Кроме того, будет показано как можно устанавливать LP(linux priority) или tc class непосредственно из локального приложения (для языков программирования C и PHP(условно недокументированная возможность)).
Для написания статьи использовался дистрибутив Linux Ubuntu 14.04 (ядро 3.13.0-24.46)
Дисциплина pfifo
pfifo это packet FIFO(т.е. простая очередь без приоритезации). При переполнении очереди, вновь поступающие пакеты отбрасываются(tail drop). bfifo – тоже самое, но единица измерения не пакет, а байт.
Для того, чтобы применить pfifo к интерфейсу(с названием eth1) нужно выполнить:
# tc qdisc add dev eth1 root handle 10: pfifo limit 25
Если раньше к интерфейсу уже была применена другая дисциплина(отличная от дисциплины по умолчанию), то нужно её удалить:
# tc qdisc del dev eth1 root
handle 10: это некий номер, ассоциированный с дисциплиной(в данном случае не имеет принципиального значения), зачем он нужен будет показано позже.
Просмотр статистики:
# tc -s qdisc show dev eth1 qdisc pfifo 10: root refcnt 2 limit 25p Sent 79898398 bytes 52779 pkt (dropped 60, overlimits 0 requeues 0) backlog 0b 0p requeues 0
Установив limit 25 это вовсе не означает, что реальный размер исходящей очереди интерфейса будет именно 25 пакетов. В действительности ещё есть исходящий буфер сетевой карты. Его размер(минимальное/максимальное) значение зависит от самой карты. На простеньких картах буфера как бы нет(равен одному-нескольким пакетам). На более продвинутых сетевых картах он есть и регулируется таким образом:
# ethtool -G eth1 tx 20
Просмотр:
# ethtool -g eth1 Ring parameters for eth1: Pre-set maximums: RX: 511 RX Mini: 0 RX Jumbo: 0 TX: 511 Current hardware settings: RX: 200 RX Mini: 0 RX Jumbo: 0 TX: 20
На разных картах минимальный размер аппаратного tx-буфера разный. Например, для BCM5723/HP NC107i(tg3 3.132, fw 5723-v3.35) он равен 18 пакетам, для vmxnet3(1.2.0.0-k-NAPI) – 32 пакета.
Зачем вообще нужна исходящая очередь на интерфейсе? Ответ довольно простой – локальные приложения(особенно, использующие tcp) генерируют сразу несколько(много) пакетов с “бесконечной” скоростью(много больше, чем скорость интерфейса). Поэтому, если буфер отсутствует, то почти весь трафик будет удаляться(кроме первого пакета). Представьте, что вы пришли в магазин, а он одновременно может обслуживать только одного покупателя и в очередь вставать нельзя. Вы не сможете в этом магазине ничего купить, если кто-то уже в него пришёл. Вам придётся прийти в него через какое-то время позже(что соответствует повтору попытки отправки пакета). Если говорить не про локальные приложения, а про маршрутизацию трафику, то ситуация аналогична – входящий интерфейс 10G, исходящие – несколько по 1G. В какой-то момент времени, на исходящем интерфейсе может быть чуть больше, чем 1G и это “чуть больше” можно помещать в буфер(исходящую очередь), если такие всплески кратковременные.
Дисциплина prio
Эта классовая дисциплина очень похожа на бессклассовую pfifo_fast(описание см. здесь). Схема для демонстрации работы этой дисциплины оттуда же:
Для применения дисциплины к интерфейсу нужно:
G ~ # tc qdisc del dev eth2 root G ~ # tc qdisc add dev eth2 root handle 10: prio bands 4 priomap 3 3 2 2 1 1 0 0 3 3 3 3 3 3 3 3
В этом случае будут созданы 4 очереди и 4 класса, однозначно соответствующие друг другу. Очередь 0 – класс 10:1, очередь 1 – класс 10:2 и т.д. (значение “префикса” “10:” было задано при назначении дисциплины на интерфейс(handle 10: ), :1, :2, :3, :4 – присвоены автоматически(исходя из значения band – количества очередей))
Чтобы посмотреть эти классы(и статистику по ним):
G ~ # tc -s class show dev eth2 class prio 10:1 parent 10: Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0) backlog 0b 0p requeues 0 class prio 10:2 parent 10: Sent 7996583508 bytes 5281818 pkt (dropped 0, overlimits 0 requeues 0) backlog 46934b 31p requeues 0 class prio 10:3 parent 10: Sent 0 bytes 0 pkt (dropped 42, overlimits 0 requeues 0) backlog 9800b 100p requeues 0 class prio 10:4 parent 10: Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0) backlog 0b 0p requeues 0
Длина каждой очереди – параметр txqueuelen интерфейса(см. ifconfig/ip). В данном примере было запущено 2 потока трафика, один с LP=4(iperf tcp), другой с LP=2(icmp ping). Ранее было показано как установить LP с помощью TOS или iptables и с помощью net_prio cgroup.
В этом примере видно, что iperf-трафик попал в очередь №1(class 10:2) и не даёт пройти ни одному пингу из очереди №2(class 10:3).
В действительности, tc class и LP это одно и тоже. Для LP=X class обозначается как “0:X”, а в самом деле это просто 32битное число 0x0000000X. Если класс трафика не соответствует ни одному классу из имеющихся на интерфейсе, то проверяются младшие 4 бита(LP), сопоставляются очереди(классу) с помощью priomap(если значение класса(в численном представлении) меньше 16). Если значение класса > 16 и не соответствует ни одному классу из имеющихся, то значение LP=0 и очередь(класс) назначается в соответствии с priomap(в данном примере это будет класс 10:4(или 3я очередь)).
Для того, чтобы явно классифицировать трафик можно использовать tc filter следующим образом:
tc filter add dev eth2 parent 10: protocol ip \ prio 20 u32 match \ ip protocol 1 0xff \ flowid 10:1
Рассказывать подробно про u32 особо не имеет смысла. Существуют сотни HOWTO на тему как классифицировать трафик с помощью u32. Вкратце, происходит сравнение поля Protocol IP-заголовка(т.е. L4Protocol) на точное совпадение(маска 0xff) со значением ‘1’, где 1 это номер протокола icmp(см. /etc/protocols) и в случае совпадения, трафику назначается класс 10:1(после чего он попадает в нулевую(самую приоритетную очередь)). prio 20(строка 2) это приоритет правила(к дисциплине prio не имеет отношения).
G ~ # ping 192.0.2.1 -c 3 G ~ # tc -s class show dev eth2 class prio 10:1 parent 10: Sent 294 bytes 3 pkt (dropped 0, overlimits 0 requeues 0) backlog 0b 0p requeues 0 ....
Кроме u32 существуют и другие классификаторы, например bpf (см. commit 7d1d65c (содержащий пример использования))
Просмотр установленных фильтров:
G ~ # tc filter show dev eth2 filter parent 10: protocol ip pref 20 u32 filter parent 10: protocol ip pref 20 u32 fh 800: ht divisor 1 filter parent 10: protocol ip pref 20 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 10:1 match 00010000/00ff0000 at 8
В завершении рассказа о prio, стоит показать как применять дисциплины к классам:
G ~ # tc qdisc add dev eth2 parent 10:1 handle 100: sfq G ~ # tc qdisc add dev eth2 parent 10:2 handle 200: pfifo limit 300 G ~ # tc qdisc add dev eth2 parent 10:4 handle 400: tbf rate 10mbit burst 200k latency 50ms G ~ # tc qdisc show qdisc prio 10: dev eth2 root refcnt 2 bands 4 priomap 3 3 2 2 1 1 0 0 3 3 3 3 3 3 3 3 qdisc sfq 100: dev eth2 parent 10:1 limit 127p quantum 1514b depth 127 divisor 1024 qdisc pfifo 200: dev eth2 parent 10:2 limit 300p qdisc tbf 400: dev eth2 parent 10:4 rate 10000Kbit burst 200Kb lat 50.0ms
Для класса 10:1 установлена дисциплина sfq(“честное” разделение полосы по потокам трафика), для 10:2 – pfifo с лимитом в 300 пакетов, класс 10:4 ограничен по скорости 10-ю Мб/с.
SO_PRIORITY socket
Даже если вы не занимаетесь программированием, то стоит прочитать этот раздел статьи, например, для того, чтобы дать грамотное тех. задание разработчикам и/или знать потенциальную угрозу от различных приложений/скриптов/прочего, запускаемого на сервере.
Устанавливать класс трафик можно так(язык программирования C):
#include <sys/socket.h> #include <netinet/in.h> #include <string.h> int main() { int sockfd; struct sockaddr_in remote_host; bzero(&remote_host, sizeof(remote_host)); char *msg="hello"; sockfd=socket(AF_INET, SOCK_DGRAM, 0); int so_priority = 0x00100001; /* 10:1 (0010:0001) */ setsockopt(sockfd, SOL_SOCKET, SO_PRIORITY, &so_priority, sizeof(so_priority)); remote_host.sin_family = AF_INET; remote_host.sin_addr.s_addr=inet_addr("192.0.2.1"); remote_host.sin_port=htons(1234); sendto(sockfd, msg, strlen(msg), 0, (struct sockaddr *)&remote_host, sizeof(remote_host)); }
Эта программа отсылает один UDP-пакет на IP-адрес 192.0.2.1 с установленным классом трафика 10:1 (0x00100001) путём задания параметра SO_PRIORITY для сокета.
# gcc tc-class.c -o tc-class G ~ # ./tc-class ; ./tc-class (с правами root) G ~ # tc -s class show dev eth2 class prio 10:1 parent 10: Sent 94 bytes 2 pkt (dropped 0, overlimits 0 requeues 0) backlog 0b 0p requeues 0 ...
(чтобы сбросить статистику по трафику, можно удалить и заново применить дисциплину к интерфейсу).
Без прав root, значение class можно устанавливать меньше 7 (0:0-0:6). Чтобы не писать огромное количество кода и не загромождать статью C-шными велосипедами(ради парсинга командной строки и сообщений об ошибках) перепишем этот код на PHP:
#!/usr/bin/php <?php $msg = "hello"; $so_priority = isset($argv[1]) ? intval($argv[1],0): 0; $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); socket_set_option($sock, SOL_SOCKET, 12, $so_priority ); socket_sendto($sock, $msg, strlen($msg), 0, '192.0.2.1', 1234); ?>
Отличия от кода на C – из командной строки принимается 1ый параметр и интерпретируется как класс трафика, выводятся сообщения об ошибках(настраивается через конфигурационный файл php.ini, по умолчанию включено).
В самом деле, PHP-функция socket_set_option это обёртка над C-функцией setsockopt, однако в документации PHP нет упоминаний SO_PRIORITY. Числовая константа ’12’ в 7ой строке это и есть SO_PRIORITY(значение позаимствовано из header-файла socket.h)
G ~ # ./tc-class.php 0x00100001 (с правами root) G ~ # su user1 -c "./tc-class.php 0x00100001" (с правами user1) PHP Warning: socket_set_option(): unable to set socket option [1]: Operation not permitted in /home/sergey/so_prio/tc-class.php on line 7 G ~ # su user1 -c "./tc-class.php 0x00000006" G ~ # su user1 -c "./tc-class.php 0x00000007" PHP Warning: socket_set_option(): unable to set socket option [1]: Operation not permitted in /home/sergey/so_prio/tc-class.php on line 7
Результат – с привилегиями пользователя можно установить LP=6 для трафика. В случае priomap по умолчанию(при использовании pfifo_fast или prio без изменения priomap), такой трафик попадёт в самую приоритетную очередь.
Для того, чтобы установить значения SO_PRIORITY больше 6 с правами пользователя, необходимо установить linux capability CAP_NET_ADMIN.
Pingback: QoS в Linux: tbf (token bucket filter) | Net-Labs.in