weidner/computer/linux/iproute/

Traffic-Shaping mit Linux

Es gibt umfangreiche Möglichkeiten, mit Linux den Datenverkehr zu begrenzen und die verfügbare Bandbreite nach Belieben aufzuteilen. Ein paar Dinge muss ich dabei jedoch beachten, sonst kann die Sache nach hinten losgehen.

Allgemeine Rahmenbedingungen

Prinzipiell kann ich an einem Rechner nur den abgehenden Datenverkehr direkt beeinflussen. Die ankommenden Daten werden von anderen Maschinen geschickt, so dass ich von dem betrachteten Rechner aus keinen direkten Einfluss habe.

Einige Protokolle, vor allem TCP, kann ich über die Bestätigungspakete steuern. Da diese Steuerung indirekt erfolgt, brauche ich umfangreiche Kenntnisse über TCP, um das gewünschte Ergebnis zu erhalten.

Weiterhin muss ich sicherstellen, dass ich den Flaschenhals kontrolliere. Das heißt, die Stelle im Netz, wo sich die meisten Datenpakete stauen. Das ist entweder der Übergang von einem schnellen Netzsegment in ein langsames oder ein Gateway mit mehreren ankommenden Leitungen und einer abgehenden.

Wenn ich, zum Beispiel, meinen Router über Ethernet an ein DSL-Modem mit 2 Mbit/s anschließe, dann muss ich den Durchsatz am Router auf etwas weniger als 2 Mbit/s begrenzen, damit sich die Datenpakete im Router stauen und hier die weiteren Maßnahmen zum Tragen kommen. Andernfalls stauen sich die Daten am Modem und werden dort verworfen, ohne dass ich meinen Einfluss geltend machen kann.

Ein dritter Punkt, den ich bei der Priorisierung und Begrenzung von ausgewähltem Datenverkehr beachten muss, ist, diesen so nahe wie möglich an der Quelle zu begrenzen.

Habe ich zwischen zwei Netzen eine relativ langsame Verbindung und möchte zum Beispiel den E-Mail-Verkehr limitieren, dann beschränke ich den Verkehr jeweils am sendenden Gateway oder schon am Mailserver, damit die Datagramme nicht erst durch die Leitung laufen müssen, bevor sie verworfen werden.

Schließlich bedeutet Traffic-Shaping nichts anderes, als Datenpakete gezielt umzusortieren, zu verzögern oder zu verwerfen.

Traffic-Shaping bei Linux

Wenn ich mir über die Rahmenbedingungen klar bin, mache ich mich an die konkreten Schritte, den Datenverkehr mit Linux zu formen. Dabei hilft mir das Linux Advanced Routing & Traffic Control HOWTO, kurz LARTC, im Internet zu finden bei http://lartc.org/. Insbesondere Kapitel 9 Queueing Disciplines for Bandwidth Management, hilft mir weiter.

Wenn der Networkstack von Linux ein Datenpaket versenden will, reiht er es in die Warteschlange für das Interface ein. Diese Warteschlange ist mit genau einer Queuing Discipline (Qdisc) verbunden, die festlegt, in welcher Reihenfolge und in welchem zeitlichen Abstand der Schnittstellentreiber die Datenpakete versendet. Dazu wendet sich dieser ebenfalls an die Qdisc, um das nächste Datenpaket zu bekommen, das auf die Leitung geht.

Die Qdisc, die die Abfolge der gesendeten Daten steuert, kann ich mit dem Befehl tc vom Paket iproute anschauen und modifizieren.

#  tc qdisc show
qdisc pfifo_fast 0: dev eth0 root refcnt 2 bands 3 priomap  \
1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth1 root refcnt 2 bands 3 priomap  \
1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
qdisc pfifo_fast 0: dev eth2 root refcnt 2 bands 3 priomap  \
1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1

Es gibt zwei Arten von Qdisc: klassenlose und klassenbasierte.

Klassenlose Qdisc

Klassenlose Qdisc beeinflussen den Verkehr für das ganze Interface ohne weitere Unterteilung. Hier kann ich keine weiteren Qdisc oder Klassen hinzufügen.

Die einfachste klassenlose Qdisc ist pfifo_fast, die als Default bei jedem Interface verwendet wird. Andere sind tbf (Token Bucket Filter), dass sich gut zur Geschwindigkeitsbegrenzung eignet, und sfq (Stochastic Fair Queueing), welches nützlich ist, wenn die abgehende Leitung voll ausgelastet und die Warteschlangen voll sind.

Klassenbasierte Qdisc

Bei klassenbasierten Qdisc können die Datagramme "klassifiziert" werden.

Zu diesem Zweck binde ich an die Qdisc Klassen, die ihrerseits wiederum klassenlose Qdisc oder Qdisc mit Unterklassen enthalten.

Filter sortieren die Datenpakete in die gewünschten Klassen ein.

Sowohl die Klassen, die in ihnen enthaltenen Qdisc als auch die Filter manipuliere ich mit dem Befehl tc.

Klassenbasierte Qdisc verwende ich, wenn ich den Datenverkehr nicht nur begrenzen oder nach dem IP-TOS-Feld priorisieren, sondern die Kapazität der Leitung zwischen verschiedenen Datenströmen aufteilen will

Es gibt verschiedene klassenbasierte Qdisc, die ich je nach Einsatzzweck auswähle:

Filter

Filter entscheiden bei klassenbasierten Qdisc, welcher Klasse ein Datagramm zugeordnet wird. Ich kann sie direkt an die oberste Qdisc oder weiter unten in der Hierarchie anbinden. Bei HTB ist es am besten, den Filter direkt an die Wurzel zu binden. Manche spezifischen Filter sind aus Gründen der Performance aber besser weiter unten in der Hierarchie aufgehoben.

Ein Filter kann Datagramme immer nur nach unten einsortieren, nicht zu den Elternklassen.

Bei einem Filter kann ich alle Teile eines IP-Paketes oder die von der Firewall angebrachten Markierungen zur Auswahl heranziehen.

Beispiel für Begrenzung von Teilen des Verkehrs

Ein Kunde sandte uns in unregelmäßigen Abständen riesige Druckjobs, die Teile des Netzes überlasteten. Die Datenrate dieser Druckjobs wollte ich beschränken.

Zunächst änderte ich die Qdisc des Interfaces auf HTB um.

# tc qdisc add dev eth1 root handle 1:0 htb default 11 r2q 65

Hierbei bedeutet root, dass diese Qdisc für das gesamte Interface gilt.

Mit handle 1:0 erzeuge ich ein Label, auf dass sich die Klassen beziehen können.

Durch Angabe von default 11 habe ich festgelegt, welche Klasse den unklassifizierten Traffic bekommen soll.

Mit r2q 65 habe ich einen Hinweis zur Bestimmung des Quantums angegeben, das zur Aufteilung des Datenverkehrs zwischen den einzelnen Klassen herangezogen wird. In den FAQ des LARTC finden sich nähere Informationen

Dann füge ich eine Klasse ein, die den gesamten Traffic auf etwas weniger als die Geschwindigkeit der Leitung begrenzt.

# tc class add dev eth1 parent 1:0 classid 1:1 htb \
   rate 99mbit ceil 99mbit burst 1200kb cburst 1200kb

Hierbei geben burst und cburst an, wie viele Daten mit maximal konfigurierter beziehungsweise Schnittstellengeschwindigkeit gesendet werden können, nachdem längere Zeit nur sehr wenig gesendet wurde.

Anschließend füge ich eine Klasse für die reduzierte Datenrate ein und eine für den restlichen Datenverkehr.

# tc class add dev eth1 parent 1:1 classid 1:10 htb \
     rate 5mbit ceil 10mbit
# tc class add dev eth1 parent 1:1 classid 1:11 htb \
     rate 20mbit ceil 99mbit burst 1200kb cburst 1200kb

Es ist wichtig, dass ich beide Klassen einfüge, damit der restliche Verkehr mit maximaler Geschwindigkeit rausgehen kann.

Ich hatte zunächst vergessen, eine Klasse für den unbeschränkten Verkehr anzulegen. Leider vergaß ich dann auch noch, zu testen ob außer der funktionierenden Beschränkung, der restliche Verkehr in voller Geschwindigkeit über die Leitung geht. Da die Benutzer der Leitung sich nicht sofort meldeten, bemerkte ich diesen Fehler erst viel später.

Schließlich füge ich noch einen Filter hinzu, der den Datenverkehr klassifiziert.

# tc filter add dev eth1 parent 1:0 prio 0 protocol ip \
     handle 10 fw flowid 1:10

Dieser Filter verwendet Markierungen von iptables:

# iptables -A FORWARD -d 192.168.7.16/32 -p tcp \
           -j MARK --set-xmark 0xa/0xffffffff 
# iptables -A FORWARD -d 192.168.7.22/32 -p tcp \
           -j MARK --set-xmark 0xa/0xffffffff 

Dabei verweist handle 10 des Filters auf den Wert 0xa, das ich mit iptables -j MARK --set-xmark am Datagramm anbringe.

Ich kann alle Möglichkeiten, die iptables und ip6tables bieten, nutzen.

Posted 2014-10-18
Tags: