Источник: Performance optimisation using SO_REUSEPORT
Мартен Гартнер (Marten Gartner) · 17 марта 2022 · 5 мин чтения
Привет, друзья!
В этой статье я пройду через процесс оптимизации программного маршрутизатора путём его параллелизации с использованием SO_REUSEPORT. Кроме того, я разберу некоторые подводные камни, которые нужно иметь в виду при использовании этой возможности.
SO_REUSEPORT позволяет нескольким сокетам привязаться к одному и тому же порту, позволяя распределять трафик между ними без единого сокета в качестве узкого места. Ядро Linux распределяет трафик между слушающими сокетами с помощью хеш-функции от четвёрки (4-tuple: IP источника/назначения и порт источника/назначения). Первый сокет должен установить опцию SO_REUSEPORT, чтобы позволить другим последующим сокетам привязаться к тому же порту. Просто использовать несколько сокетов параллельно вместо одного для операций, ограниченных сетью, звучит как замечательная возможность, но есть некоторые подводные камни, которые нужно учитывать. Так что давайте погрузимся в пример.
Чтобы протестировать SO_REUSEPORT на реальном примере, моей целью было параллелизовать SCION Border Router. SCION — это сетевая архитектура следующего поколения, которая по своей конструкции избегает определённых проблем текущего интернета, например проблем доверия, BGP-угонов (BGP hijacking) или прозрачного управления путями. Пограничные маршрутизаторы SCION (Border Routers) отвечают за пересылку пакетов между автономными системами и поэтому нацелены на специфические требования к производительности. В текущей реализации есть структура под названием DataPlane, которая отвечает за чтение пакетов со всех настроенных SCION-интерфейсов (соединяющих локальных хостов или соседних Border Router'ов) и пересылку их в пункт назначения. Моя идея была довольно проста: вместо запуска одного DataPlane в маршрутизаторе, давайте использовать SO_REUSEPORT, чтобы запустить несколько экземпляров параллельно. Классная вещь в структуре DataPlane: она не разделяет какое-либо состояние с другими компонентами приложения. Следовательно, её можно легко запустить несколько раз. Поскольку Border Router реализован на Go, давайте посмотрим, как мы можем установить эту опцию в Go:
import "github.com/libp2p/go-reuseport"
conn, err := reuseport.Listen("udp", fmt.Sprintf("%s:%d", addr, port))
if err != nil {
// обработка ошибки
}Это в основном всё для настройки SO_REUSEPORT. Теперь нам нужно запустить несколько параллельных DataPlane, обернув соответствующий код, который запускает DataPlane, в цикл for, и добавив несколько go-рутин и wait-групп, чтобы гарантировать, что все экземпляры работают конкурентно и приложение корректно ждёт их завершения. Звучит просто, правда? Теперь есть один подводный камень: в SCION Border Router есть некоторая управляющая логика, которая гарантирует, что линки работают, называемая Bidirectional Forward Detection (BFD). Эта логика отправляет управляющие пакеты по линкам между Border Router'ами в обоих направлениях. Теперь, когда у нас настроен SO_REUSEPORT, каждый DataPlane отправляет этот пакет. Поскольку пакеты все отправляются с одного и того же порта источника, нет гарантии, на каком сокете они прибудут. Следовательно, два Border Router больше не могут синхронизировать друг друга с помощью BFD. Однако исправление не такое уж сложное. Мы разделяем одну и ту же информацию о BFD-сессии между всеми DataPlane, чтобы принимать BFD-пакеты и передавать их одному контроллеру. Кроме того, только первый DataPlane отправляет BFD-пакеты. Вот и всё!
Чтобы оценить, как количество конкурентных DataPlane влияет на производительность пересылки, мы запускаем SCION Border Router с 1–10 DataPlane (поскольку у нас 10 ядер CPU) и отправляем около 9 Гбит/с SCION-трафика на линк 10 Гбит/с (используя 10 разных сокетов с разными номерами портов). Мы измеряем производительность пересылки маршрутизатора с помощью nload на исходящем сетевом интерфейсе. На следующем рисунке мы показываем достигнутую производительность пересылки с разным размером пакетов на основе настроенного MTU.
Мы наблюдаем, что производительность пересылки SO_REUSEPORT Border Router растёт почти линейно, начиная примерно с 1.3 Гбит/с для 1 DataPlane (MTU 1500) до 7.5 Гбит/с с 10 DataPlane. При использовании более крупных кадров (MTU 3000) производительность растёт схожим образом, начиная с 3 Гбит/с для 1 DataPlane и достигая 9 Гбит/с для 10 DataPlane. Поскольку 9 Гбит/с — это входящая полоса пропускания на маршрутизаторе, мы полагаем, что он пересылает почти все входящие пакеты, что фантастично для почти 10 Гбит/с.
Так что вы можете спросить: это было довольно просто, в чём подвох? Дело в том, что такой рост производительности через конкурентность возможен только если четвёрки (4-tuple) входящих пакетов различаются, чтобы достичь равного распределения трафика по всем слушающим сокетам. Если мы рассмотрим пересылку между двумя SCION Border Router'ами, это не так, поскольку все сокеты, отправляющие исходящий трафик, слушают за одним и тем же номером порта. И вот здесь мы можем использовать ещё одну классную возможность ядра Linux: eXpress Data Path (XDP). С XDP мы можем выполнять операции над пакетом до того, как он попадёт в сетевой стек Linux. Несмотря на обработку всех пакетов в XDP на одном и том же ядре CPU, мы теперь можем распределить их обработку в сетевом стеке по разным ядрам CPU. Я могу представить два разных способа достичь лучшего распределения по сокетам при одной и той же четвёрке: 1) Использовать вспомогательную функцию bpf_redirect_cpu, чтобы отправить пакет на другое ядро CPU, или 2) рандомизацию порта источника, чтобы создать разные четвёрки для входящих пакетов с одинаковой четвёркой. Я пробовал первый подход в нескольких конфигурациях, но в любом случае он закончился отбрасыванием всех пакетов. Поскольку я не был уверен, что этот хелпер решит мою проблему именно так, как я думал, я перешёл ко второму подходу. В XDP нам просто нужно проверить, адресован ли пакет интерфейсу маршрутизатора, и рандомизировать порт источника. Один небольшой подводный камень — пересчитать или обнулить контрольную сумму UDP-заголовка, иначе пакеты будут отбрасываться. Но этот подход работает нормально, по крайней мере для полосы пропускания, которая может быть обработана в XDP одним ядром, что далеко за пределами 10 Гбит/с, если XDP-программа не слишком сложная.
Подводя итог, наша адаптация для использования SO_REUSEPORT с целью параллелизации SCION Border Router была проста в реализации и принесла огромное улучшение производительности. Для пересылки «роутер-роутер» нам нужна простая XDP-программа, чтобы создать справедливое распределение пакетов по сокетам, иначе прироста производительности не будет, потому что все пакеты прибывают на один и тот же сокет. Полный код можно найти здесь. Оригинальная статья была опубликована здесь.
Как всегда, пишите мне любые отзывы, я очень ценю любые комментарии или дополнительную информацию, и я обновлю эту статью любым полезным вкладом, который получу.
Всем привет,
Мартен

