9.5. Gestionnaires de file d'attente basés sur les classes

Les gestionnaires de mise en file d'attente basés sur des classes sont très utiles si vous avez différentes sortes de trafic qui doivent être traités différemment. L'un d'entre eux est appelé CBQ, pour Class Based Queueing. Il est si souvent mentionné que les personnes identifient les gestionnaires de mise en file d'attente basés sur des classes uniquement à CBQ, ce qui n'est pas le cas.

CBQ est le mécanisme le plus ancien, ainsi que le plus compliqué. Il n'aura pas forcément les effets que vous recherchez. Ceci surprendra peut-être ceux qui sont sous l'emprise de << l'effet Sendmail >>, qui nous enseigne qu'une technologie complexe, non documentée est forcément meilleure que toute autre.

Nous évoquerons bientôt, plus à propos, CBQ et ses alternatives.

9.5.1. Flux à l'intérieur des gestionnaires basés sur des classes & à l'intérieur des classes

Quand le trafic entre dans un gestionnaire de mise en file d'attente basé sur des classes, il doit être envoyé vers l'une de ses classes ; il doit être << classifié >>. Pour déterminer que faire d'un paquet, les élements appelés << filtres >> sont consultés. Il est important de savoir que les filtres sont appelés de l'intérieur d'un gestionnaire, et pas autrement !

Les filtres attachés à ce gestionnaire renvoient alors une décision que le gestionnaire utilise pour mettre en file d'attente le paquet vers l'une des classes. Chaque sous-classe peut essayer d'autres filtres pour voir si de nouvelles instructions s'appliquent. Si ce n'est pas le cas, la classe met le paquet en file d'attente dans le gestionnaire de mise en file d'attente qu'elle contient.

En plus de contenir d'autres gestionnaires, la plupart des gestionnaires de mise en file d'attente basés sur des classes réalisent également de la mise en forme. Ceci est utile pour réaliser à la fois l'ordonnancement (avec SFQ, par exemple) et le contrôle de débit. Vous avez besoin de ceci dans les cas où vous avez une interface à haut débit (ethernet, par exemple) connectée à un périphérique plus lent (un modem câble).

Si vous n'utilisez que SFQ, rien ne devait se passer, dans la mesure où les paquets entrent et sortent du routeur sans délai : l'interface de sortie est de loin beaucoup plus rapide que la vitesse réelle de votre liaison ; il n'y a alors pas de files d'attente à réordonnancer.

9.5.2. La famille des gestionnaires de mise en file d'attente : racines, descripteurs, descendances et parents

Chaque interface à << un gestionnaire de mise en file d'attente racine >> de sortie (egress root qdisc). Par défaut, le gestionnaire de mise en file d'attente sans classes mentionné plus tôt pfifo_fast. Chaque gestionnaire peut être repéré par un descripteur (handle), qui pourra être utilisé par les prochaines déclarations de configuration pour se référer à ce gestionnaire. En plus du gestionnaire de sortie, une interface peut également avoir un gestionnaire d'entrée (ingress), qui réglemente le trafic entrant.

Ces descripteurs sont constitués de deux parties : un nombre majeur et un nombre mineur. Il est habituel de nommer le gestionnaire racine 1:, ce qui est équivalent à 1:0. Le nombre mineur d'un gestionnaire de mise en file d'attente est toujours 0.

Les classes doivent avoir le même nombre majeur que leur parent.

9.5.2.1. Comment les filtres sont utilisés pour classifier le trafic

Pour récapituler, une hiérarchie typique pourrait ressembler à ceci :

                  racine 1:
 |
 _1:1_
 /  |  \
 /   |   \
 /    |    \
 10:   11:   12:
 /   \       /   \
 10:1  10:2   12:1  12:2

Mais ne laissez pas cet arbre vous abuser ! Vous ne devriez pas imaginer le noyau être au sommet de l'arbre et le réseau en-dessous, ce qui n'est justement pas le cas. Les paquets sont mis et retirés de la file d'attente à la racine du gestionnaire, qui est le seul élément avec lequel le noyau dialogue.

Un paquet pourrait être classifié à travers une chaîne suivante :
1: -> 1:1 -> 12: -> 12:2

Le paquet réside maintenant dans la file d'attente du gestionnaire attaché à la classe 12:2. Dans cet exemple, un filtre a été attaché à chaque noeud de l'arbre, chacun choisissant la prochaine branche à prendre. Cela est réalisable. Cependant, ceci est également possible :
1: -> 12:2

Dans ce cas, un filtre attaché à la racine a décidé d'envoyer le paquet directement à 12:2.

9.5.2.2. Comment les paquets sont retirés de la file d'attente et envoyés vers le matériel

Quand le noyau décide qu'il doit extraire des paquets pour les envoyer vers l'interface, le gestionnaire racine 1: reçoit une requête dequeue, qui est transmise à 1:1 et qui, à son tour, est passée à 10:, 11: et 12:, chacune interrogeant leurs descendances qui essaient de retirer les paquets de leur file d'attente. Dans ce cas, le noyau doit parcourir l'ensemble de l'arbre, car seul 12:2 contient un paquet.

En résumé, les classes << emboîtées >> parlent uniquementà leur gestionnaire de mise en file d'attente parent ; jamais à une interface. Seul la file d'attente du gestionnaire racine est vidée par le noyau !

Ceci a pour résultat que les classes ne retirent jamais les paquets d'une file d'attente plus vite que ce que leur parent autorise. Et c'est exactement ce que nous voulons : de cette manière, nous pouvons avoir SFQ dans une classe interne qui ne fait pas de mise en forme, mais seulement de l'ordonnancement, et avoir un gestionnaire de mise en file d'attente extérieur qui met en forme le trafic.

9.5.3. Le gestionnaire de mise en file d'attente PRIO

Le gestionnaire de mise en file d'attente ne met pas vraiment en forme le trafic ; il ne fait que le subdiviser en se basant sur la manière dont vous avez configuré vos filtres. Vous pouvez considérer les gestionnaires PRIO comme une sorte de super pfifo_fast dopé, où chaque bande est une classe séparée au lieu d'une simple FIFO.

Quand un paquet est mis en file d'attente dans le gestionnaire PRIO, une classe est choisie en fonction des filtres que vous avez donnés. Par défaut, trois classes sont créées. Ces classes contiennent par défaut de purs gestionnaires de mise en file d'attente FIFO sans structure interne, mais vous pouvez les remplacer par n'importe quels gestionnaires disponibles.

Chaque fois qu'un paquet doit être retiré d'une file d'attente, la classe :1 est d'abord testée. Les classes plus élevées ne sont utilisées que si aucune des bandes plus faibles n'a pas fourni de paquets.

Cette file d'attente est très utile dans le cas où vous voulez donner la priorité à certains trafics en utilisant toute la puissance des filtres tc et en ne se limitant pas seulemenent aux options du champ TOS. Il peut également contenir n'importe quel gestionnaire de mise en file d'attente, tandis que pfifo_fast est limité aux gestionnaires simples FIFO.

Puisqu'il ne met pas vraiment en forme, on applique le même avertissement que pour SFQ. Utilisez PRIO seulement si votre lien physique est vraiment saturé ou intégrez-le à l'intérieur d'un gestionnaire de mise en file d'attente basé sur des classes qui réalisent la mise en forme. Ce dernier cas est valable pour pratiquement tous les modems-câbles et les périphériques DSL.

En termes formels, le gestionnaire de mise en file d'attente PRIO est un ordonnanceur Work-Conserving.

9.5.3.1. Paramètres PRIO & usage

Les paramètres suivants sont reconnus par tc :

bands

Nombre de bandes à créer. Chaque bande est en fait une classe. Si vous changez ce nombre, vous devez également changer :

priomap

Si vous ne fournissez pas de filtres tc pour classifier le trafic, le gestionnaire PRIO regarde la priorité TC_PRIO pour décider comment mettre en file d'attente le trafic.

Ceci fonctionne comme le gestionnaire de mise en file d'attente pfifo_fast mentionné plus tôt. Voir la section correspondante pour plus de détails.

Les bandes sont des classes et sont appelées par défaut majeur:1 à majeur:3. Donc, si votre gestionnaire de mise en file d'attente est appelé 12:, tc filtre le trafic vers 12:1 pour lui accorder une plus grande priorité.

Par itération, la bande 0 correspond au nombre mineur 1, la bande 1 au nombre mineur 2, etc ...

9.5.3.2. Configuration simple

Nous allons créer cet arbre :

     racine 1: prio
 /   |   \
 1:1  1:2  1:3
 |    |    |
 10:  20:  30:
 sfq  tbf  sfq
bande   0    1    2

Le trafic de masse ira vers 30: tandis que le trafic interactif ira vers 20: ou 10:.

Les lignes de commande :

# tc qdisc add dev eth0 root handle 1: prio
## Ceci crée *instantanément* les classes 1:1, 1:2, 1:3
# tc qdisc add dev eth0 parent 1:1 handle 10: sfq
# tc qdisc add dev eth0 parent 1:2 handle 20: tbf rate 20kbit buffer 1600 limit 3000
# tc qdisc add dev eth0 parent 1:3 handle 30: sfq                                

Regardons maintenant ce que nous avons créé :
# tc -s qdisc ls dev eth0
qdisc sfq 30: quantum 1514b
 Sent 0 bytes 0 pkts (dropped 0, overlimits 0)
 qdisc tbf 20: rate 20Kbit burst 1599b lat 667.6ms
 Sent 0 bytes 0 pkts (dropped 0, overlimits 0)
 qdisc sfq 10: quantum 1514b
 Sent 132 bytes 2 pkts (dropped 0, overlimits 0)
 qdisc prio 1: bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 174 bytes 3 pkts (dropped 0, overlimits 0) 
Comme vous pouvez le voir, la bande 0 a déjà reçu du trafic, et un paquet a été envoyé pendant l'exécution de cette commande !

Nous allons maintenant générer du trafic de masse avec un outil qui configure correctement les options TOS, et regarder de nouveau :
# scp tc ahu@10.0.0.11:./
ahu@10.0.0.11's password:
tc                   100% |*****************************|   353 KB    00:00
# tc -s qdisc ls dev eth0
qdisc sfq 30: quantum 1514b
 Sent 384228 bytes 274 pkts (dropped 0, overlimits 0)
 qdisc tbf 20: rate 20Kbit burst 1599b lat 667.6ms
 Sent 2640 bytes 20 pkts (dropped 0, overlimits 0)
 qdisc sfq 10: quantum 1514b
 Sent 2230 bytes 31 pkts (dropped 0, overlimits 0)
 qdisc prio 1: bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 389140 bytes 326 pkts (dropped 0, overlimits 0) 
Comme vous pouvez le voir, tout le trafic a été envoyé comme prévu vers le descripteur 30:, qui est la bande de plus faible priorité. Maintenant, pour vérifier que le trafic interactif va vers les bandes de plus grande priorité, nous générons du trafic interactif :

# tc -s qdisc ls dev eth0
qdisc sfq 30: quantum 1514b
 Sent 384228 bytes 274 pkts (dropped 0, overlimits 0)
 qdisc tbf 20: rate 20Kbit burst 1599b lat 667.6ms
 Sent 2640 bytes 20 pkts (dropped 0, overlimits 0)
 qdisc sfq 10: quantum 1514b
 Sent 14926 bytes 193 pkts (dropped 0, overlimits 0)
 qdisc prio 1: bands 3 priomap  1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
 Sent 401836 bytes 488 pkts (dropped 0, overlimits 0) 

Ca a marché. Tout le trafic supplémentaire a été vers 10:, qui est notre gestionnaire de plus grande priorité. Aucun trafic n'a été envoyé vers les priorités les plus faibles, qui avaient reçu au préalable tout le trafic venant de notre scp.

9.5.4. Le célèbre gestionnaire de mise en file d'attente CBQ

Comme dit avant, CBQ est le gestionnaire de mise en file d'attente disponible le plus complexe, celui qui a eu le plus de publicité, qui est le moins compris et qui est probablement le plus farceur lors de sa mise au point. Ce n'est pas parce que les auteurs sont mauvais ou incompétents, loin de là, mais l'algorithme CBQ n'est pas remarquablement précis et il ne correspond pas vraiment à la façon dont Linux fonctionne.

En plus d'être basé sur des classes, CBQ sert également à la mise en forme de trafic et c'est sur cet aspect qu'il ne fonctionne pas très bien. Il travaille comme ceci : si vous essayez de mettre en forme une connexion de 10mbit/s à 1mbits/s, le lien doit être inactif 90% du temps. Si ce n'est pas le cas, nous devons limiter le taux de sorte qu'il soit inactif 90% du temps.

Ceci est assez dur à mesurer et c'est pour cette raison que CBQ déduit le temps d'inactivité du nombre de microsecondes qui s'écoulent entre les requêtes de la couche matérielle pour avoir plus de données. Cette combinaison peut être utilisée pour évaluer si le lien est chargé ou non.

Ceci est plutôt léger et l'on arrive pas toujours à des résultats convenables. Par exemple, qu'en est-il de la vitesse de liaison réelle d'une interface qui n'est pas capable de transmettre pleinement les données à 100mbit/s, peut-être à cause d'un mauvais pilote de périphérique ? Une carte réseau PCMCIA ne pourra jamais atteindre 100mbit/s à cause de la conception du bus. De nouveau, comment calculons-nous le temps d'inactivité ?

Cela devient même pire quand on considère un périphérique réseau "pas-vraiment-réel" comme PPP Over Ethernet ou PPTP over TCP/IP. La largeur de bande effective est dans ce cas probablement déterminée par l'efficacité des tubes vers l'espace utilisateur, qui est énorme.

Les personnes qui ont effectué des mesures ont découvert que CBQ n'est pas toujours très exact, et parfois même, très éloigné de la configuration.

Cependant, il marche bien dans de nombreuses circonstances. Avec la documentation fournie ici, vous devriez être capable de le configurer pour qu'il fonctionne bien dans la plupart des cas.

9.5.4.1. Mise en forme CBQ en détail

Comme dit précédemment, CBQ fonctionne en s'assurant que le lien est inactif juste assez longtemps pour abaisser la bande passante réelle au débit configuré. Pour réaliser cela, il calcule le temps qui devrait s'écouler entre des paquets de taille moyennne.

En cours de fonctionnement, le temps d'inactivité effectif (the effective idletime) est mesuré en utilisant l'algorithme EWMA (Exponential Weighted Moving Average), qui considère que les paquets récents sont exponentiellement plus nombreux que ceux passés. La charge moyenne UNIX (UNIX loadaverage) est calculée de la même manière.

Le temps d'inactivité calculé est soustrait à celui mesuré par EWMA et le nombre résultant est appelé avgidle. Un lien parfaitement chargé a un avgidle nul : un paquet arrive à chaque intervalle calculé.

Une liaison surchargée a un avgidle négatif et s'il devient trop négatif, CBQ s'arrête un moment et se place alors en dépassement de limite (overlimit).

Inversement, un lien inutilisé peut accumuler un avgidle énorme, qui autoriserait alors des bandes passantes infinies après quelques heures d'inactivité. Pour éviter cela, avgidle est borné à maxidle.

En situation de dépassement de limite, CBQ peut en théorie bloquer le débit pour une durée équivalente au temps qui doit s'écouler entre deux paquets moyens, puis laisser passer un paquet et bloquer de nouveau le débit. Regardez cependant le paramètre minburst ci-dessous.

Voici les paramètres que vous pouvez spécifier pour configurer la mise en forme :

avpkt

Taille moyenne d'un paquet mesurée en octets. Nécessaire pour calculer maxidle, qui dérive de maxburst, qui est spécifié en paquets.

bandwidth

La bande passante physique de votre périphérique nécessaire pour les calculs du temps de non utilisation (idle time).

cell

La durée de transmission d'un paquet n'augmente pas nécessairement de manière linéaire en fonction de sa taille. Par exemple, un paquet de 800 octets peut être transmis en exactement autant de temps qu'un paquet de 806 octets. Ceci détermine la granularité. Cette valeur est généralement positionnée à 8, et doit être une puissance de deux.

maxburst

Ce nombre de paquets est utilisé pour calculer maxidle de telle sorte que quand avgidle est égal à maxidle, le nombre de paquets moyens peut être envoyé en rafale avant que avgidle ne retombe à 0. Augmentez-le pour être plus tolérant vis à vis des rafales de données. Vous ne pouvez pas configurer maxidle directement, mais seulement via ce paramètre.

minburst

Comme nous l'avons déjà indiqué, CBQ doit bloquer le débit dans le cas d'un dépassement de limite. La solution idéale est de le faire pendant exactement le temps d'inutilisation calculé, puis de laisser passer un paquet. Cependant, les noyaux UNIX ont généralement du mal à prévoir des événements plus courts que 10 ms, il vaut donc mieux limiter le débit pendant une période plus longue, puis envoyer minburst paquets d'un seul coup et dormir pendant une durée de minburst.

Le temps d'attente est appelé offtime. De plus grandes valeurs de minburst mènent à une mise en forme plus précise dans le long terme, mais provoquent de plus grandes rafales de données pendant des périodes de quelques millisecondes.

minidle

Si avgidle est inférieur à 0, nous sommes en dépassement de limite et nous devons attendre jusqu'à ce que avgidle devienne suffisamment important pour envoyer un paquet. Pour éviter qu'une brusque rafale de données n'empêche le lien de fonctionner pendant une durée prolongée, avgidle est remis à minidle s'il atteint une valeur trop basse.

La valeur minidle est spécifiée en microsecondes négatives : 10 signifie alors que avgidle est borné à -10µs.

mpu

Taille minumum d'un paquet. Nécessaire car même un paquet de taille nulle est encapsulé par 64 octets sur ethernet et il faut donc un certain temps pour le transmettre. CBQ doit connaître ce paramètre pour calculer précisément le temps d'inutilisation.

rate

Débit du trafic sortant du gestionnaire. Ceci est le << paramètre de vitesse >> !

En interne, CBQ est finement optimisé. Par exemple, les classes qui sont connues pour ne pas avoir de données présentes dans leur file d'attente ne sont pas interrogées. Les classes en situation de dépassement de limite sont pénalisées par la diminution de leur priorité effective. Tout ceci est très habile et compliqué.

9.5.4.2. Le comportement CBQ classful

En plus de la mise en forme, en utilisant les approximations idletime mentionnées ci-dessus, CBQ peut également agir comme une file d'attente PRIO dans le sens où les classes peuvent avoir différentes priorités. Les priorités de plus faible valeur seront examinées avant celles de valeur plus élevées.

Chaque fois qu'un paquet est demandé par la couche matérielle pour être envoyé sur le réseau, un processus weighted round robin (WRR) démarre en commençant par les classes de plus faibles priorités.

Celles-ci sont regroupées et interrogées si elles ont des données disponibles. Après qu'une classe ait été autorisée à retirer de la file d'attente un nombre d'octets, la classe de priorité suivante est consultée.

Les paramètres suivants contrôlent le processus WRR :

allot

Quand le CBQ racine reçoit une demande d'envoi de paquets sur une interface, il va essayer tous les gestionnaires internes (dans les classes) tour à tour suivant l'ordre du paramètre priority. A chaque passage, une classe ne peut envoyer qu'une quantité limitée de données. Le paramètre allot est l'unité de base de cette quantité. Voir le paramètre weight pour plus d'informations.

prio

CBQ peut également agir comme un périphérique PRIO. Les classes internes avec les priorités les plus faibles sont consultées en premier et, aussi longtemps qu'elles ont du trafic, les autres classes ne sont pas examinées.

weight

Le paramètre weight assiste le processus Weighted Round Robin. Chaque classe a tour à tour la possibilité d'envoyer ses données. Si vous avez des classes avec des bandes passantes significativement plus importantes, il est logique de les autoriser à envoyer plus de données à chaque tour que les autres.

Vous pouvez utiliser des nombres arbitraires dans la mesure où CBQ additionne tous les paramètres weight présents sous une classe et les normalise. La règle empirique qui consiste à prendre rate/10 semble fonctionner correctement. Le paramètre weight normalisé est multiplié par le paramètre allot pour déterminer la quantité de données à envoyer à chaque tour.

Notez, s'il vous plaît, que toutes les classes à l'intérieur d'une hiérarchie CBQ doivent avoir le même nombre majeur !

9.5.4.3. Paramètres CBQ qui déterminent le partage & le prêt du lien

En plus de purement limiter certains trafics, il est également possible de spécifier quelles classes peuvent emprunter de la bande passante aux autres classes ou, réciproquement, prêter sa bande passante.

isolated/ sharing

Une classe qui est configurée avec isolated ne prêtera pas sa bande passante à ses classes enfants. Utilisez ceci si vous avez sur votre lien deux agences concurrentes ou qui ne s'apprécient pas et qui ne veulent pas se prêter gratuitement de la bande passante.

Le programme de contrôle tc connait également sharing, qui agit à l'inverse du paramètre isolated.

bounded/ borrow

Une classe peut aussi être bornée (bounded), ce qui signifie qu'elle n'essaiera pas d'emprunter de la bande passante à ses classes enfants. tc connait également borrow, qui agit à l'inverse de bounded.

Une situation typique pourrait être le cas où vous avez deux agences présentes sur votre lien qui sont à la fois isolated et bounded. Ceci signifie qu'elles sont strictement limitées à leur débit et qu'elles ne prêteront pas aux autres leur bande passante.

A l'intérieur de ces classes d'agence, il pourrait y avoir d'autres classes qui sont autorisées à échanger leur bande passante.

9.5.4.4. Configuration simple

Cette configuration limite le trafic d'un serveur web à 5 mbit et le trafic smtp à 3 mbit. Il est souhaitable qu'ils n'occupent pas plus de 6 mbit à eux deux. Nous avons une carte réseau à 100 mbit et les classes peuvent s'emprunter mutuellement de la bande passante.
# tc qdisc add dev eth0 root handle 1:0 cbq bandwidth 100Mbit         \
 avpkt 1000 cell 8
# tc class add dev eth0 parent 1:0 classid 1:1 cbq bandwidth 100Mbit  \
 rate 6Mbit weight 0.6Mbit prio 8 allot 1514 cell 8 maxburst 20      \
 avpkt 1000 bounded
Cette partie installe la racine et la classe 1:0 habituelle. La classe 1:1 est bornée, la bande passante totale ne pourra donc pas excéder 6 mbit.

Comme dit avant, CBQ a besoin de NOMBREUX paramètres. Tous ces paramètres sont cependant expliqués au-dessus. La configuration HTB correspondante est beaucoup plus simple.

# tc class add dev eth0 parent 1:1 classid 1:3 cbq bandwidth 100Mbit  \
 rate 5Mbit weight 0.5Mbit prio 5 allot 1514 cell 8 maxburst 20      \
 avpkt 1000
# tc class add dev eth0 parent 1:1 classid 1:4 cbq bandwidth 100Mbit  \
 rate 3Mbit weight 0.3Mbit prio 5 allot 1514 cell 8 maxburst 20      \
 avpkt 1000

Ce sont nos deux classes. Notez comment nous avons configuré la valeur du paramètre weight en fonction du paramètre rate. La bande passante de l'ensemble des deux classes ne pourra jamais dépasser 6 mbit. En fait, les identifieurs de classe (classid) doivent avoir le même numéro majeur que le parent CBQ !

# tc qdisc add dev eth0 parent 1:3 handle 30: sfq
# tc qdisc add dev eth0 parent 1:4 handle 40: sfq

Les deux classes ont par défaut un gestionnaire de mise en file d'attente FIFO. Nous les remplaçons par une file d'attente SFQ de telle sorte que chaque flux de données soit traité de manière égale.
# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip \
 sport 80 0xffff flowid 1:3
# tc filter add dev eth0 parent 1:0 protocol ip prio 1 u32 match ip \
 sport 25 0xffff flowid 1: