Захват пакетов в Linux на скорости десятки миллионов пакетов в секунду без использования сторонних библиотек

от автора

Моя статья расскажет Вам как принять 10 миллионов пакетов в секунду без использования таких библиотек как Netmap, PF_RING, DPDK и прочие. Делать мы это будем силами обычного Линукс ядра версии 3.16 и некоторого количества кода на С и С++.

Сначала я хотел бы поделиться парой слов о том, как работает pcap — общеизвестный способ захвата пакетов. Он используется в таких популярных утилитах как iftop, tcpdump, arpwatch. Кроме этого, он отличается очень высокой нагрузкой на процессор.

Итак, Вы открыли им интерфейс и ждете пакетов от него используя обычный подход — bind/recv. Ядро в свою очередь получает данные из сетевой карты и сохраняет в пространстве ядра, после этого оно обнаруживает, что пользователь хочет получить его в юзер спейсе и передает через аргумент команды recv, адрес буфера куда эти данные положить. Ядро покорно копирует данные (уже второй раз!). Выходит довольно сложно, но это не все проблемы pcap.

Кроме этого, вспомним, что recv — это системный вызов и вызываем мы его на каждый пакет приходящий на интерфейс, системные вызовы обычно очень быстры, но скорости современных 10GE интерфейсов (до 14.6 миллионов вызовов секунду) приводят к тому, что даже легкий вызов становится очень затратным для системы исключительно по причине частоты вызовов.

Также стоит отметить, что у нас на сервере обычно более 2х логических ядер. И данные могут прилететь на любое их них! А приложение, которое принимает данные силами pcap использует одно ядро. Вот тут у нас включаются блокировки на стороне ядра и кардинально замедляют процесс захвата — теперь мы занимаемся не только копированием памяти/обработкой пакетов, а ждем освобождения блокировок, занятых другими ядрами. Поверьте, на блокировки может зачастую уйти до 90% процессорных ресурсов всего сервера.

Хороший списочек проблем? Итак, мы их все геройски попробуем решить!

Итак, для определенности зафиксируем, что мы работаем на зеркальных портах (что означает, что откуда-то извне сети нам приходит копия всего трафика определенного сервера). На них в свою очередь идет трафик — SYN флуд пакетами минимального размера на скорости 14.6 mpps/7.6GE.

Сетевые ixgbe, драйверы с SourceForge 4.1.1, Debian 8 Jessie. Конфигурация модуля: modprobe ixgbe RSS=8,8 (это важно!). Процессор у меня i7 3820, с 8ю логическими ядрами. Поэтому везде, где я использую 8 (в том числе в коде) Вы должны использовать то число ядер, которое есть у Вас.

Распределим прерывания по имеющимся ядрам

Обращаю внимание, что нам на порт прилетают пакеты, целевые MAC адреса которых не совпадают с MAC адресом нашей сетевой карты. В противном случае включится в работу TCP/IP стек Linux и машина подавится трафиком. Этот момент очень важен, мы сейчас обсуждаем только захват чужого трафика, а не обработку трафика, который предназначается данной машине (хотя для этого мой метод подходит с легкостью).

Теперь проверим, сколько трафика мы можем принять, если начнем слушать весь трафик.

Включаем promisc режим на сетевой карте:

ifconfig eth6 promisc 

После этого в htop мы увидим очень неприятную картину — полную перегрузку одного из ядер:

 1  [||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%]      2  [                                                            0.0%]      3  [                                                            0.0%]      4  [                                                            0.0%]      5  [                                                            0.0%] 6  [                                                            0.0%] 7  [                                                            0.0%] 8  [                                                            0.0%] 

Для определения скорости на интерфейсе воспользуемся спец-скриптом pps.sh: gist.github.com/pavel-odintsov/bc287860335e872db9a5

Скорость на интерфейсе при этом довольно маленькая — 4 миллиона пакетов секунду:
bash /root/pps.sh eth6

 TX eth6: 0 pkts/s RX eth6: 3882721 pkts/s TX eth6: 0 pkts/s RX eth6: 3745027 pkts/s 

Чтобы решить эту проблему и распределить нагрузку по всем логическим ядрам (у меня их 8) нужно запустить следующей скрипт: gist.github.com/pavel-odintsov/9b065f96900da40c5301 который распределит прерывания от всех 8 очередей сетевой карты по всем имеющимся логическим ядрам.

Прекрасно, скорость сразу вылетела до 12mpps (но это не захват, это лишь показатель того, что мы можем прочесть трафик на такой скорости из сети):

  bash /root/pps.sh eth6 TX eth6: 0 pkts/s RX eth6: 12528942 pkts/s TX eth6: 0 pkts/s RX eth6: 12491898 pkts/s TX eth6: 0 pkts/s RX eth6: 12554312 pkts/s 

А нагрузка на ядра стабилизировалась:

 1  [|||||                                                       7.4%]      2  [|||||||                                                     9.7%]      3  [||||||                                                      8.9%]     4  [||                                                          2.8%]      5  [|||                                                         4.1%] 6  [|||                                                         3.9%] 7  [|||                                                         4.1%] 8  [|||||                                                       7.8%] 

Сразу обращаю внимание, что в тексте будут использоваться два примера кода, вот они:
AF_PACKET, AF_PACKET + FANOUT: gist.github.com/pavel-odintsov/c2154f7799325aed46ae
AF_PACKET RX_RING, AF_PACKET + RX_RING + FANOUT: gist.github.com/pavel-odintsov/15b7435e484134650f20

Это законченные приложения с максимальным уровнем оптимизаций. Промежуточные (заведомо более медленные версии кода) я не привожу — но все флажки для управления всеми оптимизациями выделены и объявлены в коде как bool — можете легко повторить мой путь у себя.

Первая попытка запуска AF_PACKET захвата без оптимизаций

Итак, запускаем приложение по захвату трафика силами AF_PACKET:

 We process: 222048 pps We process: 186315 pps 

И нагрузка в потолок:

 1  [||||||||||||||||||||||||||||||||||||||||||||||||||||||||   86.1%]      2  [||||||||||||||||||||||||||||||||||||||||||||||||||||||     84.1%]      3  [||||||||||||||||||||||||||||||||||||||||||||||||||||       79.8%]      4  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||  88.3%]      5  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||    83.7%] 6  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||  86.7%] 7  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||| 89.8%] 8  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||90.9%] 

Причина тому, что ядро утонуло в блокировках, на которые тратит все процессорное время:

 Samples: 303K of event 'cpu-clock', Event count (approx.): 53015222600   59.57%  [kernel]        [k] _raw_spin_lock    9.13%  [kernel]        [k] packet_rcv    7.23%  [ixgbe]         [k] ixgbe_clean_rx_irq    3.35%  [kernel]        [k] pvclock_clocksource_read    2.76%  [kernel]        [k] __netif_receive_skb_core    2.00%  [kernel]        [k] dev_gro_receive    1.98%  [kernel]        [k] consume_skb    1.94%  [kernel]        [k] build_skb    1.42%  [kernel]        [k] kmem_cache_alloc    1.39%  [kernel]        [k] kmem_cache_free    0.93%  [kernel]        [k] inet_gro_receive    0.89%  [kernel]        [k] __netdev_alloc_frag    0.79%  [kernel]        [k] tcp_gro_receive 

Оптимизация AF_PACKET захвата с помощью FANOUT

Итак, что же делать? Немного подумать 🙂 Блокировки возникают тогда, когда несколько процессоров пытаются использовать один ресурс. В нашем случае это происходит из-за того, что у нас один сокет и его обслуживает одно приложение, что вынуждает остальные 8 логических процессоров стоять в постоянном ожидании.

Тут нам на помощь придет отличная функция — FANOUT, а если по-русски — разветвления. Для AF_PACKET мы можем запустить несколько (разумеется, наиболее оптимальным в нашем случае будет число процессов равное числу логических ядер). Кроме этого, мы можем задать алгоритм по которому данные будут распределяться по этим сокетам. Я выбрал режим PACKET_FANOUT_CPU, так как в моем случае данные очень равномерно распределяются по очередям сетевой карты и это, на мой взгляд, наименее ресурсоемкий вариант балансировки (хотя тут не ручаюсь — рекомендую посмотреть в коде ядра).

Корректируем в примере кода bool use_multiple_fanout_processes = true;

И снова запускаем приложение.

О чудо! 10 кратное ускорение:

 We process: 2250709 pps We process: 2234301 pps We process: 2266138 pps 

Процессоры, конечно, по прежнему загружены по полной:

 1  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||92.6%]      2  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.1%]      3  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.2%]      4  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.3%]      5  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.1%] 6  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.7%] 7  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.7%] 8  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.2%] 

Но карта perf top выглядит уже совершенно иначе — никаких локов больше нет:

 Samples: 1M of event 'cpu-clock', Event count (approx.): 110166379815   17.22%  [ixgbe]         [k] ixgbe_clean_rx_irq                                                                                                        7.07%  [kernel]        [k] pvclock_clocksource_read                                                                                                  6.04%  [kernel]        [k] __netif_receive_skb_core                                                                                                  4.88%  [kernel]        [k] build_skb                                                                                                                 4.76%  [kernel]        [k] dev_gro_receive                                                                                                           4.28%  [kernel]        [k] kmem_cache_free                                                                                                           3.95%  [kernel]        [k] kmem_cache_alloc                                                                                                         3.04%  [kernel]        [k] packet_rcv                                                                                                                2.47%  [kernel]        [k] __netdev_alloc_frag                                                                                                       2.39%  [kernel]        [k] inet_gro_receive                                                                                                          2.29%  [kernel]        [k] copy_user_generic_string                                                                                                  2.11%  [kernel]        [k] tcp_gro_receive                                                                                                           2.03%  [kernel]        [k] _raw_spin_unlock_irqrestore           

Кроме этого, у сокетов (хотя я не уверен про AF_PACKET) есть возможность задать буфер приема, SO_RCVBUF, но на моем тест-стенде это не дало никаких результатов.

Оптимизация AF_PACKET захвата с помощью RX_RING — кольцевого буфера

Что же делать и почему все равно медленно? Ответ тут в функции build_skb, она означает, что внутри ядра по-прежнему производится два копирования памяти!

Теперь попробуем разобраться с выделением памяти за счет использования RX_RING.

И ура 4 MPPS вершина взята!!!

 We process: 3582498 pps We process: 3757254 pps We process: 3669876 pps We process: 3757254 pps We process: 3815506 pps We process: 3873758 pps 

Такой прирост скорости был обеспечен тем, что копирование памяти из буфера сетевой карты теперь производится лишь однажды. И при передачей из пространства ядра в пространство пользователя повторное копирование не производится. Это обеспечивается за счет общего буфера выделенного в ядре и пропущенного в user space.

Также меняется подход с работой — нам теперь нельзя висеть и слушать когда придет пакет (помните — это оверхед!), теперь с помощью вызова poll мы можем ожидать сигнала, когда будет заполнен целый блок! И после этого начинать его обработку.

Оптимизация AF_PACKET захвата с помощью RX_RING силами FANOUT

Но все равно у нас проблемы с блокировками! Как же их победить? Старым методом — включить FANOUT и выделить блок памяти для каждого потока-обработчика!

 Samples: 778K of event 'cpu-clock', Event count (approx.): 87039903833   74.26%  [kernel]       [k] _raw_spin_lock    4.55%  [ixgbe]        [k] ixgbe_clean_rx_irq    3.18%  [kernel]       [k] tpacket_rcv    2.50%  [kernel]       [k] pvclock_clocksource_read    1.78%  [kernel]       [k] __netif_receive_skb_core    1.55%  [kernel]       [k] sock_def_readable    1.20%  [kernel]       [k] build_skb    1.19%  [kernel]       [k] dev_gro_receive    0.95%  [kernel]       [k] kmem_cache_free    0.93%  [kernel]       [k] kmem_cache_alloc    0.60%  [kernel]       [k] inet_gro_receive    0.57%  [kernel]       [k] kfree_skb    0.52%  [kernel]       [k] tcp_gro_receive    0.52%  [kernel]       [k] __netdev_alloc_frag 

Итак, подключаем FANOUT режим для RX_RING версии!

УРА! РЕКОРД!!! 9 MPPS!!!

 We process: 9611580 pps We process: 8912556 pps We process: 8941682 pps We process: 8854304 pps We process: 8912556 pps We process: 8941682 pps We process: 8883430 pps We process: 8825178 pps 

perf top:

 Samples: 224K of event 'cpu-clock', Event count (approx.): 42501395417   21.79%  [ixgbe]              [k] ixgbe_clean_rx_irq    9.96%  [kernel]             [k] tpacket_rcv    6.58%  [kernel]             [k] pvclock_clocksource_read    5.88%  [kernel]             [k] __netif_receive_skb_core    4.99%  [kernel]             [k] memcpy    4.91%  [kernel]             [k] dev_gro_receive    4.55%  [kernel]             [k] build_skb    3.10%  [kernel]             [k] kmem_cache_alloc    3.09%  [kernel]             [k] kmem_cache_free    2.63%  [kernel]             [k] prb_fill_curr_block.isra.57 

К слову, справедливости ради обновление на ядро 4.0.0 ветки особенного ускорения не дало. Скорость держалась в тех же пределах. Но нагрузка на ядра упала значительно!

 1  [|||||||||||||||||||||||||||||||||||||                       55.1%]      2  [|||||||||||||||||||||||||||||||||||                         52.5%]      3  [||||||||||||||||||||||||||||||||||||||||||                  62.5%]      4  [||||||||||||||||||||||||||||||||||||||||||                  62.5%]      5  [|||||||||||||||||||||||||||||||||||||||                     57.7%] 6  [||||||||||||||||||||||||||||||||                            47.7%] 7  [|||||||||||||||||||||||||||||||||||||||                     55.9%] 8  [|||||||||||||||||||||||||||||||||||||||||                   61.4%] 

Рекомендуемые к прочтению материалы:
www.kernel.org/doc/Documentation/networking/packet_mmap.txt
man7.org/linux/man-pages/man7/packet.7.html

ссылка на оригинал статьи http://habrahabr.ru/post/261161/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *