HAProxy recarga elegante con cero pérdida de paquetes

42

Estoy ejecutando un servidor de equilibrio de carga HAProxy para equilibrar la carga a múltiples servidores Apache. Necesito volver a cargar HAProxy en cualquier momento para cambiar el algoritmo de equilibrio de carga.

Todo esto funciona bien, excepto por el hecho de que tengo que volver a cargar el servidor sin perder un solo paquete (en este momento, una recarga me está dando un 99.76% de éxito en promedio, con 1000 solicitudes por segundo durante 5 segundos). He investigado muchas horas sobre esto y he encontrado el siguiente comando para "recargar con gracia" el servidor HAProxy:

haproxy -D -f /etc/haproxy/haproxy.cfg -p /var/run/haproxy.pid -sf $(cat /var/run/haproxy.pid)

Sin embargo, esto tiene poco o ningún efecto en comparación con el viejo service haproxy reload, todavía cae 0.24% en promedio.

¿Hay alguna forma de volver a cargar el archivo de configuración HAProxy sin un solo paquete descartado de ningún usuario?

Conor Taylor
fuente
66
Si necesita tanta confiabilidad, una mejor solución sería ejecutar más de una instancia de HAproxy donde pueda sacar una fuera de servicio para recargarla, volver a colocarla y repetirla para la (s) otra (s).
Yoonix

Respuestas:

32

De acuerdo con https://github.com/aws/opsworks-cookbooks/pull/40 y, en consecuencia, http://www.mail-archive.com/[email protected]/msg06885.html puede:

iptables -I INPUT -p tcp --dport $PORT --syn -j DROP
sleep 1
service haproxy restart
iptables -D INPUT -p tcp --dport $PORT --syn -j DROP

Esto tiene el efecto de descartar el SYN antes de reiniciar, de modo que los clientes reenvíen este SYN hasta que llegue al nuevo proceso.

Mxx
fuente
ambos comandos me dieron esto: iptables v1.4.14: invalid port/service --syn 'especificado`
Dmitri DB
55
@DmitriDB se supone que debes reemplazar $PORTcon el puerto real que haproxyestá escuchando. Si haproxy está escuchando en varios puertos, escritura reemplazar --dport $PORTcon --dports $PORTS_SEPARATED_BY_COMMAS, por ejemplo, --dports 80,443.
pepoluan
1
iptables 1.4.7 (Centos 6.7): también debe especificar -m mulitport si desea usar --dports. Entonces, su "iptables -I INPUT -p tcp -m multipuerto --porta 80,443 --syn -j DROP" y también para el -D
carpii
25

Yelp compartió un enfoque más sofisticado basado en pruebas meticulosas. El artículo del blog es una inmersión profunda, y vale la pena la inversión de tiempo para apreciarlo completamente.

Recarga de HAProxy con verdadero tiempo de inactividad cero

tl; dr usa Linux tc (control de tráfico) e iptables para poner en cola temporalmente los paquetes SYN mientras HAProxy se está recargando y tiene dos pids conectados al mismo puerto ( SO_REUSEPORT).

No me siento cómodo volviendo a publicar el artículo completo en ServerFault; Sin embargo, aquí hay algunos extractos para despertar su interés:

Al retrasar los paquetes SYN que ingresan a nuestros equilibradores de carga HAProxy que se ejecutan en cada máquina, podemos impactar mínimamente el tráfico durante las recargas de HAProxy, lo que nos permite agregar, eliminar y cambiar los backends de servicio dentro de nuestro SOA sin temor a afectar significativamente el tráfico de usuarios.

# plug_manipulation.sh
nl-qdisc-add --dev=lo --parent=1:4 --id=40: --update plug --buffer
service haproxy reload
nl-qdisc-add --dev=lo --parent=1:4 --id=40: --update plug --release-indefinite

# setup_iptables.sh
iptables -t mangle -I OUTPUT -p tcp -s 169.254.255.254 --syn -j MARK --set-mark 1

# setup_qdisc.sh
## Set up the queuing discipline
tc qdisc add dev lo root handle 1: prio bands 4
tc qdisc add dev lo parent 1:1 handle 10: pfifo limit 1000
tc qdisc add dev lo parent 1:2 handle 20: pfifo limit 1000
tc qdisc add dev lo parent 1:3 handle 30: pfifo limit 1000

## Create a plug qdisc with 1 meg of buffer
nl-qdisc-add --dev=lo --parent=1:4 --id=40: plug --limit 1048576
## Release the plug
nl-qdisc-add --dev=lo --parent=1:4 --id=40: --update plug --release-indefinite

## Set up the filter, any packet marked with “1” will be
## directed to the plug
tc filter add dev lo protocol ip parent 1:0 prio 1 handle 1 fw classid 1:4

Gist: https://gist.github.com/jolynch/97e3505a1e92e35de2c0

Saludos a Yelp por compartir ideas tan sorprendentes.

Steve Jansen
fuente
Excelente enlace! Pero tal vez le gustaría resumirlo aquí en caso de que el enlace caduque. Esa es la única razón para que no haya un voto a favor.
Matt
@Matt agregó algunos extractos y ejemplos de código
Steve Jansen,
8

Hay otra forma mucho más simple de recargar haproxy con un tiempo de inactividad verdadero cero: se llama volteo de iptables (el artículo es en realidad respuesta Unbounce a la solución de Yelp). Es más limpio que la respuesta aceptada ya que no hay necesidad de descartar ningún paquete que pueda causar problemas con recargas prolongadas.

Brevemente, la solución consta de los siguientes pasos:

  1. Tengamos un par de instancias de haproxy: la primera activa que recibe un tráfico y la segunda en espera que no recibe ningún tráfico.
  2. Puede reconfigurar (recargar) la instancia en espera en cualquier momento.
  3. Una vez que el modo de espera está listo con la nueva configuración, desvía todas las NUEVAS conexiones al nodo de espera que se vuelve activo . Unbounce proporciona un script bash que hace el cambio con algunos iptablecomandos simples .
  4. Por un momento tienes dos instancias activas. Debe esperar hasta que cesen las conexiones abiertas al activo anterior . El tiempo depende de su comportamiento de servicio y la configuración de mantenimiento de la vida.
  5. Tráfico a paradas activas antiguas que se convierte en nuevo modo de espera : está de vuelta en el paso 1.

Además, la solución se puede adoptar para cualquier tipo de servicio (nginx, apache, etc.) y es más tolerante a fallas, ya que puede probar la configuración en espera antes de que se ponga en línea.

gertas
fuente
4

Editar: Mi respuesta supone que el kernel solo envía tráfico al puerto más reciente para que se abra con SO_REUSEPORT, mientras que en realidad envía tráfico a todos los procesos como se describe en uno de los comentarios. En otras palabras, todavía se requiere el baile de iptables. :(

Si está en un núcleo que admite SO_REUSEPORT, entonces este problema no debería suceder.

El proceso que toma haproxy cuando se reinicia es:

1) Intente configurar SO_REUSEPORT al abrir el puerto ( https://github.com/haproxy/haproxy/blob/3cd0ae963e958d5d5fb838e120f1b0e9361a92f8/src/proto_tcp.c#L792-L798 )

2) Intente abrir el puerto (tendrá éxito con SO_REUSEPORT)

3) Si no tuvo éxito, indique al proceso anterior que cierre su puerto, espere 10 ms y vuelva a intentarlo todo. ( https://github.com/haproxy/haproxy/blob/3cd0ae963e958d5d5fb838e120f1b0e9361a92f8/src/haproxy.c#L1554-L1577 )

Primero fue compatible con el kernel Linux 3.9, pero algunas distribuciones lo han soportado. Por ejemplo, los núcleos EL6 de 2.6.32-417.el6 lo admiten.

Jason Stubbs
fuente
Sucederá con SO_REUSEPORTalgún escenario particular, especialmente con mucho tráfico. Cuando SYN se envía al antiguo proceso de haproxy y en el mismo momento se cierra el socket de escucha que da como resultado RST. Consulte el artículo de Yelp mencionado en otra respuesta anterior.
gertas
44
Eso apesta ... Solo para resumir el problema, Linux distribuye nuevas conexiones entre todos los procesos que escuchan en un puerto en particular cuando se usa SO_REUSEPORT, por lo que hay un corto tiempo en el que el proceso anterior aún pondrá las conexiones en su cola.
Jason Stubbs
2

Explicaré mi configuración y cómo resolví las recargas graciosas:

Tengo una configuración típica con 2 nodos que ejecutan HAproxy y keepalived. Keepalived rastrea la interfaz dummy0, por lo que puedo hacer un "ifconfig dummy0 down" para forzar el cambio.

El verdadero problema es que, no sé por qué, una "recarga de haproxy" todavía deja caer todas las conexiones ESTABLECIDAS :( Intenté el "volteo de iptables" propuesto por gertas, pero encontré algunos problemas porque realiza un NAT en el destino Dirección IP, que no es una solución adecuada en algunos escenarios.

En cambio, decidí usar un truco sucio CONNMARK para marcar paquetes que pertenecen a NUEVAS conexiones, y luego redirigir esos paquetes marcados al otro nodo.

Aquí está el conjunto de reglas de iptables:

iptables -t mangle -A PREROUTING -i eth1 -d 123.123.123.123/32 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1
iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
iptables -t mangle -A PREROUTING -i eth1 -p tcp --tcp-flags FIN FIN -j MARK --set-mark 2
iptables -t mangle -A PREROUTING -i eth1 -p tcp --tcp-flags RST RST -j MARK --set-mark 2
iptables -t mangle -A PREROUTING -i eth1 -m mark ! --mark 0 -j TEE --gateway 192.168.0.2
iptables -t mangle -A PREROUTING -i eth1 -m mark --mark 1 -j DROP

Las primeras dos reglas marcan los paquetes que pertenecen a los nuevos flujos (123.123.123.123 es el VIP de keepalived utilizado en el haproxy para enlazar las interfaces).

Las reglas tercera y cuarta marcan los paquetes FIN / RST. (No sé por qué, el objetivo TEE "ignora" los paquetes FIN / RST).

La quinta regla envía un duplicado de todos los paquetes marcados al otro HAproxy (192.168.0.2).

La sexta regla descarta los paquetes que pertenecen a nuevos flujos para evitar llegar a su destino original.

Recuerde deshabilitar rp_filter en las interfaces o el núcleo descartará esos paquetes marcianos.

Y por último, pero no menos importante, ¡cuidado con los paquetes que regresan! En mi caso hay enrutamiento asimétrico (las solicitudes llegan al cliente -> haproxy1 -> haproxy2 -> servidor web, y las respuestas van desde el servidor web -> haproxy1 -> cliente), pero no afecta. Funciona bien.

Sé que la solución más elegante sería usar iproute2 para hacer el desvío, pero solo funcionó para el primer paquete SYN. Cuando recibió el ACK (tercer paquete del apretón de manos de 3 vías), no lo marcó :( No pude pasar mucho tiempo para investigar, tan pronto como vi que funciona con el objetivo TEE, lo dejó allí. Por supuesto, siéntase libre de probarlo con iproute2.

Básicamente, la "recarga elegante" funciona así:

  1. Habilito el conjunto de reglas de iptables e inmediatamente veo las nuevas conexiones que van al otro HAproxy.
  2. Mantengo un ojo en "netstat -an | grep ESTABLECIDO | wc -l" para supervisar el proceso de "drenaje".
  3. Una vez que haya solo unas pocas (o cero) conexiones, "ifconfig dummy0 down" para forzar el mantenimiento de la conmutación por error, por lo que todo el tráfico irá al otro HAproxy.
  4. Elimino el conjunto de reglas de iptables
  5. (Solo para la configuración de keepalive "sin preferencia") "ifconfig dummy0 up".

El conjunto de reglas de IPtables se puede integrar fácilmente en un script de inicio / detención:

#!/bin/sh

case $1 in
start)
        echo Redirection for new sessions is enabled

#       echo 0 > /proc/sys/net/ipv4/tcp_fwmark_accept
        for f in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 0 > $f; done
        iptables -t mangle -A PREROUTING -i eth1 ! -d 123.123.123.123 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1
        iptables -t mangle -A PREROUTING -j CONNMARK --restore-mark
        iptables -t mangle -A PREROUTING -i eth1 -p tcp --tcp-flags FIN FIN -j MARK --set-mark 2
        iptables -t mangle -A PREROUTING -i eth1 -p tcp --tcp-flags RST RST -j MARK --set-mark 2
        iptables -t mangle -A PREROUTING -i eth1 -m mark ! --mark 0 -j TEE --gateway 192.168.0.2
        iptables -t mangle -A PREROUTING -i eth1 -m mark --mark 1 -j DROP
        ;;
stop)
        iptables -t mangle -D PREROUTING -i eth1 -m mark --mark 1 -j DROP
        iptables -t mangle -D PREROUTING -i eth1 -m mark ! --mark 0 -j TEE --gateway 192.168.0.2
        iptables -t mangle -D PREROUTING -i eth1 -p tcp --tcp-flags RST RST -j MARK --set-mark 2
        iptables -t mangle -D PREROUTING -i eth1 -p tcp --tcp-flags FIN FIN -j MARK --set-mark 2
        iptables -t mangle -D PREROUTING -j CONNMARK --restore-mark
        iptables -t mangle -D PREROUTING -i eth1 ! -d 123.123.123.123 -m conntrack --ctstate NEW -j CONNMARK --set-mark 1

        echo Redirection for new sessions is disabled
        ;;
esac
Vins Vilaplana
fuente