Linux QoS ist gar nicht so kompliziert

Nachdem ich mir erst vor kurzem wieder anhören mußte, wie kompliziert doch eigentlich QoS in Linux ist, nehme ich das als Herausforderung, das hier und jetzt zu wiederlegen.

Ziel ist es, auf einem Router dafür zu sorgen, daß die Pakete aus einem internen Netzwerk (der Wohnung halt) entsprechend ihrer Wichtigkeit mehr oder weniger Bandbreite erhalten. Für den Anfang seien Sachen wie P2P-Programme mal außen vor gelassen, statt dessen soll ein möglichst genereller Ansatz zum Tragen kommen: Die Pakete werden entprechend ihrer Größe in drei Klassen eingeteilt: Pakete mit weniger als 256 Byte, Pakete mit 257 bis 1024 Bytes und Pakete, die größer als 1024 Bytes sind. Erstere könnten z.B. interaktiver SSH- oder Sprach-Traffic sein, aber auch die ACK-Pakete einer bestehenden TCP-Verbindung fallen darunter. In der zweiten Klasse wird man HTTP-Requests finden, in der dritten Klasse dagegen finden sich vor allem große Dateiübertragungen etc. Die einfachste Methode, das zu realisieren ist, die zur Verfügung stehende Bandbreite zu unterteilen und jeweils einen Teil derselben für die ersten beiden Klassen zu reservieren - wenn kein anderer Verkehr herrscht. Da die größe eines Paketes im Header vermerkt ist, kostet so ein Größenvergleich kaum Rechenleistung.

QoS unter Linux arbeitet vor allem mit drei Elementen: Die sog. “Queueing-Disciplines” geben die Art und Weise an, wie in einer Klasse bzw. auf einem Interface die Warteschlange der Pakete intern gehandhabt wird. Im Folgenden kommen drei verschiedene Qdiscs zum Einsatz: Hierarchical Token Bucket, kurz HTB, weil die Unterteilung in verschiedene Klassen damit am einfachsten zu erreichen ist, weiterhin Stochastical Fair Queueing, das für die Pakete benutzt werden soll, die in die größte der drei Klassen fallen, und zu guter Letzt BFIFO, eine schnelle Implementierung der denkbar einfachsten Art und Weise, Pakete in Warteschlangen zu sortieren. Das zweite Element sind die Klassen - der Name ist glaube ich selbsterklärend. Man kann einem beliebigen Interface bzw. einer schon vorhandenen Klasse beliebige Unterklassen zuordnen, um den Traffic-Fluß dort weiter zu unterteilen. Das dritte Element sind die Filter, die dazu benutzt werden, Pakete in bestimmte Klassen einzusortieren. Man sollte nun noch wissen, daß QoS immer dann am zuverlässigsten funktioniert, wenn es ausgehende Pakete beeinflussen soll. Sprich, um die Download-Rate im internen Netzwerk zu beeinflussen, setzt man das QoS am internen Interface des Routers für dort ausgehende Pakete ein - diese spiegelverkehrte Sicht ist am Anfang nicht immer ganz einfach.

Mit diesem Vorwissen ist es recht einfach, sich darum zu kümmern, daß einem der Datei-Upload an einen Bekannten die Leitung nicht mehr verstopft. Sei eth1 das ausgehende Interface des Routers und die zur Verfügung stehende Bandbreite 600kBit/s:

/sbin/tc class add dev eth1 parent 1: classid 1:10 htb rate 585000 burst 1514

Dem Interface eth1 wird mit der ID 1:10 eine HTB-Klasse hinzugefügt, deren maximale Geschwindigkeit 585kBit/s betragen soll (ein bißchen Tiefstapeln schadet nie). Diese Klasse wird nun in drei Unterklassen geteilt:

/sbin/tc class add dev eth1 parent 1:10 classid 1:100 htb rate 219375 ceil 585000 burst 1514 prio 10
/sbin/tc class add dev eth1 parent 1:10 classid 1:200 htb rate 219375 ceil 585000 burst 1514 prio 9
/sbin/tc class add dev eth1 parent 1:10 classid 1:300 htb rate 146250 ceil 585000 burst 1514 prio 8

Mit den IDs 1:100, 1:200 und 1:300 werden hier drei Klassen als Kinder der ersten Klasse angelegt, deren Bandbreite auf je zwei bzw. ein Drittel von 585kBit/s beschränkt ist - durch die Anweisung “ceil” wird jedoch gesagt, daß die volle Bandbreite benutzt werden darf, wenn sie sonst nicht benötigt wird. Die “burst”-Anweisung sagt dabei, wie groß die Ausnahmen maximal sein dürfen - nützlich, wenn nur kurz Bandbreite benötigt wird, der Kernel drückt dann, bildlich gesprochen, “ein Auge zu”. Die “prio”-Anweisung sollte klar sein.

Für diese drei Klassen werden nun eigene QDiscs gesetzt und erhalten die IDs 10:, 20: und 30::

/sbin/tc qdisc add dev eth1 parent 1:100 handle 10: bfifo
/sbin/tc qdisc add dev eth1 parent 1:200 handle 20: bfifo
/sbin/tc qdisc add dev eth1 parent 1:300 handle 30: sfq perturb 10

Hintergrund ist, daß durch die BFIFO-Qdisc die Pakete in den kleineren, “interaktiveren” Klassen nicht unnötig gebremst werden, während in der Klasse für Downloads alle 10 Sekunden eine faire Ausbalancierung der einzelnen Datenströme gegeneinander vorgenommen wird.

Zuletzt muß nun der entstehende Traffic den Klassen zugeordnet werden:

/sbin/tc filter add dev eth1 protocol ip parent 1:0 prio 1 u32 match ip tos 0x10 0xff flowid 1:100
/sbin/tc filter add dev eth1 protocol ip parent 1:0 prio 2 u32 match u16 0x0000 0xff00 at 2 flowid 1:100
/sbin/tc filter add dev eth1 protocol ip parent 1:0 prio 3 u32 match u16 0x0000 0xfc00 at 2 flowid 1:200
/sbin/tc filter add dev eth1 protocol ip parent 1:0 prio 4 u32 match u16 0x0000 0x0000 at 2 flowid 1:300

In der ersten zeile werden hierbei unter Verwendung des u32-Filters Pakete mit dem Wert 16 im TOS-Feld in die kleinste, interaktive Klasse geschoben. Die drei Zeilen darunter verwenden einen komplizierteren Filterausdruck: Sie verlgeichen mittels des u16-Filters die zwei Bytes, die die Größe des Pakets enthalten (at 2) mit einer logischen Maske - bei Paketen, die kleiner als 256 (0x00ff) Bytes sind, ergibt eine (logische) UND-Verknüpfung der Paketgröße mit dem Wert 0xff00 auf jeden Fall 0x0000 - damit wird das Paket dann in die “flowid 1:100” eingereiht - jene Klasse, die wir oben erstellt hatten. Analog geschieht das mit den beiden anderen, ganz oben erwähnten Paketklassen.

Mehr ist nicht dahinter. Ein komplettes Skript kann man sich hier herunterladen, als gute Übersicht zu dem Thema empfiehlt sich, wie üblich, das Linux Advanced Routing & Traffic Control HOWTO.

(Hinweis: Das Skript ist verschollen.)

O.g. Skript stammt übrigens noch aus meiner Zeit in Nürnberg - danke, Marcel, daß Du Dir damals die Zeit genommen hast, mir das zu erklären!