Toto je pouze lokální záložní kopie odkazovaného článku, doporučuji navštívit původní odkaz. Připravujete se o případnou plodnou diskuzi pod článkem.

HTB - jemný úvod

Dříve nebo později narazí každý z nás, provozovatelů nějakého toho routeru, na problém s rozdělováním šířky pásma. Na toto téma bylo již napsáno mnoho, ale přesto se z mého okolí stále množí dotazy a hlavně požadavky na nějaké to mini-howto v češtině. Zde je pár tipů, jak začít...

O co vlastně jde?

Máme například malou domácí síť se třemi počítači a jedním routerem, který je přes ADSL napojen na internet. Nastavit routování jsme již zvládli a vše běhá, jak má. Problémy se však objeví, pokud k těmto třem počítačům posadíme i tři různé internetuchtivé uživatele. To se pak klidně může stát, že si uživatel A bude chtít přečíst svou denní dávku novinek na root.cz nebo abclinuxu.cz a ono nic. Diody na modemu přece blikají, tak co je? Po minutě čekání se uživatel A naštve a jde se podívat k uživateli B, aby zjistil, že ten si vesele stahuje jakési trapné skejťácké video nebo se kouká na něco tak nepodstatného, jako jsou například výsledky posledního kola fotbalové ligy. (Pokud hledáte nějakou paralelu, pak uživatel A jsem já a uživatel B můj bratr.:-) ) Proč zrovna on? Proč nedokáže ten hloupý počítač poznat, že moje surfování je důležitější? :-)

Naštěstí mu můžeme v této klasifikaci trochu napomoci. Jak? Vcelku snadno. Stačí se vklínit kamsi "mezi" to, co do routeru přijde z internetu, a to, co z něj pak předáme jednotlivým klientům. K vyřešení našeho problému použijeme kus magie v linuxovém jádře, konkrétně classful qdisc jménem HTB. Jeho výkon je srovnatelný s často preferovaným a hojně rozšířeným CBQ a často na toto téma uslyšíte i nějakou tu hádku. Co ale nemůže popřít nikdo, je snadnost konfigurace. Zatímco v HTB jde všechno skoro samo, z CBQ vás může občas začít pěkně bolet hlava. Proto výrobci praček (a já) doporučují HTB pro začátečníky a CBQ pak nechávají jako alternativu k eventuální migraci. (Stejně u HTB zůstanete... :-) )

Nejprve si však musíme osvojit základní principy, na kterých celé shapování (tedy jakési tvarování) trafficu spočívá.

Prvním a současně nejdůležitějším pravidlem, které je potřeba si vrýt hluboko do paměti, je to, že jsme naprosto neschopni ovlivnit, jaká data (a hlavně jaké množství) nám z internetu dojdou. Naštěstí je však protokol TCP koncipován pro co možná nejefektivnější přenos, a to mimo jiné i následujícím způsobem. Server začne posílat data klientovi co největší možnou rychlostí. Packety letí světem, ale bohužel, někde cestou nebo dokonce až na cílovém počítači se některé z nich ztratí a je nutné je poslat znovu. Protože serveru nechodí některé potvrzovací packety, řekne si, že je klient asi nějaký loser na pomalé lince a rychlost vysílání o něco sníží. Dá tím pak klientovi (a routerům po cestě) čas na zpracování a ještě ušetří pásmo za packety, které by se musely posílat znovu. Přesně této vlastnosti v dobrém slova smyslu "zneužijeme" a packety budeme zahazovat schválně, a to v takové míře, aby nám je server začal posílat rychlosti, kterou chceme my. Musíme ale dbát na to, abychom zmíněnou "vychytávku" aplikovali pouze na packety protokolu TCP, neboť zahazování packetů jiných protokolů (UDP, ICMP, ...) by nevedlo ke snížení rychlosti jejich posílání.

Druhé pravidlo plyne z prvního a říká, že efektivně ovládat můžeme pouze odchozí traffic.

Když nyní známe nejdůležitější pravidla, můžeme se s chutí pustit do konfigurace. Jen ještě připomenu, že k bezproblémovému fungování nasledujících příkladů budete potřebovat jádro s podporou HTB (tedy 2.4.20 a výše nebo opatchovaná starší jádra) a sadu nástrojů "iproute" (moje verze je 20010824). Konfigurace IP adres v domácí síti se může lišit, ale snad každý, kdo se chce pouštět do shapingu, by měl umět se v té mé vyznat...

Vlastní nastavení probíhá podobně jako u iptables, tzn. že jádru pomocí několika volání jistého programu s jistými parametry řekneme, co má dělat. Tím programem bude "tc" ze zmíněného balíčku "iproute". Vytvořme si tedy nějaký ten spustitelný skriptík a do něj napišme:

tc qdisc del dev eth0 root
tc qdisc add dev eth0 root handle 1:0 htb default 14

Tím jsme vymazali všechny současné qdiscy na zařízení eth0 a rovnou na něj pověsili svůj vlastní (samozřejmě typu HTB). Následuje:

tc class add dev eth0 parent 1:0 classid 1:1 htb rate 256kbit
tc class add dev eth0 parent 1:1 classid 1:11 htb rate 64kbit ceil 256kbit
tc class add dev eth0 parent 1:1 classid 1:12 htb rate 64kbit ceil 256kbit
tc class add dev eth0 parent 1:1 classid 1:13 htb rate 64kbit ceil 256kbit
tc class add dev eth0 parent 1:1 classid 1:14 htb rate 64kbit ceil 256kbit

Základní struktura je nyní vytvořena. Na tzv. "root node" (neboli kořenový uzel) jsme pověsili třídu pojmenovanou 1:1 s maximální propustností 256kbit (za sekundu). Z ní se pak větví další čtyři třídy, každá s garantovanou propustností 64kbit a stropem 256kbit. Pojmenování jednotlivých tříd je libovolné (můžeme použít pouze čísla), ale doporučuje se řídit se pravidlem "kolik číslic, tak hluboko jsme". Parametry "dev" a "parent" snad vysvětlovat nemusím, rozeberu ale ještě "rate" a "ceil". Princip si můžeme přiblížit následovně: Představte si, že ke skutečnému odeslání všech packetů dochází jen jednou za sekundu. Packety, které se mají odeslat, ale přicházejí kontinuálně. Každá třída je pak jakási krabička, do které se podle určitých pravidel (viz níže) dávají packety, aby mohly být výsledně odeslány jako celý balík. Packety stále chodí a chodí a "krabičky" se pomalu plní. Pokud je nějaká "krabička" již plná, tzn. objem dat dosáhne hodnoty určené parametrem "ceil", automaticky se zahodí. Čas letí a letí a člověk se ani nenaděje a už tu máme okamžik odeslání. Systém tedy začne nahromaděné packety postupně odesílat, a to tak, že jde od kořene (tedy "1:0") a prochází všechny potomky. Přes "1:1" se tedy dostane k "1:11", vezme nahromaděných 64kbit packetů a odešle je. To samé pro "1:12", atd... Jenže "krabička" "1:13" je (z nějakého důvodu) poloprázdná, a tak se odešle pouze 32kbit dat, která se v ní za onu vteřinu nahromadila. "1:14" je třeba úplně prázdná, tak se neodešle nic. Tím jsme zaručili každé třídě (tedy "krabičce") její garantovaný díl propustnosti. My ale půjdeme ještě dál. Otcovská třída má garantovanou propustnost 256kbit, ale z jejích synů, tedy "1:11" až "1:14", bylo odesláno méně. Proč tedy tuto šířku pásma nevyužít a neposlat ještě packety, které se nevešly do garantovaného pásma jednotlivých synů? Začneme tedy opět procházet jednotlivé větve a podle poměru jejich "rate" (!) začneme doplňovat "díru", která nám vznikla nezaplněním tříd "1:13" a "1:14". Jakmile dojde veškerá zásoba (všechny "krabičky" jsou již prázdné) nebo je "díra" zaplněna (dosáhli jsme hodnoty "rate" u otcovské třídy), skončíme s odesíláním z dané třídy a jdeme dál. Nás případ je značně jednoduchý, protože máme vlastně jediného otce, který má několik "bezdětných" synů. Celý strom ale může být značně košatý, pak se v něm všechna zmíněná pravidla uplatní rekurzivně. Garantovaná část se tedy vždy bere jako první, a pokud se vyskytne místo v garantované části nadřazené třídy, použijeme data, která sice překračují "rate", ale stále se vešla do objemu "ceil" podražené třídy.

Celé toto sáhodlouhé vysvětlování nám mimochodem objasnilo, odkud se vzal onen název "classful qdisc". Znamená totiž "classfull queueing discipline" neboli "řadicí disciplínu obsahující třídy" (fuj, nemám rád překlady).

Na každou koncovou třídu, tedy takovou, která nemá další potomky, je možné ještě pověsit jednu z ne-classful disciplín, jako například SFQ (stochastic fair queueing). Pokud neuvedeme nic, předpokládá se, že chceme připojit standardní frontu FIFO (first in - first out). Takže například:

tc qdisc add dev eth0 parent 1:11 handle 11:0 sfq perturb 10
tc qdisc add dev eth0 parent 1:12 handle 12:0 sfq perturb 10
tc qdisc add dev eth0 parent 1:13 handle 13:0 sfq perturb 10
tc qdisc add dev eth0 parent 1:14 handle 14:0 sfq perturb 10

To přidá na konec každé větve SFQ dqisc s přepočítáváním spojení každých 10 sekund (viz popis SFQ někde jinde). Všimněte si, že jako handle jsem použil "11:0" až "14:0", i když by někdo mohl očekávat značení "1:111", "1:121" apod. To proto, že se jedná o nové disciplíny, které představují další "kořeny" pro své eventuální potomky. Ti pozornější si jistě všimli, že to je v rozporu s tím, co jsem psal o pár řádků výše. Kdyby totiž takový qdisc měl své potomky, musel by být classful. Dobrá, přiznávám se, lhal jsem. Na koncové větve je možno usadit kořen jakéhokoliv dalšího qdiscu (tedy classful i ne-classful), ale silně se nedoporučuje používat další classful qdsicy, protože to vede ke zpomalování (větší náročnost na CPU) a teoreticky jde vše vyřešit jen pomocí dalších tříd navázaných na zmíněný "konec".

Nyní zbývá už jen postarat se o správné rozřazování jednotlichých packetů. K tomu slouží takzvané filtry (opět pomocí programu "tc"). Filtry obsahují základní možnosti rozlišení packetů, a to např. podle zdrojové a cílové adresy, protokolu nebo pomocí hodnoty MARK, kterou mohly pro daný packet nastavit iptables. To nám dává minimálně dvě možnosti, jak celou věc řešit. Buď si vystačíme se základními funkcemi (což je v našem případě filtrování podle cílové adresy zcela dostatečné), nebo si budeme packety značkovat podle jednoduchých (ale někdy i velmi komplexních) pravidel pomocí iptables a pak je opět rozdělíme pomocí filtru (program "tc"), tentokrát však pouze podle značky (použiji tuto metodu, protože je obecnější). Filtrům se tak nikdy nevyhneme, ale práce s iptables je (alespoň podle mého soudu) přívětivější. Takže do toho:

iptables -t mangle -A POSTROUTING -j MARK --set-mark 4
iptables -t mangle -A POSTROUTING -d 192.168.0.1 -j MARK --set-mark 1
iptables -t mangle -A POSTROUTING -d 192.168.0.2 -j MARK --set-mark 2
iptables -t mangle -A POSTROUTING -d 192.168.0.3 -j MARK --set-mark 3

Nejdříve všem packetům určeným k odeslání nastavíme značku na 4 a potom eventuálně značku změníme podle cílové adresy. Hodnota 4 je tedy "default" a je nastavená u každého packetu, který nevyhoví ani jedné podmínce z posledních tří řádků. Značkování hotovo, zbývají filtry:

tc filter add dev eth0 parent 1:0 protocol ip handle 1 fw flowid 1:11
tc filter add dev eth0 parent 1:0 protocol ip handle 2 fw flowid 1:12
tc filter add dev eth0 parent 1:0 protocol ip handle 3 fw flowid 1:13
tc filter add dev eth0 parent 1:0 protocol ip handle 4 fw flowid 1:14

Hotovo. Teď ještě, jak to funguje. Když přijde packet (na odeslání) a počítač neví, kam s ním, začne u "root node" (tedy "1:0") a podívá se, jestli na ní nevisí (kromě dalších potomků) nějaký ten filter. Pokud ano, provede se filtrování a packet se může dostat rovnou na konečnou větev (náš případ) nebo mu třeba parametr "flowid" určí další rozcestí, na kterém může najít další filter, který ho pošle dál. Zde je potřeba dát si pozor. Je totiž klidně možné napsat filtry tak, že se packet vůbec nemusí dostat na konečnou větev (třídu bez potomků) a zůstane viset někde "mimo". K jeho odeslání pak sice může dojít (teoreticky se započítává do "rate" třídy, ve které je), ale rozhodně nám bude kazit počty a celý shaping se pak může chovat zdánlivě nevysvětlitelně.

Tím by měl být náš základní a velmi primitivní příklad připraven k nasazení. Nebylo by ale fair nezmínit se o některých drobnůstkách, které by mohly vadit nebo jsem je dostatečně neobjasnil.

Můžeme s klidem vypustit označování packetu na MARK 4 a jejich následné filtrování v "tc", neboť magický parametr "default", který jsme zadali už při vytváření kořene, pošle všechny packety nevyhovující žádné z podmínek rovnou na třídu "1:14" (která je současně konečná). Větev "1:14" je zapsána ve formě "14", a to proto, že není možné poslat packet mimo samotný qdisc. Prefix "1:" je tedy zbytečné zadávat.

Označení "1:0" a "1:" jsou ekvivalentní. Já používám "1:0" čistě z estetických důvodů.

Při každém použití programu "tc" je nutné zadat i parametr "dev", neboť qdisc s označením "1:" může být klidně i na zařízení ppp0 atd.

Stávající implementace je skutečně ta nejzákladnější, a trpí tak vážnými nedostatky. Jedním z nich je, že absolutně nerozlišuje provoz, který přichází z internetu, a ten, který je generován lokálně. Když pak router slouží např. i jako file-server, všechen výstup (soubory i internet) směrem k vám je stále omezen na 64kbit.

Dále nebereme v potaz druh protokolu, a tak zahazujeme i ne-TCP packety, což je, jak jsem již uváděl, špatné.

Měli bychom shapovat i provoz směrem do internetu, protože dobrá komunikace směrem k nám závisí na potvrzovacích packetech, které posíláme zpátky serveru. Kdybychom totiž přijali 10 packetů, 7 z nich zahodili, aby server zpomalil, ale potvrzení o těch třech by se k serveru nedostalo včas, zpomalil by příliš a zbytečně by nám ještě přeposlal ty tři, které už jsme zpracovali.

Často je ještě nutné rozlišovat druh provozu (HTTP, FTP, SSH, ...) a přehazovat packety ve frontách tak, aby např. SSH provoz byl co nejvíce interaktivní.

Pokud neuvedeme parametr "ceil", dosadí se za něj automaticky hodnota "rate", která je povinná. Hodnota "ceil" u žádného potomka by neměla přesáhnout hodnotu "ceil" jeho rodiče (nedává to totiž moc smysl). Stejně tak i součet "rate" všech potomků by neměl presáhnout "rate" otce.

Hodnota "ceil" u prvního a většinou jediného potomka "root node" by měla být nastavená na hodnotu o trochu nižší, než je skutečná přenosová kapacita linky směrem k vám. Proč? Na efektivní shaping totiž potřebujeme mít vše pod kontrolou my, a to včetně všemožných bufferů a mezifront. Když totiž od providera (rozuměj nejbližší hop) přijímáme data o něco pomaleji, než je schopen nám je posílat, netvoří se u něj žádná fronta a veškeré kumulování packetů se děje na naší straně. Kdyby tomu tak nebylo, bude se fronta tvořit i někde mimo naši kontrolu, veškerá interaktivita bude v -censored-, a i když data můžou proudit závratnou rychlostí, ping-time se může prodloužit až na sekundy.

Doufám, že jsem na nic důležitého nezapomněl a v příštích dílech (pokud zase utrhnu kousek času) se podíváme na to, jak vše vylepšit k naší plné spokojenosti. Jinak doufám, že jsem vám dal dostatečný úvod do problematiky a eventuální zlepšení vymyslíte sami :-)...