Высоконагруженные шлюзы VPN, обслуживающие десятки тысяч одновременных зашифрованных туннелей, нередко сталкиваются с аппаратным дисбалансом: неравномерным распределением вычислительной нагрузки между процессорными ядрами. Серверная платформа с 32 или 64 ядрами может демонстрировать общее потребление ресурсов процессора ниже 25%, но при этом сбрасывать входящие пакеты в моменты пикового трафика. Системные метрики показывают, что ядро CPU 0 загружено на 100% программными прерываниями (ksoftirqd/0 и NET_RX_SOFTIRQ), в то время как соседние процессорные ядра простаивают.

Подобный перекос вызван тем, как сетевые контроллеры и протоколы туннелирования взаимодействуют с ядром операционной системы. Когда аппаратный механизм масштабирования Receive Side Scaling (RSS) не способен равномерно хешировать инкапсулированные датаграммы, Linux предлагает два эффективных программных механизма: Receive Packet Steering (RPS) и Receive Flow Steering (RFS). Корректная конфигурация этих подсистем позволяет распределить обработку сетевого трафика по всем доступным вычислительным ядрам с сохранением локальности кэш-памяти.

Проблема инкапсуляции: деградация очередей при обработке туннелей

Физические сетевые адаптеры (NIC) разделяют входящие кадры по аппаратным очередям приёма с помощью технологии Receive Side Scaling. Адаптер вычисляет хеш Toeplitz на основе заголовков пакета — как правило, 4-кортежа, включающего IP-адрес источника, IP-адрес назначения, порт источника и порт назначения, — направляя поток в конкретный кольцевой буфер приёма (RX ring buffer). Каждая аппаратная очередь генерирует выделенное прерывание MSI-X, привязанное к определенному ядру CPU.

Ограничения аппаратного RSS при обработке туннельного трафика:
[ Удаленные клиенты ] ---> [ Внешние пакеты WireGuard / IPsec / RiceGuard ]
                                      |
                                      v
                     [ Единый целевой порт (например, UDP 51820) ]
                                      |
                        [ Хеш Toeplitz сетевой карты ]
                                      |
       =================================================
       Все потоки попадают в очередь RX 0 (Одно прерывание)
       =================================================
                                      |
                                      v
       [ Ядро CPU 0: 100% ksoftirqd / NET_RX_SOFTIRQ ] ---> Переполнение буфера!
       [ Ядра CPU 1-31: 0% нагрузки SoftIRQ ]

Когда тысячи пользователей подключаются к концентратору VPN через протоколы WireGuard, IPsec или специализированные туннели, внешний поток пакетов адресуется на один и тот же сетевой порт шлюза. Если входящий трафик поступает через промежуточные транзитные шлюзы операторского класса (Carrier-Grade NAT), 4-кортеж сжимается до однотипных значений, в результате чего подавляющее большинство потоков попадает в одну и ту же очередь адаптера.

Виртуальные интерфейсы туннелей (tun или tap) также лишены многоочередной аппаратной балансировки по умолчанию. После расшифровки пакета поток внутренних IP-датаграмм передается в сетевой стек ядра, концентрируя нагрузку SoftIRQ именно на том ядре, которое выполняло процедуру дешифрования. Кольцевые буферы переполняются, возрастает вариация задержки (джиттер) и возникают скрытые потери пакетов, ухудшающие качество голосовой связи и потоковой передачи данных.

Receive Packet Steering: программная балансировка пакетов по ядрам

Receive Packet Steering распределяет вычислительную нагрузку по маске процессорных ядер, определяемой системным администратором, после завершения первичного аппаратного прерывания. В отличие от аппаратного RSS, функционирующего непосредственно на сетевом чипе, RPS работает на программном уровне во время опроса NAPI драйвера (netif_receive_skb).

Архитектура программного распределения пакетов RPS:
[ Входящий пакет в аппаратной очереди RX 0 ]
                    |
                    v
          [ Процедура опроса NAPI ]
                    |
      [ Битовая маска rps_cpus: например, 0x0000ffff (Ядра 0-15) ]
                    |
      [ Вычисление хеша jhash по сетевым заголовкам ]
                    |
                    v
   [ Помещение sk_buff в очередь input_pkt_queue целевого ядра ]
                    |
  [ Межпроцессорное прерывание (IPI): вызов NET_RX на целевом ядре ]
                    |
     +--------------+--------------+--------------+
     |              |              |              |
     v              v              v              v
  [Ядро 0]       [Ядро 1]       [Ядро 2]       [Ядро 3] ... [Ядро 15]

При поступлении сетевого кадра драйвер проверяет параметры sysfs для соответствующей очереди приёма. Если механизм RPS активирован, ядро вычисляет хеш заголовков с помощью алгоритма Jenkins (jhash). Полученное значение сопоставляется со списком ядер, указанных в битовой маске rps_cpus.

Принимающее ядро помещает дескриптор sk_buff во входную очередь целевого процессора (input_pkt_queue) и инициирует межпроцессорное прерывание (IPI). Выбранное целевое ядро берет на себя выполнение оставшихся сетевых процедур: фильтрацию межсетевым экраном, поиск в таблице маршрутизации и доставку пакета в пользовательский сокет.

Receive Flow Steering: привязка потоков и локальность кэш-памяти

Несмотря на то что RPS равномерно распределяет входящие пакеты по ядрам, выбор целевого ядра осуществляется исключительно на базе математического хеша. Механизм RPS не учитывает, на каком именно ядре выполняется поток пользовательского приложения, ожидающий данные из этого сокета.

Если ядро CPU 4 обрабатывает сетевой пакет в стеке операционной системы, а рабочий поток службы VPN выполняется на ядре CPU 12, данные полезной нагрузки должны передаваться через шину межпроцессорного взаимодействия. Такая миграция данных между ядрами приводит к промахам кэша L1/L2, сбросу кэш-линий и дополнительным задержкам доступа к оперативной памяти.

Receive Flow Steering устраняет эту задержку, направляя обработку пакета строго на то процессорное ядро, где поток приложения непосредственно считывает информацию из сокета.

Схема согласования потоков RFS:
1. Поток приложения на Ядре 3 вызывает recvmsg(sock_fd)
   ---> Ядро обновляет глобальную таблицу потоков (rps_sock_flow_table):
        [ Хеш потока #A18C -> Активен на Ядре 3 ]

2. Очередной пакет потока #A18C поступает на сетевой адаптер
   ---> Драйвер опрашивает rps_sock_flow_table:
        Целевое ядро = Ядро 3 (Где данные уже находятся в кэше!)

3. sk_buff направляется напрямую в input_pkt_queue Ядра 3
   ---> Обработка SoftIRQ и приложение работают в общем кэше L1/L2!

Для поддержания согласованности RFS использует две системные таблицы:

  1. Глобальная таблица сокетов (rps_sock_flow_table): Обновляется автоматически при выполнении приложением системных вызовов recvmsg(2), read(2) или epoll_wait(7). В таблице фиксируется хеш сетевого потока и идентификатор текущего ядра CPU.
  2. Локальная таблица очередей (rps_flow_table): Поддерживается индивидуально для каждой очереди приёма сетевого интерфейса. Она хранит идентификатор ядра, которое обработало предыдущий пакет данного потока.

Перед передачей пакета на ядро, указанное в глобальной таблице, RFS сверяется с таблицей очереди. Если приложение переместилось на другое ядро, переключение обработки произойдет только после того, как все ранее поставленные в очередь пакеты на старом ядре будут полностью обработаны. Это гарантирует сохранение строгого порядка следования пакетов.

Практическая настройка: включение RPS и RFS на узле VPN

Ниже приведена последовательность команд для оптимизации 16-ядерного сервера VPN под управлением Linux с внешним сетевым интерфейсом eth0 и туннелем WireGuard wg0.

Шаг 1: Назначение битовой маски ядер для RPS

Для системы с 16 ядрами (от 0 до 15) маска 0xffff включает все ядра. На специализированных шлюзах рекомендуется оставить ядра 0 и 1 для аппаратных прерываний и системных задач, выделив ядра со 2 по 15 (маска 0xfffc) под обработку сетевого трафика:

# Включение RPS для ядер 2-15 на всех очередях RX интерфейса eth0
for rx_queue in /sys/class/net/eth0/queues/rx-*; do
    echo "fffc" > "$rx_queue/rps_cpus"
done

# Включение RPS для виртуального туннельного интерфейса
echo "fffc" > /sys/class/net/wg0/queues/rx-0/rps_cpus

Шаг 2: Конфигурация размеров таблиц RFS

Размер таблицы потоков выбирается исходя из максимального числа одновременных подключений. Для концентратора на 32 768 сессий значение округляется до ближайшей степени двойки — 65 536:

# Установка размера глобальной таблицы потоков
sysctl -w net.core.rps_sock_flow_entries=65536

# Распределение записей по аппаратным очередям (для 4 очередей: 65536 / 4 = 16384)
for rx_queue in /sys/class/net/eth0/queues/rx-*; do
    echo 16384 > "$rx_queue/rps_flow_cnt"
done

Шаг 3: Увеличение входной очереди пакетов ядра

При кратковременных всплесках пакетного трафика входная очередь целевого ядра может переполниться. Увеличение значения netdev_max_backlog предотвращает потери пакетов, регистрируемые в /proc/net/softnet_stat:

# Увеличение лимита очереди ожидания пакетов с 1000 до 10000
sysctl -w net.core.netdev_max_backlog=10000

# Увеличение квоты обработки пакетов за один цикл опроса NAPI
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=4000

Архитектурная интеграция в RiceGuard VPN

Сетевой движок RiceGuard, созданный командой VPNRice для высокоскоростной маршрутизации на уровне L3, тесно взаимодействует с алгоритмами распределения пакетов ядра Linux:

  1. Привязка к топологии NUMA: RiceGuard распределяет обработку аппаратных прерываний, криптографические потоки и циклы ввода-вывода сокетов по соответствующим узлам NUMA. Если трафик поступает через сетевой адаптер, подключенный к узлу NUMA 0, маска rps_cpus настраивается исключительно на ядра, разделяющие кэш L3 с этим контроллером.
  2. Синхронизация таблиц потоков: При динамическом подключении клиентов рабочие потоки RiceGuard регистрируют подсказки привязки в таблицах ядра, удерживая обработку SoftIRQ и пользовательских потоков в рамках одной кэш-иерархии.
  3. Результаты нагрузочного тестирования: На магистральных каналах со скоростью 10 Гбит/с при 25 000 активных клиентских туннелей синхронизированная настройка RPS и RFS снизила задержку 99-го перцентиля (p99) с 8,4 мс до 1,9 мс и полностью ликвидировала потери пакетов.

Часто задаваемые вопросы

В чем ключевое отличие между RSS, RPS и RFS?

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

Увеличивает ли RPS общую нагрузку на процессор?

RPS создает незначительный дополнительный расход вычислительных ресурсов (около 1–2% CPU) на вычисление хеша Jenkins и отправку межпроцессорных прерываний. Однако устранение перегрузки отдельных ядер существенно поднимает предельную пропускную способность всей системы.

Можно ли использовать RPS и RFS для интерфейсов tun/tap?

Да. Виртуальные адаптеры tun и tap получают значительный прирост производительности от применения RPS. Поскольку виртуальные устройства обычно функционируют с одной очередью, активация rps_cpus на очереди rx-0 позволяет распределить расшифрованные пакеты по всем ядрам процессора.