Tech & GadgetsAI-generated

Hoe TCP de snelheid van je lijn raadt

Een TCP-verbinding weet bij de start niets over de weg die ze aflegt. Gigabit-glasvezel of een overvolle 4G-mast, een router met een grote of een kleine buffer: de zender ziet het niet. Toch moet hij beslissen hoeveel data hij tegelijk de lijn op stuurt. Te weinig en je laat bandbreedte liggen. Te veel en routers gooien pakketten weg, ook van iedereen die dezelfde lijn deelt.

Dat raadwerk heet congestion control. Hieronder volg je één verbinding over een lijn van 17,5 Mbit/s met 20 ms RTT. Het model is vereenvoudigd, maar de regels zijn echt.

TCP congestion controlHoeveel past er op de lijn?
010203040500153045607590tijd (RTT's) →cwnd (segmenten)lijn = ?hoe snel mag ik zenden?

zender weet: niets · lijnsnelheid: ? · buffer: ?

alleen ACK's vertellen hem iets

Hoeveel past er op de lijn?

Op deze lijn passen per rondreis 17,5 Mbit/s × 0,02 s = 350 kbit, ongeveer 30 segmenten van 1460 bytes. Dat is het bandwidth-delay product (BDP): zoveel data moet er onderweg zijn om de lijn vol te houden. De router op het knelpunt kan daarbovenop nog 12 pakketten in zijn buffer zetten.

De zender kent geen van die getallen. Hij ziet alleen ACK's terugkomen: hoe lang een rondreis duurt en of er iets kwijt is.

Het congestion window

De zender houdt een getal bij: het congestion window, cwnd. Dat is het maximum aan onbevestigde data dat tegelijk onderweg mag zijn. De ontvanger adverteert daarnaast zijn eigen receive window; de zender houdt zich aan de kleinste van de twee.

Een nieuwe verbinding begint met 10 segmenten, zo'n 14,6 kB. Dat is IW10 uit RFC 6928. Linux gebruikt het sinds kernel 2.6.39 (2011); daarvoor was het 2 tot 4 segmenten.

Slow start

Voor elk segment dat bevestigd wordt, mag cwnd één segment groeien. Komen alle ACK's van een rondreis binnen, dan is het window dus verdubbeld: 10, 20, 40, 80. Slow start is dus exponentiële groei. De naam is historisch: het is trager dan alles in één keer de lijn op gooien, wat TCP-stacks deden tot Van Jacobson het in 1988 repareerde.

Er is nog geen rem. De drempel ssthresh staat op oneindig.

Het eerste verlies

Bij 43 segmenten zit de lijn vol en de buffer ook. Het volgende pakket wordt weggegooid. De ontvanger merkt het gat en blijft het laatste segment bevestigen dat wel aankwam. Na drie dubbele ACK's concludeert de zender dat er een pakket kwijt is. Hij stuurt het direct opnieuw (fast retransmit) en halveert: ssthresh en cwnd worden 21.

Voor een Reno-achtige zender is verlies het enige signaal dat de grens bereikt is. Hij weet pas waar die ligt als hij eroverheen gaat.

AIMD: de zaagtand

Boven ssthresh zit de verbinding in congestion avoidance. Het window groeit nog maar met één segment per RTT (additive increase) en halveert bij elk verlies (multiplicative decrease). Samen heet dat AIMD. Vandaar de zaagtand.

Door dat halveren delen TCP-verbindingen een lijn redelijk eerlijk: wie het grootste window heeft, levert bij verlies ook het meest in.

Time-out: terug naar 1

Soms komen er geen dubbele ACK's, bijvoorbeeld omdat er een hele reeks pakketten weg is. Dan loopt de retransmission timeout (RTO) af. Dat is een veel zwaarder signaal: ssthresh gaat naar de helft van wat er onderweg was en cwnd terug naar 1 segment. De verbinding doet opnieuw slow start tot ssthresh en gaat dan weer lineair verder.

CUBIC

Dat ene extra segment per RTT is een probleem op snelle, lange lijnen. Een verbinding van 10 Gbit/s met 100 ms RTT heeft een BDP van zo'n 85.000 segmenten. Na één halvering duurt het 42.800 rondreizen om terug te komen, ruim 70 minuten.

CUBIC, sinds kernel 2.6.19 (2006) de standaard in Linux en sinds 2023 RFC 9438, pakt dat anders aan. Het laat het window groeien als functie van de tijd sinds het laatste verlies, langs een derdegraadscurve: eerst snel terug naar het oude maximum Wmax, daar voorzichtig vlak, en daarna steeds sneller verder zoeken. Bij verlies gaat het window naar 0,7 keer het oude, niet naar de helft. Windows en macOS gebruiken ook CUBIC.

Bufferbloat en BBR

Kijk naar het oranje vlak. Reno en CUBIC groeien door tot de buffer overloopt, dus die buffer staat structureel vol. Elk pakket moet dan eerst achteraan in de wachtrij: de RTT loopt hier op van 20 naar zo'n 29 ms. Echte modems hebben vaak veel grotere buffers: 256 kB buffer voor een upload van 10 Mbit/s is ruim 200 ms extra vertraging. Dat heet bufferbloat, en je merkt het als je videocall hapert zodra iemand in huis een grote upload start.

BBR (Google, 2016) rekent niet met verlies maar met een model: het meet de hoogste afleversnelheid en de laagste RTT, en stuurt ongeveer één BDP de lijn op. De eerste versie stuurde elke acht rondreizen één RTT lang 25 procent meer en daarna 25 procent minder, om te zien of er ruimte bij was gekomen. De buffer blijft zo grotendeels leeg.

Speel zelf

Nu met willekeurig pakketverlies. Schuif met de verlieskans, de RTT en de lijnsnelheid. De simulatie draait 400 rondreizen Reno en middelt de doorvoer vanaf het eerste verlies. Daarnaast staat de Mathis-formule:

doorvoer ≈ (MSS / RTT) · C / √p     met C = √(3/2) ≈ 1,22

Let op de RTT in de noemer. Bij 0,1 procent verlies en 20 ms haal je zo'n 22,6 Mbit/s. Bij dezelfde lijn met 100 ms RTT blijft er ruim 4 over. Een verre server is voor verlies-gebaseerde TCP dus letterlijk een tragere server.

Wat niet in het plaatje past

BBR is niet klaar. De eerste BBR-versie kon CUBIC-verbindingen verdringen en veroorzaakte veel retransmissies op lijnen met kleine buffers. BBRv3, in 2023 op IETF 117 gepresenteerd, reageert weer deels op verlies en is beschreven in de IETF-draft draft-ietf-ccwg-bbr. Google gebruikt het voor zijn eigen verkeer. De mainline Linux-kernel heeft nog de oude BBR; v3 zit alleen in Google's eigen tree en in kernels als XanMod.

Bufferbloat los je in de router op. Een slimme wachtrij als fq_codel of CAKE houdt de wachtrij kort, welk algoritme de zender ook gebruikt. In OpenWrt zet je ze aan via SQM.

Zelf proberen

Op Linux zie je welk algoritme draait en welke er beschikbaar zijn:

sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_available_congestion_control

Met ss -ti zie je per live verbinding het algoritme, rtt, cwnd en eventueel ssthresh. BBR proberen kan met sudo modprobe tcp_bbr en sudo sysctl -w net.ipv4.tcp_congestion_control=bbr.

macOS gebruikt CUBIC. sysctl net.inet.tcp.use_newreno geeft daar 0, en sysctl net.inet.tcp.cubic_sockets telt hoeveel sockets CUBIC nu gebruiken.

Tot slot

Wat me bij het bouwen van deze simulatie opviel: de hele zaagtand draait om één regel, "bij verlies halveren". Die regel stamt uit 1988 en het internet draait er nog steeds op. En toen ik de Mathis-formule naast de simulatie zette, begreep ik eindelijk waarom downloads van een server aan de andere kant van de wereld traag zijn, ook als je eigen lijn niets staat te doen.