QoS в Linux: pfifo, prio, tc filter, SO_PRIORITY socket

В этой заметке будут рассмотрены две дисциплины – бесклассовая 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(описание см. здесь). Схема для демонстрации работы этой дисциплины оттуда же:

Linux QoS prio scheme

Для применения дисциплины к интерфейсу нужно:

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.

Advertisements

One thought on “QoS в Linux: pfifo, prio, tc filter, SO_PRIORITY socket

  1. Pingback: QoS в Linux: tbf (token bucket filter) | Net-Labs.in

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