Les passerelles VPN à forte charge traitant des dizaines de milliers de tunnels chiffrés simultanés rencontrent fréquemment un goulot d'étranglement matériel : un déséquilibre marqué de la charge de calcul entre les différents cœurs du processeur. Un serveur équipé de 32 ou 64 cœurs peut afficher une utilisation globale du CPU inférieure à 25 %, tout en subissant des pertes de paquets massives lors des pointes de trafic. L'analyse des métriques système révèle généralement que le cœur CPU 0 sature à 100 % sous les interruptions logicielles (ksoftirqd/0 et NET_RX_SOFTIRQ), tandis que les autres cœurs demeurent inactifs.
Ce déséquilibre découle de la manière dont les cartes réseau et les protocoles d'encapsulation de tunnel interagissent avec la pile réseau du système d'exploitation. Lorsque la mise à l'échelle matérielle Receive Side Scaling (RSS) ne parvient pas à disperser uniformément les datagrammes encapsulés, Linux met à disposition deux mécanismes d'accélération logicielle : Receive Packet Steering (RPS) et Receive Flow Steering (RFS). Une configuration rigoureuse de ces sous-systèmes permet de répartir le traitement des paquets sur l'ensemble des cœurs de calcul disponibles tout en préservant la localité spatiale de la mémoire cache.
Le goulot d'étranglement de l'encapsulation dans le trafic entrant
Les cartes d'interface réseau physiques (NIC) distribuent les trames entrantes entre plusieurs files d'attente de réception matérielles via l'algorithme RSS. La carte calcule un hachage Toeplitz sur les en-têtes du paquet — généralement un quadruplet composé de l'adresse IP source, de l'adresse IP de destination, du port source et du port de destination — pour assigner chaque flux à un tampon circulaire de réception (RX ring buffer). Chaque file déclenche une interruption matérielle MSI-X assignée à un cœur CPU spécifique.
Limites de la technologie RSS face au trafic VPN encapsulé :
[ Clients distants ] ---> [ Paquets UDP WireGuard / IPsec / RiceGuard ]
|
v
[ Port de destination unique (ex. UDP 51820) ]
|
[ Hachage Toeplitz de la carte ]
|
=================================================
Tous les flux convergent vers la file RX 0 (Une seule IRQ)
=================================================
|
v
[ Cœur CPU 0 : 100% ksoftirqd / NET_RX_SOFTIRQ ] ---> Saturation du tampon !
[ Cœurs CPU 1 à 31 : 0% de charge SoftIRQ ]
Lorsque des milliers d'utilisateurs se connectent à un concentrateur VPN via des tunnels WireGuard, IPsec ou des datagrammes personnalisés, le flux externe converge vers un port d'écoute unique sur la passerelle. Si de multiples connexions transitent en amont par des passerelles NAT d'opérateurs (Carrier-Grade NAT), le hachage du quadruplet s'effondre sur un sous-ensemble très réduit de files d'attente matérielles.
Par ailleurs, les interfaces virtuelles de tunnel (tun ou tap) ne disposent pas par défaut de mécanisme multi-file matériel. Lorsque la passerelle déchiffre les données et transmet les paquets IP internes à la pile réseau du noyau, le traitement des interruptions logicielles se concentre lourdement sur le cœur exécutant le thread de déchiffrement. Les tampons se remplissent, la gigue et les temps de latence s'envolent, provoquant des pertes de paquets silencieuses qui dégradent les communications en temps réel.
Receive Packet Steering : équilibrage logiciel multi-cœur
Receive Packet Steering distribue la charge de traitement des paquets sur un masque de cœurs CPU désigné par l'administrateur, une fois l'interruption matérielle initiale achevée. Contrairement au RSS matériel qui réside dans les puces électroniques de la carte réseau, RPS s'exécute de manière logicielle durant la phase de scrutation NAPI du pilote (netif_receive_skb).
Architecture de distribution logicielle multi-cœur RPS :
[ Paquet entrant sur la file de réception RX 0 ]
|
v
[ Routine de scrutation NAPI ]
|
[ Masque CPU rps_cpus : ex. 0x0000ffff (Cœurs 0 à 15) ]
|
[ Calcul du hachage jhash sur les en-têtes réseau ]
|
v
[ Mise en file sk_buff dans la file input_pkt_queue du cœur cible ]
|
[ Interruption inter-processeur (IPI) : déclenche NET_RX sur la cible ]
|
+-----------------+-----------------+-----------------+
| | | |
v v v v
[Cœur 0] [Cœur 1] [Cœur 2] [Cœur 3] ... [Cœur 15]
Lorsqu'un paquet arrive, le pilote vérifie si RPS est activé sur la file correspondante via l'arborescence sysfs. Si tel est le cas, le noyau calcule un hachage des en-têtes réseau et transport via l'algorithme Jenkins (jhash). Le résultat est mis en correspondance avec l'un des cœurs CPU autorisés par la chaîne binaire rps_cpus.
Le processeur récepteur initial place le descripteur sk_buff dans la file d'attente interne (input_pkt_queue) du cœur cible et envoie une interruption inter-processeur (IPI). Le cœur cible prend alors en charge la suite du traitement réseau : filtrage du pare-feu, consultation de la table de routage et transmission finale vers la socket applicative.
Receive Flow Steering : affinité de flux et localité du cache
Bien que RPS distribue efficacement les calculs sur plusieurs cœurs, il détermine le processeur cible uniquement à partir d'un hachage mathématique. Il ignore sur quel cœur s'exécute le thread de l'application en espace utilisateur qui lit les données de la socket.
Si le cœur CPU 4 traite le paquet au sein du noyau, mais que le service VPN lit les données sur le cœur CPU 12, les octets de la charge utile doivent transiter à travers les bus d'interconnexion du processeur. Ce transfert de données inter-cœur engendre des défauts de cache L1/L2, invalide les lignes de cache et dégrade le débit de la mémoire système.
Receive Flow Steering résout cette inefficacité en orientant directement le traitement du paquet vers le cœur CPU sur lequel le thread applicatif effectue ses opérations de lecture sur la socket.
Fonctionnement de l'affinité de flux RFS :
1. Le thread applicatif sur le Cœur 3 appelle recvmsg(sock_fd)
---> Le noyau met à jour la table globale (rps_sock_flow_table) :
[ Hachage du flux #A18C -> Exécuté sur le Cœur 3 ]
2. Le paquet suivant du flux #A18C parvient à la carte réseau
---> Le pilote interroge rps_sock_flow_table :
Cœur cible = Cœur 3 (Données présentes dans la mémoire cache locale !)
3. sk_buff est placé directement dans l'input_pkt_queue du Cœur 3
---> Les interruptions logicielles et l'application partagent le même cache L1/L2 !
RFS coordonne deux tables internes :
- Table globale des flux de sockets (
rps_sock_flow_table) : Enrichie automatiquement dès qu'un thread appelle les fonctions systèmerecvmsg(2),read(2)ouepoll_wait(7). Le noyau consigne le hachage du flux et l'identifiant du cœur CPU appelant. - Table locale par file (
rps_flow_table) : Tenue à jour sur chaque file de réception d'interface. Elle stocke l'identifiant du cœur CPU ayant pris en charge le paquet le plus récent de chaque flux.
Avant de rediriger un paquet vers le cœur désigné par la table globale, RFS compare ces données avec la table locale de la file. Si l'application a migré vers un autre cœur, RFS attend que tous les paquets en attente sur l'ancien cœur soient purgés avant de faire basculer le traitement. Cette précaution élimine tout risque de déséquencement des paquets.
Mise en œuvre pratique : configuration sur un nœud VPN
Voici la procédure d'optimisation recommandée pour un serveur VPN Linux 16 cœurs doté d'une carte réseau externe eth0 et d'une interface WireGuard wg0.
Étape 1 : Définir le masque CPU pour RPS
Sur un système à 16 cœurs (CPU 0 à 15), le masque hexadécimal 0xffff cible tous les processeurs. Il est judicieux de réserver les cœurs 0 et 1 aux interruptions matérielles et aux tâches d'administration, en dédiant les cœurs 2 à 15 (masque 0xfffc) au traitement réseau :
# Activation de RPS pour les cœurs 2-15 sur l'ensemble des files RX d'eth0
for rx_queue in /sys/class/net/eth0/queues/rx-*; do
echo "fffc" > "$rx_queue/rps_cpus"
done
# Activation de RPS sur l'interface virtuelle de tunnel
echo "fffc" > /sys/class/net/wg0/queues/rx-0/rps_cpus
Étape 2 : Dimensionner les tables RFS globales et locales
La taille des tables de flux doit être proportionnelle au volume de sessions simultanées. Pour une passerelle prenant en charge jusqu'à 32 768 clients, on retient la puissance de deux supérieure, soit 65 536 :
# Configuration de la capacité de la table globale des flux de sockets
sysctl -w net.core.rps_sock_flow_entries=65536
# Répartition des entrées sur chaque file matérielle (ex. 4 files : 65536 / 4 = 16384)
for rx_queue in /sys/class/net/eth0/queues/rx-*; do
echo 16384 > "$rx_queue/rps_flow_cnt"
done
Étape 3 : Augmenter la capacité des files d'attente internes
En cas de rafales soudaines de trafic, la file d'attente du processeur cible peut déborder si le traitement des interruptions logicielles prend du retard. L'augmentation du paramètre netdev_max_backlog élimine les abandons répertoriés dans /proc/net/softnet_stat :
# Augmentation de la file d'attente par cœur de 1000 à 10000 paquets
sysctl -w net.core.netdev_max_backlog=10000
# Augmentation du quota d'exécution par cycle NAPI
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=4000
Intégration architecturale dans RiceGuard VPN
RiceGuard, le moteur de routage de niveau 3 conçu par VPNRice, s'articule directement avec les fonctions de distribution de paquets du noyau Linux :
- Isolement par nœuds NUMA : RiceGuard segmente la gestion des interruptions matérielles, les calculs cryptographiques et les boucles d'événements de socket sur des nœuds NUMA distincts. Pour le trafic arrivant sur une carte réseau connectée au nœud NUMA 0, le masque
rps_cpusest restreint aux cœurs partageant la mémoire cache L3 avec ce bus PCIe. - Synchronisation d'affinité en continu : Lors de montées en charge rapides, les répartiteurs de RiceGuard enregistrent des repères d'affinité dans la table du noyau, assurant une parfaite adéquation de cache entre l'espace utilisateur et les routines SoftIRQ.
- Résultats en conditions réelles : Lors de bancs d'essai sur des liaisons 10 Gb/s soutenant 25 000 tunnels clients actifs, l'activation conjointe de RPS et RFS a réduit la latence au 99e centile (p99) de 8,4 ms à 1,9 ms, éliminant totalement les blocages de traitement.
Questions fréquemment posées
Quelle est la distinction principale entre RSS, RPS et RFS ?
RSS est une fonctionnalité matérielle de la carte réseau qui répartit les paquets via des files physiques et des interruptions MSI-X. RPS est un sous-système logiciel du noyau Linux qui équilibre la charge sur plusieurs cœurs après l'interruption matérielle. RFS optimise RPS en orientant le paquet spécifiquement vers le cœur CPU exécutant l'application propriétaire de la socket.
L'activation de RPS augmente-t-elle l'utilisation globale du processeur ?
RPS engendre un surcoût modéré d'environ 1 % à 2 % de CPU pour exécuter le hachage Jenkins et émettre les interruptions inter-processeurs (IPI). En revanche, en éliminant la saturation d'un cœur unique, il accroît considérablement le débit global de la passerelle.
Est-il possible d'appliquer RPS et RFS aux interfaces virtuelles tun/tap ?
Oui. Les interfaces virtuelles tun et tap tirent un avantage substantiel de RPS. Comme ces interfaces ne disposent généralement que d'une seule file d'attente, activer rps_cpus sur rx-0 permet de répartir le déchiffrement des paquets sur les cœurs disponibles, supprimant ainsi un point de contention majeur.



