Високонавантажені шлюзи 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 для інкапсульованого трафіку VPN:
[ Віддалені клієнти ] ---> [ Зовнішні пакети 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 усуває цю проблему, скеровуючи обробку пакета безпосередньо на те ядро CPU, де потік програми зчитує інформацію із сокета.

Схема узгодження потоків 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): Зберігається окремо для кожної черги прийому мережевого адаптера. Вона містить ID ядра, яке обробило попередній пакет цього потоку.

Перед передачею пакета на ядро, визначене глобальною таблицею, RFS звіряється з таблицею черги. Якщо потік перемістився на інше ядро, 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, протокол маршрутизації 3-го рівня, розроблений командою VPNRice, безпосередньо взаємодіє з механізмами балансування пакетів ядра Linux:

  1. Прив'язка до топології NUMA: RiceGuard ізолює апаратні переривання, криптографічні пули та цикли сокетів на відповідних вузлах NUMA. Якщо трафік надходить через мережеву карту, підключену до вузла NUMA 0, маска rps_cpus налаштовується виключно на ядра, які мають спільний кеш L3 із цією шиною PCIe.
  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 допомагає розподілити обробку дешифрованих пакетів між вільними ядрами CPU.