Производительность сетевого стека современных серверов зависит не только от скорости процессора или объема оперативной памяти. Критически важным, но часто упускаемым из виду фактором является архитектура приема пакетов на уровне сетевой карты (NIC) и их распределение по ядрам процессора. Когда программная оптимизация (добавление потоков, тюнинг сокетов, профилирование горячих путей) перестает давать прирост, узким местом оказывается «железо» — именно способ, которым сетевая карта доставляет пакеты в память и сигнализирует процессору об их llegada.
В этой статье мы подробно разберем эволюцию механизмов приема: от однопоточных очередей к многоочередной архитектуре, аппаратному масштабированию через RSS (Receive Side Scaling) и программным механизмам RPS (Receive Packet Steering) и RFS (Receive Flow Steering) в ядре Linux.
Проблема однопоточной очереди (Single Queue NIC)
Традиционные или бюджетные сетевые карты часто имеют только одну пару очередей приема/передачи (RX/TX Ring). В таком сценарии:
- Все входящие пакеты, независимо от их типа или принадлежности к потоку, попадают в единое кольцевое кольцо (Ring Buffer).
- Сетевая карта поднимает одно прерывание (IRQ) для уведомления CPU.
- Обработку всех пакетов выполняет единственное ядро процессора.
На низких нагрузках это не создает проблем. Однако при росте пакетной нагрузки (packet rate) возникает «узкое горлышко» сериализации:
- Ядро, обрабатывающее прерывания, загружается на 100% (softirq / NAPI poll).
- Кольцевой буфер переполняется, происходят потери пакетов (drops).
- Остальные ядра процессора простаивают, не участвуя в обработке сети.
Важно: Это не программная проблема. Нельзя решить её добавлением потоков в приложение или тюнингом SO_REUSEPORT. Проблема кроется ниже — на уровне аппаратной архитектуры NIC.
Решение: Многоочередные сетевые карты (Multi-Queue NIC)
Современные серверные NIC (Intel X710, E810, Mellanox ConnectX и др.) предоставляют N независимых пар очередей RX/TX. Каждая пара представляет собой полноценный конвейер:
- RX Ring (Descriptor Ring): Массив дескрипторов, указывающих на буферы в оперативной памяти (DMA), куда NIC записывает входящие пакеты.
- TX Ring: Дескрипторы для пакетов, которые CPU готовит к отправке.
- Выделенный вектор прерывания MSI-X: Каждая очередь имеет свой уникальный номер прерывания (IRQ). Это позволяет ядру направить обработку конкретной очереди на конкретное ядро CPU.
Когда срабатывает прерывание от очереди 0 (IRQ N), планировщик NAPI запускает функцию опроса (napi_poll) для этой очереди на привязанном к ней ядре (например, CPU 0). Это обеспечивает истинный параллелизм на уровне железа: 4 очереди позволяют 4 ядрам обрабатывать пакеты одновременно без блокировок (lockless) и конкуренции за кэш.
Но просто наличие очередей недостаточно. Нужен механизм, который решит: в какую именно очередь положить конкретный пакет? Ответ на этот вопрос дает RSS.
RSS (Receive Side Scaling) — Аппаратное распределение потоков
RSS — это технология, реализованная в прошивке/железе сетевой карты. Она вычисляет хеш от заголовков пакета и на основе этого хеша выбирает целевую очередь.
Ключ хеширования: 5-кортеж (5-tuple)
Стандартный вход для хеш-функции — это 5-кортеж:
- Source IP (Исходный IP)
- Destination IP (Целевой IP)
- Source Port (Исходный порт)
- Destination Port (Целевой порт)
- Protocol (Протокол: TCP, UDP и т.д.)
Алгоритм хеширования, как правило, основан на хешировании Тёплица (Toeplitz hashing) с настраиваемым секретным ключом.
Главное свойство RSS: Аффинити потока (Flow Affinity)
Пакеты, принадлежащие одному и тому же потоку (одинаковый 5-кортеж), всегда имеют одинаковый хеш и попадают в одну и ту же очередь.
Это критически важно для производительности:
- Локальность кэша (Cache Locality): Структуры сокета (
struct sock), TCP-буферы и данные приложения остаются в кэше (L1/L2/L3) того ядра, которое обрабатывает этот поток. Нет миграции данных между ядрами (cache bouncing). - Порядок пакетов: Пакеты внутри одного TCP-потока обрабатываются строго последовательно на одном ядре, что исключает переупорядочивание (reordering) и сложные гонки данных.
- Отсутствие блокировок: Нет необходимости в спинлоках (spinlocks) для защиты структур сокета от одновременного доступа с разных ядер.
Качество хеш-ключа (RSS Key)
Не все ключи одинаково хороши. Симметричный ключ, частая ошибка: он дает одинаковый хеш для прямого и обратного направлений одного TCP-соединения (Source<->Dest меняются местами). В результате оба направления трафика ложатся на одну очередь, перегружая одно ядро, в то время как другие простаивают.
Современные драйверы используют асимметричные ключи по умолчанию. Проверить распределение можно командой:
ethtool -x eth0 Ограничения RSS и программные механизмы ядра
RSS работает на уровне железа и имеет ограничения:
- Фиксированное число очередей: Нельзя создать больше очередей, чем поддерживает NIC (обычно 16–64, редко больше).
- Негибкость хеша: Нельзя легко изменить логику распределения для специфических задач (например, приоритетный трафик на выделенные ядра).
- Инкапсуляция (Tunneling): При VXLAN/GENEVE/GRE «внутренний» 5-кортеж скрыт внутри внешнего заголовка. Старые NIC хешируют только внешний IP (те же VTEP IP), отправляя весь туннелированный трафик в одну очередь.
Для решения этих проблем в ядре Linux реализованы программные механизмы: RPS и RFS.
RPS (Receive Packet Steering) — Программный RSS
RPS — это программная реализация логики RSS, выполняемая на стадии netif_receive_skb (после того, как пакет уже получен драйвером из кольца NIC и выделен sk_buff).
Как работает RPS:
- Пакет приходит в единственную (или малую группу) аппаратную очередь NIC.
- Драйвер обрабатывает прерывание на CPU 0 (например).
- В функции
netif_receive_skbядро вычисляет хеш (используя тот же 5-кортеж, что и RSS, но программно). - На основе хеша и настроенной «карты» (RPS flow table / CPU map) пакет ставится в очередь бэклога (backlog) другого CPU (CPU 1, CPU 2…).
- Целевое CPU получает IPI (Inter-Processor Interrupt) и обрабатывает пакет в своем контексте softirq (NET_RX_SOFTIRQ).
Настройка RPS:
Управляется через sysfs:
/sys/class/net/eth0/queues/rx-0/rps_cpus, битовая маска CPU, которые могут обрабатывать пакеты с этой очереди./sys/class/net/eth0/queues/rx-0/rps_flow_cnt, размер хеш-таблицы потоков (для сохранения аффинити).
Плюсы и минусы RPS:
| Плюсы | Минусы |
|---|---|
| Работает на любом NIC (даже с 1 очередью). | Первое прерывание и ранняя обработка (DMA, аллокация skb) всё равно на CPU 0. |
| Гибкая логика (можно кастомизировать хеш). | Нагрузка на кэш: метаданные skb «путешествуют» между ядрами (cache line bouncing). |
| Помогает при туннелировании (может хешировать внутренние заголовки, если настроено). | Дополнительная задержка (latency) из-за IPI и перепланирования. |
RFS (Receive Flow Steering), Умное направление к сокету
RFS — это расширение RPS. Если RPS распределяет пакеты просто «равномерно по ядрам», то RFS пытается направить пакет именно на то ядро, где в данный момент выполняется приложение-получатель (где находится сокет).
Механизм RFS:
- Приложение вызывает
recvmsg/epoll_waitна CPU X. Ядро запоминает: «Сокет S аффинен к CPU X» (записывает вrps_sock_flow_table). - Приходит пакет для сокета S.
- RFS смотрит в таблицу: «Ага, сокет S был недавно использован на CPU X».
- Пакет ставится в бэклог CPU X.
Преимущество RFS:
Максимальная локальность данных. Структура struct sock, приемный буфер сокета (receive buffer) и память приложения уже «горячие» в кэше CPU X. Это дает наилучшую производительность по задержкам (latency) и пропускной способности для коротких соединений / RPC.
Настройка RFS:
/proc/sys/net/core/rps_sock_flow_entries, глобальный размер таблицы аффинити сокетов (рекомендуется: число очередей * среднее число потоков на очередь, например 32768)./sys/class/net/eth0/queues/rx-0/rps_flow_cnt— должен быть установлен (обычно = rps_sock_flow_entries / N_queues).
Современные NIC и туннелирование: RSS для инкапсулированного трафика
Современные карты (поддержка RSS в туннелях / Tunnel RSS) умеют «заглядывать» внутрь VXLAN/GENEVE/GRE и вычислять хеш по внутреннему 5-кортежу (Inner IP, Inner Ports).
Это позволяет распределять трафик контейнеров/VM по разным очередям NIC уже на железе, не нагружая CPU 0 и не требуя RPS для разбора туннелей. Требует поддержки драйвером (например, mlx5, ice, i40e) и включения в ethtool:
ethtool -K eth0 rx-gro-hw on
ethtool -N eth0 rx-flow-hash udp4 sdfn # пример настройки хеша для UDP туннелей Практический чек-лист тюнинга приема пакетов
- Проверьте количество очередей NIC:
ethtool -l eth0. Убедитесь, что Combined RX/TX queues = числу ядер (или NUMA-ноды). - Включите/проверьте RSS:
ethtool -x eth0(покажет таблицу перенаправления indirection table и ключ). - Распределите IRQ (irqbalance или ручная привязка): Каждой очереди — свое ядро. Избегайте коллизий с другими устройствами (NVMe, GPU).
- Настройте RPS/RFS (если очередей меньше ядер или есть туннели):
- Включите
rps_cpusдля очередей. - Выставьте
rps_sock_flow_entriesиrps_flow_cnt.
- Включите
- Включите XPS (Transmit Packet Steering) для TX: Аналог RPS для отправки, привязывает TX очереди к ядрам, откуда идет
send./sys/class/net/eth0/queues/tx-*/xps_cpus. - Мониторьте:
cat /proc/softirqs(NET_RX),ethtool -S eth0 | grep -i drop,mpstat -P ALL 1(баланс нагрузки по ядрам).
Управление приемом пакетов — это иерархия механизмов:
- RSS (Hardware) — фундамент. Дает параллелизм без накладных расходов, сохраняет порядок и кэш. Обязателен для высоконагруженных систем.
- RPS (Software) — «костыль» и расширение. Спасает, если очередей NIC мало, или для туннелированного трафика на старом железе. Платит за гибкость накладными расходами на межпроцессорное взаимодействие.
- RFS (Software + App Awareness) — вершина оптимизации локальности. Направляет пакет туда, где ждет приложение. Дает лучшую латентность.
Понимание того, как пакет путешествует от DMA-буфера NIC через прерывание, NAPI, RPS/RFS к сокету приложения, позволяет инженерам не просто «крутить ручки» в sysctl, а строить предсказуемые, масштабируемые высоконагруженные системы.