Корпоративные шлюзы VPN и распределенные пограничные узлы функционируют на высокоплотных многоядерных серверах с 64, 128 или 256 физическими потоками CPU. При обработке непрерывного совокупного трафика в десятки гигабит в секунду через сотни тысяч одновременных зашифрованных туннелей сетевая пропускная способность редко упирается в тактовую частоту процессора. Снижение производительности происходит из-за узких мест в подсистеме памяти: переброски кэш-линий между ядрами (cache-line bouncing), перегрузки межпроцессорных шин в архитектуре NUMA и накладных расходов на межпроцессорные прерывания (IPI).
При стандартных настройках ядра Linux сетевые пакеты, поступающие на многоочередные сетевые карты (multi-queue NIC), проходят обработку softirq на одном процессорном ядре, после чего передаются рабочему потоку в пространстве пользователя, выполняющемуся на совершенно другом ядре. Такое архитектурное разделение аннулирует кэши L1 и L2 для каждого пакета, заставляя системную шину непрерывно синхронизировать память между кластерами ядер.
Для достижения линейного масштабирования и стабильной задержки менее одной миллисекунды высокопроизводительные прокси-серверы задействуют специализированные опции сокетов Linux: SO_INCOMING_CPU и SO_INCOMING_NAPI_ID. Связывая диспетчеризацию сокетов напрямую с аппаратными очередями приема и ядрами, выполняющими softirq ядра, эти инструменты гарантируют локализацию данных в кэше без межпроцессного взаимодействия, превращая многоядерные серверы в параллельные неблокирующие конвейеры передачи данных.
Барьер масштабируемости многоядерных систем: Конфликты кэша и межъядерные издержки
Чтобы понять причину задержек в работе сетевых шлюзов на многоядерных платформах, проследим путь пакета через физические компоненты сервера:
- Аппаратный прием (Ingress): Зашифрованный пакет поступает на сетевой адаптер. Механизм Receive-Side Scaling (RSS) вычисляет хэш Toeplitz по полям заголовков и направляет кадр в конкретную аппаратную очередь приема (например, RX Queue 3).
- Прерывание и цикл NAPI: Сетевая карта инициирует прерывание MSI-X, передавая выполнение Ядру 3. В цикле NAPI ядра внутри потока
ksoftirqd/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 в асинхронный пограничный шлюз требует четкой последовательности инициализации:
- Определение топологии: Чтение каталогов
/sys/devices/system/cpu/и/sys/class/net/<iface>/device/numa_nodeдля выявления узлов NUMA и физических ядер. - Создание слушающих сокетов: Формирование массива сокетов с флагами
SO_REUSEPORTиSO_INCOMING_CPU, сопоставленных с каждым ядром CPU. - Привязка потоков: Для каждого рабочего процесса вызов
pthread_setaffinity_np()для ограничения выполнения выделенным ядром. - Регистрация в 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/Lock) 34% 7%
Благодаря устранению межъядерной инвалидации кэша снижается паразитная нагрузка на системную шину памяти. Сервер стабильно справляется с резкими всплесками трафика без потерь пакетов в кольцевых буферах сетевого драйвера.
Тракт данных RiceGuard: Изоляция ядер для телеметрии и высокоскоростная передача
VPNRice интегрирует механизмы SO_INCOMING_CPU и SO_INCOMING_NAPI_ID непосредственно в тракт обработки данных узлов RiceGuard. При обслуживании скоростных корпоративных соединений и пограничных точек поддержание стабильной скорости обработки пакетов критически важно для защиты инфраструктуры.
На узлах RiceGuard входящие пакеты проходят фильтрацию XDP перед передачей в процессы пространства пользователя с привязкой к узлам NUMA. Согласование аппаратных очередей с рабочими потоками устраняет взаимные блокировки и накладные расходы на переключение контекста. Такая архитектурная изоляция гарантирует, что интенсивные волны запросов на подключение не нарушат работу активных туннелей пользователей, сохраняя высокую безопасность, низкую задержку и надежную цифровую конфиденциальность.



