Buffer méretezés kezdőknek és halandóknak

Még tavaly év végén volt egy kis workshop [1] buffer méretezés témában a Stanford Egyetemen. Minden előadó próbálta kicsit más szemszögből bemutatni, milyen hálózati paramétereket kell figyelembe venni mikor a routerek buffereit méretezik. A helyzet az, hogy a bufferek méretei nagyban befolyásolják, hogy milyen lesz a hálózati kapacitás kihasználtsága, meg mennyire lesznek egymással igazságosak a TCP folyamok. Az sem utolsó szempont, mekkora késleltetést szenvednek majd a sorban várakozással a csomagok. Ez egy leterhelt routerben a tényleges körülfordulási idő (TCP csomag, majd rá visszaérkező nyugta) az az RTT többszöröse is lehet. Meg még van egy csomó aspektusa a témának és bármilyen meglepő még napjainkban is lehet érdekes eredményeket kihozni belőle. A továbbiakban röviden ismertetném néhány mérésünk eredményét, aztán pedig a mérések technikai részleteit és pár jótanácsot ha valaki hasonló mérésekre szánja el magát.

A Linux alap TCP verziója a Cubic. Ez egy egészen jól működő torlódásszabályozó algoritmus, valamikor 2007 környékén került a Linuxba, legtöbb szerver ma amihez az interneten csatlakozhatunk ezt használja. Főleg nagy fájlok letöltésénél lesz érdekes, hogy a TCP milyen gyorsan találja meg a fogadó ablakméretét, csomagvesztéskor mennyivel csökkenti ezt a becslést, milyen agresszivitással próbálja aztán újra növelni, hogy elérje a szűk keresztmetszet kapacitását. Részletekkel senkit nem untatnék. Mint minden más TCP algoritmus, ez is úgy lett megtervezve, hogy ha van egy normál FIFO kiszolgálós szűk keresztmetszet, ennek a kapacitásán egyenlően osztozzanak a TCP folyamok. Például van 1Gbps max sávszélességű router valahol a neten, ezen megy 10 TCP letöltés, akkor ideális esetben minden folyam 100Mbps sávszélességet fog kapni.

Emellett vannak még más TCP verziók. Ezek is a jó fairnessre törekednek, csak az algoritmusuk kicsit eltérő, mert más felhasználásra szánták őket. Vannak például olyan verziók, amik datacenterben működnek nagyon jól (DCTCP), van ami Wi-Fi-n, valami nagyon nagy RTT-s linkeken (mondjuk szatellit), valami 3G meg LTE hálózatokon, meg van ami próbál mindenre lőni egyszerre, azért a legtöbb felhasználáshoz átlagosan jó lesz, de sehol nem kiemelkedő. Aztán pár éve a Google előállt egy modell alapú TCP verzióval, a BBR-el. Mivel a Google szerverei elég nagy részét teszik ki az internet forgalmának, nem lehet szó nélkül elmenni mellette. És ő sem megy el szó nélkül más TCP verziók folyamai mellett, ha épp közös buffert kezdenek megtölteni épp. A BBR úgy lett kifejlesztve, hogy nagyjából kitalálja, mekkora a buffer (vagy a legrövidebb buffer) a hálózatban a kommunikáló felek között, és úgy próbál küldeni, hogy sosem nőjön a bufferelt csomagok aránya a teljes bufferméret 5-10%-a fölé. Ennek az előnye, hogy kicsi bufferekkel is elérhető a teljes kihasználtság. Hátránya viszont, hogy más TCP verziókkal, amik meg teletöltötték a buffert nem feltétlenül fog jól együttműködni. Egy kutatás szerint a TCP BBR a BDP másfélszeresétől kisebb bufferekkel ideális körülmények között is igazságtalan arányban sajátítja ki a sávszélességet. BDP itt a bandwidth-delay product rövidítése, azaz sávszélesség szorozva RTT. Például egy 100Mbps sávszélességű és 40ms RTT-jű link BDP-je úgy jön ki, hogy (100Mbps / 8) * 0.04s = 500kbyte. A bufferre ami pont ekkora, mondjuk hogy 1 BDP méretű, a példánál 1Mbyte-os buffer az 2 BDP, 100kbyte-os pedig 0.2 BDP méretű. Mekkora BDP méretű buffer kell, ami elég kicsi késleltetést ad az átvitelhez, de elég hogy kihasználjuk a link teljes kapacitását? Attól függ. A válasz nem egyszerű és aktívan kutatott. Függ attól is, hány TCP folyam osztozik az adott bufferen. Léteznek modellek és empirikus mérések is rá. Ennek a cikknek [3] a 6-os ábrája jól mutatja, hogy bizonyos esetben nagyon kis (50-100 csomagnyi) bufferrel is elérhető a link kapacitásának a 90%-a. Ez nem rossz, ha belegondolunk hogy ekkora buffert már tisztán optikai elemekből is fel lehet építeni, meg a gyors memória egyébként is drága ezekben a routerekben. Terabites sebességeknél jó, ha elég keveset kell felhasználni nagyon gyors hozzáférésű memóriából.

A Google 2019-ben kicsit módosított a BBR-en és kijött a BBRv2-vel. Ebben igyekeztek javítani a hagyományos TCP verziókkal való együttműködést. Most már (legalábbis optimális körülmények között) igazságos a TCP Cubic-al is - legalábbis ezt ígéri a Google. Mi pedig annak jártunk utána hogy ez mennyire felel meg a valóságnak. Tudomásom szerint a cikkünk az első független tanulmány, ami a Cubic és a BBRv2 közötti fairensst vizsgálja. A Google felrakta a BBRv2-es Linux forkját a netre így szabadon elérhető és bárki kipróbálhatja vagy mérhet vele. Több forgatókönyvet is kipróbáltunk: mi van különböző BDP-s bufferek esetén (ez a fő kérdés), mi van ha alkalmazunk sima taildrop FIFO (ha tele a buffer, a következő beérkező csomagot eldobjuk) mellett más, aktív sorkezelési algoritmusokat (AQM, azaz active queue management, vagyis kiszámoljuk melyik csomagot fogjuk eldobni a bufferből) is használunk. Mi történik, ha a buffert megtöltő TCP átvitelek számát növeljük (2, 10, 20, 100 darab TCP folyam)? Mi van akkor, ha közös bufferen osztoznak különböző RTT-jű TCP folyamok? Továbbá mi magunk is javasolunk egy AQM eljárást, ami nagyon jó fairnesst ad minden lehetséges forgatókönyvnél. Ez az általunk javasolt AQM sajnos valós hálózaton nehezen telepíthető mert megjelöli a csomagokat egy értékkel - aminek ugye valahol hely kell, nem írhatja bele az IP címbe vagy a TCP fejlécbe - de aki valami hasonlót akar a gyakorlatban az próbálkozhat a Linuxba beépített FQ-CoDeL AQM konfigurálgatásával.

Akit érdekel a teljes cikk az összes eredményünkkel részletesen, az itt [2] elérheti.

Néhány kulcs megállapításunk:

  • A linkek kihasználtsága általában jó. Nagyon kis buffereknél, pl. a BDP 5-10%-a és kevés TCP-nél nincs kihasználva a teljes link (pl. 1Gbps helyett csak 6-700Mbps), de picit növelve a buffert vagy a TCP folyamok számát a kihasználtság nagyon jó lesz

  • Az általunk javasolt PPV AQM minden esetben jó fairnesst ad. Ott, ahol közös bufferen osztoznak a kis és nagy RTT-s TCP folyamok, melyek közt vannak Cubic és BBRv2 verziós is, ott nagyon nem igazságosan osztoznak a sávszélességen más AQM-ek esetén.

  • Ha egyforma TCP verziót használ minden folyam, úgy elég jó a fairness (intra-protocol fairness). Ez nem nagy meglepetés, de sok folyamnál akadhatnak ezzel is gondok. Továbbá egyes AQM-ek rá vannak tuningolva egyes TCP verziókra, pl. az általunk is vizsgált GSP AQM a Cubic-re, na ez szépen el is rontja a BBRv2-k közötti fairness-t, viszont  Cubic-al nagyon szép. 

  • Cubic és BBRv2 közötti fairness nagyon függ a hálózat paramétereitől és az AQM beállításoktól. Ha nagy az RTT és a sávszélesség (pl. 1Gbps és 100ms RTT) akkor nagyon rossz lesz. Kisebb RTT-vel már sokkal jobb. Sima taildrop FIFO esetén a buffer mérete fogja főleg mehatározni a fairness-t. PIE és GSP AQM-ek nagyban nem befolyásolják a fairnesst. 

  • Heterogén RTT-s forgatókönyveknél (10 vs. 100ms TCP folyamok) a PIE és GSP AQM-ek csak a Cubic vs Cubic esetekben segítenek, BBRv2 vs BBRv2 vagy Cubic vs BBRv2 esetekben nem. Referenciának a taildrop FIFO-t vettük.

  • Heterogén RTT-s méréseknél ha minden folyam BBRv2-t használ, érdekesen alakul a fair share. Az történik, hogy közepes bufferekkel a legjobb a fairness (a kisebb RTT-hez kiszámolt BDP fele) alatt meg fölötte is rosszabb. Ha mindenki Cubic, úgy egyértelműen a nagyobb bufferrel javul a fairness is. 

 

A mérő környezetünk igazából a létező legegyszerűbb topológia: három gépből áll, egy adó és egy nyelő, közöttük pedig egy routerként funkcionáló gép. Bár max 1Gbps-t használtunk maximum a bottleneckben, a kártyák 40Gbps-t támogatnak, ezért minden AQM-ből DPDK verziót használtunk. Aki nem ismerné a DPDK-t (Data Plane Development Kit) annak röviden úgy tudnám összefoglalni, hogy egy user space hálózati csomagok kezelésére alkalmas library. Úgy használtuk, hogy amit az adó gép küldött, azt a router gépen beolvassuk a kártyáról (a kártya ilyenkor közvetlenül user space memóriába DMA-zza a csomagokat) ahol eldobhatjuk (free() a pointerén), átírhatunk benne dolgokat (IP/TCP fejlécben, rakrészben) és kiküldhetjük őket a másik hálókártyán. Ez nagyjából egy nagyságrenddel gyorsabb mint a Linux hálózati stackje, ilyen feladatokhoz tökéletesen használható. Itt teljes kontroll a mi kezünkben van, bájtra pontosan méretezhetjük a bufferünket. A DPDK egy C library, ad pár helper függvényt ezekhez a műveletekhez, minden másról mi döntünk.

Forgalom generáláshoz kezdetben iperf2-t majd később iperf3-at használtunk. Sajnos egyik sem tökéletes, mindkettőben elég sok apró hiányosság van, ami pont precíz méréseknél nagyon sokat számít. Az iperf2 jó választásnak tűnt, mert több szálon fut. Ez azt jelenti, hogy minden egyes TCP kapcsolatnak nyit egy új szálat, ami csak annyit csinál hogy küld/fogad adatokat, így külön CPU magon futhat, jól skálázható. A mérésével viszont főleg sok kapcsolat esetében már voltak problémák. Találtam például egy legalább 10 éves bugot, ami miatt 127 TCP kapcsolatnál több esetében a mérés végén nem írja ki az összegzett sebességet. Persze python scripttel minden megoldható (manuális összegzés is) de végül inkább ezt javítottam ki. A másik érdekesség, hogy sok folyam esetében elég hülye eredmények jöttek ki néha. Ez abból adódott, hogy bár meg volt adva hány TCP hány másodpercig forgalmazzon, ha egy folyam beakadt (nem tudta rendesen bezárni valami miatt, pl. az AQM előbb leállt vagy FIN csomagot eldobta) akkor ő számolta tovább a másodperceket, de már nem forgalmazott. Így mondjuk 180 másodpercen át forgalmazott, aztán még 20 másodpercig kínlódott mire timeoutolt az utolsó beakadt TCP is, akkor a 180 mp-nyi átvitt adatot osztotta 200 másodperccel ami így kisebb átlagsebességet jelentett a végén. A másik érdekes dolog, hogy sima Ctrl+C nem mindig tudja kilőni teljesen. Nem baj, van kill -9, csak ott meg volt olyan, hogy árván maradtak egyes socketek, amiket aztán sehogy nem tudtam kinyírni, csak a gép újraindítása segített. Ezek néha 1-1 csomagot megpróbáltak újraküldeni a már rég nem létező nyelő oldali párjuknak.

A helyzet valamivel jobb iperf3-al. Viszont az meg csak egy szálon fut, több socketet selecttel olvassa, az meg egy darabig bírja a sávszélt meg a kapcsolat számának a növelését, aztán szép lassan limitálódik CPU miatt a maximális sávszélesség amivel meg tudja hajtani a hálózatot. Nálunk ez a cikknél nem volt szerencsére gond, mert 1Gbps-t még kényelmesen ki tudott hajtani. Van egy másik furcsasága az iperf3-nak ami sok flow-nál és nagy RTT-nél jön elő csak. Egymás után nyitja a kapcsolatokat és kezdi rajtuk a forgalmazást. 100ms RTT-nél és 100 TCP kapcsolatnál az első és utoljára indított kapcsolat között 10 másodperc telik el. Ez szintén torzítja a mérés végeredményét, mert egyszerre fogja leállítani őket az iperf3. Persze 180 másodperces mérésnél ez nem egy nagy hiba, de ilyesmibe már bele lehet kötni. iperf3-nak viszont van jó kis JSON kimenete, amit könnyen tudtunk parseolni pythonnal meg ábrákat generálni belőlük a cikkhez.

Néhány tanulság, ha valaki automatizáltan szeretne futtatni nagy számú mérést valamelyik iperf verzióval, hogy tesztelje a hálózat sebességét:

  • iperf2 több szálon fut, így a CPU-k számával együtt skálázódik. Az iperf2 szerver oldala szintén új szálat indít minden bejövő TCP kapcsolnak, így a fogadó oldal is skálázódik

  • iperf2-ből a 2.0.14-es verziót érdemes használni ha sok TCP kapcsolattal mérünk, mert az a mérés végén kiírja az összes kapcsolat együttes sebességét. Akkor is, ha TCP 1000 folyamot indítunk.

  • Egy idő után (nagyjából 1500 TCP kapcsolat) már a Linux fog szólni, hogy nem nyithatsz több threadet. Ha mondjuk 10000 TCP-vel mérnél, érdemes 10 darab iperf2-t indítani különböző portokkal, egyenként 1000 TCP kapcsolattal

  • Ha connection reset errorok jönnek sok TCP kapcsolat nyitásakor iperf2-nél, az azért van, mert a Linux alapból 128 kapcsolatra limitálja a TCP backlogot (ez a paraméter amit a listen() rendszerhívás megkap mikor TCP socketre hívod). Ha nem érkezi elég gyorsan kiszolgálni a kb. egyszerre bejövő 1000 SYN-t, akkor ezek egy random részét eldobja (stateless conn-resetet küld). Megoldás sudo sysctl -w net.core.somaxconn=65536 beállítása. Ez az érték alapból 128 volt az elmúlt 20 évben, vicces hogy az elmúlt hetekben, Linux 5.5-ben fel lett emelve 1024-re default

  • Ha kill -9-el szeretnéd kilőni az iperf2-t, nem akarod megvárni míg minden szál megáll (ami ha valamelyik valamiért beakadt, akkor sosem fog) akkor érdemes a net.ipv4.tcp_max_orphans változót 0-ra állítani. Ez nem hagyja, hogy egy kilőtt alkalmazás socketjei tovább éljenek

  • iperf3 egy szálon fut, nem nagyon skálázható jól sok kapcsolatra. Még ha látszik valamekkora skálázódás, akkor is érdemes gyanakodni, hogy nem-e lehetne nagyobb a sávszélesség, nem-e limitál a CPU

  • Nagy RTT-n sok kapcsolatnál ha azt szeretnénk, hogy egyszerre induljon minden kapcsolaton az átvitel, használjunk iperf2-t, ha pedig azt, hogy egymás után az egyes kapcsolatokon, akkor iperf3-at. Ez azt is kivédi, hogy connection reset-et kapjunk, mert az iperf3 sosem fogja túltölteni a nyelő oldal listen backlogját.

  • Nagy sávszélesség és nagy RTT esetén ne felejtsük el beállítani a Linux TCP socketjeinek a küldő és fogadó memória limitjeit. Mindkettő esetében három értéket kell megadni: minimum, default és maximum. Ezt azért kell, mert alapból ezek nem túl nagy értékek, hogy a TCP kapcsolatok sorainak memória lábnyoma ne legyen túl nagy. Van ugyanis egy küldési sor, amiben a még nem nyugtázott csomagokat tartja a TCP. Ez nagy BDP esetén limitáló tényező lehet, mert ha megtelik akkor lassulni fog a küldés. Fogadásnál ugyanez: ha a fogadó sor telik meg, akkor a küldő mindig kap majd egy zero-window flages nyugtát, ami miatt a TCP vissza szabályoz így nem fogja kihajtani a kapacitást, akkor sem ha nincs sem torlódás sem csomagvesztés. Ha 1 TCP nem hajtja ki a rendelkezésre álló sávszélességet akkor kezdjünk ezekre gyanakodni. Ha Wireshark nem lát újraküldéseket vagy csomagvesztést de nem érjük el a sebességet, akkor is. Maximális küldő és fogadó memória limit 2Gbyte, ami extrém esetben kevés lehet. Ez azt jelenti, hogy 1 TCP kapcsolat 168ms RTT mellett még ki tud tömni egy 100Gbps-es drótot, de többet már nem (ha valakinek összejön meghívom sörözni). Ezek az értékek a net.ipv4.tcp_wmem és net.ipv4.tcp_rmem változóban állíthatóak be. 

  • GSO/GRO (Generic Segmentation Offload és Generic Receive Offload) legyen bekapcsolva nagy sebességeknél. Ez sokat segít, egyes hálókártyák driverei ezeket offloadolni tudják. Itt az történik, hogy küldésnél az skbuff-ban lévő adatokat egyben megkapja a kártya (ez max szegmens méretű is lehet, 65kbyte) és ő majd akár hardveren feldarabolja szegmensekre és beírja a TCP fejlécekbe az MTU méretű csomagokhoz kiszámított sorszámokat. Fogadásnál ugyan ez, hálókártya bufferében összevár pár csomagot, ezeket összeforrasztja és egy nagyobb skbuff-ot allokál. 

Elég grafomán poszt lett, köszönöm ha elolvastad. Bármi kérdésre szívesen válaszolok kommentben, meg észrevételeket is várom, lehet helyenként hülyeségeket is írtam. A cikkünk mellett szeretném felhívni a figyelmet a workshop többi előadására is [1], kiváló ipari és akadémiai kutatómérnökök voltak jelen (ipari oldalról AT&T, Facebook, Cisco, Google, Netflix, Alibaba, Verizon, stb. akadémiai oldalról MIT, Stanford, Princeton, Cambridge, Yale, TU Darmstadt, ETH Zürich, stb.).

 

Linkek

[1] Workshop on Buffer Sizing 2019: http://buffer-workshop.stanford.edu/program/

[2] Saját cikkünk “Who will Save the Internet from the Congestion Control Revolution?”: http://buffer-workshop.stanford.edu/papers/paper19.pdf

[3] Sizing Router Buffers (Redux): https://dl.acm.org/doi/pdf/10.1145/3371934.3371957

Hozzászólások

Nagyon érdekel, el fogom olvasni