Chapitre 621 minIntermédiaire
Répartition de charge : HAProxy, DNS round-robin et contrôles de santé
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- comparer le DNS round-robin et un répartiteur de charge ;
- choisir un algorithme de répartition et des contrôles de santé ;
- configurer HAProxy en HTTPS devant deux serveurs web ;
- gérer les sessions et retirer un serveur sans coupure.
5 exercices corrigés1 schéma
La répartition de charge (load balancing) distribue les requêtes des clients entre plusieurs serveurs qui rendent le même service. Un répartiteur de charge (load balancer) choisit, pour chaque requête, un serveur en bonne santé : il augmente la capacité du service et retire automatiquement les serveurs en panne. Ce chapitre compare le DNS round-robin et les répartiteurs, présente les algorithmes, les contrôles de santé et la gestion des sessions, puis configure HAProxy pas à pas, jusqu'à la maintenance sans coupure.
Répartir la charge : deux objectifs
Un service répartit sa charge pour deux raisons, qu'il faut savoir distinguer à l'oral :
- la capacité : quand un serveur ne suffit plus, on peut en acheter un plus gros (montée en charge verticale, limitée et coûteuse) ou en ajouter un second (montée en charge horizontale, presque sans limite). La répartition de charge rend la seconde possible ;
- la disponibilité : le répartiteur vérifie en permanence l'état de chaque serveur et cesse d'envoyer des requêtes à celui qui ne répond plus. Le service survit à la panne d'un serveur, et on peut en arrêter un pour le mettre à jour sans couper personne.
Il existe trois grandes familles de solutions : la répartition par le DNS, le répartiteur réseau (niveau 4, qui voit des connexions TCP ou UDP) et le répartiteur applicatif (niveau 7, qui comprend le protocole, le plus souvent HTTP).
Le DNS round-robin
La solution la plus simple consiste à publier plusieurs enregistrements A pour le même nom. Dans une zone BIND :
intranet 300 IN A 10.10.20.31
intranet 300 IN A 10.10.20.32
Le serveur DNS renvoie les deux adresses, en changeant leur ordre à chaque réponse (c'est le round-robin), et la plupart des clients utilisent la première. La charge se répartit, sans aucun équipement supplémentaire. Le serveur DNS de Windows Server fait de même ; attention, il applique aussi par défaut le tri par masque de réseau (netmask ordering), qui place en tête l'adresse la plus proche du réseau du client et peut défaire la rotation.
Mais le DNS ne sait rien de l'état des serveurs :
| DNS round-robin | Répartiteur de charge | |
|---|---|---|
| Serveur en panne | Son adresse continue d'être distribuée : une partie des clients échoue | Retiré du groupe en quelques secondes |
| Retrait pour maintenance | Modifier la zone, puis attendre l'expiration des caches (la durée de vie, ici 300 s) | Une commande, effet immédiat |
| Répartition | Approximative, dépend des caches et des clients | Maîtrisée par l'algorithme choisi |
| Sessions | Aucune gestion | Persistance possible |
| Coût | Nul | Une ou deux machines de plus |
Le DNS round-robin n'est donc pas une solution de haute disponibilité en soi. Il sert en complément : par exemple pour répartir les clients entre deux adresses virtuelles de répartiteurs, chacune elle-même protégée par VRRP.
Répartiteur de niveau 4 ou de niveau 7
| Niveau 4 (transport) | Niveau 7 (application) | |
|---|---|---|
| Ce qu'il voit | Adresses IP et ports | Le contenu de la requête : URL, nom d'hôte, en-têtes, cookies |
| Ce qu'il fait | Transmet chaque connexion TCP à un serveur | Reçoit la requête HTTP, choisit un serveur, lui retransmet la requête |
| Protocoles | Tous ceux qui passent sur TCP ou UDP : HTTPS non déchiffré, LDAPS, SMTP, MariaDB | HTTP et HTTPS (après déchiffrement) |
| Possibilités | Rapide, simple | Routage selon l'URL (/api vers un groupe, / vers un autre), persistance par cookie, ajout d'en-têtes, déchiffrement TLS centralisé |
| Exemples | HAProxy en mode tcp, LVS (IPVS) piloté par keepalived | HAProxy en mode http, Nginx, Traefik |
Un répartiteur de niveau 7 est un proxy inverse (reverse proxy) : le client parle au répartiteur, et le répartiteur parle au serveur. Deux conséquences pratiques :
- le certificat TLS est installé sur le répartiteur, qui déchiffre (on parle de terminaison TLS) ; il transmet ensuite en HTTP vers les serveurs, dans un VLAN serveurs protégé, ou rechiffre si la politique de sécurité l'exige. En niveau 4, le flux chiffré passe tel quel (TLS passthrough) et chaque serveur porte son certificat ;
- l'adresse du client : les serveurs voient arriver toutes les requêtes depuis l'adresse du répartiteur. Pour garder l'adresse réelle dans les journaux, le répartiteur l'ajoute dans l'en-tête
X-Forwarded-For, et le serveur web est configuré pour la lire.
Les algorithmes de répartition
| Algorithme (mot-clé HAProxy) | Principe | Quand l'utiliser |
|---|---|---|
Tourniquet (roundrobin) | Chaque serveur à son tour | Requêtes courtes et de durée comparable : le cas de la plupart des sites web |
Tourniquet pondéré (roundrobin et weight) | Un serveur de poids 3 reçoit trois fois plus de requêtes qu'un serveur de poids 1 | Serveurs de puissances différentes |
Moins de connexions (leastconn) | Le serveur qui a le moins de connexions en cours | Connexions longues : bases de données, bureaux à distance, WebSocket |
Hachage de l'adresse source (source) | Un même client va toujours au même serveur, tant que le groupe ne change pas | Persistance sans cookie, en niveau 4 |
Hachage de l'URL (uri) | Une même URL va toujours au même serveur | Serveurs de cache |
Premier disponible (first) | Remplit le premier serveur jusqu'à sa limite, puis le suivant | Économiser des serveurs (on éteint ceux qui ne servent pas) |
Les contrôles de santé
Un contrôle de santé (health check) est une vérification que le répartiteur effectue régulièrement sur chaque serveur pour décider s'il peut recevoir des requêtes. C'est lui qui transforme une simple répartition en solution de haute disponibilité.
| Type de contrôle | Ce qu'il vérifie | Ce qu'il rate |
|---|---|---|
| Connexion TCP | Le port 80 du serveur accepte une connexion | Une application qui répond par des erreurs 500 |
| Requête HTTP simple | Une page répond avec le code 200 | Une application qui ne joint plus sa base de données, si la page testée n'en a pas besoin |
| Requête HTTP « profonde » | Une page dédiée vérifie les dépendances (la base, le stockage) et répond 200 ou 503 | Rien d'important, mais attention : si la base tombe, tous les serveurs échouent ensemble et le service affiche une erreur du répartiteur |
Trois paramètres règlent la réactivité, avec leurs valeurs dans HAProxy :
inter: l'intervalle entre deux contrôles (2 secondes par défaut) ;fall: le nombre d'échecs consécutifs avant de retirer le serveur (3 par défaut) ;rise: le nombre de succès consécutifs avant de le réintégrer (2 par défaut).
Avec inter 2s fall 3 rise 2, un serveur qui tombe est retiré au bout d'environ 6 secondes (3 contrôles ratés à 2 secondes d'intervalle), et réintégré 4 secondes après son retour. Pendant ces 6 secondes, une partie des requêtes échoue encore : HAProxy peut les renvoyer vers un autre serveur avec option redispatch et retries.
La page de santé. Sur un serveur Nginx, une page de santé simple tient en une ligne dans le bloc server :
location = /sante {
access_log off;
return 200 "OK\n";
}
Une page de santé profonde, en PHP, vérifie que l'application joint sa base de données :
<?php
// /var/www/intranet/sante.php : 200 si la base répond, 503 sinon
require '/etc/intranet/sante-config.php'; // définit $dsn, $utilisateur, $motDePasse (hors du site)
try {
$pdo = new PDO($dsn, $utilisateur, $motDePasse, [PDO::ATTR_TIMEOUT => 2]);
$pdo->query('SELECT 1');
http_response_code(200);
echo "OK\n";
} catch (Throwable $e) {
http_response_code(503);
echo "KO\n";
}
Le compte utilisé n'a besoin d'aucun droit sur les tables. Et la page ne doit rien afficher d'autre que OK ou KO : un message d'erreur détaillé donnerait des informations à un attaquant.
Les sessions : persistance ou serveurs sans état
Une application PHP range par défaut les sessions des utilisateurs dans des fichiers, sur le serveur qui les a créées. Si la première requête arrive sur WEB1 (connexion de l'utilisateur) et la suivante sur WEB2, WEB2 ne connaît pas la session : l'utilisateur est renvoyé à la page de connexion. Deux familles de solutions :
- La persistance (sticky sessions) : le répartiteur renvoie toujours un même utilisateur vers le même serveur. En niveau 7, il insère un cookie qui désigne le serveur ; en niveau 4, il utilise le hachage de l'adresse source. C'est simple, mais la répartition devient inégale (un gros client derrière une seule adresse NAT pèse sur un seul serveur), et les sessions du serveur qui tombe sont perdues : ses utilisateurs doivent se reconnecter.
- Des serveurs sans état (stateless) : les sessions sont rangées dans un stockage partagé (une base Redis, la base de données de l'application), et les fichiers déposés par les utilisateurs dans un stockage commun. N'importe quel serveur peut alors traiter n'importe quelle requête, et la panne d'un serveur ne déconnecte personne. C'est la bonne architecture, mais elle demande de configurer l'application.
En pratique, on combine souvent les deux : un cookie de persistance pour l'efficacité des caches, et des sessions partagées pour survivre aux pannes.
HAProxy pas à pas
HAProxy est le répartiteur libre de référence, en niveau 4 comme en niveau 7. Debian 13 fournit sa branche 3.0, maintenue à long terme. Sur LB1 et LB2 :
apt install haproxy socat
Le fichier /etc/haproxy/haproxy.cfg de Ravelle Conseil, avec la VIP 10.10.20.30 partagée par keepalived (chapitre 5) :
global
log /dev/log local0
user haproxy
group haproxy
daemon
stats socket /run/haproxy/admin.sock mode 660 level admin
defaults
mode http
log global
option httplog
option forwardfor
option redispatch
retries 2
timeout connect 5s
timeout client 30s
timeout server 30s
frontend intranet
bind 10.10.20.30:80
bind 10.10.20.30:443 ssl crt /etc/haproxy/certs/intranet.pem
http-request redirect scheme https unless { ssl_fc }
default_backend web_intranet
backend web_intranet
balance roundrobin
option httpchk GET /sante
http-check expect status 200
cookie SRV insert indirect nocache
default-server inter 2s fall 3 rise 2
server web1 10.10.20.31:80 check cookie w1
server web2 10.10.20.32:80 check cookie w2
listen stats
bind :8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:Remplace-Moi-Par-Une-Longue-Phrase
| Section | Rôle |
|---|---|
global | Le processus : journalisation vers syslog, compte non privilégié, socket d'administration pour les commandes en direct |
defaults | Les réglages communs : mode HTTP, ajout de X-Forwarded-For, nouvel essai sur un autre serveur en cas d'échec, délais d'expiration |
frontend intranet | Ce qui est exposé aux clients : la VIP en HTTP et en HTTPS, la redirection de HTTP vers HTTPS, le groupe de serveurs par défaut |
backend web_intranet | Le groupe de serveurs : algorithme, contrôle de santé sur /sante, cookie de persistance, paramètres des contrôles, et les serveurs avec le mot-clé check |
listen stats | La page de statistiques : état de chaque serveur, nombre de sessions, erreurs |
Le fichier intranet.pem contient le certificat suivi de sa clé privée (lisible par root seul). Le mot-clé check est indispensable sur chaque ligne server : sans lui, HAProxy n'effectue aucun contrôle et continue d'envoyer des requêtes à un serveur mort. La page de statistiques ne doit être accessible que depuis le VLAN d'administration : filtre le port 8404 sur le pare-feu.
Vérifier, recharger, contrôler.
haproxy -c -f /etc/haproxy/haproxy.cfg # « Configuration file is valid »
systemctl reload haproxy # rechargement sans couper les connexions
echo "show servers state" | socat stdio /run/haproxy/admin.sock
Côté serveurs web, Nginx doit lire l'adresse réelle du client dans l'en-tête ajouté par les répartiteurs, et seulement quand la requête vient d'eux :
set_real_ip_from 10.10.20.28;
set_real_ip_from 10.10.20.29;
real_ip_header X-Forwarded-For;
Prouver que ça marche. Dépose sur chaque serveur une page qui.html qui affiche son nom, puis, depuis un poste :
for i in 1 2 3 4; do curl -s https://intranet.ravelle.lan/qui.html; done
# web1 / web2 / web1 / web2 : curl n'envoie pas le cookie, le tourniquet s'applique
Arrête Nginx sur WEB2 : la page de statistiques le passe en rouge (DOWN) au bout de quelques secondes, et la boucle ne renvoie plus que web1, sans aucune erreur. Redémarre-le : il repasse en vert (UP) après deux contrôles réussis.
La maintenance sans coupure
Le répartiteur permet de mettre à jour les serveurs un par un, sans que les utilisateurs le voient. La socket d'administration accepte trois états :
drain: le serveur ne reçoit plus de nouveaux utilisateurs, mais termine les sessions en cours (y compris celles liées par le cookie) ;maint: le serveur est retiré complètement ;ready: il reprend du service.
echo "set server web_intranet/web1 state drain" | socat stdio /run/haproxy/admin.sock
# attendre que les sessions en cours se terminent (page de statistiques)
echo "set server web_intranet/web1 state maint" | socat stdio /run/haproxy/admin.sock
# mettre à jour WEB1, redémarrer, tester directement http://10.10.20.31/sante
echo "set server web_intranet/web1 state ready" | socat stdio /run/haproxy/admin.sock
On recommence ensuite pour WEB2. Cette mise à jour progressive (rolling update) suppose que le serveur restant tienne seul la charge pendant l'opération : c'est la règle N+1 du chapitre 1.
Le répartiteur ne doit pas devenir le SPOF
Un répartiteur unique est un SPOF placé devant des serveurs redondants. On le double :
- en actif-passif : deux répartiteurs identiques, une VIP partagée par keepalived. C'est l'architecture de Ravelle Conseil ;
- en actif-actif : deux VIP, chacune active sur un répartiteur et en secours sur l'autre, et un DNS round-robin qui distribue les deux adresses. Les deux répartiteurs travaillent, et chacun peut tout reprendre.
Les deux répartiteurs doivent avoir la même configuration. Modifie-la sur LB1, vérifie-la avec haproxy -c, puis recopie-la sur LB2 (avec scp, un script ou un outil de gestion de configuration comme Ansible), vérifie et recharge. Un secours dont la configuration a trois mois de retard trahit le jour de la bascule.
Exercices
Exercice 1 sur 5 Choisir l'algorithme Réussi À revoir
Pour chaque service, choisis un algorithme de répartition et justifie.
- Un site web de pages courtes, sur trois serveurs identiques.
- Une ferme de serveurs de bureau à distance, où chaque connexion dure plusieurs heures.
- Deux serveurs web, l'un deux fois plus puissant que l'autre.
- Un service TCP non HTTP qui a besoin qu'un client retrouve toujours le même serveur.
Voir la réponseMasquer la réponse de l'exercice 1
- Tourniquet (
roundrobin) : les requêtes sont courtes et comparables, chacun son tour suffit. - Moins de connexions (
leastconn) : le tourniquet ignorerait que certains serveurs portent déjà beaucoup de sessions longues. - Tourniquet pondéré :
weight 2sur le serveur puissant,weight 1sur l'autre. Sur 300 requêtes, il en reçoit 200 et l'autre 100. - Hachage de l'adresse source (
source) : en niveau 4, pas de cookie possible ; la même adresse source mène au même serveur, tant que le groupe ne change pas.
Tu avais trouvé ?
Exercice 2 sur 5 Combien de temps pour retirer un serveur ? Réussi À revoir
Un groupe HAProxy a les paramètres inter 3s fall 4 rise 3.
- Combien de temps faut-il environ pour qu'un serveur en panne soit retiré ?
- Combien de temps après son retour est-il réintégré ?
- Le responsable trouve ce délai trop long. Que proposes-tu, et quel est le risque ?
Voir la réponseMasquer la réponse de l'exercice 2
- 4 contrôles ratés à 3 secondes d'intervalle : environ 12 secondes.
- 3 succès à 3 secondes d'intervalle : environ 9 secondes.
- Réduire
inter(2 secondes, voire 1) oufall(2). Le risque : retirer un serveur simplement lent pendant un pic de charge, ce qui reporte sa charge sur les autres et peut les faire tomber à leur tour. On complète paroption redispatchetretries, qui renvoient vers un autre serveur une requête qui a échoué, pendant que le contrôle de santé n'a pas encore conclu.
Tu avais trouvé ?
Exercice 3 sur 5 Déconnectés au hasard Réussi À revoir
Depuis la mise en place de deux serveurs web derrière HAProxy, les utilisateurs de l'intranet sont renvoyés à la page de connexion au hasard, plusieurs fois par heure. Le backend contient balance roundrobin et deux lignes server … check, rien d'autre.
- Explique la cause.
- Propose une correction rapide dans HAProxy, avec les lignes à ajouter.
- Quelle est la limite de cette correction, et quelle est la solution durable ?
Voir la réponseMasquer la réponse de l'exercice 3
- Les sessions PHP sont stockées en fichiers sur chaque serveur. Le tourniquet envoie les requêtes d'un même utilisateur tantôt à WEB1, tantôt à WEB2 : le serveur qui ne connaît pas la session renvoie à la page de connexion.
- Une persistance par cookie :
cookie SRV insert indirect nocache server web1 10.10.20.31:80 check cookie w1 server web2 10.10.20.32:80 check cookie w2 - Si un serveur tombe, ses utilisateurs basculent sur l'autre, qui ne connaît pas leur session : ils doivent se reconnecter. La solution durable est de rendre les serveurs sans état : sessions dans un stockage partagé (Redis, ou la base de données de l'application), fichiers dans un stockage commun.
Tu avais trouvé ?
Exercice 4 sur 5 Trouver les erreurs Réussi À revoir
Un camarade te montre sa configuration et se plaint que « la haute disponibilité ne marche pas » : quand il arrête WEB2, un rechargement de page sur deux affiche une erreur 503.
frontend site
bind 10.10.20.28:80
default_backend web
backend web
balance roundrobin
option httpchk GET /Sante
server web1 10.10.20.31:80 check
server web2 10.10.20.32:80
Trouve trois problèmes, dont celui qui explique les erreurs.
Voir la réponseMasquer la réponse de l'exercice 4
web2n'a pas le mot-clécheck: HAProxy ne le contrôle jamais et continue de lui envoyer une requête sur deux, d'où les erreurs quand il est arrêté. C'est la cause du symptôme.- Le chemin
/Santeavec une majuscule. Ici,web1reste UP, puisqu'il reçoit une requête sur deux : une page répond donc aussi sous ce nom. Mais si la page de santé s'appelle/sante, comme partout dans ce cours, le contrôle deweb1échouera (404, Nginx distingue les majuscules) etweb1sera retiré alors qu'il fonctionne. Toutes les requêtes iraient alors àweb2, et l'arrêt de WEB2 couperait tout le service. Le chemin du contrôle doit être exactement celui de la page. - Le
bindsur 10.10.20.28, l'adresse réelle de LB1, et pas sur la VIP 10.10.20.30 : si LB1 tombe, keepalived déplace la VIP vers LB2, mais les clients qui visaient .28 n'ont plus rien. Le service doit être publié sur la VIP.
Tu avais trouvé ?
Exercice 5 sur 5 Le DNS round-robin et la panne Réussi À revoir
Le nom intranet d'une petite structure pointe, en round-robin, vers deux serveurs web. Le serveur .32 tombe en panne et personne ne s'en aperçoit.
- Quelle proportion approximative des premières tentatives de connexion échoue ?
- Pourquoi supprimer l'enregistrement de .32 ne règle-t-il pas tout immédiatement ?
- Qu'apporterait un répartiteur à la place ?
Voir la réponseMasquer la réponse de l'exercice 5
- Environ une sur deux : le DNS continue de donner .32 en première position une fois sur deux. Certains navigateurs essaient ensuite la seconde adresse, après un délai, d'autres non.
- Les clients et les serveurs DNS intermédiaires ont gardé la réponse en cache pendant sa durée de vie (le TTL de l'enregistrement) : ils continuent d'utiliser .32 jusqu'à expiration.
- Un contrôle de santé qui retire .32 en quelques secondes, sans toucher au DNS ; un retrait propre pour maintenance (
drain,maint) ; une répartition maîtrisée. À condition de doubler aussi le répartiteur.
Tu avais trouvé ?