Balanceo de carga con Lustitia

per Victor Carceler darrera modificació 2020-04-06T10:53:55+01:00

Cuando una organización cuenta con varios enlaces a Internet de inmediato surgen dos preguntas:

  • ¿Cómo se puede aprovechar el ancho de banda conjunto de todos ellos?
  • ¿Es posible conseguir tolerancia a fallos y que la organización siga conectada aunque fallen uno o varios enlaces?

Una posible respuesta es el Channel bonding, mediante esta técnica es posible utilizar varios enlaces de forma combinada repartiendo el tráfico entre todos ellos. Se consigue fragmentando la información y transmitiendo los fragmentos mediante diferentes interfaces, de forma similar al striping de datos que se realiza con RAID pero con adaptadores de red. Se puede realizar en redes ethernet (normalmente se llama link aggregation o port trunking), con enlaces ADSL u otras tecnologías. Además de aprovechar muy bien el ancho de banda conjunto proporciona tolerancia a fallos. De manera que a medida que se caen enlaces se degrada la conexión pero no se pierde el acceso.

El inconveniente es que estas técnicas necesitan colaboración en ambos extremos (emisor/receptor) y en ocasiones en los equipos intermedios como los switches. En el caso práctico de un centro educativo que haya contratado varios enlaces ADSL no es una opción que permita gestionar de forma eficiente el acceso a Internet. Lo sería si los enlaces son del mismo ISP y nos ofrece el servicio, pero en la mayoría de los centros los enlaces ADSL son de proveedores diferentes.

Pero aunque tengamos varios enlaces heterogéneos a Internet aún no estará todo perdido. Linux puede manejar rutas multihop, en estas rutas se conocen varias pasarelas para llegar al destino. Y es posible repartir las conexiones (pensando en TCP) entre las diferentes pasarelas teniendo en cuenta que:

  • Una sola conexión no dispondrá de más ancho de banda del permitido por el enlace asignado.
  • Si hay varias conexiones es posible que cada una utilice un enlace diferente, de manera que en conjunto se aprovecha el ancho de banda disponible.
  • A la hora de asignar enlaces a las conexiones es posible tener en cuenta la capacidad de cada enlace. Aquellos que tengan mayor capacidad recibirán un mayor número de conexiones.

En este caso no se fragmenta la información para transmitirla simultáneamente por todas las pasarelas. Se asignan conexiones a las pasarelas y el reparto es mucho más tosco. Pero aún así, si el número de conexiones es elevado se puede suponer que el reparto tiende a ser homogéneo. ¿Siempre? no, aunque se tengan muchas conexiones no habrá reparto si tienen el mismo destino.

Cache de rutas en Linux

Linux mantiene una cache de rutas. Cuando se establece una conexión nueva si el destino no está en la cache se consulta la tabla de rutas para averigüar qué pasarela utilizar. Si el destino es una ruta multihop, con varias pasarelas, se escogerá una. Se utilizará para la conexión y se registrará en la cache de rutas que el destino se puede alcanzar mediante dicha pasarela. Esto asegura que los sucesivos paquetes de la conexión sean encaminados a través de la pasarela original, y no cada uno de ellos por un enlace diferente, pero también asegura que futuras conexiones al mismo destino utilicen el mismo enlace. Todo esto es cierto mientras exista la entrada en la cache de rutas, pero cuando una entrada lleva cierto tiempo en cache sin utilizarse se borra.

El fichero /proc/sys/net/ipv4/route/gc_timeout define el plazo de tiempo para que una ruta se mantenga en la cache, que por defecto suele ser 300 segundos. El fichero /proc/sys/net/ipv4/route/flush permite vaciar el contenido de la cache. Si se especifica un plazo demasiado corto, o si se borra de forma manual la cache, nos podemos encontrar con problemas en los servicios que mantienen una sesión abierta. Así que hay que aceptar que la cache de rutas asigne las conexiones a las puertas de enlace en función del destino.

Es posible consultar la cache de rutas mediante la sentencia: ip route show cache

Tolerancia a fallos

Cuando un enlace falla es necesario modificar la ruta multihop para sacar de la lista a aquellas pasarelas que no son utilizables, al hacer esto se actualiza automáticamente la cache de rutas. Por supuesto cuando el enlace vuelva a estar disponible será necesario añadir la pasarela que permite utilizarlo. Así que es necesario monitorizar continuamente el estado de los diferentes enlaces para descubrir cuando se caen y cuando vuelven a estar activos.

Pero cómo encaminar tráfico, o al menos intentarlo, por una pasarela concreta si hay múltiples disponibles y o bien se escoge una alternativamente o bien se escoge la que está en cache y permite alcanzar el destino ? No nos basta con enviar un ping a una IP de Internet y comprobar si funciona o no, debemos enviar el ping mediante una puerta de enlace concreta, aquella que intentamos monitorizar.

Hay que preparar un tinglado que nos permita enviar tráfico selectivamente mediante cada una de las pasarelas disponibles. Afortunadamente esto no es ningún problema gracias a las avanzadas capacidades de encaminamiento de Linux. El fichero /etc/iproute2/rt_tables nos permite definir distintas tablas de encaminamiento y mediante las reglas adecuadas podremos definir cuando se utilizará una u otra. Así podemos definir una tabla para cada puerta de enlace, dicha tabla sólo contendrá las rutas necesarias para alcanzar la puerta de enlace y para utilizarla como puerta de enlace por defecto. El tráfico al que se le aplique dicha tabla saldrá necesariamente por la única pasarela disponible en la tabla.

Pero cómo lo haremos para hacer que a los paquetes de prueba se le asigne una tabla u otra ? Las reglas para escoger tabla de encaminamiento permiten filtrar por muchos criterios, uno de los más prácticos para nuestro caso es la dirección de origen. Es extremadamente sencillo colocar varias direcciones de red a una interfaz, o varias, de nuestro equipo y utilizarlas para monitorizar cada una de las pasarelas disponibles.

Ejemplo:

Si una organización dispone de cuatro routers (192.168.1.1, 192.168.2.1, 192.168.3.1 y 192.168.4.1) necesitaremos que el equipo que va a realizar el balanceo de carga tenga:

  • Una interfaz que le permita alcanzar cada una de las pasarelas. Suponiendo una red /16 bastaría con una IP como 192.168.0.100 para alcanzar a los cuatro routers, o bien suponiendo redes /24 serian necesarias cuatro IPs como 192.168.1.100, 192.168.2.100, 192.168.3.100 y 192.168.4.100
  • Una interfaz que le permita monitorizar cada una de las pasarelas. Aquí necesitamos UNA dirección DIFERENTE para monitorizar cada uno de los routers. Así que nuestro equipo podría tener las IPs 192.168.1.101, 192.168.2.101, 192.168.3.101 y 192.168.4.101
  • La definición de 4 tablas de encaminamiento extra (definidas en rt_tables) con las rutas necesarias para alcanzar y utilizar en exclusiva una pasarela. También necesitamos las reglas necesarias para indicar que el tráfico proveniente de 192.168.1.101 utilizará la tabla en la que tiene por pasarela a 192.168.1.1, ...  el tráfico proveniente de 192.168.x.101 utilizará la tabla que tiene por pasarela a 192.168.x.1
  • En la tabla de encaminamiento por defecto, debe haber una ruta multihop que utilice los cuatro routers
  • Algún proceso debe monitorizar de forma continua el estado de cada pasarela y actualizar la ruta multihop cuando sea necesario

 

Es decir, el tráfico que salga a través de la interfaz 192.168.0.100 o bien las IPs 192.168.x.100 será balanceado entre los routers disponibles, pero el tráfico que salga por 192.168.x.101 siempre será (si es que funciona el enlace) encaminado a través de 192.168.x.1.

 

Todo este esquema es el que utiliza Lustitia para hacer balanceo de carga entre todos los routers disponibles, actualizando la lista de routers disponibles dinámicamente en función de su estado.

 

Lustitia

Es un pequeño script que monitoriza nuestros routers y cambia la ruta por defecto para la máquina (añadiendo o quitando pasarelas) a medida que los enlaces se caen o levantan.

Una vez que se ha descargado el fichero puede desarchivarse en /opt/lustitia. Allí nos encontraremos con los ficheros lustitia_boot y lustitia_mon. El primero se debe invocar una sola vez tras el arranque de la máquina (por ejemplo en /etc/rc.local) para configurar las interfaces que permitan monitorizar a los routers (los valores por defecto son 192.168.1.1 y 192.168.2.1) e inicializar las tablas (por defecto enlace1 y enlace2) que permiten utilizar en exclusiva cada router. También introduce las reglas necesarias para que el tráfico proveniente de 192.168.1.1 utilice la tabla enlace1 y el proveniente de 192.168.2.1 utilice la tabla enlace2. Por último inserta la ruta por defecto haciendo uso de todos los routers disponibles (en el ejemplo 2) con sus correspondientes pesos.

El script lustitia_mon debe mantenerse en ejecución mientras la máquina esté en servicio. Su función es poner a prueba periódicamente cada uno de los routers para comprobar su estado, y si ha habido cambios (pérdidas de conexión o enlaces restaurados) actualizar la ruta multihop en consecuencia.

Del script debe personalizarse las 5 primeras variables de entorno:

TESTIP=192.168.3.10
Dirección de red a la que se harán pings para comprobar la disponibilidad de cada enlace. Lo normal es que se trate de una dirección de Internet como 213.176.161.13
PINGTIMEOUT=2
Tiempo máximo que se esperará el comando ping antes de considerar que no se ha recibido respuesta
GATEWAY_PING_SRC=(192.168.1.1 192.168.2.1)
Lista de direcciones de origen para los pings. Cada una permitirá (gracias a las tablas de encaminamiento y las reglas que se han insertado) que el ping salga por un router particular
GATEWAY_LIST=(192.168.1.10 192.168.2.10)
Lista de puertas de enlace a monitorizar, el orden de la lista debe mantener una correspondencia con GATEWAY_PING_SRC
GATEWAY_WEIGHT_LIST=(1 1)
Lista de pesos para las puertas de enlace, el orden de la lista debe mantener una correspondencia con GATEWAY_LIST
SLEEP=5
Tiempo que transcurrirá entre un ciclo de monitorizado y el siguiente

 

XEiLL: ¿Qué ocurre con los servicios que se ofrecen al exterior?

Si nuestra organización está ofreciendo algún servicio en Internet, será necesario algún tipo de mecanismo que asegure que el tráfico de salida viaje por el enlace adecuado. En el caso particular de la XEiLL es habitual que los centros cuenten con un enlace ADSL proporcionado por la XTEC y otro que han contratado de forma particular. El enlace de la XTEC proporciona una IP fija en Internet, de manera que es adecuado para ofrecer servicios al exterior.

Así por ejemplo, cuando alguien abre en el navegador la dirección http://eoisantacoloma.xeill.net utiliza el router de la XTEC en la EOI para alcanzar al servidor Apache. El problema es que el servidor Apache está en una máquina que puede utilizar dos routers para contestar: 192.168.0.1 (xtec) y 172.27.12.252 (router particular). Si la respuesta viaja del servidor al navegador mediante el router de la XTEC no hay problema, pero si lo hace por el router particular tenemos un problema importante. Pues el navegador ha conectado con una dirección de Internet (la IP del router de la EOI en Internet) y obtiene respuesta desde otra IP diferente, por lo que descarta la respuesta y no se establece la conexión.

Tenemos que volver a hacer uso de iproute2 para:

  • Crear una tabla que contenga de puerta de enlace por defecto a 192.168.0.1
  • Añadir una regla para que las respuestas de Apache utilicen dicha tabla

Ok, y qué quiere decir 'las respuestas de Apache' ? Quiere decir todo el tráfico generado localmente, que salga por el puerto 80 de la interfaz 192.168.0.254 (que es la IP del servidor de la XEiLL en la EOI). Ojo, no es todo el tráfico que salga por el puerto 80 pues este servidor también actúa en las direcciones 172.x.x.x y 10.x.x.x de la XEiLL.

Es necesario hacer uso de iptables para marcar todos los paquetes especificados:

iptables -t mangle -A OUTPUT -p tcp -s 192.168.0.254 --sport 80 -j MARK --set-mark 1

Y después es necesario inicializar la tabla router_xtec e introducir la regla para decidir cuando se utiliza:

ip route add default via 192.168.0.1 table router_xtec
ip rule add fwmark 1 table router_xtec

Evidentemente estas tres sentencias deben ir a parar a /etc/rc.local para que se ejecuten en cada arranque.

 

XEiLL: ¿Qué ocurre con los túneles OpenVPN?

Suponiendo, como es el caso de la EOI, que únicamente se es cliente de dos centros, es necesario asegurar que las peticiones a cada servidor se hagan de forma continuada mediante el mismo enlace.

Los túneles de la XEiLL están montandos sobre UDP. Al tratarse de un protocolo sin conexión, no sirve de nada la cache de rutas. Sucesivos datagramas de un mismo túnel pueden salir por cualquiera de los routers disponibles. Esto plantea un problema en los servidores, que ven conectar al mismo cliente desde diferentes IPs y por lo tanto cierran la conexión.

La solución más sencilla consiste en añadir una ruta estática para alcanzar cada uno de los servidores de OpenVPN mediante un enlace determinado.

En el caso de la EOI:

# Queremos que los túneles SIEMPRE salgan por el mismo router (uno para cada tunel)
route add -host 85.192.101.49 gw 192.168.0.1
route add -host 85.192.100.29 gw 172.27.12.252

 

Enlaces