Перехватывайте Пакеты В Linux Со Скоростью Десятков Миллионов Пакетов В Секунду Без Использования Сторонних Библиотек.

Моя статья расскажет вам, как получать 10 миллионов пакетов в секунду без использования таких библиотек, как Netmap, PF_RING, DPDK и других.

Мы сделаем это, используя обычное ядро Linux версии 3.16 и немного кода на C и C++.



Перехватывайте пакеты в Linux со скоростью десятков миллионов пакетов в секунду без использования сторонних библиотек.
</p><p>

Во-первых, я хотел бы сказать несколько слов о том, как работает 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 на сетевой карте:

   
 
 1  [||||||||||||||||||||||||||||||||||||||||||||||||||||||||||100.0%]     
 2  [                                                            0.0%]     
 3  [                                                            0.0%]     
 4  [                                                            0.0%]     
 5  [                                                            0.0%]
 6  [                                                            0.0%]
 7  [                                                            0.0%]
 8  [                                                            0.0%]
 
После этого в htop мы увидим весьма неприятную картину — полную перегрузку одного из ядер:
 
 TX eth6: 0 pkts/s RX eth6: 3882721 pkts/s
 TX eth6: 0 pkts/s RX eth6: 3745027 pkts/s
 
Для определения скорости на интерфейсе воспользуемся специальным скриптом pps.sh: gist.github.com/pavel-odintsov/bc287860335e872db9a5 Скорость на интерфейсе достаточно низкая — 4 миллиона пакетов в секунду: bash /root/pps.sh eth6
 
  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
 
Чтобы решить эту проблему и распределить нагрузку по всем логическим ядрам (их у меня 8), нужно запустить следующий скрипт: gist.github.com/pavel-odintsov/9b065f96900da40c5301 который будет распределять прерывания из всех 8 очередей сетевых карт по всем доступным логическим ядрам.

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

 
 1  [|||||                                                       7.4%]     
 2  [|||||||                                                     9.7%]     
 3  [||||||                                                      8.9%]    
 4  [||                                                          2.8%]     
 5  [|||                                                         4.1%]
 6  [|||                                                         3.9%]
 7  [|||                                                         4.1%]
 8  [|||||                                                       7.8%]
 
И нагрузка на ядра стабилизировалась:
 
 We process: 222048 pps
 We process: 186315 pps
 
Сразу обратите внимание, что в тексте будут использованы два примера кода, вот они: 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:
 
 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
 
Причина в том, что ядро утоплено в блокировках, на что тратится всё процессорное время:
 
 We process: 2250709 pps
 We process: 2234301 pps
 We process: 2266138 pps
 


Оптимизация захвата AF_PACKET с использованием FANOUT

Так что делать? Подумайте немного :) Блокировка возникает, когда несколько процессоров пытаются использовать один ресурс.

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

Здесь нам на помощь придет отличная функция – FANOUT, или по-русски – ветки.

Для AF_PACKET мы можем запустить несколько (конечно, наиболее оптимальным в нашем случае будет количество процессов, равное количеству логических ядер).

Кроме того, мы можем задать алгоритм, по которому данные будут распределяться по этим сокетам.

Я выбрал режим PACKET_FANOUT_CPU, так как в моем случае данные распределяются по очередям сетевой карты очень равномерно и это, на мой взгляд, наименее ресурсоемкий вариант балансировки (хотя за это не ручаюсь - рекомендую глядя в код ядра).

Исправляем это в примере кода bool use_multiple_fanout_processes = true; И снова запускаем приложение.

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

 
 1  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||92.6%]     
 2  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.1%]     
 3  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.2%]     
 4  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.3%]     
 5  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.1%]
 6  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.7%]
 7  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.7%]
 8  [|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||93.2%]
 
Процессоры, конечно, пока загружены полностью:
 
 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
 
А вот карта perf top выглядит совсем по-другому — замков больше нет:
 
 We process: 3582498 pps
 We process: 3757254 pps
 We process: 3669876 pps
 We process: 3757254 pps
 We process: 3815506 pps
 We process: 3873758 pps
 
Кроме того, сокеты (правда, насчет AF_PACKET я не уверен) имеют возможность устанавливать буфер приема SO_RCVBUF, но на моем тестовом стенде это не дало никаких результатов.



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

Что делать и почему все еще медленно? Ответ кроется в функции build_skb, а это значит, что внутри ядра по-прежнему создаются две копии памяти! Теперь попробуем разобраться с распределением памяти с помощью RX_RING. И ура 4 MPPS вершина взята!!!
 
 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
 
Такой прирост скорости был обеспечен за счет того, что память теперь копируется из буфера сетевой карты только один раз.

А при переносе из пространства ядра в пространство пользователя повторное копирование не производится.

Это обеспечивается общим буфером, выделенным в ядре и переданным в пространство пользователя.

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

заполненный! И после этого приступайте к его обработке.



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

Но у нас по-прежнему проблемы с блокировкой! Как их победить? Старый метод — включить FANOUT и выделить блок памяти для каждого потока-обработчика!
 
 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
 
Итак, подключим режим FANOUT для версии RX_RING! УРА! ЗАПИСЫВАТЬ!!! 9 МПС!!!
 
 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
 
перфорированный верх:
 
 1  [|||||||||||||||||||||||||||||||||||||                       55.1%]     
 2  [|||||||||||||||||||||||||||||||||||                         52.5%]     
 3  [||||||||||||||||||||||||||||||||||||||||||                  62.5%]     
 4  [||||||||||||||||||||||||||||||||||||||||||                  62.5%]     
 5  [|||||||||||||||||||||||||||||||||||||||                     57.7%]
 6  [||||||||||||||||||||||||||||||||                            47.7%]
 7  [|||||||||||||||||||||||||||||||||||||||                     55.9%]
 8  [|||||||||||||||||||||||||||||||||||||||||                   61.4%]
 
Кстати, справедливости ради, обновление ветки ядра 4.0.0 особого ускорения не дало.

Скорость держалась в тех же пределах.

Зато нагрузка на ядра значительно снизилась!

ifconfig eth6 promisc

В заключение хотелось бы добавить, что Linux — это просто потрясающая платформа для анализа трафика даже в среде, где невозможно собрать какой-либо специализированный модуль ядра.

Это очень, очень приятно.

Есть надежда, что в следующих версиях ядра можно будет обрабатывать 10GE на полной скорости провода 14,6 млн/пакетов в секунду с помощью 1800-мегагерцового процессора :) Рекомендуемые материалы для чтения: www.kernel.org/doc/Documentation/networking/packet_mmap.txt man7.org/linux/man-pages/man7/packet.7.html Теги: #ddos #netmap #pf_ring #DPDK #pcap #информационная безопасность #Системное программирование

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.