Rechargement gracieux HAProxy avec aucune perte de paquet

42

J'utilise un serveur d'équilibrage de charge HAProxy pour équilibrer la charge sur plusieurs serveurs Apache. Je dois recharger HAProxy à tout moment pour modifier l'algorithme d'équilibrage de charge.

Tout cela fonctionne bien, à part le fait que je dois recharger le serveur sans perdre un seul paquet (pour le moment, un rechargement me donne un taux de réussite de 99,76% en moyenne, avec 1000 demandes par seconde pendant 5 secondes). J'ai effectué 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)

Toutefois, cela n’a que peu ou pas d’effet par rapport à l’ancien service haproxy reload, il reste en baisse de 0,24% en moyenne.

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

Conor Taylor
la source
6
Si vous avez besoin de cette fiabilité, une meilleure solution consisterait à exécuter plus d'une instance de HAproxy où vous pouvez en déconnecter une pour la recharger, la remettre en place et la répéter pour la ou les autres.
Yoonix

Réponses:

32

Selon https://github.com/aws/opsworks-cookbooks/pull/40 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, de sorte que les clients le renverront jusqu'à ce qu'il atteigne le nouveau processus.

Mxx
la source
Ces deux commandes m'ont donné ceci: iptables v1.4.14: invalid port/service --syn 'spécifié`
Dmitri DB
5
@DmitriDB que vous êtes censé remplacer $PORTpar le port actuel haproxysur lequel vous écoutez. Si haproxy écoute sur plusieurs ports, écriture remplacer --dport $PORTavec --dports $PORTS_SEPARATED_BY_COMMAS, par exemple, --dports 80,443.
pepoluan
1
iptables 1.4.7 (Centos 6.7) - vous devez également spécifier -m mulitport si vous souhaitez utiliser --dports. Donc, son "iptables -I INPUT -p tcp -m multiport --porte 80,443 --syn -j DROP" et de même pour le -D
carpii le
25

Yelp partageait une approche plus sophistiquée basée sur des tests méticuleux. L'article du blog est une plongée profonde, et vaut bien l'investissement de temps pour l'apprécier pleinement.

Recharges d'HAProxy avec True Zero Downtime

tl; dr utilise Linux tc (contrôle du trafic) et iptables pour mettre temporairement en file d'attente les paquets SYN pendant le rechargement de HAProxy et a deux PID 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 les paquets SYN entrant dans nos équilibreurs de charge HAProxy qui s'exécutent sur chaque machine, nous sommes en mesure d'imposer un impact minimal sur le trafic lors des rechargements d'HAProxy, ce qui nous permet d'ajouter, de supprimer et de modifier les backends de services au sein de notre SOA sans craindre 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/97e3505a1e92e35de2c0

Bravo à Yelp pour le partage de ces idées étonnantes.

Steve Jansen
la source
Excellent lien! Mais peut-être voudriez-vous le résumer ici au cas où le lien expirerait. C'est la seule raison pour laquelle aucun vote positif.
Matt
@Matt a ajouté des extraits et des exemples de code
Steve Jansen le
8

Il existe un autre moyen beaucoup plus simple de recharger haproxy avec un temps d'indisponibilité égal à zéro - il s'appelle iptables flipping (l'article est en fait une réponse sans rebond à la solution Yelp). Il est plus propre que la réponse acceptée car il n’est pas nécessaire de supprimer les paquets susceptibles de causer des problèmes lors de recharges longues.

En bref, la solution comprend les étapes suivantes:

  1. Prenons 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 en veille est prêt avec une nouvelle configuration vous déviez toutes les connexions nouveau vers le noeud veille qui devient nouveau actif . Unbounce fournit un script bash qui effectue le retournement avec quelques iptablecommandes simples .
  4. Un instant, vous avez deux instances actives. Vous devez attendre que les connexions avec l' ancien actif cessent. L'heure dépend de votre comportement de service et des paramètres de maintien en activité.
  5. Le trafic vers les anciens arrêts actifs qui devient une nouvelle veille - vous êtes de retour à l’étape 1.

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

Gertas
la source
4

Edit: Ma réponse part du principe que le noyau n’envoie que du trafic au dernier port à ouvrir avec SO_REUSEPORT, alors qu’il envoie en réalité du trafic à 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 prenant en charge SO_REUSEPORT, le problème ne devrait pas se produire.

Le processus suivi par haproxy lors du redémarrage est le suivant:

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

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

3) Si cela n’a pas abouti, signalez à l’ancien processus de fermer son port, attendez 10 ms et essayez à nouveau. ( https://github.com/haproxy/haproxy/blob/3cd0ae963e958d5dbf388e120f1b0e9361a92f8/src/haproxy.c#L1554-L1577 )

Il a été supporté pour la première fois dans le noyau Linux 3.9, mais certaines distributions l’ont rétroporté. Par exemple, les noyaux EL6 des versions 2.6.32-417.el6 le prennent en charge.

Jason Stubbs
la source
Cela se produira SO_REUSEPORTdans certains cas de figure - en particulier dans des conditions de trafic intense. Lorsque SYN est envoyé à l'ancien processus haproxy et au même moment, il ferme le socket d'écoute, ce qui entraîne RST. Voir l'article de Yelp mentionné dans l'autre réponse ci-dessus.
Gertas
4
Ça craint ... Juste pour résumer le problème, Linux distribue de nouvelles connexions entre tous les processus écoutant un port particulier lorsque SO_REUSEPORT est utilisé, de sorte qu'il reste peu de temps pendant lequel l'ancien processus aura toujours des connexions placées dans sa file d'attente.
Jason Stubbs
2

Je vais expliquer ma configuration et comment j'ai résolu les recharges gracieuses:

J'ai une configuration typique avec 2 nœuds sous HAproxy et keepalived. Keepalived suit l'interface dummy0, afin que je puisse faire un "ifconfig dummy0 down" pour forcer le basculement.

Le vrai problème est que, je ne sais pas pourquoi, un "rechargement haproxy" laisse toujours tomber toutes les connexions ESTABLISHED :( j'ai essayé le "retournement iptables" proposé par gertas, mais j'ai trouvé quelques problèmes parce qu'il effectue un NAT sur la destination L'adresse IP, 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 de rediriger ces paquets marqués vers l'autre nœud.

Voici le jeu 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 VIP keepalived utilisé sur haproxy pour lier les interfaces).

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 une copie de tous les paquets marqués à l'autre HAproxy (192.168.0.2).

La sixième règle supprime les paquets appartenant aux nouveaux flux afin d’empêcher d’atteindre leur destination initiale.

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

Et enfin, attention aux paquets qui reviennent! 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 faire le renvoi, mais cela ne fonctionnait que pour le premier paquet SYN. Quand il a reçu l'ACK (3ème paquet de la poignée de main à trois), il ne l'a pas marqué :( Je ne pouvais pas passer beaucoup de temps à enquêter, dès que j'ai vu que ça fonctionne avec TEE target, il l'a laissé là. Bien sûr, n'hésitez pas à l'essayer avec iproute2.

En gros, le "rechargement gracieux" fonctionne comme ceci:

  1. J'active le jeu de règles iptables et vois immédiatement les nouvelles connexions passer à 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 n'y a plus que quelques (ou zéro) connexions, "ifconfig dummy0 down" pour forcer Keepalived à basculer, de sorte que tout le trafic passe sur l'autre HAproxy.
  4. Je supprime le jeu de règles iptables
  5. (Uniquement pour la configuration keepalive "non-préemptive") "ifconfig dummy0 up".

Le jeu 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
Vins Vilaplana
la source