Aller au contenu
TCP : fiabilité, débit et congestion

TCP : fiabilité, débit et congestion

200 Pratiquer ⏱ 1 h 15 reseaulinuxtcp

À la fin, vous saurez

  • Expliquer les acquittements cumulatifs, la retransmission sur délai, la retransmission rapide et les acquittements sélectifs
  • Distinguer contrôle de flux (fenêtre de réception) et contrôle de congestion (fenêtre de congestion)
  • Calculer le produit débit par délai d'un chemin et en déduire la fenêtre nécessaire
  • Décrire le démarrage lent et les algorithmes CUBIC et BBR, et expliquer pourquoi une connexion neuve est lente
  • Mettre en évidence l'interaction entre l'algorithme de Nagle et l'acquittement différé, et savoir quand utiliser TCP_NODELAY
  • Lire les informations internes d'une connexion avec ss -ti et les compteurs de retransmission avec nstat

Prérequis

Testé avec iproute2 6.1.0 noyau 7.0 (HWE) python 3.12.3 ubuntu 24.04 , vérifié le 5 octobre 2026

Pourquoi

Le réseau que traversent les paquets de Signalements n'est pas fiable. Des paquets se perdent quand un tampon de routeur déborde, d'autres arrivent dans le désordre parce qu'ils ont emprunté deux chemins, certains sont retardés de plusieurs centaines de millisecondes. La leçon 5 l'a dit : IP fait « au mieux », sans rien promettre. Pourtant, quand Gunicorn écrit 2 Mo de réponse JSON, l'application cliente reçoit exactement ces 2 Mo, dans l'ordre, sans un octet de travers.

C'est TCP qui transforme ce réseau imparfait en flux fiable, et il le fait en même temps qu'il essaie d'aller le plus vite possible sans écraser le réseau ni le destinataire. Ces trois objectifs (fiabilité, débit, équité) tirent dans des directions différentes, et leurs compromis expliquent des phénomènes que l'on observe tous les jours :

  • une sauvegarde de la base vers Varsovie plafonne à quelques dizaines de mégabits par seconde sur un lien qui en offre mille ;
  • la première requête sur une connexion neuve est plus lente que les suivantes ;
  • un client qui envoie de petits messages subit des latences de 40 millisecondes, ni plus ni moins, que rien dans son code n'explique ;
  • une perte de paquets de 1 % ne ralentit pas les transferts de 1 %, mais bien davantage.

Cette leçon ouvre la connexion établie à la leçon 10 et regarde comment les données y circulent. L'objectif n'est pas de régler le noyau (il le fait très bien tout seul dans la grande majorité des cas), mais de savoir reconnaître ces phénomènes et d'en tirer les bonnes décisions côté application et architecture.

Les concepts

Acquitter : ce que dit un ACK

Chaque octet du flux a un numéro de séquence (leçon 10). Le récepteur renvoie dans ses segments un numéro d'acquittement : le numéro du prochain octet qu'il attend. Cet acquittement est cumulatif : « ACK 5001 » veut dire « j'ai reçu, dans l'ordre et sans trou, tout jusqu'à l'octet 5000 ». Un seul acquittement peut donc confirmer plusieurs segments, et un acquittement perdu est rattrapé par le suivant.

Le récepteur n'acquitte pas chaque segment immédiatement. Avec l'acquittement différé (delayed ACK), il attend un court instant, dans l'espoir de pouvoir joindre l'acquittement à une réponse qui partirait de toute façon, ou d'acquitter deux segments d'un coup. La RFC 5681 recommande d'acquitter au moins un segment sur deux, et de ne pas attendre plus de 500 millisecondes ; Linux attend au minimum 40 millisecondes. Ce détail reviendra plus bas.

Retransmettre sur délai

L'émetteur garde en mémoire tout ce qu'il a envoyé et qui n'est pas encore acquitté. Pour le segment le plus ancien, il arme une minuterie, le délai de retransmission (RTO, Retransmission TimeOut). Si elle expire avant l'acquittement, le segment est renvoyé.

Choisir le RTO est délicat. Trop court, on retransmet des segments qui n'étaient pas perdus, seulement en retard, et l'on encombre le réseau. Trop long, on attend inutilement après une vraie perte. La RFC 6298 fixe la méthode : l'émetteur mesure en continu le temps aller-retour (RTT, Round-Trip Time) des segments acquittés, et en tient une moyenne lissée (SRTT) et une mesure de la variation (RTTVAR). Le délai vaut :

RTO = SRTT + 4 × RTTVAR

Trois règles complètent le calcul :

  • Avant toute mesure, le RTO vaut 1 seconde (c'est le délai du premier SYN de la leçon 10).
  • À chaque expiration, le RTO double (recul exponentiel) : si le réseau est saturé, il ne faut surtout pas retransmettre plus vite.
  • On ne mesure pas le RTT d'un segment retransmis (algorithme de Karn), parce qu'on ne saurait pas lequel des deux envois l'acquittement concerne ; les horodatages TCP (RFC 7323) lèvent cette ambiguïté.

La RFC recommande un RTO minimum d'une seconde. Linux s'en écarte délibérément et utilise un minimum de 200 millisecondes, mieux adapté aux réseaux actuels ; le compteur RtoMin de /proc/net/snmp l'affiche, et la documentation du noyau décrit comment le modifier route par route. Une expiration du RTO reste un événement coûteux : l'émetteur est resté silencieux au moins 200 millisecondes, et le contrôle de congestion va, en plus, réduire fortement son débit.

Retransmettre vite : les acquittements dupliqués et SACK

Attendre le RTO à chaque perte serait trop lent. Quand un segment se perd au milieu d'un flux, les segments suivants arrivent quand même ; le récepteur, qui attend toujours l'octet manquant, renvoie à chaque fois le même numéro d'acquittement. Ce sont des acquittements dupliqués. La RFC 5681 en tire la retransmission rapide (fast retransmit) : au troisième acquittement dupliqué, l'émetteur considère que le segment est perdu et le renvoie sans attendre le délai.

L'acquittement cumulatif a une faiblesse : il ne dit que le début du trou. Si trois segments non consécutifs se perdent dans la même fenêtre, l'émetteur ne le découvre qu'un par un. L'option SACK (Selective Acknowledgment, RFC 2018), négociée dans les SYN et activée par défaut sous Linux, permet au récepteur de décrire les blocs reçus au-delà du trou : « j'attends 5001, mais j'ai déjà 6001 à 9000 et 10001 à 12000 ». L'émetteur ne renvoie alors que ce qui manque vraiment. Les piles modernes, dont Linux, combinent ces informations avec une détection des pertes fondée sur le temps (RACK, RFC 8985), plus robuste quand les paquets sont simplement réordonnés.

Le contrôle de flux : ne pas noyer le destinataire

Le récepteur range les données reçues dans un tampon de réception, en attendant que l'application les lise. Si l'application lit lentement (un client sur un téléphone lent, un processus occupé), le tampon se remplit. Pour éviter de perdre des données faute de place, chaque segment annonce la fenêtre de réception (rwnd) : la place encore libre dans ce tampon. L'émetteur ne doit jamais avoir plus d'octets non acquittés que cette fenêtre. Quand elle tombe à zéro, il s'arrête et sonde périodiquement le récepteur jusqu'à ce qu'elle se rouvre.

Le champ fenêtre de l'en-tête ne fait que 16 bits : 65 535 octets au plus. C'était largement suffisant en 1981 ; c'est ridicule aujourd'hui. L'option de facteur d'échelle (window scale, RFC 7323), négociée dans les SYN, multiplie la valeur annoncée par une puissance de deux (jusqu'à 2^14), ce qui porte la fenêtre maximale à environ 1 Gio. Sous Linux, le noyau ajuste lui-même la taille des tampons à chaque connexion (autotuning, réglé par net.ipv4.tcp_moderate_rcvbuf), entre les bornes de net.ipv4.tcp_rmem (réception) et net.ipv4.tcp_wmem (émission). Chacun de ces réglages contient trois valeurs, minimum, valeur initiale et maximum ; sur la machine de test :

$ cat /proc/sys/net/ipv4/tcp_rmem /proc/sys/net/ipv4/tcp_wmem
4096	131072	33554432
4096	16384	4194304

Les maxima dépendent de la mémoire de la machine, d'après la documentation du noyau.

Le produit débit par délai

Combien d'octets faut-il « en vol », envoyés et pas encore acquittés, pour remplir un tuyau ? Exactement le débit du chemin multiplié par son temps aller-retour : c'est le produit débit par délai (bandwidth-delay product, BDP).

Prenons la sauvegarde nocturne de sig-db, copiée de Paris vers une instance de stockage à Varsovie, sur un chemin à 1 Gbit/s avec un RTT de l'ordre de 30 millisecondes :

BDP = 1 000 000 000 bit/s × 0,030 s = 30 000 000 bits = 3,75 Mo

Pour utiliser tout le lien, il faut que 3,75 Mo soient en vol en permanence. Si la fenêtre est limitée à 64 Kio (pas de facteur d'échelle, ou une application qui fixe elle-même un petit tampon avec SO_RCVBUF), le débit maximal tombe à :

débit max = fenêtre / RTT = 65 535 octets × 8 / 0,030 s ≈ 17,5 Mbit/s

C'est la première explication du transfert qui « plafonne » : quel que soit le lien, une connexion TCP ne peut pas dépasser fenêtre / RTT. Plus le chemin est long, plus la fenêtre doit être grande. Vers Montréal, avec un RTT de l'ordre de 80 millisecondes, la même fenêtre de 64 Kio ne donnerait plus qu'environ 6,5 Mbit/s.

Le contrôle de congestion : ne pas noyer le réseau

Le contrôle de flux protège le destinataire, pas le réseau. Si tous les émetteurs envoyaient aussi vite que les récepteurs le permettent, les files des routeurs déborderaient, les pertes déclencheraient des retransmissions, qui ajouteraient du trafic... En octobre 1986, une partie de l'Internet de l'époque s'est effondrée exactement ainsi, et Van Jacobson a proposé en 1988 les mécanismes dont dérivent ceux d'aujourd'hui, normalisés dans la RFC 5681.

L'émetteur tient une seconde fenêtre, privée, la fenêtre de congestion (cwnd), qui estime ce que le réseau peut absorber. À chaque instant, il envoie au plus le minimum de cwnd et de rwnd. La fenêtre de congestion évolue en deux phases :

  • Le démarrage lent (slow start), dont le nom est trompeur : la connexion commence avec une petite fenêtre, puis l'augmente d'un segment par segment acquitté, ce qui la double à chaque aller-retour. La croissance est exponentielle, jusqu'à une perte ou jusqu'à un seuil (ssthresh).
  • L'évitement de congestion : au-delà du seuil, la fenêtre croît plus prudemment.

Quand une perte est détectée, la fenêtre est réduite : de moitié environ après une retransmission rapide, et jusqu'à un seul segment après une expiration du RTO, avec un nouveau démarrage lent. C'est pourquoi un taux de perte de 1 % coûte bien plus que 1 % du débit : chaque perte divise la fenêtre, et il faut de nombreux allers-retours pour la regagner.

Les algorithmes diffèrent par la façon dont ils font croître et décroître la fenêtre :

AlgorithmePrincipeOù on le rencontre
Reno, NewRenoCroissance d'un segment par aller-retour, division par deux à la perteLa référence historique de la RFC 5681, toujours disponible sous Linux
CUBIC (RFC 9438)La fenêtre suit une fonction cubique du temps écoulé depuis la dernière perte : elle remonte vite vers la valeur où la perte a eu lieu, ralentit autour, puis explore au-delà. Indépendant du RTT, efficace sur les chemins longs à haut débitAlgorithme par défaut de Linux depuis 2006, de macOS et de Windows
BBR (Google, 2016)Ne réagit pas aux pertes, mais modélise le chemin : il mesure le débit de livraison maximal et le RTT minimal, et envoie à ce rythme, avec une régulation (pacing)Disponible dans Linux depuis la version 4.9 ; utilisé par Google pour ses services et sa dorsale

Les auteurs de BBR, dans leur article de 2016, partent d'un constat : les algorithmes fondés sur les pertes, comme CUBIC, ne ralentissent qu'une fois les tampons des routeurs pleins. Sur les liens dotés de grands tampons, ils remplissent ces files et ajoutent de la latence pour tout le monde (bufferbloat) ; sur les liens qui perdent un peu de paquets sans être saturés, ils réduisent leur débit à tort. BBR évite les deux, au prix d'un comportement parfois moins équitable envers les flux CUBIC qui partagent le même lien. Une troisième voie, la notification explicite de congestion (ECN), permet aux routeurs de marquer les paquets au lieu de les jeter ; Linux l'accepte quand l'autre côté la demande (net.ipv4.tcp_ecn = 2 par défaut).

L'algorithme utilisé se lit et se choisit par machine :

$ cat /proc/sys/net/ipv4/tcp_congestion_control
cubic
$ cat /proc/sys/net/ipv4/tcp_available_congestion_control
reno cubic

BBR n'apparaît dans la liste qu'une fois son module (tcp_bbr) chargé. Une application peut aussi choisir son algorithme connexion par connexion, avec l'option de socket TCP_CONGESTION décrite dans tcp(7).

Ce que cela change pour une application

Une connexion neuve est lente. Une connexion commence avec une fenêtre de congestion de 10 segments (RFC 6928, en vigueur sous Linux depuis 2011), soit environ 14 Kio. Une réponse de 200 Kio demande donc plusieurs allers-retours de démarrage lent avant que le débit ne soit atteint, en plus de la poignée de main TCP et, souvent, de la négociation TLS. Pour un client à 80 millisecondes, c'est plusieurs centaines de millisecondes avant le dernier octet. Par défaut, Linux réduit aussi la fenêtre d'une connexion restée inactive (net.ipv4.tcp_slow_start_after_idle = 1) : une connexion longtemps muette redémarre lentement.

Réutiliser les connexions est un gain de débit, pas seulement de poignées de main. Une connexion qui a déjà servi a une fenêtre de congestion ouverte et un RTT mesuré. C'est la raison d'être du keep-alive HTTP, des pools de connexions vers PostgreSQL et du multiplexage de HTTP/2, qui fait passer de nombreuses requêtes dans une connexion.

Les petits messages et l'algorithme de Nagle. En 1984, John Nagle a constaté (RFC 896) que des applications interactives envoyaient un caractère par segment : 40 octets d'en-têtes pour 1 octet de données. Son algorithme est simple : tant qu'un petit segment envoyé n'est pas acquitté, l'émetteur retient les petites écritures suivantes et les regroupe. Couplé à l'acquittement différé du récepteur, il produit un blocage bien connu : le client écrit l'en-tête d'un message puis son corps en deux appels ; le premier part, le second attend l'acquittement du premier ; le serveur, qui attend le message complet pour répondre, retarde son acquittement de 40 millisecondes. Chaque échange prend alors 40 millisecondes de plus. L'option de socket TCP_NODELAY désactive Nagle ; la plupart des bibliothèques de bases de données et des serveurs HTTP l'activent d'eux-mêmes. La bonne pratique reste d'écrire un message en un seul appel.

Le blocage en tête de file. TCP livre un flux ordonné : si un segment se perd, tous les octets suivants attendent sa retransmission, même s'ils sont arrivés. Avec HTTP/2, qui multiplexe plusieurs requêtes dans une connexion, une perte bloque toutes les requêtes en cours (head-of-line blocking). C'est la principale motivation de QUIC, protocole de transport construit sur UDP (leçon 9), qui porte HTTP/3 et gère l'ordre flux par flux. Le cours HTTP et les API web y revient.

En pratique

Les sorties de cette section ont été produites sur une machine Ubuntu 24.04, avec LC_ALL=C, sur l'interface locale 127.0.0.1. Sur l'interface locale, le RTT se compte en microsecondes et rien ne se perd : les chiffres ne ressemblent pas à ceux d'un vrai chemin, mais les mécanismes et les outils sont les mêmes.

Lire l'intérieur d'une connexion avec ss -ti

Un serveur qui lit lentement (une pause d'une milliseconde entre chaque lecture) et un client qui envoie aussi vite qu'il peut pendant trois secondes :

$ python3 -c '
import socket, time
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 8003)); s.listen(1)
c, _ = s.accept(); t = time.time()
while time.time() - t < 4:
    c.recv(65536); time.sleep(0.001)' &
$ python3 -c '
import socket, time
c = socket.create_connection(("127.0.0.1", 8003)); b = b"x" * 1000000; t = time.time()
while time.time() - t < 3: c.sendall(b)' &
$ ss -tin 'dport = :8003'
State Recv-Q Send-Q  Local Address:Port  Peer Address:PortProcess
ESTAB 0      2020352     127.0.0.1:53356    127.0.0.1:8003
	 cubic wscale:10,10 rto:202 rtt:1.139/0.021 mss:47616 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:82179691 bytes_acked:82179692 segs_out:1984 segs_in:1322 data_segs_out:1982 send 3344407375bps lastsnd:2 lastrcv:1474 pacing_rate 6688080760bps delivery_rate 668294736bps delivered:1983 busy:1473ms rwnd_limited:1473ms(100.0%) rcv_space:65495 rcv_ssthresh:65495 notsent:2020352 minrtt:0.07

L'option -i ajoute les informations internes de TCP. Les champs utiles, décrits dans ss(8) :

  • cubic : l'algorithme de contrôle de congestion de cette connexion.
  • wscale:10,10 : les facteurs d'échelle de fenêtre négociés (émission, réception), ici 2^10.
  • rto:202 : le délai de retransmission courant, en millisecondes : le minimum de 200 ms de Linux, plus une marge.
  • rtt:1.139/0.021 : le RTT lissé et sa variation, en millisecondes. minrtt:0.07 est le plus petit RTT observé.
  • mss:47616 : la taille de segment utilisée en émission. Sur l'interface locale, dont la MTU est de 65 536 octets, les segments sont énormes ; sur Ethernet, vous verrez 1448 (1500 moins les en-têtes IP et TCP et l'option d'horodatage).
  • cwnd:10 : la fenêtre de congestion, en segments.
  • bytes_sent, bytes_acked : octets envoyés et acquittés (l'écart d'un octet correspond au SYN, qui compte pour un).
  • retrans : n'apparaît que s'il y a eu des retransmissions, sous la forme retrans:<en cours>/<total>.
  • rwnd_limited:1473ms(100.0%) : la connexion a passé 100 % de son temps actif bridée par la fenêtre de réception. Le goulet n'est ni le réseau ni la congestion : c'est le destinataire, qui lit trop lentement. notsent:2020352 et la colonne Send-Q montrent les 2 Mo qui attendent dans le tampon d'émission.

Sur une vraie connexion lente, les mêmes champs répondent à la question « qui limite ? » : rwnd_limited élevé, c'est le récepteur ; cwnd petit avec des retrans, c'est le réseau qui perd ; un champ app_limited, c'est l'application émettrice qui n'a rien à envoyer.

Les compteurs du noyau

Le noyau compte, depuis le démarrage, les retransmissions et leurs causes :

$ grep '^Tcp:' /proc/net/snmp
Tcp: RtoAlgorithm RtoMin RtoMax MaxConn ActiveOpens PassiveOpens AttemptFails EstabResets CurrEstab InSegs OutSegs RetransSegs InErrs OutRsts InCsumErrors
Tcp: 1 200 120000 -1 82000 9435 20425 2788 38 13557292 35745411 20115 197 82218 0
$ nstat -asz TcpRetransSegs TcpExtTCPTimeouts TcpExtTCPFastRetrans TcpExtTCPSackRecovery TcpExtTCPSynRetrans
#kernel
TcpRetransSegs                  20115              0.0
TcpExtTCPSackRecovery           90                 0.0
TcpExtTCPFastRetrans            2821               0.0
TcpExtTCPTimeouts               5999               0.0
TcpExtTCPSynRetrans             2510               0.0
  • RtoMin et RtoMax (200 et 120 000 ms) sont les bornes du délai de retransmission de Linux.
  • RetransSegs rapporté à OutSegs donne un taux de retransmission global : ici 20 115 / 35 745 411, environ 0,06 %, sans alarme. Au-delà de quelques pour mille de façon durable, cherchez la perte.
  • TCPTimeouts compte les expirations de RTO, les retransmissions coûteuses ; TCPFastRetrans et TCPSackRecovery celles qui ont été traitées rapidement. Une proportion élevée d'expirations signale des pertes en rafale, ou des pairs qui disparaissent.
  • TCPSynRetrans compte les SYN retransmis : des connexions qui ont eu du mal à s'ouvrir, symptôme d'un filtrage ou d'un serveur saturé (leçon 10).

Ce sont des compteurs cumulés : ce qui compte, c'est leur évolution. Sans -a, nstat affiche les variations depuis son dernier appel, ce qui permet de mesurer un incident en cours. Les exportateurs de métriques système les publient pour qu'on en trace la courbe.

Mettre Nagle en évidence

Ce programme fait dialoguer un client et un serveur sur la même machine. Le client envoie un en-tête de 10 octets, puis un corps de 10 octets, en deux appels, et attend une réponse de 2 octets ; le serveur ne répond qu'après avoir lu les deux. Vingt échanges, sans puis avec TCP_NODELAY :

# nagle.py
import socket, threading, time

def serveur(s):
    while True:
        c, _ = s.accept()
        with c:
            while True:
                d = c.recv(10)          # en-tête de 10 octets
                if not d: break
                c.recv(10)              # corps de 10 octets
                c.sendall(b"ok")

s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 8005)); s.listen(1)
threading.Thread(target=serveur, args=(s,), daemon=True).start()
for nodelay in (0, 1):
    c = socket.create_connection(("127.0.0.1", 8005))
    c.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, nodelay)
    t = time.perf_counter()
    for i in range(20):
        c.sendall(b"E" * 10)            # deux petites écritures...
        c.sendall(b"C" * 10)
        c.recv(2)                       # ...puis on attend la réponse
    duree = (time.perf_counter() - t) / 20 * 1000
    print(f"TCP_NODELAY={nodelay} : {duree:.1f} ms par échange")
    c.close()
$ python3 nagle.py
TCP_NODELAY=0 : 39.2 ms par échange
TCP_NODELAY=1 : 0.1 ms par échange

Quatre cents fois plus lent, sur une machine où le RTT est de quelques dizaines de microsecondes. Les 39 millisecondes sont presque exactement l'acquittement différé minimal de Linux (40 ms) : le second sendall attend l'acquittement du premier (Nagle), et le serveur retarde cet acquittement parce qu'il n'a encore rien à répondre. Avec TCP_NODELAY, le corps part immédiatement. Une troisième correction, souvent la meilleure, consiste à écrire b"E" * 10 + b"C" * 10 en un seul appel : essayez-la sans TCP_NODELAY.

Sous le capot

L'horloge des acquittements. Un émetteur TCP n'envoie pas « au rythme qu'il veut » : chaque acquittement qui revient libère de la place dans la fenêtre, ce qui autorise l'envoi de nouveaux segments. Le rythme des acquittements, lui-même fixé par le lien le plus lent du chemin, cadence donc l'émission. Van Jacobson appelait ce mécanisme l'auto-cadencement (self-clocking). Il explique pourquoi TCP s'adapte à un goulet d'étranglement sans le connaître, et pourquoi BBR, qui n'attend pas les pertes, complète cette horloge par une régulation explicite du rythme d'envoi (le pacing_rate de ss -ti).

Où vivent les fenêtres. La fenêtre de réception est une donnée du récepteur, qu'il annonce dans chaque segment. La fenêtre de congestion est une variable privée de l'émetteur, qui n'apparaît dans aucun paquet : on ne peut la voir qu'avec ss -ti sur la machine émettrice, ou la déduire d'une capture par la quantité de données en vol. C'est pourquoi un diagnostic de débit se fait du côté de l'émetteur.

Pourquoi la perte est un signal de congestion. Sur un réseau filaire, les erreurs de transmission sont rarissimes : quand un paquet se perd, c'est presque toujours qu'une file d'attente a débordé, donc qu'il y a congestion. Les algorithmes fondés sur la perte reposent sur cette hypothèse. Elle est fausse sur les liens radio (Wi-Fi, mobile) où des pertes surviennent sans congestion, ce qui explique une partie de l'intérêt de BBR et des mécanismes de réparation propres à ces liens.

Les tampons de la pile. Entre l'application et le câble, chaque octet séjourne dans le tampon d'émission (jusqu'à son acquittement, pour pouvoir être retransmis), puis, à l'arrivée, dans le tampon de réception (jusqu'à ce que l'application le lise). Send-Q et Recv-Q de ss mesurent leur remplissage. Un Recv-Q élevé et durable côté serveur dit que l'application ne lit pas assez vite ; un Send-Q élevé côté émetteur, que le réseau ou le destinataire ne suit pas.

Pièges courants

Régler les tampons au jugé. Les guides qui recommandent des tcp_rmem gigantesques datent souvent d'avant l'autotuning, désormais efficace. Pire : une application qui fixe elle-même SO_RCVBUF ou SO_SNDBUF désactive l'autotuning pour sa socket, et plafonne parfois son débit sans le savoir. Ne touchez à ces réglages qu'après avoir mesuré, avec ss -ti, que la fenêtre est bien le facteur limitant.

Accuser le réseau d'un débit faible. Sur un chemin long, une connexion unique est limitée par fenêtre / RTT. Avant de mettre en cause le fournisseur, calculez le produit débit par délai, regardez rwnd_limited et retrans. Pour une sauvegarde, plusieurs connexions en parallèle (ce que font rclone ou les clients S3 en découpant les fichiers) contournent la limite d'une connexion.

Les 40 millisecondes mystérieuses. Une latence qui vaut presque exactement 40 ms (ou 200 ms sur d'autres systèmes) est la signature de Nagle et de l'acquittement différé. Cherchez les écritures en plusieurs morceaux, et TCP_NODELAY.

Confondre perte et latence. Une connexion qui « rame » peut souffrir de pertes (des retrans dans ss -ti, des TCPTimeouts qui augmentent) ou d'une latence qui gonfle sans perte (des files d'attente pleines, le bufferbloat, visible comme un rtt très supérieur au minrtt). Les remèdes diffèrent.

Ouvrir une connexion par requête. Chaque connexion neuve paie la poignée de main, le démarrage lent, souvent TLS, et laisse un TIME-WAIT. Pour une API appelée en boucle ou une base de données, le pool de connexions n'est pas une optimisation facultative.

Sécurité

  • Les tampons et les délais sont des ressources. Un client malveillant peut ouvrir des connexions et envoyer sa requête un octet toutes les dix secondes, juste assez pour éviter les délais d'inactivité : l'attaque slowloris épuise ainsi les workers d'un serveur qui consacre un processus par connexion, comme Gunicorn en mode synchrone. La parade est architecturale : un répartiteur ou un serveur mandataire (Traefik, HAProxy, Nginx) en frontal, qui absorbe les connexions lentes et ne transmet que des requêtes complètes, avec des délais de lecture des en-têtes.
  • Injecter un RST. Un équipement placé sur le chemin, qui voit les numéros de séquence, peut injecter un RST et couper une connexion : des fournisseurs d'accès et des dispositifs de censure l'ont fait. Hors du chemin, l'attaque est beaucoup plus difficile depuis que les numéros de séquence sont imprévisibles et que Linux applique les protections de la RFC 5961 (un RST dont le numéro n'est pas exactement attendu déclenche un simple acquittement de vérification). TCP n'authentifie rien : seul TLS garantit l'intégrité de ce qui circule, mais ne protège pas contre la coupure.
  • Les compteurs de retransmission sont aussi des indicateurs de sécurité. Une hausse brutale des TCPSynRetrans ou des ListenOverflows peut signaler une attaque autant qu'une panne.

En production

  • Mesurez avant de régler. Dans la très grande majorité des cas, les valeurs par défaut de Linux (CUBIC, autotuning, SACK, horodatages, facteur d'échelle) sont les bonnes. Les gains réels viennent de l'architecture : réutilisation des connexions, proximité des serveurs avec leurs clients, compression des réponses.
  • BBR se choisit pour des cas précis : serveurs qui envoient beaucoup de données vers des clients lointains ou sur des liens imparfaits (distribution de contenus, sauvegardes inter-régions). Il se teste sur un échantillon, en mesurant débit et latence des deux algorithmes. Il s'active par net.ipv4.tcp_congestion_control = bbr, accompagné de la discipline de file fq (net.core.default_qdisc) sur les noyaux anciens ; vérifiez les recommandations de la version de noyau utilisée.
  • Surveillez le taux de retransmission (RetransSegs / OutSegs) et les expirations de RTO par machine. Une hausse localisée à une zone ou à un chemin est souvent le premier signe d'un problème réseau chez le fournisseur.
  • Rapprochez ce qui se parle beaucoup. La base sig-db et les instances sont dans la même région : un RTT de l'ordre de la milliseconde. Une application qui fait cent requêtes SQL par page supporte ce RTT ; placée à 30 ms de sa base, elle passerait 3 secondes par page à attendre le réseau. La latence se multiplie par le nombre d'allers-retours, ce qui justifie souvent davantage le placement que n'importe quel réglage.
  • Pour les transferts lointains, découpez en plusieurs connexions parallèles et vérifiez que rien sur le chemin (application, mandataire) ne fixe de petits tampons.

Exercices

1. Calculer un produit débit par délai (niveau 100). Un lien entre Paris et Montréal offre 500 Mbit/s avec un RTT de 80 ms. Quelle fenêtre faut-il pour le remplir avec une seule connexion ? Quel débit obtient une connexion dont la fenêtre est bloquée à 256 Kio ?

Solution

BDP = 500 000 000 × 0,080 = 40 000 000 bits = 5 Mo : il faut une fenêtre d'environ 5 Mo. Avec 256 Kio (262 144 octets), le débit maximal vaut 262 144 × 8 / 0,080 ≈ 26,2 Mbit/s, environ 5 % du lien. Remèdes : laisser l'autotuning agrandir la fenêtre (vérifier que l'application ne fixe pas SO_RCVBUF, et que le maximum de tcp_rmem dépasse 5 Mo), ou ouvrir une vingtaine de connexions en parallèle.

2. Diagnostiquer un transfert lent (niveau 200). Une copie de sauvegarde vers Varsovie est lente. Sur l'émetteur, ss -ti affiche pour cette connexion cwnd:12 retrans:0/1834 rtt:31.2/4.1 minrtt:29.8, sans rwnd_limited. Qu'en concluez-vous, et que vérifiez-vous ensuite ?

Solution

Le RTT est stable (31 ms pour un minimum de 30 : pas de files qui gonflent). Le récepteur ne limite pas. En revanche, 1 834 segments ont été retransmis et la fenêtre de congestion reste minuscule (12 segments, environ 17 Ko, soit environ 4,5 Mbit/s à 31 ms) : le chemin perd des paquets, et CUBIC divise sa fenêtre à chaque perte. On vérifie le taux de pertes (mtr vers la destination, compteurs TCPTimeouts et TCPFastRetrans), un éventuel problème de MTU (leçon 6), et l'on signale le chemin au fournisseur. En attendant, plusieurs connexions parallèles ou BBR, moins sensible aux pertes isolées, peuvent améliorer le débit.

3. Nagle (niveau 200). Reprenez nagle.py. Remplacez les deux sendall par un seul (c.sendall(b"E" * 10 + b"C" * 10)) et gardez TCP_NODELAY=0. Mesurez. Expliquez le résultat, puis dites pourquoi cette correction est préférable à TCP_NODELAY dans le cas général.

Solution

Le temps tombe au niveau de la version avec TCP_NODELAY (une fraction de milliseconde) : il n'y a plus de second petit segment retenu en attendant un acquittement. Écrire chaque message en un seul appel produit des segments pleins et moins nombreux, quel que soit le réglage de Nagle. TCP_NODELAY, lui, fait partir chaque petite écriture immédiatement : sans regroupement côté application, il multiplie les petits segments et le coût par octet. Les deux se combinent : les bibliothèques réseau sérieuses regroupent leurs écritures et activent TCP_NODELAY.

4. Choisir l'architecture (niveau 200). L'équipe envisage de déplacer sig-app-1 à Amsterdam (RTT de l'ordre de 10 ms avec Paris) en gardant sig-db à Paris. Une page de Signalements déclenche 40 requêtes SQL successives. Estimez le coût en latence et proposez deux corrections.

Solution

40 allers-retours × 10 ms = 400 ms d'attente réseau par page, contre environ 40 ms dans la même région (de l'ordre d'une milliseconde par aller-retour). Corrections : garder l'application dans la même région que sa base (le plus simple) ; réduire le nombre d'allers-retours (requêtes regroupées, jointures, chargement en lot) ; et, dans tous les cas, utiliser un pool de connexions pour ne pas ajouter de poignées de main. Aucun réglage TCP ne supprime le temps de propagation.

Récapitulatif

  • L'acquittement cumulatif dit « tout reçu jusqu'à N - 1 » ; le récepteur acquitte en différé (40 ms au minimum sous Linux).
  • Une perte se répare par retransmission sur délai (RTO = SRTT + 4 × RTTVAR, minimum 200 ms sous Linux, doublé à chaque expiration) ou, bien plus vite, par retransmission rapide après trois acquittements dupliqués ; SACK décrit précisément les trous.
  • Le contrôle de flux (fenêtre de réception, facteur d'échelle, autotuning) protège le destinataire ; le contrôle de congestion (fenêtre de congestion privée de l'émetteur) protège le réseau. L'émetteur envoie au plus le minimum des deux.
  • Une connexion ne dépasse jamais fenêtre / RTT ; il faut une fenêtre au moins égale au produit débit par délai.
  • Le démarrage lent double la fenêtre à chaque aller-retour à partir de 10 segments ; CUBIC est l'algorithme par défaut de Linux, BBR modélise le chemin au lieu d'attendre les pertes.
  • Réutiliser les connexions, écrire un message en un seul appel, connaître Nagle et TCP_NODELAY : les gains se font côté application.
  • ss -ti montre cwnd, rtt, retrans et ce qui limite la connexion (rwnd_limited) ; nstat et /proc/net/snmp comptent retransmissions et expirations.

Pour aller plus loin

  • La RFC 5681 pour les mécanismes de base, et la RFC 9438 pour CUBIC, dont la section sur la fonction de croissance se lit bien avec un graphique.
  • L'article BBR: Congestion-Based Congestion Control (ACM Queue, 2016), qui explique avec beaucoup de clarté pourquoi les pertes sont un mauvais signal sur les réseaux d'aujourd'hui.
  • Le chapitre 16 de TCP/IP Illustrated, Volume 1, consacré au contrôle de congestion, avec des traces commentées.
  • Le cours HTTP et les API web, pour HTTP/2, HTTP/3 et QUIC, et le cours Performance et diagnostic sous Linux, pour la mesure fine des connexions.
  • La leçon suivante, Diagnostiquer un problème réseau, qui rassemble tout le cours en une méthode.
Voir ma constellation →

Sources