Aller au contenu

Couche transport

TCP, UDP et QUIC — fiabilité, performance et les compromis entre les deux.


Le rôle de la couche transport

La couche réseau livre des paquets de machine a machine. La couche transport va plus loin : elle livre des données de processus a processus. Sur une même machine, des dizaines d'applications communiquent simultanément — la couche transport demultiplexe le trafic grâce aux ports.

Deux décisions fondamentales définissent un protocole de transport :

  1. Fiabilité : garantit-on que chaque octet arrive, dans l'ordre, sans duplication ?
  2. Contrôle de flux : empêché-t-on l'émetteur de submerger le récepteur ou le réseau ?

TCP répond oui aux deux questions. UDP répond non aux deux. QUIC reinvente les réponses. Le choix du protocole de transport conditionne la performance, la complexité et le comportement en panne de toute application distribuée.


TCP — Transmission Control Protocol

TCP (RFC 793, révisé par RFC 9293) est le protocole de transport dominant sur Internet. HTTP, SMTP, SSH, PostgreSQL — la majorité des protocoles applicatifs utilisent TCP. Sa promesse : livrer un flux d'octets fiable, ordonné, sans duplication, avec contrôle de flux et contrôle de congestion.

L'établissement de connexion : le three-way handshake

Avant d'échanger des données, TCP etablit une connexion en trois messages :

sequenceDiagram
    participant C as Client
    participant S as Serveur

    C->>S: SYN (seq=x)
    Note over S: Alloue ressources
    S->>C: SYN-ACK (seq=y, ack=x+1)
    Note over C: Alloue ressources
    C->>S: ACK (ack=y+1)
    Note over C,S: Connexion etablie

Le handshake prend un round-trip time (RTT) complet avant que les données puissent circuler. Sur un lien a 50 ms de RTT, c'est 50 ms de latence pure avant le premier octet utile. C'est pourquoi les connexions TCP sont reutilisees (keep-alive, connection pooling) plutôt que recreees à chaque requête.

Numéros de sequence et acquittements

TCP numerote chaque octet envoye. Le récepteur acquitte en indiquant le prochain octet attendu. Si un segment se perd, le récepteur continue d'acquitter le dernier octet reçu dans l'ordre — c'est le mécanisme des duplicate ACKs.

Concept Mécanisme
Numéro de sequence Position du premier octet du segment dans le flux
ACK Prochain octet attendu par le récepteur
Retransmission Renvoi d'un segment non acquitte après timeout ou 3 duplicate ACKs
Selective ACK (SACK) Le récepteur indique les blocs reçus hors ordre

Contrôle de flux : la fenêtre de reception

Le récepteur annonce une fenêtre de reception (rwnd) — le nombre d'octets qu'il est prêt à recevoir sans acquittement. L'émetteur ne doit jamais avoir plus de rwnd octets en vol (envoyes mais non acquittes).

Ce mécanisme empêche un émetteur rapide de submerger un récepteur lent. Si le récepteur est débordé (buffer plein), il annonce rwnd = 0. L'émetteur s'arrêté et sonde périodiquement le récepteur avec des window probes.

Contrôle de congestion : ne pas ecrouler le réseau

Le contrôle de flux protégé le récepteur. Le contrôle de congestion protégé le réseau. TCP maintient une fenêtre de congestion (cwnd) qui limite le volume de données en vol. L'émetteur envoie au minimum de rwnd et cwnd.

Les phases du contrôle de congestion :

graph LR
    SS["Slow Start<br>cwnd double<br>chaque RTT"] --"cwnd atteint ssthresh"--> CA["Congestion Avoidance<br>cwnd += 1 MSS<br>par RTT"]
    CA --"perte detectee<br>(3 dup ACKs)"--> FR["Fast Retransmit<br>+ Fast Recovery<br>cwnd /= 2"]
    FR --"recuperation"--> CA
    CA --"perte detectee<br>(timeout)"--> SS
Phase Comportement Croissance de cwnd
Slow Start Croissance exponentielle Double chaque RTT
Congestion Avoid. Croissance lineaire +1 MSS par RTT (AIMD)
Fast Retransmit Retransmission immédiate après 3 dup ACKs cwnd /= 2
Timeout Reset complet cwnd = 1 MSS, ssthresh /= 2

Slow Start est mal nomme — la croissance est exponentielle, donc rapide. Mais elle part de 1 MSS (Maximum Segment Size, typiquement 1460 octets sur Ethernet). Sur un lien a 1 Gbps avec 50 ms de RTT, il faut environ 15 RTT (750 ms) pour atteindre le débit maximal. C'est une des raisons pour lesquelles les connexions courtes sont inefficaces en TCP.

Les algorithmes de contrôle de congestion ont évolue :

  • Reno (1990) : le classique, AIMD pur
  • CUBIC (2008) : défaut de Linux, utilise une fonction cubique pour la croissance de cwnd
  • BBR (2016, Google) : modèle la bande passante disponible et le RTT minimum pour reguler le débit

Warning

Le contrôle de congestion TCP est collaboratif. Si un émetteur ne le respecte pas (ou utilise un algorithme agressif), il peut monopoliser la bande passante aux depens des autres flux. BBR est parfois critique pour son agressivite vis-à-vis des flux CUBIC concurrents. En interne dans un datacenter, ou tous les nœuds sont sous le même contrôle, on peut choisir l'algorithme adapté. Sur Internet, le comportement est imprévisible.

La fermeture de connexion

La fermeture TCP nécessité quatre messages (FIN/ACK dans chaque direction) et laisse un socket en état TIME_WAIT pendant 2 * MSL (Maximum Segment Lifetime, typiquement 60 secondes). Pendant cet état, le port ne peut pas être reutilise.

Sur un serveur qui géré des milliers de connexions courtes, les sockets TIME_WAIT peuvent épuiser les ports disponibles. Les options SO_REUSEADDR et SO_REUSEPORT attenent ce problème.


UDP — User Datagram Protocol

UDP (RFC 768) est le protocole de transport minimal. Son en-tête fait 8 octets : port source, port destination, longueur, checksum. Pas de connexion, pas de fiabilité, pas de contrôle de flux, pas de contrôle de congestion.

Pourquoi choisir UDP

Cas d'usage Raison du choix UDP
DNS Requête-réponse unique, pas besoin de connexion
Streaming video/audio La perte d'un paquet est préférée au délai de retransmission
Jeux en ligne Latence minimale, l'application géré la fiabilité
VoIP 20 ms de délai est inacceptable, 1% de perte est tolerable
DHCP La machine n'a pas encore d'adresse IP
Protocoles de découverte Multicast/broadcast nécessaire

UDP n'est pas "TCP sans fiabilité". C'est une fondation sur laquelle on peut construire exactement le niveau de fiabilité dont on a besoin. QUIC en est la demonstration.

Note

DNS utilisé UDP par défaut (port 53) pour les requêtes courtes (< 512 octets historiquement, < 4096 avec EDNS). Pour les transferts de zone et les réponses DNSSEC volumineuses, DNS bascule sur TCP. C'est un bon exemple de choix pragmatique : UDP pour la majorité des cas (rapide, sans connexion), TCP quand la fiabilité est nécessaire.


QUIC — le successeur

QUIC (RFC 9000, 2021) est un protocole de transport construit au-dessus d'UDP par Google, puis normalise par l'IETF. Il est conçu pour résoudre les limitations structurelles de TCP qui ne peuvent pas être corrigees sans changer les middleboxes (firewalls, NAT, proxies) qui parsent les en-têtes TCP.

Les innovations de QUIC

Connexion en 0-RTT : QUIC intégré TLS 1.3 dans le protocole. La première connexion nécessité 1 RTT (vs 2-3 RTT pour TCP + TLS). Les connexions suivantes peuvent envoyer des données en 0-RTT — le client envoie des données chiffrees dans le premier paquet.

sequenceDiagram
    participant C as Client
    participant S as Serveur

    Note over C,S: Premiere connexion (1-RTT)
    C->>S: Initial (crypto + donnees)
    S->>C: Handshake (crypto)
    C->>S: Donnees chiffrees

    Note over C,S: Connexions suivantes (0-RTT)
    C->>S: 0-RTT donnees + crypto
    Note over S: Traitement immediat
    S->>C: Reponse

Multiplexage sans head-of-line blocking : TCP livre un flux d'octets unique. Si un segment se perd, tous les segments suivants sont bloques en attente de retransmission — même s'ils appartiennent a des requêtes HTTP différentes. QUIC géré des streams indépendants : la perte sur un stream ne bloque pas les autres.

Migration de connexion : une connexion TCP est identifiée par le 4-tuple (IP source, port source, IP destination, port destination). Quand un téléphone passe du Wi-Fi au 4G, l'adresse IP change et la connexion TCP est perdue. QUIC identifié ses connexions par un Connection ID indépendant de l'adresse IP — la connexion survit au changement de réseau.


TCP vs UDP vs QUIC

Critère TCP UDP QUIC
Fiabilité Oui Non Oui (par stream)
Ordre Oui (flux unique) Non Oui (par stream)
Connexion 1-2 RTT (+TLS) Aucune 0-1 RTT (avec TLS)
Multiplexage Non (HOL blocking) N/A Oui (sans HOL)
Contrôle de congestion Obligatoire Non Obligatoire
Chiffrement Optionnel (TLS) Non Obligatoire (TLS 1.3)
Migration de connexion Non N/A Oui (Connection ID)
Support middleboxes Universel Universel Parfois bloque
Espace utilisateur Noyau Noyau Espace utilisateur

Tip

QUIC est implémenté en espace utilisateur (userspace), pas dans le noyau. Cela permet des mises à jour rapides — pas besoin d'attendre un patch kernel — mais au prix d'un overhead CPU plus élevé (context switches entre userspace et kernel). Pour les serveurs a très haut débit, cette différence peut être significative. Google rapporte 2% de CPU supplémentaire pour servir le même trafic en QUIC vs TCP.


Ports et sockets

Un socket est le point de terminaison d'une communication. Il est identifié par un 5-tuple : (protocole, IP source, port source, IP destination, port destination).

Les ports sont sur 16 bits (0-65535) :

Plage Nom Usage
0-1023 Well-known Services standard (80=HTTP, 443=HTTPS)
1024-49151 Registered Applications (3306=MySQL, 5432=PostgreSQL)
49152-65535 Ephemeral Ports sources attribues dynamiquement

Un serveur écoute sur un port fixe (ex: 443). Chaque connexion cliente utilise un port éphémère différent. Le nombre de connexions simultanées est donc limite par le nombre de ports éphémères (~16000 par défaut sur Linux, configurable) par adresse IP de destination. Un serveur qui doit gérer des millions de connexions a besoin de plusieurs adresses IP ou d'un load balancer en amont.


Les états TCP et leurs pieges

Une connexion TCP traverse une sequence d'états. Comprendre ces états est essentiel pour diagnostiquer les problèmes en production.

État Signification Durée typique
LISTEN Le serveur attend des connexions Permanente
SYN_SENT Le client a envoye un SYN, attend le SYN-ACK Quelques secondes
SYN_RECEIVED Le serveur a reçu le SYN, attend l'ACK final Quelques secondes
ESTABLISHED Connexion active, données en transit Variable
FIN_WAIT_1 Fermeture initiée, FIN envoye Quelques secondes
FIN_WAIT_2 FIN acquitte, attend le FIN du pair Quelques secondes
CLOSE_WAIT FIN reçu du pair, application n'a pas encore ferme Dépend de l'appli
TIME_WAIT Fermeture complète, attend les segments retardataires 2 * MSL (~60s)
LAST_ACK FIN envoye après reception du FIN du pair, attend l'ACK Quelques secondes

Les états les plus problematiques en production :

CLOSE_WAIT accumule : si l'application ne ferme pas ses sockets après avoir reçu un FIN (bug ou socket leak), les connexions s'accumulent en CLOSE_WAIT. Elles ne seront jamais nettoyees par le noyau — c'est la responsabilité de l'application. Des milliers de CLOSE_WAIT signifient un bug applicatif, pas un problème réseau.

TIME_WAIT accumule : après la fermeture d'une connexion, le socket reste en TIME_WAIT pendant 60 secondes. Un serveur qui géré des milliers de connexions courtes par seconde accumule des dizaines de milliers de sockets TIME_WAIT. Les options net.ipv4.tcp_tw_reuse (Linux) permettent de réutiliser ces sockets plus tôt.

SYN_RECEIVED accumule : un grand nombre de connexions en SYN_RECEIVED peut indiquer une attaque SYN flood. Le serveur alloue des ressources pour chaque SYN reçu mais ne reçoit jamais l'ACK final. Les SYN cookies (net.ipv4.tcp_syncookies) permettent de se defendre sans allouer de ressources avant la fin du handshake.

Warning

La commande ss -tnp (ou netstat -tnp sur les anciens systèmes) est l'outil de diagnostic essentiel pour les connexions TCP. Un ss -s rapide donne un résumé de tous les états — c'est souvent la première commande a exécuter quand un service "rame" sans explication apparente. Des milliers de connexions ESTABLISHED vers un service backend qui n'en attend que quelques centaines révèlent un problème de connection pooling.


Nagle, delayed ACK et la latence cachee

Deux optimisations TCP interagissent pour créer une latence surprenante :

L'algorithme de Nagle (RFC 896) : au lieu d'envoyer immédiatement chaque petit segment, TCP attend d'avoir assez de données pour remplir un segment complet (MSS) ou de recevoir l'ACK du segment précédent. Objectif : réduire le nombre de petits paquets sur le réseau.

Delayed ACK (RFC 1122) : au lieu d'acquitter immédiatement chaque segment reçu, TCP attend jusqu'à 200 ms pour combiner l'ACK avec des données a envoyer dans l'autre direction (piggyback). Objectif : réduire le nombre de paquets ACK purs.

Le problème survient quand les deux sont actifs : l'émetteur envoie un petit message et attend l'ACK avant d'envoyer la suite (Nagle). Le récepteur retarde l'ACK de 200 ms (delayed ACK). Résultat : 200 ms de latence artificielle sur chaque échange requête-réponse de petite taille.

La solution classique est de désactiver Nagle avec TCP_NODELAY sur les sockets interactifs (bases de données, RPC, API). La plupart des libraries HTTP et des drivers de bases de données le font par défaut, mais c'est un piege classique pour les protocoles personnalises.


Impact pour l'architecte

Connection pooling : ouvrir une connexion TCP coute 1 RTT + le temps TLS. Un pool de connexions reutilisees élimine ce coût pour les requêtes suivantes. C'est la configuration la plus impactante pour les applications qui appellent des bases de données ou des services distants.

Timeouts : un timeout TCP trop court provoque des faux positifs (le service est vivant mais lent). Un timeout trop long bloque des ressources inutilement. La règle : le timeout doit être au moins 2x le p99 de latence observe en conditions normales.

Keep-alive vs short-lived : les connexions TCP courtes souffrent du slow start et du coût du handshake. Les connexions longues consomment des ressources sur le serveur (mémoire, file descriptors). Le bon compromis dépend du pattern de trafic.

QUIC pour le mobile : si les utilisateurs sont sur mobile (changement frequent de réseau), QUIC élimine les reconnexions. HTTP/3 utilisé QUIC par défaut. C'est un gain de latence réel pour les applications mobiles.

Head-of-line blocking : avec TCP, un seul flux perdu bloque tous les flux multiplexes en HTTP/2. C'est pourquoi HTTP/2 n'a pas tenu sa promesse de performance sur les liens lossy. HTTP/3 (QUIC) résout structurellement ce problème.

Ephemeral port exhaustion : un client qui ouvre des milliers de connexions courtes par seconde vers un même serveur peut épuiser ses ports éphémères. Les symptomes : erreurs EADDRNOTAVAIL ou Cannot assign requested address. Les solutions : connection pooling (réduire le nombre de connexions), SO_REUSEADDR (réutiliser les ports en TIME_WAIT), ou augmenter la plage de ports éphémères (net.ipv4.ip_local_port_range).

TCP keep-alive vs application keep-alive : TCP a son propre mécanisme de keep-alive (sonde envoyee après une periode d'inactivite, typiquement 2 heures par défaut). Mais ce délai est trop long pour détecter les pannes rapidement. Les applications utilisent leurs propres heartbeats (gRPC keepalive, WebSocket ping/pong) avec des intervalles beaucoup plus courts (10-30 secondes). Ne pas confondre les deux — et ne pas oublier que les load balancers et les firewalls ont leurs propres idle timeouts qui peuvent couper les connexions silencieusement.


Chapitre suivant : Couche application — DNS, HTTP, TLS et les protocoles que l'architecte manipule au quotidien.