Qualidade de serviço (QoS) na Internet

O objetivo deste artigo é introduzir o tema "qualidade de serviço" (QoS) na Internet, com algum foco nas ferramentas de QoS do Linux. A documentação autoritativa sobre o assunto ainda é o velho site http://lartc.org.

Afinal, existe QoS no TCP/IP e na Internet?

A resposta curta: "tem não". A única coisa que você pode controlar na Internet é o ritmo dos pacotes que você envia.

Você nem sequer pode controlar os pacotes que recebe. Pode até ignorá-los, mas aí a Inês é morta, eles já ocuparam espaço no seu link. Para sacanear o link de alguém, basta juntar uma meia dúzia de pessoas e fazer todas mandarem montes de pacotes para a vítima.

Além disso, o TCP tem a tendência natural de usar toda a banda disponível. Em tese, diversas conexões TCP em paralelo vão compartilhar a banda disponível, mas na prática seria mais correto dizer que elas vão brigar pela banda. Em particular as conexões "novatas" vão sofrer muito para abrir caminho. Todo mundo que já tentou acessar um site enquanto puxava uma imagem ISO sabe como é.

A aplicação pode em tese controlar o volume de dados, mas apenas uma minoria delas realmente faz isso. Como o TCP/IP não oferece mecanismos diretos para descobrir por exemplo a velocidade do seu link, seria difícil para o browser auto-configurar-se para usar, digamos, apenas metade da banda disponível.

Para implementar qualidade de serviço na Internet, seria necessário que todos os roteadores suportassem o conceito, ou pelo menos os que estão na boca de um gargalo (imagine o link internacional do seu odiado provedor de acesso à Internet :). Se você fizer uma conexão TCP entre Brasil e Nova Zelândia e quiser uma banda mínima garantida de 100kbps, todos os roteadores do caminho têm de garantir 100kbps. Isto garante que a) nossos dados vão fluir; e b) a conexão TCP não vai monopolizar o link.

Implementar essa garantia é perfeitamente possível, existe até o protocolo (RSVP) e roteadores que o suportam, mas a coisa pára numa simples pergunta: quem vai pagar a conta? Todo mundo quer QoS mas ninguém quer pagar a mais por isso, e se fosse de graça, certamente seria alvo de abuso.

Conforme veremos ao longo do texto, a maioria das estratégias giram mesmo em volta de "fechar a nossa boca", controlando os pacotes que as interfaces de rede emitem.

Mas a minha Internet funciona, que diferença faz?

Dadas as limitações e características do TCP/IP, eu fico sinceramente surpreso que, na maior parte do tempo, a coisa funciona. Mesmo serviços como VoIP e videoconferência são utilizados por milhões de pessoas leigas ao redor do globo. Como é que pode?

Isto se deve, em primeiríssimo lugar, ao controle de congestionamento do TCP, que é o "antídoto" para a tendência de comer toda a banda. Ao primeiro sinal de perda de pacotes, o TCP reduz bruscamente a taxa de envio. Implementações mais recentes também suportam ECN (Explicit Congestion Notification), bits no pacote IP que se setados causam a redução antes de realmente haver perda de pacote.

Além de evitar o congestionamento generalizado da Internet, esta característica do TCP permite que usemos alguns truques para implementar QoS, conforme veremos.

Outro recurso pouco comentado do TCP/IP é o TOS (Type Of Service). É um pequeno mapa de bits do IP (abaixo do TCP), que sugere prioridade e tipo de tráfego. Há três bits de prioridade, três bits de tipo e dois bits que são hoje utilizados pelo ECN.

Embora nenhum roteador intermediário seja obrigado a prestar atenção a estes bits (até porque podem ser abusados), a maioria dos aplicativos, roteadores e sistemas operacionais respeita estes bits e prioriza o tráfego de acordo. Lembrando novamente que isto só afeta o tráfego upstream, cuja fila o nosso roteador pode controlar e priorizar.

Mas afinal, o que é qualidade de serviço (QoS)?

Uma definição informal seria: vários serviços usando o mesmo link para acessar a Internet, cada um com requisitos diferentes, e todos conseguindo fazer o seu trabalho. Fazer download rápido, usar Skype sem "cortar" a voz e acessar o VNC remoto com baixo tempo de resposta, tudo ao mesmo tempo.

Os parâmetros de qualidade de serviço que nos interessam normalmente são:

BANDA: muitos serviços precisam de uma banda mínima para funcionar, como no caso de voz sobre IP ou videoconferência. E é óbvio que, quanto mais banda disponível houver, mais rápidos serão os downloads, aberturas de páginas Web etc.

LATÊNCIA: é o tempo que o pacote leva para chegar ao destino. Alta latência prejudica muito jogos e VNC/ssh remoto, e também prejudica um pouco voz sobre IP. Quando muito alta, também causa demoras para abrir páginas Web, coisa bastante irritante.

Muitas vezes, um problema de latência é percebido como "falta de banda". As primeiras ADSL tinham apenas o dobro da banda dos modems discados, mas pareciam ter dez vezes mais, simplesmente porque sua latência é muito menor (RTT de 30ms para ADSL e 140ms para modem).

JITTER: é a variação da latência. Causa problemas mais visíveis na voz sobre IP, embora incomode qualquer conexão TCP, já que o TCP pressupõe latência pouco variável e considera o pacote perdido se ele demora a chegar.

No caso do VoIP, a aplicação tem a opção de implementar um "jitter buffer", que nada mais é que um retardo entre a chegada dos pacotes e o áudio. Isto faz com que o áudio toque sem cortes, mesmo quando há jitter, mas tem a séria desvantagem de aumentar a latência percebida. Um usuário fala, o outro demora a ouvir, e demora mais ainda para responder. Aí começa aquele "Alô?... alô?...".

Como infernizar os demais usuários da sua rede

Se não houver controle de QoS no acesso à Internet, basta colocar uns downloads bem grandes para baixar. As consequências serão as seguintes:

a) As conexões TCP de download aumentarão o ritmo até ocupar toda a banda disponível.

b) Como provavelmente o seu link com a Internet é o trecho mais "estreito" do caminho até o servidor dos arquivos, o roteador do seu provedor de acesso começará a enfileirar pacotes. No caso de links ADSL, esta fila pode ser muito longa, equivalente a até 5 segundos de tráfego.

c) Qualquer outro usuário que inicie uma outra conexão à Internet vai entrar lá no fim na fila na hora de receber a resposta. A conexão dele vai demorar 5 segundos para abrir, em vez dos usuais 0,03.

d) Se qualquer usuário tentar usar voz sobre IP, enfrentará a fila e verá que a voz do interlocutor demorará até 5 segundos para chegar, bem pior que uma ligação internacional.

e) O roteador do provedor vai finalmente começar a descartar pacotes, o que faz seu download "maneirar" momentaneamente no consumo da banda. Isto faz o tamanho da fila variar, portanto faz oscilar a latência, aumentando o jitter.

Se você abriu diversos downloads simultâneos, elas vão "brigar" entre si, o excesso de banda de uma causando a perda de pacotes de outra, fazendo o comprimento da fila variar ainda mais, estragando completamente a Internet dos seus colegas...

Neste cenário, apenas o download é prejudicado. O envio de pacotes para fora ainda está livre. Para "resolver" isto, você dispara um Torrent também, de preferência oferecendo conteúdo bastante popular, e sem configurar limites de banda. Aí vai acontecer que:

a) O Torrent vai começar a mandar pacotes upstream até ocupar toda a banda. O seu roteador local, ou access point, vai começar também a enfileirar pacotes;

b) Agora os demais usuários enfrentam duas filas: uma na saída (roteador local) e outra na entrada (roteador do provedor). A latência pode dobrar, e o jitter aumentar muito.

O Wally (do Dilbert) não poderia ter feito melhor. Internet 'parada', todo mundo pode ficar bebendo café na copa e reclamando da chefia que "não abre a mão pra arrumar um link mais rápido".

Como evitar que um usuário acabe com o acesso à Internet

Ok, agora é a vez de jogar com as pretas: o administrador de rede ou fabricante de roteadores contra-ataca.

A rigor, não há defesa perfeita contra um usuário decidido a entupir o acesso à Internet. Uma política simples e muito eficiente é simplesmente policiar o tráfego, usando alguma ferramenta como ntop, e puxar a orelha de quem estiver fazendo mau uso.

Mas às vezes os usuários, e os próprios sistemas operacionais, usam mal a Internet sem a intenção. O simples fato de acessar uma página Web mais 'pesada' já causa problemas. Então é preciso adotar algumas táticas proativas e automáticas.

Vamos imaginar uma rede bem simples com roteador NAT, que pode ser um simples access point:

rede local     +----------+
---------------|eth0      |
               | roteador |          +--------------+
	       |      ppp0|----------| roteador ISP |----> Internet	
	       +----------+   ADSL   +--------------+

A primeira coisa a fazer é: NÃO DEIXAR ENFILEIRAR PACOTES NO ROTEADOR DO PROVEDOR (ISP). Não podemos mexer no roteador ISP, nem podemos controlar que pacotes ele nos manda. Mas podemos atrasar a entrega desses pacotes à rede local (saída da interface eth0). Podemos até descartar pacotes.

Em qualquer caso, isto fará as conexões TCP entrar em modo de controle de congestionamento, e diminuir o ritmo. O objetivo final é que esta redução de ritmo "limpe" a fila do roteador ISP.

Para que isto realmente aconteça, temos de "estrangular" eth0, deixando passar uma banda (um pouco) menor que o nosso link ADSL.

Não há garantias de que esta estratégia funcione, e nem que o efeito seja imediato, uma vez detectado o excesso de pacotes. Mas em geral funciona bem.

Escolher a banda de eth0 é mais adivinhação que ciência. Quanto menor a banda de eth0, menor a chance de enfileiramento, e mais rápido o link se recupera de um eventual congestionamento. Seria ótimo ter um link de 100Mbps e limitar a banda a 50Mbps... mas então nosso link ficaria 50% ocioso.

Esta é uma característica inevitável do QoS sobre TCP/IP: para obter qualidade, precisamos dar alguma coisa em troca, geralmente será um pouco de banda.

No caso de um link de 2Mbps (que na verdade é menos, pois há o overhead do ATM e do PPPoE), uma limitação da banda a 1600kbps dá bons resultados, com uma perda na taxa de download de aproximadamente 12%.

Exemplo de configuração usando o controle de tráfego do Linux:

DOWN=1600
tc qdisc del dev eth0 root    2> /dev/null > /dev/null

# Disciplina downstream, feito na interface eth0
# Nem todo tráfego da eth0 deve ser disciplinado; apenas
# o que veio da Internet. Tráfego da rede local corre solto.

tc qdisc add dev eth0 root handle 1: htb r2q 100

# Classe com a capacidade downstream da ADSL 
tc class add dev eth0 parent 1: classid 1:20 htb rate ${DOWN}kbit ceil ${DOWN}kbit

# Novamente usamos SFQ para justiça entre conexões
tc qdisc add dev eth0 parent 1:20 handle 20: sfq perturb 11

# Agora, marcamos o tráfego que deve ser controlado pela regra acima
# Critério: marcar os pacotes IP/IPv6 que vêm de interfaces expostas
# à Internet
iptables -t mangle -F PREROUTING
ip6tables -t mangle -F PREROUTING
iptables  -A PREROUTING -t mangle -i ppp0 -j MARK --set-mark 20
ip6tables -A PREROUTING -t mangle -i sixxs -j MARK --set-mark 20
tc filter add dev eth0 protocol ip parent 1:0 handle 20 fw flowid 1:20
tc filter add dev eth0 protocol ipv6 parent 1:0 handle 20 fw flowid 1:20

# Para ver estatísticas:
# tc -s -d qdisc show dev eth0
# tc -s -d class show dev eth0

Todo o script aí em cima destina-se unicamente a "estrangular" o tráfego vindo da Internet e destinado a nossa rede local. Ele não controla, por exemplo, o tráfego destinado ao próprio roteador local.

A segunda coisa a fazer, que na verdade o script aí em cima já faz, é ASSEGURAR JUSTIÇA ENTRE CONEXÕES CONCORRENTES. Conforme dissemos antes, as conexões TCP disputam a banda umas com as outras. Mas, agora que criamos uma fila na interface de rede eth0, podemos usá-la para impedir que uma conexão monopolize a banda.

Vamos observar os dois comandos a seguir, que são o "coração" do script acima:

tc class add dev eth0 parent 1: classid 1:20 htb rate ${DOWN}kbit ceil ${DOWN}kbit
tc qdisc add dev eth0 parent 1:20 handle 20: sfq perturb 11

A primeira linha cria uma CLASSE de tráfego, que é onde estrangulamos a banda. A segunda linha cria uma DISCIPLINA DE FILA. Utilizamos a disciplina SFQ, que distribui internamente as conexões em sub-filas e dá a cada sub-fila a chance de mandar um pacote.

Num computador rodando Linux sem QoS configurado, não há classe de tráfego, e cada interface tem apenas uma disciplina de fila: QFIFO_FAST, que leva em conta os bits TOS do TCP/IP. Ou seja, mesmo sem configuração, já existe algum controle de tráfego.

A estrutura interface-classe-disciplina é definida de cima para baixo, mas o pacote de rede na verdade flui de baixo para cima. O pacote vindo da Internet é encaixado numa disciplina de fila, e fica (obviamente) esperando na fila. Pode haver várias filas; é o comando "tc filter" que direciona o pacote à fila certa.

Quando a interface de rede está livre e pronta para mandar um pacote, ela requisita à classe "root", que por sua vez requisita à classe que criamos. Se a classe estiver pronta (o que só acontece se não tivermos extrapolado a banda configurada), ela requisita um pacote à fila (ou a uma sub-classe, se houver). A fila entrega o pacote, e ele sobe até a interface.

A terceira coisa a fazer é GERENCIAR A FILA NO NOSSO PRÓPRIO ROTEADOR. Não precisamos necessariamente descartar pacotes para esvaziar a fila; podemos fazer melhor, jogando os pacotes mais importantes mais para frente na fila.

O script abaixo implementa este controle na interface ppp0:

UP=320
tc qdisc del dev ppp0 root
tc qdisc add dev ppp0 root handle 1: htb default 1 r2q 20
tc class add dev ppp0 parent 1: classid 1:1 htb rate ${UP}kbit ceil ${UP}kbit
tc qdisc add dev ppp0 parent 1:1 handle 10: sfq perturb 10

Assim como fizemos para eth0, também definimos uma classe que limita a banda, e uma disciplina que assegura tratamento mais ou menos equitativo entre as diversas conexões.

Em tese, não precisaríamos limitar a banda, já que ela é naturalmente limitada pela velocidade da ADSL. O problema é que a interface de rede também tem sua própria fila, que é bastante longa (até 1000 pacotes) e fora do alcance do SFQ. Limitar a banda na classe garante que, quando uma classe libera um pacote, ele seja imediatamente transmitido.

Mantendo os roteadores livres de longas filas, a latência fica baixa, o jitter também, e novas conexões conseguem ser feitas muito rapidamente. A única coisa que falta é reservar banda para serviços que exigem banda garantida, como voz sobre IP. Faríamos isso adicionando sub-classes. Com o tempo, vou alterando este artigo e incrementando-o com estas técnicas.

Opcionalmente, podemos adicionar um controle adicional, quando nosso roteador (que às vezes é um velho computador rodando Linux) roda alguns serviços locais. É o "ingress":

DOWN_PRE=1750
tc qdisc del dev ppp0 ingress
tc qdisc add dev ppp0 handle ffff: ingress
tc filter add dev ppp0 parent ffff: protocol ip prio 50 u32 match ip src \
  0.0.0.0/0 police rate ${DOWN_PRE}kbit burst 20k drop flowid :1

O Linux não permite fazer grandes controles em cima do tráfego entrante de uma interface de rede; é por isso que controlamos a saída de eth0. Os comandos acima implementam um controle grosseiro, baseado em "tc filter", que simplesmente descarta pacotes que ultrapassarem a banda de 1750kbps.

Note que esta "pré-filtragem" usa uma banda maior que os 1600kbps de eth0, tanto para compensar o overhead quanto para garantir que o pré-filtro não torne inútil o filtro eth0. Isto seria desastroso, porque o pré-filtro não faz "justiça" (não tem uma disciplina de tráfego SFQ).

Existe uma outra alternativa para tratar o tráfego entrante de forma mais refinada: o projeto IMQ (Intermediate Queueing Device).

O IMQ implementa uma interface de rede virtual, onde os pacotes ingressos da ppp0 são jogados. Então, criamos classes e disciplinas para a interface IMQ "emitir" os pacotes destinados ao próprio roteador, exatamente como fizemos para a interface eth0 (que emite pacotes para usuários da rede local).

A desvantagem do IMQ é que ele ainda não está no kernel padrão do Linux. É preciso "patchear" o kernel, recompilar etc. Acaba sendo mais fácil reservar o roteador para uso exclusivo e usar outro computador para rodar os serviços que estavam no próprio roteador (como proxy, webmail etc.). Muita gente usa o IMQ com sucesso; se você é forçado a usar o roteador para outros serviços de rede, o IMQ salva o seu dia.

Miscelânea

Algumas coisas soltas que devo mencionar por completeza.

Podemos controlar banda de duas formas básicas: atrasando os pacotes ou descartando-os. Atrasar (sem descartar) é "work-conserving" ("conserva o trabalho"), ou seja, evita que um pacote perfeitamente válido tenha de ser retransmitido. A classe HTB que usamos mais acima é work-conserving na maior parte do tempo.

O HTB suporta o conceito de "borrowing" (empréstimo), onde diversas classes, cada uma com banda reservada, possa ceder banda a outras quando estiver ociosa. Normalmente é uma boa idéia permitir que isto aconteça.

HTB significa "hierachical token bucket". Token bucket é uma técnica de controle de banda, uma variação do "balde furado", que vaza a uma taxa fixa independente do volume de água dentro.

O Linux implementa outras classes, como o CBQ, onde a banda é controlada estabelecendo-se uma espera entre transmissão de pacotes. Assim, o CBQ obrigatoriamente conta pacotes e não bytes, e a estimativa do pacote médio tem de ser boa.

Quase sempre a disciplina de fila SFQ é a utilizada para "fechar" cada classe, mas existem outras. Uma disciplina que alguns scripts usam é a PRIO, uma versão elaborada da PFIFO_FAST que baseia-se igualmente nos bits de TOS do pacote IP.

Conclusão

Os recursos de controle de tráfego do Linux estão há muito tempo no kernel: desde o início dos anos 2000. Assim como o iptables, mudou muito pouco desde então. Isto não é necessariamente sinal de estagnação, mas sim de maturidade: tanto o iptables quanto o tc "encerram o assunto".

O controle de tráfego do Linux tem fama de ser muito difícil de aprender e usar. Na verdade não é tão complicado. Talvez o maior problema seja a sintaxe dos comandos, bastante chata, associada a uma documentação nada amigável. Diz-se também que o CBQ, mais difícil de "acertar" que o HTB, é um grande responsável por essa má fama (o HTB só foi integrado ao kernel mais tarde).

Outro "problema" do tc é que ele não faz milagre, nem tira leite de pedra. Muita gente fica tentando gambiarras mirabolantes para atender 2000 clientes com um linkzinho de 1Mbps. Aí naturalmente o negócio não funciona direito e o culpado é o Linux...

Script completo de exemplo de QoS

#!/bin/sh

UP=320
DOWN_PRE=1750
DOWN=1600

# Elimina velharias

tc qdisc del dev ppp0 root    2> /dev/null > /dev/null
tc qdisc del dev eth0 root    2> /dev/null > /dev/null
tc qdisc del dev ppp0 ingress 2> /dev/null > /dev/null

[ "$1" = "off" ] && exit 0

# Topo da disciplina para a interface ppp0

# r2q é escolhido de forma que
# banda / r2q fique apenas um pouco maior que o MTU
# No caso, 320kbits = 40kbytes / 20 = 2kbytes > 1500

tc qdisc add dev ppp0 root handle 1: htb default 1 r2q 20

# Classe 1:1, limita a taxa à capacidade do link upstream
tc class add dev ppp0 parent 1: classid 1:1 htb rate ${UP}kbit ceil ${UP}kbit

# Para a classe 1:1, a disciplina de tráfego é SFQ,
# que assegura justiça entre as muitas conexões
tc qdisc add dev ppp0 parent 1:1 handle 10: sfq perturb 10

# Também fazemos um "shaping" prévio no downstream,
# embora o controle fino do downstream seja feito na eth0

tc qdisc add dev ppp0 handle ffff: ingress
tc filter add dev ppp0 parent ffff: protocol ip prio 50 u32 match ip src \
  0.0.0.0/0 police rate ${DOWN_PRE}kbit burst 20k drop flowid :1

# Disciplina downstream, feito na interface eth0
# Nem todo tráfego da eth0 deve ser disciplinado; apenas
# o que veio da Internet. Tráfego da rede local corre solto.

tc qdisc add dev eth0 root handle 1: htb # r2q 100

# Classe com a capacidade downstream da ADSL 
tc class add dev eth0 parent 1: classid 1:20 htb rate ${DOWN}kbit ceil ${DOWN}kbit

# Novamente usamos SFQ para justiça entre conexões
tc qdisc add dev eth0 parent 1:20 handle 20: sfq perturb 11

# Agora, marcamos o tráfego que deve ser controlado pela regra acima
# Critério: marcar os pacotes IP/IPv6 que vêm de interfaces expostas
# à Internet
iptables -t mangle -F PREROUTING
ip6tables -t mangle -F PREROUTING
iptables  -A PREROUTING -t mangle -i ppp0 -j MARK --set-mark 20
ip6tables -A PREROUTING -t mangle -i sixxs -j MARK --set-mark 20
tc filter add dev eth0 protocol ip parent 1:0 handle 20 fw flowid 1:20
tc filter add dev eth0 protocol ipv6 parent 1:0 handle 20 fw flowid 1:20

# O tráfego não marcado não passará por classe nenhuma, e aparece
# apenas na estatística da disciplina "root".

# tc -s -d qdisc show dev ppp0
# tc -s -d class show dev ppp0
blog comments powered by Disqus