web-dev-qa-db-fra.com

Recharge gracieuse HAProxy sans perte de paquets

J'exécute un serveur d'équilibrage de charge HAProxy pour équilibrer la charge sur plusieurs serveurs Apache. J'ai besoin de recharger HAProxy à tout moment afin de changer l'algorithme d'équilibrage de charge.

Tout cela fonctionne bien, sauf que je dois recharger le serveur sans perdre un seul paquet (pour le moment, un rechargement me donne un succès de 99,76% en moyenne, avec 1000 requêtes par seconde pendant 5 secondes). J'ai fait de nombreuses heures de recherche à ce sujet et j'ai trouvé la commande suivante pour "recharger gracieusement" le serveur HAProxy:

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

Cependant, cela n'a que peu ou pas d'effet par rapport à l'ancien simple service haproxy reload, il baisse toujours de 0,24% en moyenne.

Existe-t-il un moyen de recharger le fichier de configuration HAProxy sans qu'un seul paquet ne soit supprimé d'un utilisateur?

42
Conor Taylor

Selon https://github.com/aws/opsworks-cookbooks/pull/4 et par conséquent http://www.mail-archive.com/[email protected]/ msg06885.html vous pouvez:

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

Cela a pour effet de supprimer le SYN avant un redémarrage, afin que les clients renvoient ce SYN jusqu'à ce qu'il atteigne le nouveau processus.

32
Mxx

Yelp a partagé une approche plus sophistiquée basée sur des tests minutieux. L'article de blog est une plongée profonde et vaut bien le temps investi pour l'apprécier pleinement.

True Zero Downtime HAProxy Reloads

tl; dr utilise Linux tc (contrôle du trafic) et iptables pour mettre temporairement en file d'attente les paquets SYN pendant que HAProxy est en cours de rechargement et a deux pids attachés au même port (SO_REUSEPORT).

Je ne suis pas à l'aise de republier l'intégralité de l'article sur ServerFault; néanmoins, voici quelques extraits pour piquer votre intérêt:

En retardant l'arrivée des paquets SYN dans nos équilibreurs de charge HAProxy qui s'exécutent sur chaque machine, nous sommes en mesure d'avoir un impact minimal sur le trafic lors des rechargements HAProxy, ce qui nous permet d'ajouter, de supprimer et de modifier les backends de service au sein de notre SOA = sans crainte d'avoir un impact significatif sur le trafic des utilisateurs.

# 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/97e3505a1e92e35de2c

Bravo à Yelp pour avoir partagé de telles idées incroyables.

27
Steve Jansen

Il existe un autre moyen beaucoup plus simple de recharger l'haproxy avec un véritable temps d'arrêt zéro - il est nommé iptables flipping (l'article est en fait une réponse Unbounce à la solution Yelp). Il est plus propre que la réponse acceptée car il n'est pas nécessaire de supprimer les paquets qui peuvent causer des problèmes avec de longs rechargements.

En bref, la solution comprend les étapes suivantes:

  1. Ayons une paire d'instances haproxy - la première active qui reçoit un trafic et la seconde en veille qui ne reçoit aucun trafic.
  2. Vous reconfigurez (rechargez) l'instance de secours à tout moment.
  3. Une fois que le mode veille est prêt avec une nouvelle configuration, vous détournez toutes les NOUVELLES connexions vers le nœud de secours qui devient nouveau actif . Unbounce fournit script bash qui fait le flip avec quelques commandes simples iptable .
  4. Pendant un instant, vous avez deux instances actives. Vous devez attendre que les connexions ouvertes à les anciennes actives cessent. Le temps dépend de votre comportement de service et des paramètres de conservation.
  5. Le trafic vers les anciens arrêts actifs devient nouveau mode veille - vous êtes de retour étape 1.

De plus, la solution peut être adoptée pour tout type de service (nginx, Apache, etc.) et est plus tolérante aux pannes car vous pouvez tester la configuration de secours avant sa mise en ligne.

8
gertas

Edit: Ma réponse fait l'hypothèse que le noyau envoie uniquement du trafic vers le port le plus récent à ouvrir avec SO_REUSEPORT, alors qu'il envoie en fait du trafic vers tous les processus comme décrit dans l'un des commentaires. En d'autres termes, la danse iptables est toujours requise. : (

Si vous utilisez un noyau qui prend en charge SO_REUSEPORT, ce problème ne devrait pas se produire.

Le processus que prend haproxy au redémarrage est:

1) Essayez de définir SO_REUSEPORT lors de l'ouverture du port ( https://github.com/haproxy/haproxy/blob/3cd0ae963e958d5d5fb838e120f1b0e9361a92f8/src/proto_tcp.c#L792-L798 )

2) Essayez d'ouvrir le port (réussira avec SO_REUSEPORT)

3) S'il n'a pas réussi, signalez à l'ancien processus de fermer son port, attendez 10 ms et réessayez. ( https://github.com/haproxy/haproxy/blob/3cd0ae963e958d5d5fb838e120f1b0e9361a92f8/src/haproxy.c#L1554-L1577 )

Il a d'abord été pris en charge dans le noyau Linux 3.9, mais certaines distributions l'ont rétroporté. Par exemple, les noyaux EL6 de 2.6.32-417.el6 le prennent en charge.

4
Jason Stubbs

Je vais expliquer ma configuration et comment j'ai résolu les rechargements gracieux:

J'ai une configuration typique avec 2 nœuds exécutant HAproxy et keepalived. Keepalived suit l'interface dummy0, donc je peux faire un "ifconfig dummy0 down" pour forcer le basculement.

Le vrai problème est que, je ne sais pas pourquoi, un "rechargement haproxy" supprime toujours toutes les connexions ÉTABLIES :( J'ai essayé le "flip iptables" proposé par gertas, mais j'ai trouvé quelques problèmes car il effectue un NAT sur l'adresse IP de destination, ce qui n'est pas une solution appropriée dans certains scénarios.

Au lieu de cela, j'ai décidé d'utiliser un hack sale CONNMARK pour marquer les paquets appartenant à de NOUVELLES connexions, puis rediriger ces paquets marqués vers l'autre nœud.

Voici l'ensemble de règles 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

Les deux premières règles marquent les paquets appartenant aux nouveaux flux (123.123.123.123 est le keepalived VIP utilisé sur le haproxy pour lier les frontends)).

Les troisième et quatrième règles marquent les paquets FIN/RST. (Je ne sais pas pourquoi, la cible TEE "ignore" les paquets FIN/RST).

La cinquième règle envoie un double de tous les paquets marqués à l'autre HAproxy (192.168.0.2).

La sixième règle supprime les paquets appartenant à de nouveaux flux pour éviter d'atteindre leur destination d'origine.

N'oubliez pas de désactiver rp_filter sur les interfaces ou le noyau supprimera ces paquets martiens.

Et enfin, faites attention aux retours de paquets! Dans mon cas, il y a un routage asymétrique (les demandes arrivent au client -> haproxy1 -> haproxy2 -> serveur Web, et les réponses vont du serveur Web -> haproxy1 -> client), mais cela n'affecte pas. Ça fonctionne bien.

Je sais que la solution la plus élégante serait d'utiliser iproute2 pour effectuer le détournement, mais cela n'a fonctionné que pour le premier paquet SYN. Quand il a reçu l'ACK (3e paquet de la poignée de main à 3 voies), il ne l'a pas marqué :( Je n'ai pas pu passer beaucoup de temps à enquêter, dès que j'ai vu que cela fonctionnait avec la cible TEE, il l'a laissé là. Bien sûr, n'hésitez pas à l'essayer avec iproute2.

Fondamentalement, le "rechargement gracieux" fonctionne comme ceci:

  1. J'active l'ensemble de règles iptables et je vois immédiatement les nouvelles connexions allant vers l'autre HAproxy.
  2. Je garde un œil sur "netstat -an | grep ESTABLISHED | wc -l" pour superviser le processus de "vidange".
  3. Une fois qu'il y a juste quelques connexions (ou zéro), "ifconfig dummy0 down" pour forcer keepalived au basculement, donc tout le trafic ira à l'autre HAproxy.
  4. Je supprime l'ensemble de règles iptables
  5. (Uniquement pour la configuration keepalive "sans préemption") "ifconfig dummy0 up".

L'ensemble de règles IPtables peut être facilement intégré dans un script de démarrage/arrêt:

#!/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
2
Vins Vilaplana