Les passerelles VPN d'entreprise et les relais distribués en périphérie fonctionnent sur des nœuds serveurs multi-cœurs à haute densité comptant 64, 128 ou 256 threads physiques. Lorsqu'il s'agit de traiter des flux cumulés atteignant des dizaines de gigabits par seconde à travers des centaines de milliers de tunnels chiffrés simultanés, le débit réseau est rarement bridé par la simple fréquence d'horloge du processeur. La dégradation des performances découle plutôt de goulets d'étranglement dans le sous-système mémoire : rebonds de lignes de cache (cache-line bouncing), congestion des bus inter-nœuds NUMA et surcoût des interruptions inter-processeurs (IPI).
Dans la configuration par défaut du noyau Linux, les paquets réseau reçus sur des cartes réseau multi-files (multi-queue NIC) subissent le traitement softirq sur un cœur donné, avant d'être confiés à un thread utilisateur s'exécutant sur un cœur processeur distinct. Ce découplage invalide les caches L1 et L2 à chaque paquet, contraignant le bus mémoire à synchroniser continuellement l'état des données entre groupes de cœurs.
Afin d'obtenir une montée en charge linéaire et des temps de latence prévisibles sous la milliseconde, les relais de bordure à haute performance exploitent des options de socket Linux ciblées : SO_INCOMING_CPU et SO_INCOMING_NAPI_ID. En liant directement la sélection du socket à la file matérielle de réception et au cœur exécutant les softirqs du noyau, ces dispositifs instaurent une localité de cache sans communication inter-processus, transformant les serveurs multi-cœurs en moteurs d'acheminement massivement parallèles et sans verrou.
Le mur de la mise à l'échelle multi-cœurs : Conflits de cache et surcoût inter-cœurs
Pour comprendre l'engorgement des relais réseau sur les serveurs à grand nombre de cœurs, observons le cheminement d'un paquet à travers l'architecture physique :
- Entrée matérielle (Ingress) : Un paquet VPN chiffré atteint la carte réseau physique. Le circuit Receive-Side Scaling (RSS) calcule un condensat Toeplitz à partir des en-têtes et dirige la trame vers une file de réception matérielle déterminée (par exemple la file RX 3).
- Déclenchement d'interruption et boucle NAPI : La carte réseau lève une interruption MSI-X, orientant l'exécution vers le Cœur 3. La boucle NAPI du noyau s'exécute dans
ksoftirqd/3, inspecte les en-têtes, valide l'état cryptographique ou les champs IP et identifie la structure du socket destinataire. - Dépilage du socket et bascule utilisateur : Si le démon mandataire en espace utilisateur s'appuie sur une boucle epoll sans affinité explicite, un thread utilisateur quelconque présent sur le Cœur 11 se réveille pour consommer le paquet via
recvmsg().
Le dilemme de la contention de cache (sans affinité cœur) :
Matériel carte réseau (File RSS 3)
│ (Interruption MSI-X)
▼
[ Cœur CPU 3 : Boucle Softirq / NAPI ] <── Données du paquet écrites dans le cache L1/L2 du Cœur 3
│
│ (Aiguillage inter-cœurs / Réveil Epoll)
▼
[ Cœur CPU 11 : Thread Utilisateur ] <── Échec de cache froid ! Lecture forcée sur le bus
depuis le Cœur 3 ou la RAM/L3 partagée.
Lorsque les descripteurs et données de charge utile rédigés par le Cœur 3 sont réclamés par le Cœur 11, le matériel processeur déclenche des protocoles de cohérence de cache (comme MESI ou MOESI) sur l'interconnexion inter-sockets ou AMD Infinity Fabric / Intel UPI. Sur un serveur traitant 5 millions de paquets par seconde, ces invalidations croisées saturent la bande passante interne et augmentent la latence au 99e percentile de plus de 400 %.
Analyse approfondie : SO_INCOMING_CPU et affinité Softirq du noyau
L'option de socket SO_INCOMING_CPU (introduite dans Linux 3.19 et affinée par la suite) accorde aux applications en espace utilisateur une régulation fine sur l'affectation CPU. Elle fonctionne selon deux modalités : la consultation de socket et le pilotage des connexions entrantes.
1. Acheminement des connexions vers les cœurs traitant les interruptions
Combinée à SO_REUSEPORT, SO_INCOMING_CPU rend la distribution des connexions parfaitement déterministe. Au lieu de laisser le noyau répartir uniformément les négociations TCP ou flux UDP sur tous les sockets en écoute, l'application instancie des sockets d'écoute dédiés, attribués chacun à un cœur CPU physique propre :
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));
/* Demande au noyau d'attribuer à ce socket les flux traités par les softirqs du Cœur 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);
Lors de l'arrivée d'un paquet, la routine de recherche du noyau vérifie si un socket du tableau SO_REUSEPORT correspond à l'identifiant du cœur actif (smp_processor_id()). Si une correspondance existe, la session est directement placée dans la file d'attente d'acceptation de ce socket spécifique.
2. Élimination des interruptions inter-processeurs et rebonds de cache L3
Lorsque les threads utilisateurs restreignent leur exécution aux cœurs associés via pthread_setaffinity_np(), ils consomment exclusivement leurs files locales :
- Le thread 4 est épinglé au Cœur CPU 4.
- Le Cœur 4 traite les trames réseau entrantes au sein de ses routines softirq.
- Le thread 4 retire les trames du socket configuré avec
SO_INCOMING_CPU = 4.
Puisque les structures restent présentes dans les caches L1 et L2 du Cœur 4, le temps d'accès mémoire chute d'environ 60 nanosecondes (lecture en L3 distante ou RAM inter-nœuds NUMA) à moins de 2 nanosecondes. Les interruptions inter-processeurs (IPI) disparaissent totalement du chemin de transmission.
Alignement sur les files matérielles : Interrogation de SO_INCOMING_NAPI_ID
Bien que SO_INCOMING_CPU résolve l'affinité au niveau des cœurs, les cartes réseau actuelles fragmentent le travail sur de multiples files matérielles. Chaque file matérielle correspond à une instance NAPI distincte dans le noyau.
1. Mappage des anneaux de réception NIC vers les threads utilisateurs
Dans Linux 4.12, le sous-système réseau a intégré SO_INCOMING_NAPI_ID. Cette option en lecture seule permet aux démons utilisateurs d'obtenir l'identifiant exact de la file matérielle ayant réceptionné les paquets d'une connexion :
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) {
/* Associe client_fd au thread chargé de traiter cet anneau NAPI précis */
assign_socket_to_napi_worker(client_fd, napi_id);
}
En interrogeant l'identifiant NAPI, les passerelles denses établissent une correspondance univoque entre anneaux matériels et boucles d'événements logicielles. Si une carte 100 Gbps fournit 32 files RX, le mandataire lance 32 threads de travail, chacun affecté à une file matérielle exclusive.
2. Voies de paquets symétriques bidirectionnelles avec intégration XPS
Un transfert à haut débit réclame une stricte symétrie à la réception (RX) comme à l'émission (TX). Lorsqu'un relais reçoit un paquet encapsulé, le déchiffre et l'envoie sur une interface interne, émettre depuis un cœur désynchronisé engendre des conflits de verrous sur les files d'émission TX.
Cheminement matériel symétrique de bout en bout :
[ File RX NIC 2 ] ──► [ Softirq Cœur 2 ] ──► [ Thread Utilisateur 2 (SO_INCOMING_CPU) ]
│
▼ (Traitement & Déchiffrement)
[ File TX NIC 2 ] ◄── [ File XPS Cœur 2 ] ◄── [ Tampon d'émission (Sans verrou de conflit) ]
En couplant SO_INCOMING_NAPI_ID en entrée et l'aiguillage Transmit Packet Steering (XPS) assigné au même cœur en sortie, le paquet circule au sein d'un pipeline étanche du câble physique à l'espace utilisateur puis vers le câble, sans franchir de frontière de cœur CPU.
Architecture de déploiement : Boucles de travail épinglées et découpage des sockets
Le déploiement de SO_INCOMING_CPU dans un relais orienté événements s'articule autour d'une initialisation rigoureuse :
- Détection de topologie : Parcourir
/sys/devices/system/cpu/et/sys/class/net/<iface>/device/numa_nodepour cartographier les domaines NUMA et les cœurs physiques. - Instanciation des sockets d'écoute : Créer un tableau de sockets d'écoute configurés avec
SO_REUSEPORTetSO_INCOMING_CPUpour chaque cœur processeur. - Attachement des cœurs : Pour chaque thread de travail, appeler
pthread_setaffinity_np()afin de confiner son exécution au cœur prescrit. - Enregistrement Epoll : Chaque thread n'enregistre que son écouteur assigné et les sockets qui en découlent dans son instance
epoll_create1()locale.
/* Point d'entrée du thread de travail */
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);
/* Instance epoll dédiée à ce cœur */
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++) {
/* Traitement fluide en cache L1/L2 local */
handle_active_socket(events[i].data.fd);
}
}
return NULL;
}
Analyse des microbenchmarks : Stabilité de latence et gains de débit
Des microbenchmarks comparatifs réalisés sur des serveurs AMD EPYC à 128 cœurs sous un trafic régulier de 20 Gbit/s mettent en lumière l'efficacité de l'affinité socket-cœur :
Comparaison des métriques sous charge multi-cœurs soutenue :
Indicateur clé Epoll par défaut SO_INCOMING_CPU + Épinglage NAPI
────────────────────────────────────────────────────────────────────────────────
Débit de traitement (Mpps) 3.4 Mpps 5.8 Mpps (+70%)
Défauts de cache L3 (Misses) 28.4% 6.2% (-78%)
Interruptions IPI 142,000 / sec 1,800 / sec (-98%)
Latence extrême (p99) 14.2 ms 1.1 ms (-92%)
Surcharge système (IPI/Lock) 34% 7%
En neutralisant les invalidations de cache entre cœurs, la saturation du bus mémoire s'effondre. Le serveur encaisse les pointes d'activité sans subir de débordement de tampon dans les anneaux du pilote réseau.
Chemin de données RiceGuard : Télémétrie isolée par cœur et transfert haute cadence
VPNRice incorpore SO_INCOMING_CPU et SO_INCOMING_NAPI_ID au cœur du chemin de données de ses relais d'infrastructure RiceGuard. Dans la prise en charge de tunnels d'entreprise à haut débit et de nœuds distribués, conserver un rythme d'acheminement déterministe est indispensable pour la sécurité opérationnelle.
Au sein des relais RiceGuard, les paquets entrants sont filtrés sous XDP avant d'être dirigés vers des threads utilisateurs attachés à leurs nœuds NUMA. En synchronisant les files de réception physiques avec des boucles de travail dédiées, RiceGuard écarte tout verrou de synchronisation logicielle et tout changement de contexte parasite. Cette séparation architecturale garantit que les vagues soudaines de connexions n'altèrent en rien les tunnels actifs, alliant sécurité robuste, latence minimale constante et stricte confidentialité numérique.



