Linux: Limitación de Descarga y Subida a nivel de Kernel

Teoría:

Cuando alcanzamos los límites de nuestra conexión, ya sea en subida y/o en bajada, los tiempos de respuesta (latencia, ping) se disparan, debido a que se están formando colas en el router. Y en general, los routers domésticos, gestionan muy mal las colas (son sistemas embebidos con muy poca memoria).

Por ello, es mejor acumular las colas en nuestros potentes ordenadores, en lugar de nuestros tristes routers que regalan con las portabilidades.

Realmente, lo de acumular la cola en el host, solo sirve para la subida, ya que la subida solo depende de nosotros. La técnica para controlar el límite de bajada es el siguiente: nuestro ordenador recibe paquetes, y tiene capacidad para gestionar todo el flujo pero intencionadamente se descartan paquetes hasta conseguir el flujo deseado. El punto anterior en la traza hasta nosotros, se dará cuenta que estamos descartando paquetes. Todos los routers interpretan esto como congestión, aunque sea congestión simulada. El router emisor se relaja hasta que le aceptemos todos los paquetes, que será cuando alcanzemos el flujo de descarga deseado. El único problema, es que cíclicamente los emisores probarán suerte volviendo aumentar el flujo para ver si hemos superado la congestión, y necesariamente cíclicamente se descartarán paquetes. Esto no es grave, también pasa cuando descargamos algo, y llega al auténtico límite de nuestra conexión, pero es interesante comentarlo.

Todo esto de controlar la congestión mediante colas en el propio host para mantener las latencias se denomina QoS. Calidad de servicio.

Aunque algunos routers caseros han intentado implimentar el QoS, en si experiencia, dejan bastante que desear. Si queremos un QoS de calidad, la única opción es montar un host sobre Linux que hará la función de router, entre otras cosas gestionando todos los flujos con QoS y prácticamente capacidad para colas tan grandes como nuestra capacidad RAM.

Si nuestra LAN es pequeña, este esquema no es barato, tener que tener un pc 24 horas con la función principal de rutear (pero router de calidad empresarial eso sí.). Otra forma sería poner los límites en cada uno de los hosts.

Evidenetemente las restricciones las pondrá un root, para que no haya forma de saltarse los límites de descarga/subida. Esto en Linux en fácil, en Windows, por su mal diseño, te obliga a trabajar como administrador, y por tanto podrías cerrar procesos como NetLimiter que realizan tambien QoS, pero en Windows.

Linux utiliza el comando «tc» para controlar los flujos, pero su uso es realmente extenso, por ello existe un script cuya funcionalidad se limita a un caso de uso muy común (limitar todo el ancho de banda, bajada y subida sin diferenciar Web de Emule, etc …). El script en cuestión es wondershaper.

Bueno, pasamos a lo interesante xD.

Práctica:

Hacemos un test de velocidad, asegurate que nadie use internet, o desenchufa a todos en el switch xD. Necesitamos los datos de la conexión en Kbps (bits). Es decir para 6 Mb de bajada 1 Mb de subida son 6144Kbps / 1024Kbps teorícos pero por el over heat del TCP/IP será un 15-20% menos, supongamos 5100/750. Ese es el limite real, que sera lo que deevuelva un test de velocidad. A ese resultado reducirlo otra 20% más, como margen para permitir una ventana de concurrencia con los demás hosts, quedandose 4080 Kbps / 600 Kbps. Hemos puesto un colchon de un 20%, debeis probar varios valores, entre mayor sea el colchón más asegurado esta el ping. con un 20% debería no subir nunca el ping, pero si de por sí vuestro router es malo(puede repartir mal el tiempo entre bocas), por puede que necesiteis más colchón para asegurar la calidad del servicio. Ahora simplemente INSTALAR Y LIMITAR.

Instalamos el script: sudo apt-get install wondershaper
Con ifconfig miramos nuestro interfaz en uso: Por ejemplo, en mi caso eth0.
Limitramos la conexión: sudo wondershaper eth0 4080 600
Yo aparte me he puesto un alias en el .bash_aliases: alias limitar=’sudo wondershaper eth0′

Ahora hay que hacer pruebas, limitar todos los hosts, aumentar la concurrencia y en todo host debería tener unos tiempos mínimos de latencia. Si realmente necesitas un colchón muy grande, como un 60% o así, es que tienes un problema en tu red, probablemente exceso de colisiones, puede que uses un hub, o que la red sea muy grande y necesites separar los dominios de colisión con routers o puentes.

El script depende de iproute, en otras distribuciones tambien llamado iproute2, en ubuntu viene instalado, pero igual en otras distribuciones no viene.

Cuando ya tengais los valores correctos, para hacerlo definitivos:

1º Editamos: sudo gedit /etc/network/interfaces
2º Y lo dejáis así, pero con vuestros valores :D:

auto lo
iface lo inet loopback
up /usr/sbin/wondershaper eth0 4080 600
down /usr/sbin/wondershaper remove eth0

Existen otras formas, más dinámicas y válidas para cualquier adaptador con los script de if.up e if.down pero a mi me vale así.

Por último para eliminar el QoS (y se puede deducir del paste anterior) escribimos:
sudo wondershaper remove eth0

Ya puedo jugar el Counter Strike, mientras los demás están con el JDownloader, Emule, Torrent … !! :D

Sincronización Pura en Java

Yo no soy muy de Java, pero estoy haciendo la una práctica sobre concurrencia en Java y bueno he tenido que recordar algunas cosas. Me he hecho una implementación de para la sincronización pura entre 2 hilos en Java.

Existen muchas variantes de la explicación del problema del consumidor / productor. Yo para probar la clase SincronizadorPuro he considerado el problema de la siguiente forma.

  • El consumidor va a consumir N veces
  • El productor va a producir N veces
  • Siempre lo deben hacer alternativamente

El recurso de lo que se consume o produce debe ser protegido, y por tanto realmente estamos hablando de comunicación sincronizada, pero realmente el problema que quiero abordar es el de la sincronización pura. Además que solucionar el problema de comunicación es tan simple como hacer sincronizados los metodos get / set del atributo a proteger.

El diagrama de precedencia sería:

A—-> B —-> A —-> B —- …..—->A ——> B

Este diagrama se resuelve con 2 semaforos iniciados a 0 con máximo 1. Que es lo que abstroigo en la clase SincronizacionPura con los métodos: esperar() / avisar() que corresponden al típico wait / signal de los semáforos.

Por ello el algoritmo para A es simplemente:

  • Esperará a B excepto si es la primera iteración.
  • Se ejecuta el código A, en mi ejemplo pone el contador del for en el recurso compartido.
  • Se avisa a B de que ha terminado.

Y analogamente, el algoritmo de B:

  • Primero esperará a A siempre.
  • Ejecuta el código asociado a B, que en mi caso es imprimir el valor del recurso.
  • Se avisa a A excepto en la última iteración.

Código del ejemplo:

package parquetematico;

import java.util.concurrent.Semaphore;

class Productor implements Runnable
{
    int INICIO;
    int FIN;
    int INC;
    Recurso recurso;
    SincronizadorPuro sync_productor;
    SincronizadorPuro sync_consumidor;

    public Productor(int INICIO, int FIN, int INC, Recurso recurso, SincronizadorPuro sync_productor, SincronizadorPuro sync_consumidor)
    {
        this.INICIO = INICIO;
        this.FIN = FIN;
        this.INC = INC;
        this.recurso = recurso;
        this.sync_productor = sync_productor;
        this.sync_consumidor = sync_consumidor;
    }

    public void run()
    {
        int i = INICIO;

        while (i <= FIN)
        {
            if(INICIO != i)
                sync_consumidor.esperar();

            recurso.setX(i);
            System.out.println("produce " + recurso.getX());

            sync_productor.avisar();

            i+=INC;
        }
    }

}

 class Consumidor implements Runnable
 {

    int INICIO;
    int FIN;
    int INC;
    Recurso recurso;
    SincronizadorPuro sync_productor;
    SincronizadorPuro sync_consumidor;

    public Consumidor(int INICIO, int FIN, int INC, Recurso recurso, SincronizadorPuro sync_productor, SincronizadorPuro sync_consumidor)
    {
        this.INICIO = INICIO;
        this.FIN = FIN;
        this.INC = INC;
        this.recurso = recurso;
        this.sync_productor = sync_productor;
        this.sync_consumidor = sync_consumidor;
    }

    public void run()
    {
        int i = INICIO;

        while (i <= FIN)
        {
            sync_productor.esperar();

            System.out.println("consume " + recurso.getX());

            if(FIN != i)
                sync_consumidor.avisar();

            i+=INC;
        }
    }

}

class SincronizadorPuro
{
    Semaphore sync;

    public SincronizadorPuro(int max) {
        // crea un semaforo en (max, max)
        sync = new Semaphore(max);

        // lo dejamos como (0, max)
        for(int i=0 ; i<max ; i++)
        {
            try {
            sync.acquire();
            } catch (InterruptedException e) {}
        }
    }

    /*
     * Pseudo-implementación conceptual
     *
    WAIT / P / ESPERAR / ACQUIRE
    {
        cont--;
        if (cont < 0)
        {
            while (true)
            {
                try
                {
                    wait();
                    break;
                }
                catch (InterruptedException e)
                {
                    if (cont >= 0)
                        break;
                    else
                        continue;
                }
            }
        }
    }
    */
    public void esperar()
    {
        try {
        sync.acquire();
        } catch (InterruptedException e) {}
    }

    /*
     * Pseudo-implementación conceptual
     *
    SIGNAL/ V / AVISAR / RELEASE
    {
        cont++;
        if (cont <= 0)
            notify();
        if (cont > 1)
            cont = 1;
    }
    */
    public void avisar()
    {
        sync.release();
    }

    public int getContador()
    {
        return sync.availablePermits();
    }
}

class Recurso
{
    private int x;

    public Recurso(int x)
    {
        this.x = x;
    }

    public synchronized void setX(int x)
    {
        this.x = x;
    }

    public synchronized int getX()
    {
        return x;
    }

}

public class principal
{

    public static void main(String[] args)
    {
        Recurso r1 = new Recurso(0);
        SincronizadorPuro sync_productor = new SincronizadorPuro(1);
        SincronizadorPuro sync_consumidor = new SincronizadorPuro(1);

        // produce/consume desde 0 hasta 5000 en pasos de 1
        Productor p1 = new Productor(0,5000, 1, r1, sync_productor, sync_consumidor);
        Consumidor c1 = new Consumidor(0,5000, 1, r1, sync_productor, sync_consumidor);
        Thread h1 = new Thread(p1);
        Thread h2 = new Thread(c1);
        h1.start();
        h2.start();
        try
        {
            h1.join();
            h2.join();
        }
        catch(InterruptedException e) {}
    }

}