Dans les architectures réseau contemporaines, la réactivité et la maîtrise de la latence sont tout aussi capitales que le débit brut. Les passerelles VPN d'entreprise, les tunnels de secours WireGuard encapsulés dans TCP et les terminaisons TLS acheminent fréquemment des flux multi-gigabits tout en devant préserver une interactivité instantanée. Cependant, à mesure que les liaisons physiques sont passées de 1 Gbps à 100 Gbps, une dégradation insidieuse des performances s'est généralisée : le bufferbloat de la file d'écriture au sein de la pile réseau du noyau Linux.

Lorsqu'une application envoie des données sur une liaison TCP, le noyau Linux adapte dynamiquement la taille du tampon d'émission (sk->sk_sndbuf) grâce à ses mécanismes d'autorégulation. Sur des liaisons longue distance caractérisées par un produit bande passante-délai (BDP) élevé, le noyau élève régulièrement ce tampon à 4 Mo, 8 Mo, voire 16 Mo par socket. Bien que cet espace généreux permette de saturer les canaux à grande vitesse, il engendre un goulot d'étranglement structurel : les applications de l'espace utilisateur déversent de volumineux mégaoctets de données dans les files d'attente du noyau bien plus vite que le réseau physique ne peut les évacuer.

Cette mise en mémoire tampon excessive crée des files d'attente stagnantes invisibles au sein de la mémoire du système d'exploitation. Si un paquet urgent—tel qu'une frappe de terminal interactif, un paquet vocal VoIP ou un échange de clés cryptographiques—doit traverser le tunnel, il se retrouve bloqué derrière des mégaoctets de transferts de fichiers volumineux déjà engagés dans le noyau. Pour éliminer cette file d'attente artificielle et restituer le contrôle du flux à la couche applicative, le noyau Linux propose l'option de socket TCP_NOTSENT_LOWAT.


Le goulot d'étranglement de la file d'écriture : Inflation du BDP et bufferbloat dans le noyau

La cause principale du bufferbloat à l'émission réside dans la dissociation entre les boucles d'événements de l'espace utilisateur et les automates à états TCP du système. Un daemon asynchrone (reposant sur epoll ou io_uring) transmet des données dès que le socket signale un espace d'émission disponible (EPOLLOUT) :

  1. Dimensionnement du tampon noyau : Le noyau applique la formule sk_sndbuf = 2 * BDP, allouant jusqu'à 16 Mo pour garantir que la fenêtre de congestion ne soit jamais restreinte.
  2. Écritures anticipées massives : Tant que la quantité de données en file reste inférieure à sk_sndbuf, l'application vide intégralement ses tampons internes dans la file du socket.
  3. Accumulation de files dormantes : Les données qui ne peuvent pas être transmises immédiatement sur le câble (en raison du pacing, des limites de congestion ou de la fenêtre du récepteur) s'empilent dans la file d'écriture (sk_write_queue).
File d'attente d'écriture standard sans TCP_NOTSENT_LOWAT :

[ Boucle applicative ] ──► write(16 Mo)
                                 │
                                 ▼
[ Tampon de socket noyau : sk_sndbuf (16 Mo alloués) ]
┌─────────────────────────────────────────────────────────────┬─────────────┐
│ Données non transmises en attente dans le noyau (15,5 Mo)   │ Données en  │
│ (Provoque un blocage en tête de ligne majeur pour les flux) │ vol (0,5 Mo)│
└─────────────────────────────────────────────────────────────┴─────────────┘
                                 │
                                 ▼  (Écoulement lent selon le rythme du lien)
                    [ Interface réseau physique ]

Cette accumulation de données stagnantes dans le noyau soulève deux périls techniques majeurs :

  • Blocage en tête de ligne (Head-of-Line Blocking) : L'application perd toute capacité d'annuler, de réordonner ou d'intercaler des paquets prioritaires, car les octets sont déjà verrouillés dans la file d'attente du système.
  • Amplification de la consommation mémoire : Lorsqu'un nœud de relais supporte 20 000 connexions concurrentes conservant chacune 8 Mo de données dans des structures sk_buff, la machine monopolise jusqu'à 160 Go de mémoire vive uniquement pour héberger ces files d'attente passives.

Au cœur de TCP_NOTSENT_LOWAT : Mécanismes du noyau et événements de socket

Conçue par le mainteneur réseau Eric Dumazet, l'option TCP_NOTSENT_LOWAT redéfinit la condition d'éligibilité d'un socket à l'écriture. Au lieu d'évaluer la disponibilité globale dans sk_sndbuf, le noyau mesure précisément la quantité d'octets non envoyés (unsent bytes) présents dans la file d'attente.

Le noyau quantifie les données non transmises comme la différence entre les octets écrits et les octets actuellement propagés sur le support physique :

$$\text{Octets non envoyés} = \text{write_seq} - \text{snd_nxt}$$

Lorsque TCP_NOTSENT_LOWAT est configuré avec un seuil donné (par exemple 16 Ko ou 32 Ko), le noyau retient l'événement EPOLLOUT tant que la quantité de données en attente dépasse ce plafond.

Pipeline d'émission régulé avec TCP_NOTSENT_LOWAT = 16 Ko :

[ Tampons applicatifs ] ── (Conservés dans l'espace utilisateur pour priorité)
          │
          │ 1. La boucle d'événements attend EPOLLOUT
          ▼
[ File d'attente noyau : sk_write_queue ]
┌──────────────────────────────────────────────┬────────────────────────────┐
│ Données non envoyées : Plafonnées à <= 16 Ko │ Données actives en vol     │
│ (Injection continue et juste-à-temps)        │ (Canal BDP saturé à 100 %) │
└──────────────────────────────────────────────┴────────────────────────────┘
          │
          ▼  (Acheminement immédiat vers la carte réseau)
[ Interface réseau physique ]

1. Découplage de la capacité tampon et des données non transmises

Il convient de souligner que TCP_NOTSENT_LOWAT ne réduit aucunement sk_sndbuf et ne bride pas la fenêtre de congestion (snd_cwnd). La pile TCP conserve la capacité de maintenir un volume substantiel de paquets en transit sur des routes intercontinentales à fort BDP.

Le mécanisme découple en réalité la taille de la fenêtre de transport du comportement de pré-remplissage des applications. Le système continue d'ajuster le volume en vol en fonction des acquittements, mais l'espace utilisateur n'injecte de nouveaux segments que lorsque les précédents ont effectivement quitté la machine.

2. Signalement EPOLLOUT guidé par les événements et régulation du flux

Dans le code source de la pile réseau Linux (net/ipv4/tcp_output.c), la fonction tcp_write_xmit() contrôle la profondeur de file :

/* Évaluation interne au sein de tcp_stream_memory_free */
static inline bool tcp_stream_memory_free(const struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    u32 notsent = tp->write_seq - tp->snd_nxt;

    return notsent < tp->notsent_lowat;
}

Dès que la carte réseau expédie les paquets planifiés et fait avancer le pointeur snd_nxt, le volume d'octets en attente diminue. Dès que notsent passe sous le seuil tp->notsent_lowat, le noyau réveille la file d'attente du socket, notifiant à epoll_wait() que le descripteur peut recevoir de nouvelles écritures.


Arbitrage latence et mémoire : Sysctl global contre ajustement par socket

Les administrateurs d'infrastructure peuvent appliquer ce plafonnement à l'échelle globale du serveur ou ajuster finement les valeurs socket par socket.

Arborescence de configuration pour la régulation des files d'émission :

[ Paramètre système global Sysctl ]
 net.ipv4.tcp_notsent_lowat = 16384 (Valeur par défaut pour l'hôte)
            │
            ├─► Héritée par les sockets TCP standards
            │
            └─► Remplacée localement via setsockopt() :
                 setsockopt(fd, SOL_TCP, TCP_NOTSENT_LOWAT, &val, sizeof(val))

1. Application globale via net.ipv4.tcp_notsent_lowat

L'approche la plus directe pour assainir l'ensemble d'un nœud consiste à modifier la directive sysctl du noyau :

# Vérification de la configuration actuelle (souvent 0xFFFFFFFF, soit désactivé)
cat /proc/sys/net/ipv4/tcp_notsent_lowat

# Définition d'un plafond de 16 Ko pour l'ensemble des connexions TCP
sudo sysctl -w "net.ipv4.tcp_notsent_lowat = 16384"

Positionner tcp_notsent_lowat = 16384 fixe une limite de 16 Ko sur les données non envoyées pour tous les flux de la machine, empêchant les processus en arrière-plan d'accaparer déraisonnablement les blocs de mémoire slab.

2. Contrôle granulaire par socket dans les relais partagés

Pour les serveurs de transit accueillant des flux hétérogènes, la configuration par programmation offre une flexibilité incomparable. Un daemon peut assigner des seuils très stricts (comme 4 Ko ou 8 Ko) aux canaux interactifs, tout en maintenant 32 Ko ou 64 Ko pour les flux vidéo volumineux :

int notsent_limit = 16384; /* Plafond de 16 Ko de données en attente */

if (setsockopt(client_fd, SOL_TCP, TCP_NOTSENT_LOWAT, &notsent_limit, sizeof(notsent_limit)) < 0) {
    perror("Échec de configuration de TCP_NOTSENT_LOWAT");
    return -1;
}

Guide opérationnel : Intégrer TCP_NOTSENT_LOWAT dans les daemons haute performance

L'optimisation d'un proxy asynchrone pour exploiter TCP_NOTSENT_LOWAT nécessite une coordination étroite entre la gestion des tampons applicatifs et la boucle réseau :

  1. Conserver les tampons en espace utilisateur : Bannir les écritures massives et anticipées. Les données en partance doivent résider dans des structures locales du processus (tampons circulaires ou files de fragments).
  2. Réagir strictement à l'événement EPOLLOUT : À la réception d'un événement EPOLLOUT, transmettre uniquement le fragment éligible jusqu'à la frontière logique suivante, puis rendre la main à la boucle.
  3. Prendre en compte le mode Edge-Triggered : En configuration EPOLLET, écrire séquentiellement jusqu'à ce que write() retourne EAGAIN ou EWOULDBLOCK, signalant l'atteinte du seuil.
Organigramme de la boucle d'écriture pour minimiser la latence :

[ Nouvelles données disponibles ] ──► Contrôle de la file de priorité interne
                                                    │
                                                    ▼
                                       Le socket est-il prêt en écriture ?
                                             /                     \
                                          (Oui)                   (Non)
                                           /                         \
                                          ▼                           ▼
                        Écriture d'un segment dans le socket  Attente d'un signal epoll
                                         │
                                         ▼
                            L'appel retourne-t-il EAGAIN ?
                                   /             \
                                (Oui)           (Non)
                                 /                 \
                                ▼                   ▼
                   Réinitialisation de l'état  Poursuite de la boucle d'envoi

Avantages du multiplexage : Élimination du blocage en tête de ligne

Pour les protocoles combinant plusieurs canaux logiques sur un transport TCP unique (tels que HTTP/2, gRPC ou les liaisons VPN encapsulées), TCP_NOTSENT_LOWAT apporte un avantage décisif : la suppression du blocage en tête de ligne entre l'espace utilisateur et le noyau.

Imaginons un tunnel d'entreprise combinant une sauvegarde lourde et une session interactive de terminal :

  • Sans TCP_NOTSENT_LOWAT : La sauvegarde remplit instantanément 8 Mo de tampon noyau. Dès qu'une touche de clavier est saisie, elle est reléguée à la fin de cette file de 8 Mo, subissant 200 ms de latence superflue.
  • Avec TCP_NOTSENT_LOWAT : Le noyau ne conserve que 16 Ko de la sauvegarde à un instant donné. Dès qu'une commande interactive survient, l'ordonnanceur applicatif la place immédiatement en tête de sa propre file. En quelques microsecondes, dès que le bloc de 16 Ko en cours est libéré, le paquet prioritaire part sur le réseau sans attente.

Architecture RiceGuard : Contrôle adaptatif des tampons pour relais à haut débit

VPNRice incorpore TCP_NOTSENT_LOWAT au cœur même du moteur de transfert de RiceGuard. Pour les terminaux mobiles itinérants, les passerelles d'interconnexion d'entreprise et les flux multimédias exigeants utilisant une encapsulation TCP, l'élimination du bufferbloat est essentielle pour préserver la fluidité des échanges.

Le moteur RiceGuard analyse en temps réel les indicateurs de transmission—comme les variations de RTT, le rythme d'émission et les exigences de débit des clients. Il module dynamiquement TCP_NOTSENT_LOWAT entre 8 Ko et 32 Ko, garantissant que les liaisons restent pleinement exploitées tout en maintenant les temps de séjour en file sous la milliseconde. En mariant les fonctionnalités fines du noyau Linux à une planification logicielle intelligente, RiceGuard procure une réactivité remarquable, une stabilité sans faille et une protection rigoureuse de la vie privée numérique.