Проблема єдиної черги у віртуальних мережевих інтерфейсах
Сучасні корпоративні концентратори VPN, програмно-визначені шлюзи та прикордонні маршрутизатори обробляють колосальні обсяги мережевого трафіку через канали 40 GbE та 100 GbE. Хоча фізичні мережеві адаптери (NIC) уже понад десять років оснащуються апаратними чергами (Receive Side Scaling / RSS), віртуальні інтерфейси у просторі користувача часто залишаються обмеженими застарілими архітектурними шаблонами.
В операційній системі Linux стандартом передачі IP-пакетів третього рівня (Layer 3) між службами простору користувача та мережевим стеком ядра є драйвер Universal TUN/TAP (/dev/net/tun). У класичних реалізаціях служба відкриває /dev/net/tun один раз, отримує єдиний дескриптор файлу та реєструє один мережевий інтерфейс (наприклад, tun0).
Під час зростання швидкості потоку понад кілька гігабітів на секунду ця модель із єдиною чергою наштовхується на критичні обмеження:
- Серіалізація обчислень на ядрі CPU 0: Через те, що тільки один дескриптор файлу читає та записує кадри віртуального пристрою, уся обробка трафіку зосереджується на тому ядрі процесора, де виконується відповідний потік вводу-виводу. Навіть на 64-ядерному сервері ядро 0 може завантажуватися на 100%, тоді як решта 63 ядер залишаються вільними.
- Конфлікти блокувань spinlock у
tun.c: В інтерфейсі TUN із однією чергою всі операції приймання та відправлення пакетів проходять через єдиний спінлок ядра (tun->lock). Спроби пулу потоків одночасно звертатися до цього дескриптора спричиняють масові перемикання контексту та втрату процесорних тактів. - Нівелювання переваг апаратного розподілу: Фізичні адаптери паралельно розподіляють пакети по апаратних кільцях (RX rings) за допомогою хешування Toeplitz. Зведення цих паралельних потоків в один потік віртуального інтерфейсу повністю нівелює апаратні переваги мережевої карти.
Щоб усунути це вузьке місце, починаючи з версії ядра Linux 3.8 було реалізовано підтримку параметра IFF_MULTI_QUEUE для підсистеми TUN/TAP.
Внутрішня архітектура IFF_MULTI_QUEUE в ядрі Linux
Багаточергова архітектура змінює базовий принцип взаємодії між мережевими інтерфейсами та файловими дескрипторами в ядрі. Замість жорсткої прив'язки 1:1 між структурою struct net_device та символьним пристроєм, IFF_MULTI_QUEUE дозволяє підключати велику кількість незалежних файлових дескрипторів (struct tun_file) до одного віртуального мережевого пристрою (struct tun_struct).
+-------------------------------------------------------------------------+
| Багатопотоковий VPN-рушій у просторі користувача |
+-------------------------------------------------------------------------+
| Робочий потік 0 | Робочий потік 1 | ... | Робочий потік N-1 |
| (Прив'язка CPU 0) | (Прив'язка CPU 1) | | (Прив'язка CPU N-1) |
+--------------------+--------------------+--------+----------------------+
| | |
| fd_0 | fd_1 | fd_n-1
v v v
+-------------------------------------------------------------------------+
| Незалежні файлові дескриптори (/dev/net/tun) |
+-------------------------------------------------------------------------+
| | |
v Читання/запис v Читання/запис v Читання/запис
+-------------------------------------------------------------------------+
| Підсистема TUN у ядрі Linux |
| |
| +--------------+ +--------------+ +--------------+ |
| | tun_file [0] | | tun_file [1] | ... | tun_file[n-1]| |
| | Черга сокета | | Черга сокета | | Черга сокета | |
| +--------------+ +--------------+ +--------------+ |
| \ | / |
| \ | / |
| v v v |
| +-----------------------------------------------------------------+ |
| | struct tun_struct (net_device: tun0) | |
| | - Маршрутизація потоків через skb_get_hash() | |
| | - Неблокуючий розподіл через масив RCU | |
| +-----------------------------------------------------------------+ |
+-------------------------------------------------------------------------+
| ^
v Передавання пакетів | Приймання пакетів
+-------------------------------------------------------------------------+
| Мережевий стек IP та Netfilter ядра Linux |
+-------------------------------------------------------------------------+
1. Неблокуючий вибір черг через механізм RCU
У вихідному файлі ядра drivers/net/tun.c активні черги відстежуються через масив вказівників, захищений механізмом Read-Copy-Update (RCU): struct tun_file __rcu *tfiles[MAX_TAP_QUEUES]. Під час передавання вихідного пакета в інтерфейс tun0 ядро обирає потрібну чергу без захоплення загального блокування:
// Принцип вибору черги всередині tun.c ядра Linux
static struct tun_file *tun_get_queue(struct tun_struct *tun, struct sk_buff *skb)
{
u32 hash = skb_get_hash(skb);
u16 numqueues = READ_ONCE(tun->numqueues);
u16 index = reciprocal_scale(hash, numqueues);
return rcu_dereference(tun->tfiles[index]);
}
Завдяки безблокувальній RCU-індексації маршрутизація пакетів масштабується лінійно відповідно до кількості задіяних процесорних ядер.
2. Симетричний розподіл потоків
Щоб запобігти порушенню послідовності доставки пакетів у з'єднаннях TCP або UDP, ядро розраховує симетричний хеш за 4 параметрами заголовка за допомогою skb_get_hash(). Усі пакети одного клієнтського з'єднання завжди потрапляють до однієї черги, зберігаючи цілісність сесій при паралельній обробці тунелів.
Реалізація виділення кількох черг мовою C
Для створення інтерфейсу TUN із кількома чергами застосунок у просторі користувача має відкрити файл /dev/net/tun окремо для кожного робочого потоку. Після цього кожен потік викликає ioctl(TUNSETIFF) зі спільним ім'ям інтерфейсу та прапорцем IFF_MULTI_QUEUE.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <errno.h>
/**
* Приєднує новий файловий дескриптор до пристрою TUN із кількома чергами.
* @param dev Ім'я мережевого інтерфейсу (наприклад, "tun0")
* @return Виділений дескриптор у разі успіху, -1 у разі помилки
*/
int tun_alloc_mq(const char *dev)
{
struct ifreq ifr;
int fd;
// Відкриття незалежного екземпляра символьного пристрою
if ((fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK)) < 0) {
perror("Помилка під час відкриття /dev/net/tun");
return -1;
}
memset(&ifr, 0, sizeof(ifr));
// Режим TUN рівня L3, підтримка multiqueue та вимкнення зайвих заголовків
ifr.ifr_flags = IFF_TUN | IFF_MULTI_QUEUE | IFF_NO_PI;
if (dev && *dev) {
strncpy(ifr.ifr_name, dev, IFNAMSIZ - 1);
}
// Реєстрація дескриптора в мережевому інтерфейсі ядра
if (ioctl(fd, TUNSETIFF, (void *)&ifr) < 0) {
perror("Помилка системного виклику ioctl(TUNSETIFF)");
close(fd);
return -1;
}
return fd;
}
Додаткові ключові прапорці: IFF_NO_PI та IFF_VNET_HDR
Для досягнення максимальної швидкодії під час роботи з чергами прапорець IFF_MULTI_QUEUE комбінують із оптимізаційними налаштуваннями:
IFF_NO_PI(Без службових заголовків пакета): За замовчуванням драйвер TUN додає 4 байти службових даних (flagsтаproto) на початку кожного кадру. ПрапорецьIFF_NO_PIприбирає цей заголовок, дозволяючи сирим пакетам безпосередньо розміщуватися в пам'яті для SIMD-шифрування.IFF_VNET_HDR(Заголовок віртуальної мережі Virtio): Вмикає підтримку технологій Generic Segmentation Offload (GSO) та Generic Receive Offload (GRO) між ядром та користувацьким простором. Замість передавання окремих пакетів по 1500 байтів застосунок може передавати великі блоки розміром до 64 КБ, заощаджуючи до 75% системних викликів.
Архітектурні практики для багатопотокових VPN-шлюзів
Експлуатація IFF_MULTI_QUEUE у навантажених мережах потребує узгодження системних параметрів ядра, планувальника CPU та мережевих механізмів.
Вхідний зашифрований трафік WAN (Фізичний адаптер 100GbE)
|
v Апаратний розподіл RSS
+-------------------------------------------------------------------+
| Дешифрування пакетів та маршрутизація потоків у ядрі |
+-------------------------------------------------------------------+
| | |
v Ядро 0 v Ядро 1 v Ядро 2
+---------------+ +---------------+ +---------------+
| tun0 Queue 0 | | tun0 Queue 1 | | tun0 Queue 2 |
| fd_0 | | fd_1 | | fd_2 |
+---------------+ +---------------+ +---------------+
| | |
v v v
+---------------+ +---------------+ +---------------+
| Фіксація CPU 0| | Фіксація CPU 1| | Фіксація CPU 2|
| Потік Rice | | Потік Rice | | Потік Rice |
+---------------+ +---------------+ +---------------+
1. Жорстка прив'язка потоків до ядер процесора
Для забезпечення субмілісекундних затримок передавання кожен дескриптор черги повинен обслуговуватися окремим потоком, зафіксованим на ізольованому ядрі CPU за допомогою pthread_setaffinity_np(). Це зберігає дані в кеші L1/L2 і виключає втрати на між'ядерну синхронізацію шини пам'яті.
2. Налаштування довжини черги передавання
Кожен дескриптор tun_file має власний буфер приймання в ядрі. Під час різких стрибків трафіку стандартні ліміти можуть призводити до непомітної втрати пакетів. Шлюзам рекомендується збільшити довжину черги передавання (txqueuelen):
# Збільшення довжини черги інтерфейсу
ip link set dev tun0 txqueuelen 10000
3. Поєднання з асинхронним мультиплексуванням I/O
Кожен потік контролює свій дескриптор черги за допомогою механізмів epoll із прапорцем EPOLLET або кілець io_uring. Потоки зчитують пакети блоками за допомогою векторних викликів (readv або recvmmsg), запобігаючи переповненню буферів при пікових навантаженнях.
Масштабування багатоядерної обробки даних у RiceGuard
У мережевій інфраструктурі VPNRice рушій RiceGuard використовує можливості IFF_MULTI_QUEUE для неблокуючого оброблення трафіку у високощільних серверних кластерах.
- Топологія черг з урахуванням архітектури NUMA: RiceGuard автоматично визначає межі вузлів NUMA та канали оперативної пам'яті. Черги розподіляються симетрично та закріплюються за ядрами процесора, які мають прямий доступ до локальної шини PCIe, усуваючи перевантаження міжпроцесорних шин QuickPath / UltraPath.
- Векторне оброблення без зайвого копіювання: Поєднання багаточергових дескрипторів TUN із прапорцем
IFF_VNET_HDRта оптимізованими алгоритмами ChaCha20-Poly1305 дозволяє RiceGuard обробляти понад 25 мільйонів пакетів на секунду на стандартному серверному обладнанні. - Ізоляція в мережевих просторах імен: Кожен інтерфейс TUN із кількома чергами розгортається всередині ізольованого простору імен Linux network namespace. Така конфігурація запобігає витоку незашифрованого трафіку в загальні таблиці маршрутизації чи на зовнішні мережеві інтерфейси, забезпечуючи високу конфіденційність і повну утилізацію апаратних ресурсів.



