Aller au contenu

Sessions persistantes HAProxy

Quand et comment épingler un client à un backend dans HAProxy — insertion de cookie, règles stick à la appsession, affinité par IP source, et les modes d'échec qu'on planifie.

Les sessions persistantes épinglent un client à un serveur backend précis pour la durée d’une session logique. Parfois c’est nécessaire (uploads avec état, reconnexions WebSocket vers le même nœud, applications héritées sans magasin de session partagé) ; souvent c’est un contournement pour combler l’absence d’une couche d’état partagée. Cet article couvre les trois mécanismes de persistance de HAProxy et les mises en garde de production pour chacun.

Comment vérifier

Pour une config existante, l’API d’exécution expose les stick tables actives et les cookies que HAProxy insère.

echo "show table be_app" | sudo socat /run/haproxy/admin.sock -
echo "show servers state" | sudo socat /run/haproxy/admin.sock -
curl -v http://localhost/ 2>&1 | grep -i 'set-cookie'
curl -v -b "SRVID=app1" http://localhost/ 2>&1 | grep -i 'x-served-by\|cookie'

La sortie show table liste chaque clé que la table contient actuellement (IP source, valeur de cookie) avec le serveur vers lequel elle pointe. Le curl verbeux confirme si HAProxy insère le cookie et vers quel serveur il a routé le client.

Ce qui se passe

HAProxy supporte trois approches :

  • Directive cookie (HTTP uniquement) — HAProxy insère ou réécrit un cookie identifiant le serveur backend. Les requêtes suivantes du même client portent le cookie et HAProxy route en conséquence.
  • stick-table + stick on (HTTP ou TCP) — HAProxy maintient une table en mémoire indexée par un attribut de la requête (IP source, valeur de cookie, valeur d’en-tête) qui pointe vers un serveur. Survit à travers plusieurs requêtes du même clé.
  • balance source (TCP ou HTTP) — un hash de l’IP client choisit le serveur. Pas d’état ; rééquilibre naturellement quand des serveurs joignent ou partent.

cookie est le défaut de production pour HTTP parce qu’il survit aux changements d’IP du client (réseaux mobiles, NAT) et est explicite — vous voyez le cookie dans DevTools. stick-table est ce qu’on utilise quand on ne peut pas insérer un cookie (frontends TCP, clients tiers qui retirent les cookies). balance source est le repli quand on n’a aucun des deux.

Le gros compromis : la persistance épingle un client à un serveur, ce qui signifie qu’un serveur malade continue de recevoir les requêtes de ses clients épinglés jusqu’à passer DOWN. L’option redispatch existe précisément pour ça ; sans elle, une session persistante attachée à un serveur mort renvoie des erreurs pour toujours.

La procédure

  1. Persistance par cookie. Insérez un cookie nommant le serveur, scope-le au chemin, et laissez HAProxy réécrire la sélection du serveur :

    backend be_app
        balance roundrobin
        cookie SRVID insert indirect nocache httponly secure
        option httpchk
        http-check send meth GET uri /healthz
        http-check expect status 200
        server app1 10.0.1.11:8080 check cookie app1
        server app2 10.0.1.12:8080 check cookie app2

    Le cookie app1 sur la ligne server est la valeur que HAProxy écrit dans le cookie SRVID quand ce serveur est choisi. Les flags insert indirect nocache httponly secure sont la combinaison de production : insert signifie que HAProxy ajoute le cookie, indirect signifie que HAProxy le retire de la requête avant de la transmettre (le backend ne le voit pas), nocache empêche la survie en caches, httponly empêche l’accès JS, secure exige HTTPS.

  2. Stick-table par IP source. Pour les frontends TCP ou quand l’insertion de cookie n’est pas possible :

    backend be_app
        balance leastconn
        stick-table type ip size 100k expire 30m
        stick on src
        server app1 10.0.1.11:8080 check
        server app2 10.0.1.12:8080 check

    size 100k est le nombre maximum d’entrées ; expire 30m est le timeout d’inactivité par entrée. La table vit en mémoire seulement — voir Stick tables HAProxy pour la persistance à travers les reloads et Protocole peer HAProxy pour la réplication entre instances.

  3. Stick sur une valeur d’en-tête. Quand l’identité du client est dans un en-tête personnalisé :

    backend be_app
        stick-table type string len 64 size 50k expire 1h
        stick on hdr(x-tenant-id)
        server app1 10.0.1.11:8080 check
        server app2 10.0.1.12:8080 check

    type string len 64 alloue 64 octets par clé ; le dimensionnement compte parce que la table est préallouée.

  4. balance source — sans état. Un hash de l’IP source choisit le serveur :

    backend be_app
        balance source
        hash-type consistent
        server app1 10.0.1.11:8080 check
        server app2 10.0.1.12:8080 check

    hash-type consistent utilise un anneau de type Karger afin qu’ajouter ou retirer un serveur ne mélange que ~1/N des clients. Sans consistent, chaque client est mélangé quand le pool change.

  5. Combiner cookie et redispatch. Le motif sûr en production : épingler les clients à un serveur, mais redispatcher si ce serveur est en panne :

    defaults
        option redispatch
        retries 3
    
    backend be_app
        cookie SRVID insert indirect nocache httponly secure
        option httpchk
        http-check send meth GET uri /healthz
        http-check expect status 200
        server app1 10.0.1.11:8080 check cookie app1
        server app2 10.0.1.12:8080 check cookie app2

    option redispatch est ce qui vous sauve quand un client coincé essaie d’atteindre une épingle morte : HAProxy abandonne après les retries et route vers un serveur sain.

  6. Persister la stick-table à travers les reloads. Sans persistance, un systemctl reload haproxy vide la table :

    backend be_app
        stick-table type ip size 100k expire 30m peers cluster_peers
        stick on src
        server app1 10.0.1.11:8080 check
        server app2 10.0.1.12:8080 check

    La clause peers cluster_peers réplique la table vers les peers. Au reload, HAProxy tire l’état depuis un peer.

Pièges courants

  • Une session persistante sans option redispatch continue d’essayer le serveur mort pour toujours ; le client voit des erreurs jusqu’à l’expiration du cookie.
  • La persistance par cookie avec insert réécrit l’en-tête Set-Cookie — si le backend définit déjà SRVID pour ses propres besoins, vous entrez en collision. Utilisez un nom de cookie unique.
  • balance source sans hash-type consistent mélange tous les clients à chaque échange de serveur. Avec l’autoscaling, c’est une horde tonitruante contre les caches froids.
  • Les stick tables sont par processus — sans peers, un cluster HAProxy multi-instances a N tables indépendantes. Le client dont l’état vit sur l’instance A mais qui touche l’instance B à la prochaine requête est mélangé.
  • Le cookie que HAProxy insère est HttpOnly et Secure par bonne pratique, mais si l’app lit explicitement son propre cookie de session et que HAProxy en insère aussi un, vos logs de debug se rempliront du mauvais cookie. Utilisez des noms distincts.

Stack Harbor recommande de passer à un magasin de session partagé (Redis, base de données, JWT) en première option et de traiter les sessions persistantes comme le repli pour les applications héritées. Quand la persistance est nécessaire, on déploie avec la réplication peers afin qu’un reload ou la perte d’une instance n’unsticke pas tout le monde d’un coup. Cette continuité fait partie de la façon dont on opère les Environnements en grappe.