Корпоративні шлюзи VPN та розподілені вузли маршрутизації функціонують на багатоядерних серверах високої щільності з 64, 128 або 256 фізичними обчислювальними потоками. Під час опрацювання значного сукупного трафіку обсягом у десятки гігабітів на секунду через сотні тисяч одночасних захищених тунелів мережева продуктивність рідко обмежується базовою тактовою частотою процесора. Погіршення швидкодії виникає внаслідок вузьких місць у підсистемі пам'яті: переміщення ліній кешу між ядрами (cache-line bouncing), перевантаження внутрішніх шин зв'язку вузлів NUMA та накладних витрат на міжпроцесорні переривання (IPI).

За типових налаштувань ядра Linux мережеві пакети, які надходять на багаточергові мережеві карти (multi-queue NIC), проходять обробку softirq на одному процесорному ядрі, після чого передаються робочому потоку в просторі користувача, що діє на зовсім іншому ядрі. Такий розрив призводить до скидання кешів L1 та L2 для кожного окремого пакета, змушуючи системну шину безперервно синхронізувати стан пам'яті між різними кластерами процесора.

Для досягнення лінійного масштабування та збереження стабільної затримки передачі на рівні до однієї мілісекунди продуктивні проксі-сервери застосовують спеціалізовані опції сокетів Linux: SO_INCOMING_CPU та SO_INCOMING_NAPI_ID. Завдяки прямому зв'язуванню сокетів із чергами апаратного прийому та ядрами, що виконують код softirq, ці механізми забезпечують локалізацію кешу без використання міжпроцесної взаємодії, перетворюючи багатоядерні сервери на паралельні неблокуючі конвеєри передачі даних.


Бар'єр масштабованості багатоядерних систем: Конфлікти кешу та між'ядерні витрати

Щоб збагнути причину затримок у роботі мережевих шлюзів на багатоядерних платформах, розглянемо фізичний рух пакета через компоненти сучасного сервера:

  1. Апаратний прийом (Ingress): Зашифрований пакет надходить на мережевий адаптер. Механізм Receive-Side Scaling (RSS) обчислює хеш Toeplitz на основі заголовків і спрямовує кадр у відповідну апаратну чергу прийому (наприклад, RX Queue 3).
  2. Переривання та цикл NAPI: Мережева карта надсилає переривання MSI-X, передаючи виконання Ядру 3. Цикл NAPI всередині потоку ksoftirqd/3 перевіряє заголовки пакета, валідує криптографічний стан або поля IP і знаходить потрібну структуру сокета.
  3. Вибірка із сокета та диспетчеризація: Якщо фоновий процес користувацького простору послуговується звичайним опитуванням epoll без прив'язки ядер, випадковий робочий потік на Ядрі 11 активується для читання пакета через виклик recvmsg().
Складнощі через скидання кешу (без прив'язки до процесорних ядер):

Мережева карта (Черга RSS 3)
         │ (Переривання MSI-X)
         ▼
[ Ядро CPU 3: Цикл Softirq / NAPI ]    <── Дані пакета записано у кеш L1/L2 Ядра 3
         │
         │ (Між'ядерна передача / Пробудження Epoll)
         ▼
[ Ядро CPU 11: Робочий потік процесу ] <── Холодний промах кешу! Примусове зчитування
                                            через шину пам'яті з Ядра 3 або спільного L3/RAM.

Коли структури опису та корисне навантаження, підготовлені Ядром 3, зчитуються Ядром 11, процесор змушений запускати протоколи узгодження кешу (MESI або MOESI) через шини зв'язку чи канали AMD Infinity Fabric / Intel UPI. На сервері, який опрацьовує 5 мільйонів пакетів за секунду, перехресне скидання кешу повністю вичерпує пропускну здатність внутрішніх шин, збільшуючи затримку p99 більш ніж на 400%.


Глибокий аналіз: SO_INCOMING_CPU та прив'язка до Softirq ядра

Опція сокета SO_INCOMING_CPU (впроваджена у ядрі Linux 3.19) надає програмам користувацького простору гнучкий контроль над розподілом з'єднань між процесорними ядрами. Вона діє у двох режимах: аналіз сокета та цілеспрямоване спрямування вхідних потоків.

1. Спрямування з'єднань на ядра, що опрацьовують мережеві переривання

У парі з прапорцем SO_REUSEPORT механізм SO_INCOMING_CPU гарантує чіткий вибір сокета. Замість випадкового розподілу нових сесій TCP чи потоків UDP між усіма відкритими сокетами, програма відкриває окремі сокети під кожне фізичне ядро CPU:

int cpu_id = 4;
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

int enable_reuseport = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &enable_reuseport, sizeof(enable_reuseport));

/* Вказівка ядру направляти з'єднання від softirq Ядра 4 у цей сокет */
setsockopt(listen_fd, SOL_SOCKET, SO_INCOMING_CPU, &cpu_id, sizeof(cpu_id));

bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(listen_fd, 1024);

Під час надходження пакета системна процедура пошуку перевіряє, чи відповідає ідентифікатор активного ядра (smp_processor_id()) сокету в масиві SO_REUSEPORT. За наявності збігу з'єднання розміщується безпосередньо у черзі прийому цього конкретного сокета.

2. Усунення міжпроцесорних переривань та міграції ліній кешу L3

Коли робочі потоки простору користувача фіксуються на відповідних ядрах через виклик pthread_setaffinity_np(), вони зчитують дані суто з локальних черг:

  • Потік 4 виконується на Ядрі CPU 4.
  • Ядро 4 опрацьовує вхідні пакети у межах підсистеми softirq.
  • Потік 4 забирає кадри із сокета з налаштуванням SO_INCOMING_CPU = 4.

Оскільки інформація залишається у локальних кешах L1 та L2 Ядра 4, час доступу до пам'яті скорочується з приблизно 60 наносекунд (у разі вибірки з віддаленого кешу L3 або пам'яті іншого вузла NUMA) до менш ніж 2 наносекунд. Сигнали міжпроцесорних переривань (IPI) зникають із тракту обробки даних.


Синхронізація з апаратними чергами: Запит SO_INCOMING_NAPI_ID

Тоді як SO_INCOMING_CPU здійснює закріплення на рівні ядер, мережеві плати розподіляють потоки даних між фізичними чергами. Кожній апаратній черзі відповідає окрема структура NAPI всередині ядра.

1. Зіставлення черг мережевої карти з робочими потоками програми

У ядрі Linux 4.12 з'явилася опція сокета SO_INCOMING_NAPI_ID. Завдяки їй процес простору користувача може дізнатися точний номер апаратної черги, яка прийняла пакети поточного з'єднання:

unsigned int napi_id = 0;
socklen_t optlen = sizeof(napi_id);

if (getsockopt(client_fd, SOL_SOCKET, SO_INCOMING_NAPI_ID, &napi_id, &optlen) == 0) {
    /* Передача client_fd потоку, призначеному для цього кільця NAPI */
    assign_socket_to_napi_worker(client_fd, napi_id);
}

Зчитуючи NAPI ID, навантажені шлюзи встановлюють точне співвідношення 1:1 між фізичними чергами адаптера та робочими циклами подій. Якщо мережева карта на 100 Гбіт/с підтримує 32 черги RX, проксі створює 32 потоки, закріплюючи кожен за відповідною чергою.

2. Симетричні двоспрямовані шляхи пакетів з інтеграцією XPS

Висока продуктивність потребує симетрії як під час отримання (RX), так і під час надсилання (TX). Коли шлюз приймає пакет, розшифровує його та спрямовує у внутрішній мережевий інтерфейс з неузгодженого ядра, це спричиняє затримки на чергах надсилання TX.

Симетричний апаратний конвеєр обробки трафіку:

[ Черга RX NIC 2 ] ──► [ Softirq Ядра 2 ] ──► [ Потік 2 (SO_INCOMING_CPU) ]
                                                            │
                                                            ▼ (Опрацювання та розшифрування)
[ Черга TX NIC 2 ] ◄── [ Черга XPS Ядра 2 ] ◄── [ Буфер відправлення (Без блокувань) ]

Поєднуючи SO_INCOMING_NAPI_ID на вході та технологію Transmit Packet Steering (XPS), прив'язану до того ж ядра на виході, пакет проходить ізольований маршрут від кабелю до простору користувача і знову в кабель без виходу за межі процесорного ядра.


Архітектура розгортання: Закріплені робочі цикли та поділ сокетів

Інтеграція SO_INCOMING_CPU в асинхронний мережевий шлюз передбачає чітку послідовність початкових дій:

  1. Визначення топології: Перевірка системних каталогів /sys/devices/system/cpu/ та /sys/class/net/<iface>/device/numa_node для з'ясування розташування вузлів NUMA та фізичних ядер.
  2. Створення сокетів очікування: Побудова масиву сокетів із прапорцями SO_REUSEPORT та SO_INCOMING_CPU, співвіднесених із кожним ядром CPU.
  3. Прив'язка обчислень: Для кожного робочого потоку виконується pthread_setaffinity_np() для обмеження роботи призначеним ядром.
  4. Реєстрація в Epoll: Кожен потік додає до свого локального екземпляра epoll_create1() лише власний слухаючий сокет та отримані з нього з'єднання.
/* Точка запуску робочого потоку */
void *worker_loop(void *arg) {
    int core_id = (int)(intptr_t)arg;
    
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(core_id, &cpuset);
    pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset);
    
    /* Окремий epoll-цикл для обраного ядра */
    int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listeners[core_id], &ev);
    
    while (running) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        for (int i = 0; i < nfds; i++) {
            /* Швидке опрацювання у гарячому локальному кеші L1/L2 */
            handle_active_socket(events[i].data.fd);
        }
    }
    return NULL;
}

Аналіз мікробенчмарків: Стабільність затримки та приріст пропускної здатності

Порівняльні дослідження на 128-ядерних серверах AMD EPYC за умов постійного потоку трафіку 20 Гбіт/с наочно підтверджують ефективність прив'язки сокетів до ядер:

Порівняння показників за значного багатоядерного навантаження:

Критерій оцінки               Звичайний Epoll      SO_INCOMING_CPU + Прив'язка NAPI
────────────────────────────────────────────────────────────────────────────────
Швидкість обробки (Mpps)      3.4 Mpps             5.8 Mpps (+70%)
Промахи кешу L3 (Misses)      28.4%                6.2% (-78%)
Переривання IPI               142,000 / сек        1,800 / сек (-98%)
Хвостова затримка (p99)       14.2 мс              1.1 мс (-92%)
Системне навантаження (IPI)   34%                  7%

Завдяки усуненню між'ядерного скидання кешу рівень навантаження на системну шину істотно знижується. Сервер впевнено витримує різкі напливи трафіку без втрати пакетів у кільцевих буферах мережевого драйвера.


Тракт даних RiceGuard: Ізоляція ядер для телеметрії та швидкісна передача

VPNRice впроваджує технології SO_INCOMING_CPU та SO_INCOMING_NAPI_ID безпосередньо у тракт обробки пакетів вузлів RiceGuard. Під час обслуговування швидкісних корпоративних тунелів та розподілених точок збереження передбачуваного темпу пересилання є головною умовою стабільної безпеки.

На серверах RiceGuard вхідний трафік фільтрується за допомогою XDP до переходу в процеси простору користувача з прив'язкою до вузлів NUMA. Узгодження апаратних черг із робочими потоками усуває взаємні блокування та зайві перемикання контексту. Така системна ізоляція гарантує, що інтенсивні запити на нові підключення не вплинуть на роботу активних сесій, поєднуючи високу безпеку, постійно малу затримку та сувору цифрову конфіденційність.