QoS в Linux: поведение по умолчанию: pfifo_fast, mq

Этой заметкой начинается цикл статей на русском языке о QoS в Linux с актуальностью на момент времени 2014Q2. Базовой user space утилитой для управления трафиком является tc(traffic control), документация к которой устарела и содержит неточности. Различные HOWTO в большинстве своём датируются началом 2000-ых годов. Единственным достоверным источником о том как оно работает являются исходники ядра. Предполагается, что читатель владеет такими понятиями как классификация, маркировка, планирование(scheduling).

В качестве linux-дистрибутива будет использоваться Ubuntu 14.04(ядро 3.13.0-24-generic, но в один момент будет заменено на 3.14.1-031401-generic)

В Linux управление трафиком сосредоточено на исходящих интерфейсах, что вполне логично, поскольку напрямую мы не можем влиять на то, сколько трафика приходит на интерфейс; управление входящим трафиком, вообще говоря, может быть осуществлено только косвенно. По отношению к входящему(ingress) трафику тоже можно применять некоторые действия(в основном, policing, т.е. тупое удаление лишнего трафика), но в данной заметке это рассмотрено не будет. В действительности, для применения более сложных методов(чем простое удаление лишнего трафика) по отношению к входящему трафику существует два подхода – использование виртуальных интерфейсов ifb(с перенаправлением входящего трафика на ifb, а с ifb уже на исходящий интерфейс) или управление трафиком на исходящем интерфейсе(второй способ неудобен тем, что может быть два активных исходящих интерфейса и тем, что требуется классификация входящего трафика(в случае ISP, это уметь классифицировать абонента)). Таким образом, достаточно научиться управлять исходящим(egress) трафиком.

По умолчанию в Linux на интерфейсах используется дисциплина, отличная от простого FIFO, т.е. можно сказать что QoS настроен по умолчанию. Для того, чтобы узнать как именно, посмотрим дисциплины, которые применены к интерфейсам:

# tc qdisc show
qdisc mq 0: dev eth0 root 
qdisc pfifo_fast 0: dev eth1 root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth2 root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

Как видно из вывода команды, к eth0 применена дисциплина mq(строка 2), а к eth1 и eth2 pfifo_fast(строки 3 и 4). До 2009 года по умолчанию к интерфейсам назначалась дисциплина pfifo_fast, однако в результате commit 6ec1c69 дисциплина по умолчанию для сетевых карт с несколькими очередями(multiqueue devices) – mq, для остальных pfifo_fast.

pfifo_fast

Это бесклассовая дисциплина(т.е. не имеет ветвлений(классов), на которые можно было бы навешивать другие дисциплины), осуществляющая абсолютную приоритезацию(буква “p” в названии означает priority) среди 3ёх очередей(с номерами 0,1 и 2). Очередь номер 0 самая приоритетная. Только если в ней нет пакетов, то обрабатывается очередь №1, а если нет пакетов в обеих очередях, то обрабатывается очередь №2. При появлении пакетов в более приоритетной очереди, переход осуществляется к ней. Код дисциплины находится в sch_generic.c. Единственный параметр, влияющий на работу этой дисциплины является txqueuelen (конфигурируемый утилитой ifconfig/ip), это длина очереди.

Собственно, отстаётся единственный вопрос – каким образом трафик попадает в ту или иную очередь и ответ на него – priomap. Priomap состоит из 16 значений, т.е. индекс priomap изменяется от 0 до 15, а самим значением является номер очереди в которую попадёт пакет. Индекс priomap-а это внутренний приоритет пакета в ядре linux(skb->priority), который может быть задан явно для трафика от локальных приложений(man 7 socket, см. SO_PRIORITY), с использованием механизма net_prio cgroups(пример) или же с помощью задания поля TOS в заголоке IP. В ядре linux автоматически осуществляется маппинг значений TOS в значение внутреннего приоритета.

Ввиду ряда исторических особенностей, связанных с тем, что IP precedence, TOS bits и DSCP используют один и тот же байт, маппинг TOS->internal priority, на сегодняшний день, выглядет как минимум странно, кроме того, после commit 4a2b9c3(2011 год), в документации(man 8 tc-prio(git)), содержащей таблицу маппинга TOS field в internal priority(и, соответственно, номер очереди) теперь есть ошибка, а именно в строке:

TOS  Bits  Means                   Linux Priority  Band
-------------------------------------------------------
0x2  1     Minimize Monetary Cost  1 Filler        2

В самом деле для TOS=0x2(00000010) значение linux priority(internal priority) будет 0(Best Effort) и, в соответствии с priomap, номер очереди(band) 1, а не 2.
Для того, чтобы получить реальную картину по маппингам, выполним следующий код в пространстве ядра(kernel space):

static int __init priovals_init(void) {
  int i, j, tos;
  u8 prio, dscp;
  printk("TOS TOSh DS  DSh P| TOS TOSh DS  DSh P| ");
  printk("TOS TOSh DS  DSh P| TOS TOSh DS  DSh P\n");
  for(i=0; i<64; i+=4) {
    for(j=0; j<=3; j++) {
      tos = i+j*64;
      prio = rt_tos2priority((u8)tos);
      dscp = ((u8)tos)>>2;
      printk("%3d 0x%02x%3d 0x%02x %u| ",tos,tos,dscp,dscp, prio);
    }
    printk("\n");
  }
  return 0;
}

На выходе получаем таблицу:

# insmod priovals.ko
# dmesg --notime | tail -n 17

TOS TOSh DS  DSh P| TOS TOSh DS  DSh P| TOS TOSh DS  DSh P| TOS TOSh DS  DSh P
  0 0x00  0 0x00 0|  64 0x40 16 0x10 0| 128 0x80 32 0x20 0| 192 0xc0 48 0x30 0
  4 0x04  1 0x01 0|  68 0x44 17 0x11 0| 132 0x84 33 0x21 0| 196 0xc4 49 0x31 0
  8 0x08  2 0x02 2|  72 0x48 18 0x12 2| 136 0x88 34 0x22 2| 200 0xc8 50 0x32 2
 12 0x0c  3 0x03 2|  76 0x4c 19 0x13 2| 140 0x8c 35 0x23 2| 204 0xcc 51 0x33 2
 16 0x10  4 0x04 6|  80 0x50 20 0x14 6| 144 0x90 36 0x24 6| 208 0xd0 52 0x34 6
 20 0x14  5 0x05 6|  84 0x54 21 0x15 6| 148 0x94 37 0x25 6| 212 0xd4 53 0x35 6
 24 0x18  6 0x06 4|  88 0x58 22 0x16 4| 152 0x98 38 0x26 4| 216 0xd8 54 0x36 4
 28 0x1c  7 0x07 4|  92 0x5c 23 0x17 4| 156 0x9c 39 0x27 4| 220 0xdc 55 0x37 4
 32 0x20  8 0x08 0|  96 0x60 24 0x18 0| 160 0xa0 40 0x28 0| 224 0xe0 56 0x38 0
 36 0x24  9 0x09 0| 100 0x64 25 0x19 0| 164 0xa4 41 0x29 0| 228 0xe4 57 0x39 0
 40 0x28 10 0x0a 2| 104 0x68 26 0x1a 2| 168 0xa8 42 0x2a 2| 232 0xe8 58 0x3a 2
 44 0x2c 11 0x0b 2| 108 0x6c 27 0x1b 2| 172 0xac 43 0x2b 2| 236 0xec 59 0x3b 2
 48 0x30 12 0x0c 6| 112 0x70 28 0x1c 6| 176 0xb0 44 0x2c 6| 240 0xf0 60 0x3c 6
 52 0x34 13 0x0d 6| 116 0x74 29 0x1d 6| 180 0xb4 45 0x2d 6| 244 0xf4 61 0x3d 6
 56 0x38 14 0x0e 4| 120 0x78 30 0x1e 4| 184 0xb8 46 0x2e 4| 248 0xf8 62 0x3e 4
 60 0x3c 15 0x0f 4| 124 0x7c 31 0x1f 4| 188 0xbc 47 0x2f 4| 252 0xfc 63 0x3f 4

Допустим, имеется два потока, один с DSCP=46 (класс обслуживания EF) и второй с DSCP=36 (AF42). В соответствии с этой таблицей, для DSCP=46 linux priority(P)=4, что соответствует band=1(4ый элемент в priomap, нумерация элементов с 0), для DSCP=36 P=6 и band=0, т.е. в случае с pfifo_fast, трафик с DSCP=AF42 будет иметь абсолютный приоритет по сравнению с потоком DSCP=EF. В реальной жизни специально так никто не настраивает QoS.

К сожаленью, использовать ESXi или veth-интерфейсы между netns для полноценной демонстрации QoS не получится(потому что скорость на виртуальных картах vmware и linux veth не ограничена(упирается в CPU)), поэтому придётся воспользоваться реальным паткордом и двумя сетевыми картами:

Linux QoS pfifo_fast scheme

В данном случае важно, что eth2 – это очень простенькая сетевая карта, не имеющая аппаратного tx-буфера(точнее, имеющего, но равного примерно одному-нескольким пакетам)

# lspci -vv -s 03:01
03:01.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8139/8139C/8139C+ (rev 10)
	Subsystem: Compex FN22-3(A) LinxPRO Ethernet Adapter
...
	Kernel driver in use: 8139too

Если у вас сетевая карточка поддерживает задание размера аппаратного tx-буфера(чтобы проверить, нужно выполнить ethtool -g eth2), то для изучения QoS в ядре Linux лучше минимизировать его размер(ethtool -G eth2 tx 3).

Теперь настроим интерфейсы и запустим iperf от eth2 к eth1 c DSCP=EF(46) (TOS=184), и 3 icmp-потока от eth2 к eth1 с:
– DSCP=0(TOS=0)
– DSCP=AF42(36)(TOS=144)
– DSCP=AF31(26)(TOS=104)

host ~ # ip netns add G ; ip netns add C
host ~ # ip link set eth1 netns C ; ip link set eth2 netns G
host ~ # hostname G ; ip netns exec G bash (терминал 1)
host ~ # hostname C ; ip netns exec C bash (терминал 2)
C ~ # ifconfig eth1 192.0.2.1/30 up
C ~ # iperf -s -i 1 (приёмник tcp-трафика)
G ~ # ifconfig eth2 192.0.2.2/30 txqueuelen 100 up
G ~ # iperf -c 192.0.2.1 -t 1000 --tos 184 &

Смотрим в терминал 2, убеждаемся, что на сервер приходит трафик со скоростью ~95Мбит/с. Запускаем пинги:

G ~ # ping -Q 0 192.0.2.1 -c 3
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.
64 bytes from 192.0.2.1: icmp_req=1 ttl=64 time=4.42 ms
64 bytes from 192.0.2.1: icmp_req=2 ttl=64 time=4.35 ms
64 bytes from 192.0.2.1: icmp_req=3 ttl=64 time=4.33 ms
--- 192.0.2.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 4.338/4.371/4.421/0.084 ms

G ~ # ping -Q 144 192.0.2.1 -c 3
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.
64 bytes from 192.0.2.1: icmp_req=1 ttl=64 time=0.635 ms
64 bytes from 192.0.2.1: icmp_req=2 ttl=64 time=0.600 ms
64 bytes from 192.0.2.1: icmp_req=3 ttl=64 time=0.550 ms
--- 192.0.2.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 0.550/0.595/0.635/0.034 ms

G ~ # ping -Q 104 192.0.2.1 -c 3
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.
--- 192.0.2.1 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 1999ms

Результат объясняется довольно просто. TOS=0 попадает в band=1(очередь 1) и имеет “равные права” с трафиком, генерируемым iperf-ом(потому что TOS=184 тоже попадает в band=1), для TOS=144 band=0 и поэтому этот трафик приоритетнее iperf-а и идёт вперёд него. Для TOS=104 band=2 и он всё время пропускает трафик iperf-а, а поскольку iperf постоянно генерирует трафик и всё время пакеты от него есть в очереди, то все ping-и с TOS=104 так и не ушли с интерфейса(застряли в очереди).

Для просмотра статистики используется опция -s:

G ~ # tc -s qdisc show dev eth2
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 12916472968 bytes 8531424 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 46934b 31p requeues 0

В этом эксперименте размер очереди на отправку пропорционален размеру окна tcp. Как видно из этой статистики, tcp-поток занимает в буфере в каждый момент времени примерно 30 пакетов. Запустим ещё 3 iperf-а(чтобы выйти за установленный лимит txqueuelen=100) с таким же значением TOS и убедимся, что буфер начнёт переполняться и пойдут дропы:

# tc -s qdisc show dev eth2
qdisc pfifo_fast 0: root refcnt 2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 25473723488 bytes 16825556 pkt (dropped 161, overlimits 0 requeues 0) 
 backlog 145344b 96p requeues 0

backlog будет изменяться от ~80 до 100 пакетов(в этом можно убедиться, несколько раз выполнив указанную команду)

В реальной жизни, на хороших сетевых картах имеется tx-буфер, его максимальный размер варьируется от ~256 до 8К пакетов(или ещё больше), который является как бы “продолжением” программного, поэтому 31 пакет, которые были видны в программном буфере запросто влезут в аппаратный целиком(после чего ядро считает, что трафик отправлен), а за ними туда же пойдут пакеты из более низкоприоритетных программных очередей, поэтому эффекта с 100% “потерей” пакетов из низкоприоритетной очереди можно и не добиться. Однако, не смотря на это, дисциплина pfifo_fast в ряде случаев является потенциальной возможностью для DOS-атаки от локальных приложений или со стороны клиентов(если сервер является маршрутизатором) путём генерации трафика со значениями TOS, такими, чтобы трафик попадал в band=0. Например, p2p-приложение ktorrent умеет выставлять DSCP(Network->Advanced->DSCP value for IP packets), в сочетании с uTP(работающего поверх udp), может возникнуть большой поток высокоприоритетного трафика, блокирующего нормальную работу других приложений/пользователей. Точно так же можно использовать pfifo_fast для приоритезации “хорошего” трафика(например, control, rtp и web) над “плохим”(p2p).

Если локальное приложение не умеет устанавливать internal priority или TOS, то локальную приоритезацию трафика можно осуществить с помощью iptables:

# iptables -t mangle -I OUTPUT -p icmp -j CLASSIFY --set-class 0:6

Такое правило установит skb->priority=6 для исходящего icmp-трафика. В соответствии с priomap, band будет равен 0 для такого трафика и он станет самым приоритетным.

На этом завершается описание дисциплины pfifo_fast. За всеми подробностями можно обратиться к исходному коду ядра. Существует 2 похожих дисциплины, но с большими возможностями – prio(классовое обобщение pfifo_fast(что даёт возможность, например, ограничивать каждую очередь по полосе) и возможностью задания количества очередей и priomap), а также mqprio – дисциплина, позволяющая использовать аппаратный QoS сетевой карты, т.е. очереди являются не программными, а аппаратными, а извлечением пакетов из этих очередей осуществляет сама сетевая карта по заданным или заранее определённым правилам, которые устанавливаются с помощью user space утилит lldpad/ethtool. Подробнее см. man 8 tc-mqprio (git)

Дисциплина mq

В начале заметки был вывод tc qdisc show, в котором видно, что к устройству eth0 применена дисциплина mq. Это классовая дисциплина, в которой каждый класс привязан к аппаратной tx-очереди. Однако в выводе tc нет внутренней структуры, это связано с особенностью, поведение которой изменено в commit 95dc192. Ядро 3.13 не включает в себя этот коммит, поэтому заменим его на 3.14.1(можно скомпилировать или взять готовое здесь). Если у вас для экспериментов нет сетевой карты с поддержкой multiqueue, то можно воспользоваться vmware ESXi с сетевой картой vmxnet3(количество очередей будет равно числу vCPU) или другим продуктом с поддержкой vmxnet3.

После замены ядра вывод tc становится следующим:

# tc qdisc show 
qdisc mq 0: dev eth0 root 
qdisc pfifo_fast 0: dev eth0 parent :1 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth0 parent :2 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth0 parent :3 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth0 parent :4 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
...

Здесь мы видем, что на каждую аппартную очередь прикрепрелена дисциплина pfifo_fast. Чтобы заменить дисциплину для конкретной очереди(ассоциированной с фиксированным номером класса):

# tc qdisc add dev eth0 parent 0:1 handle 0: sfq
# tc qdisc add dev eth0 parent 0:2 handle 0: sfq
# tc qdisc show
qdisc mq 0: dev eth0 root 
qdisc pfifo_fast 0: dev eth0 parent :3 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth0 parent :4 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc sfq 8006: dev eth0 parent :2 limit 127p quantum 1514b depth 127 divisor 1024 
qdisc sfq 8007: dev eth0 parent :1 limit 127p quantum 1514b depth 127 divisor 1024 

В данном случае это означает, что пакет, прежде чем отправиться в аппаратные очереди №1 и 2 (соответствующие классам 0:2 и 0:3) пройдёт через дисциплину sfq (которая не рассматривается в этой статье).

Чтобы заменить mq целиком на какую-нибудь другую дисциплину(например, pfifo_fast) нужно выполнить:

# tc qdisc add dev eth0 root  handle 1: pfifo_fast
# tc qdisc show
qdisc pfifo_fast 1: dev eth0 root refcnt 5 bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

Чтоб вернуть обратно нужно удалить корневую дисциплину(после чего к интерфейсу применится дисциплина по умолчанию):

# tc qdisc del dev eth0 root
Advertisements

3 thoughts on “QoS в Linux: поведение по умолчанию: pfifo_fast, mq

  1. Pingback: QoS в Linux: 802.1p, net_prio cgroups | Net-Labs.in

  2. Pingback: QoS в Linux: pfifo, prio, tc filter, SO_PRIORITY socket | 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