Ce que fait un répartiteur de charge
Un répartiteur de charge se place entre les clients et un groupe de serveurs qui peuvent tous répondre à la même requête, et décide quel serveur reçoit chacune d'elles. Les clients se connectent à une seule adresse ; le répartiteur de charge choisit un backend sain, transmet la requête, et renvoie la réponse.
Il remplit trois rôles. Il répartit la charge, si bien que trois serveurs peuvent absorber environ trois fois le trafic d'un seul. Il retire les serveurs en panne : les contrôles de santé remarquent un backend qui a cessé de répondre et n'y envoient plus personne jusqu'à son rétablissement. Et il rend les changements invisibles : vous pouvez sortir un serveur, le corriger, y déployer et le remettre sans que les visiteurs voient une erreur.
Le deuxième rôle compte souvent le plus : deux serveurs derrière un répartiteur de charge, c'est moins une question de capacité que la permission donnée à l'un d'eux de tomber en panne. Un répartiteur de charge de couche 7 est une sorte de reverse proxy dont le rôle est de choisir entre des backends identiques ; HAProxy et nginx font les deux.
Répartition de charge couche 4 contre couche 7
La couche indique quelle part du trafic le répartiteur de charge lit avant de décider.
Un répartiteur de charge de couche 4 travaille avec des connexions TCP et des paquets UDP. Il voit les adresses et ports source et destination, choisit un backend à l'ouverture de la connexion, et copie les octets dans les deux sens. Il ne peut pas voir une URL, un en-tête ou un cookie, donc chaque requête sur cette connexion va au même serveur. En échange, il est rapide, agnostique au protocole (bases de données, MQTT, SMTP, serveurs de jeu) et peut laisser passer le TLS tel quel, si bien que le certificat reste sur les backends. Le mode tcp de HAProxy et le bloc stream {} de nginx fonctionnent ici.
Un répartiteur de charge de couche 7 parle HTTP. Il termine la connexion, lit chaque requête, et peut router par hôte, chemin, en-tête ou cookie : /api vers un pool, / vers un autre, un cookie de canary vers la nouvelle version. Il peut retenter une requête échouée sur un autre serveur et répartir les requêtes d'une même connexion HTTP/2 entre plusieurs backends (HAProxy mode http, nginx http {}).
Pour lire le HTTP, un répartiteur de couche 7 doit le déchiffrer, donc la terminaison TLS fait partie du lot. Le certificat vit sur le répartiteur de charge et les backends reçoivent du HTTP en clair sur un réseau privé, ou une seconde connexion TLS si le réseau n'est pas de confiance. Le coût est que le backend ne voit plus le client : il voit le répartiteur de charge. Les répartiteurs de couche 7 corrigent cela avec X-Forwarded-For et X-Forwarded-Proto ; les répartiteurs de couche 4 utilisent le protocole PROXY, que le backend doit être configuré pour accepter.
Algorithmes de répartition : round robin, least connections, hachage, poids
Le round robin distribue les requêtes à chaque serveur à tour de rôle. C'est le défaut presque partout et il convient quand les requêtes coûtent à peu près la même chose et que les serveurs sont identiques.
Le round robin pondéré donne une part plus grande à un serveur plus puissant : avec des poids de 2, 2 et 1, le troisième serveur reçoit un cinquième du trafic. Les poids servent aussi à faire tourner une version canary : mettez la nouvelle version dans le pool avec un petit poids, surveillez son taux d'erreur, puis augmentez-le.
Least connections envoie la prochaine requête au serveur avec le moins de connexions actives. Utilisez-le quand le coût des requêtes varie beaucoup (rapports à côté de pages vues, envois à côté d'appels d'API) ou que les connexions sont de longue durée, comme avec les WebSockets.
Le hachage d'IP (balance source chez HAProxy, ip_hash chez nginx) fait correspondre chaque adresse client au même serveur à chaque fois. Il donne une affinité sans cookies, mais la distribution suit la population de clients : un bureau ou un opérateur mobile derrière une adresse partagée peut faire atterrir des milliers d'utilisateurs sur un seul backend.
Le hachage cohérent sur une clé (l'URI, un identifiant utilisateur) garde la même clé sur le même serveur, et quand un serveur est ajouté ou retiré, seule une petite part des clés se déplace. C'est ce qu'il vous faut devant des caches : hachez par URI et chaque objet vit sur un seul cache au lieu de tous. nginx l'écrit hash $request_uri consistent;, HAProxy balance uri avec hash-type consistent.
Quel que soit votre choix, l'algorithme compte moins que des contrôles de santé qui disent la vérité.
Contrôles de santé et basculement
Un répartiteur de charge n'est jamais meilleur que son idée de quels serveurs sont vivants.
Les contrôles de santé actifs sondent chaque backend sur une minuterie : ouvrir une connexion TCP, ou demander une URL comme /healthz et attendre un 200. Après fall échecs consécutifs, le serveur est marqué en panne et ne reçoit plus rien ; après rise succès, il revient. HAProxy le fait dans sa version open source. nginx open source ne le fait pas : max_fails et fail_timeout sont des contrôles passifs, qui comptent les vraies requêtes échouées et mettent le serveur au repos pendant un moment. Les contrôles actifs de nginx font partie de la version commerciale NGINX Plus. Le timing est un compromis : un contrôle toutes les deux secondes avec fall 3 retire un serveur mort en environ six secondes ; toutes les 30 secondes, cela représente une minute et demie d'erreurs.
Le basculement est ce qui se passe ensuite. Le trafic se déplace vers les serveurs restants, qui doivent donc avoir de la place : trois serveurs tournant chacun à 80 % ne peuvent pas absorber la perte de l'un d'eux. Un serveur backup ne reçoit du trafic que lorsque tous les serveurs primaires sont en panne, ce qui convient à une réserve froide ou à une page de maintenance.
Faites répondre le point de contrôle de santé à la question que pose le répartiteur de charge : *ce serveur doit-il recevoir du trafic en ce moment ?* Cela signifie vérifier ce qui est local au processus (il a démarré, il peut atteindre ses propres dépendances, il n'est pas en train de s'arrêter) et répondre rapidement. Pendant un déploiement, faites échouer le contrôle en premier, laissez les requêtes en vol se terminer (vidange de connexions), puis arrêtez le processus.
Persistance de session (sticky sessions)
Si une application garde la session d'un utilisateur connecté dans la mémoire d'un serveur, la requête suivante doit atteindre ce serveur ou l'utilisateur est déconnecté. La persistance de session fait retenir au répartiteur de charge qui va où.
Les méthodes habituelles sont un cookie inséré (le répartiteur de charge ajoute un cookie nommant le serveur, comme le fait HAProxy avec cookie SRV insert indirect nocache plus une valeur cookie sur chaque ligne server), un cookie d'application qu'il apprend (PHPSESSID, JSESSIONID), ou le hachage de l'IP source. nginx open source n'a que les méthodes de hachage (ip_hash ou hash $cookie_name) ; la directive sticky appartient à NGINX Plus.
La persistance a un prix. La charge cesse d'être équilibrée, parce que quelques gros utilisateurs restent épinglés à un serveur. Vider un serveur prend aussi longtemps que sa plus longue session, et quand un serveur meurt, ses utilisateurs perdent leur session de toute façon.
La correction durable est de rendre les serveurs interchangeables : garder les sessions dans un magasin partagé comme Redis ou la base de données, ou dans un cookie signé, et laisser n'importe quel serveur répondre à n'importe quelle requête. Alors un serveur en panne ne coûte à personne sa connexion.
Une configuration HAProxy qui fonctionne
Une configuration HAProxy se lit de haut en bas : global pour le processus, defaults hérité par chaque section, un frontend qui accepte les connexions et un backend qui contient le pool.
L'exemple termine le TLS sur le 443 (le fichier .pem contient la chaîne de certificats et la clé privée ensemble), redirige le HTTP simple, ajoute les en-têtes de transmission, et répartit avec least connections. Le contrôle de santé est une vraie requête HTTP avec un en-tête Host, parce que de nombreuses applications répondent 404 ou une redirection à une requête qui n'en a pas, et un contrôle qui attend 200 échoue alors sur un serveur sain. http-check send nécessite HAProxy 2.2 ou plus récent ; les versions plus anciennes placent la requête sur la ligne option httpchk.
default-server fixe le timing du contrôle une fois pour toutes : une sonde toutes les deux secondes, trois échecs pour marquer un serveur en panne, deux succès pour le faire revenir. app3 est une machine plus petite et reçoit la moitié de la part des autres ; spare ne reçoit du trafic que lorsque les trois autres sont en panne. Pour rendre les sessions persistantes, ajoutez cookie SRV insert indirect nocache au backend et cookie app1 (et ainsi de suite) à chaque ligne server.
Vérifiez le fichier avec haproxy -c -f /etc/haproxy/haproxy.cfg avant chaque reload.
/etc/haproxy/haproxy.cfg : terminaison TLS, least connections, contrôles de santé HTTP
global
log /dev/log local0
maxconn 20000
defaults
mode http
log global
option httplog
timeout connect 5s
timeout client 60s
timeout server 60s
frontend fe_web
bind :80
bind :443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
http-request redirect scheme https unless { ssl_fc }
option forwardfor
http-request set-header X-Forwarded-Proto https
default_backend be_app
backend be_app
balance leastconn
option httpchk
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host app.example.com
http-check expect status 200
default-server inter 2s fall 3 rise 2
server app1 10.0.0.11:8080 check weight 100
server app2 10.0.0.12:8080 check weight 100
server app3 10.0.0.13:8080 check weight 50
server spare 10.0.0.20:8080 check backup
Répartition de charge avec nginx upstream
Dans nginx, le pool est un bloc upstream et proxy_pass le désigne par son nom. Sans ligne d'algorithme, nginx utilise le round robin pondéré ; least_conn, ip_hash, hash … consistent et random changent cela.
Quelques détails de l'exemple sont faciles à manquer. keepalive 32 garde les connexions inactives vers les backends ouvertes pour réutilisation, mais fonctionne seulement avec proxy_http_version 1.1 et un en-tête Connection vide ; sans ces deux lignes, nginx ouvre une nouvelle connexion pour chaque requête. max_fails=3 fail_timeout=10s sort un serveur pendant dix secondes après trois requêtes échouées en dix secondes : c'est le contrôle de santé passif de nginx. backup ne fonctionne qu'avec les méthodes round robin, pondérée et least_conn, pas avec les méthodes de hachage.
proxy_next_upstream décide quand une requête échouée est retentée sur le serveur suivant. Par défaut, nginx retente sur les erreurs de connexion et les timeouts, et depuis la version 1.9.13, il ne retente jamais un POST, LOCK ou PATCH sauf si vous ajoutez non_idempotent. Gardez-le ainsi : retenter un paiement parce que le premier serveur a expiré après avoir débité la carte est pire qu'une erreur. proxy_next_upstream_tries 2 empêche une requête lente de parcourir tout le pool. Les en-têtes sont ceux dont a besoin tout reverse proxy ; le guide nginx couvre le reste.
nginx : un pool upstream avec poids, contrôles passifs, backup et keep-alive
upstream app {
least_conn;
server 10.0.0.11:8080 weight=2 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 weight=2 max_fails=3 fail_timeout=10s;
server 10.0.0.13:8080 max_fails=3 fail_timeout=10s;
server 10.0.0.20:8080 backup;
keepalive 32;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/example.com/privkey.pem;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
proxy_connect_timeout 3s;
}
}
Global contre local : DNS, GeoDNS, anycast et répartiteurs de charge cloud
Tout ce qui précède est de la répartition de charge locale : un site, un pool, un proxy dans le chemin de la requête. La répartition globale décide quel site, quelle région ou quel datacentre un visiteur atteint en premier lieu, et elle se fait généralement avant qu'aucun proxy ne voie la requête.
Le round robin DNS est la forme la plus ancienne : publier plusieurs enregistrements A et les résolveurs les distribuent dans un ordre tournant. Cela ne coûte rien et répartit le trafic grossièrement, mais le DNS ne connaît rien à la santé des serveurs. L'adresse d'un serveur mort continue d'être distribuée jusqu'à ce que quelqu'un la retire, et les résolveurs et navigateurs mettent en cache l'ancienne réponse pour le TTL de l'enregistrement, et parfois plus longtemps.
Le GeoDNS répond différemment selon l'origine de la requête, si bien que les visiteurs d'un pays reçoivent l'adresse du site le plus proche. Combiné à des contrôles de santé côté DNS, cela devient un basculement DNS : une région qui échoue à ses contrôles est retirée des réponses. Les limites sont les mêmes que la mise en cache par TTL et le fait que la localisation est celle du résolveur, pas du visiteur, à moins que le résolveur ne transmette une partie de l'adresse du client (EDNS Client Subnet). Le guide DNS couvre les TTL et les résolveurs en détail.
L'anycast annonce la même adresse IP depuis de nombreux emplacements via BGP, et le routage d'internet livre chaque paquet au plus proche. Il n'y a pas de TTL à attendre : quand un emplacement cesse d'annoncer, les routes se déplacent en quelques secondes à quelques minutes. C'est ainsi que les grands services DNS et les CDN atteignent leurs nœuds. Les couches s'empilent : le DNS ou l'anycast choisit l'emplacement, un proxy à l'intérieur choisit le serveur.
Les répartiteurs de charge cloud emballent les mêmes idées en service managé. AWS a l'Application Load Balancer (couche 7) et le Network Load Balancer (couche 4) ; Google Cloud Load Balancing propose des variantes globales et régionales, applicatives et réseau ; Azure sépare Azure Load Balancer (couche 4) de l'Application Gateway (couche 7). Dans Kubernetes, un Service répartit les connexions entre les pods et un contrôleur Ingress fait du routage de couche 7. Les algorithmes, contrôles de santé et pièges de ce guide s'y appliquent sans changement.
Erreurs courantes qui provoquent des pannes
Le répartiteur de charge devient le point de défaillance unique. Trois serveurs d'application derrière une seule boîte HAProxy signifie que la boîte est désormais ce qui peut vous faire tomber. Exécutez-en deux et déplacez une IP flottante entre elles avec VRRP (keepalived est l'outil habituel), ou mettez une couche managée ou de niveau DNS devant. Puis testez en éteignant celle qui est active.
Le contrôle de santé menti, dans un sens ou dans l'autre. Un /healthz qui renvoie toujours 200 garde en rotation un serveur dont le pool de connexions à la base de données est épuisé, si bien qu'une partie des requêtes échoue pendant que le tableau de bord affiche tout en vert. L'inverse est tout aussi mauvais : un contrôle qui interroge la base de données partagée marque *tous* les serveurs en panne pendant un à-coup de cinq secondes de la base, et un court ralentissement devient une panne complète. Vérifiez le serveur, pas les systèmes que tous les serveurs partagent.
Des timeouts mal assortis. Si le backend ferme les connexions keep-alive inactives plus tôt que ne l'attend le répartiteur de charge, celui-ci envoie parfois une requête sur une connexion que le backend vient de fermer, et le client reçoit un 502 sporadique. Gardez le timeout keep-alive du backend plus long que le timeout d'inactivité du répartiteur de charge. D'autres causes sont dans le guide 502 Bad Gateway.
L'adresse du client disparaît. Sans X-Forwarded-For (couche 7) ou le protocole PROXY (couche 4), l'application journalise, limite le débit et géolocalise l'IP du répartiteur de charge.
Des serveurs qui ne sont pas identiques. Des builds différents, une configuration différente, ou des horodatages de fichiers différents qui changent l'ETag par défaut, si bien que la revalidation d'un navigateur renvoie un 200 complet au lieu d'un 304 chaque fois que l'autre serveur répond. Déployez un seul artefact partout.
Le test est simple. Exposez le nom du backend dans un en-tête de réponse pendant les tests et bouclez sur les requêtes ; observez la distribution, puis arrêtez un backend et regardez les requêtes se déplacer.
# which backend answered? (expose the server name in a debug header first)
for i in $(seq 1 10); do
curl -s -o /dev/null -D - https://example.com/ | grep -i '^x-served-by'
done
# HAProxy: live state of every server through the runtime socket
# (needs "stats socket /run/haproxy/admin.sock mode 660 level admin" in global)
echo "show servers state be_app" | socat stdio /run/haproxy/admin.sock
# take one server out gracefully before a deploy, then put it back
echo "set server be_app/app1 state drain" | socat stdio /run/haproxy/admin.sock
echo "set server be_app/app1 state ready" | socat stdio /run/haproxy/admin.sock
Où se place un CDN, et ce que fait CDN.com.tr
Un CDN est de la répartition de charge globale que vous n'avez pas à construire. Les visiteurs sont dirigés vers un serveur de périphérie proche d'eux, les périphéries répondent depuis le cache tout ce qu'elles peuvent, et seuls les cache miss voyagent jusqu'à votre origine. CDN.com.tr exploite des serveurs de périphérie en Turquie et à l'étranger et répartit les visiteurs entre eux, donc répartir les visiteurs entre les emplacements est déjà fait avant qu'une requête n'atteigne quoi que ce soit que vous exploitez.
Ce qui reste à votre charge, c'est l'origine. Pour un site en Pull CDN, la périphérie récupère depuis l'adresse source que vous définissez dans le panneau, une IP ou un domaine. Si vous exploitez plusieurs serveurs d'origine, le répartiteur de charge qui choisit entre eux se trouve derrière cette adresse, et tout ce guide s'y applique. Verrouillez-le pour qu'il n'accepte des connexions que depuis la périphérie : la recommandation du panneau lui-même est de garder l'adresse de l'origine hors du DNS public, pour que le seul chemin passe par la périphérie.
La périphérie atténue aussi les pannes d'origine. Le contenu déjà dans le cache de périphérie continue d'être servi pendant son TTL alors que l'origine est en panne, et le rapport Qualité du trafic marque une copie en cache servie alors que l'origine était lente ou en panne comme STALE. Le même rapport montre la santé de l'origine : requêtes vers l'origine, part des reprises, erreurs de connexion (502/504) et le temps moyen jusqu'au premier octet de l'origine. Les objets non mis en cache et expirés ont toujours besoin d'une origine qui fonctionne, c'est pourquoi l'origine a toujours besoin de sa propre redondance pour les pages dynamiques.
Si vous préférez ne pas exploiter cette couche du tout, les applications conteneur de CDN.com.tr prennent un nombre de répliques et un contrôle de santé (chemin HTTP, TCP ou aucun) par application. Le trafic entre par la périphérie ; avec plusieurs répliques, si un conteneur redémarre ou est en mauvaise santé, les autres continuent de servir, et les déploiements sont conditionnés à la santé, si bien que déployer une nouvelle version n'ouvre pas de fenêtre où l'application est inatteignable. Les sessions vont dans Redis managé, si bien qu'un utilisateur reste connecté quelle que soit la réplique qui sert la prochaine requête : la conception sans état recommandée plus haut, sans le cookie persistant.
FAQ répartiteur de charge
HAProxy ou nginx pour la répartition de charge ?
Les deux sont rapides et fiables. HAProxy a des contrôles de santé actifs, la persistance par cookie, une API d'exécution pour vider les serveurs et des statistiques très détaillées dans la version gratuite, ce qui en fait le répartiteur de charge pur le plus complet. nginx convient mieux quand la même machine sert aussi des fichiers, met en cache ou fait le routage de plusieurs sites, mais sa version gratuite n'a que des contrôles de santé passifs.
Quelle est la différence entre la répartition de charge de couche 4 et de couche 7 ?
La couche 4 choisit un serveur par connexion TCP ou flux UDP et ne lit jamais le contenu, donc elle fonctionne pour n'importe quel protocole et peut laisser passer le TLS sans y toucher. La couche 7 lit chaque requête HTTP, donc elle peut router par URL, en-tête ou cookie, retenter sur un autre serveur et ajouter des en-têtes de transmission, au prix de terminer le TLS.
Quel algorithme de répartition de charge devrais-je utiliser ?
Commencez par round robin pour des serveurs identiques et des requêtes similaires. Passez à least connections quand la durée des requêtes varie beaucoup ou que les connexions restent ouvertes, comme avec les WebSockets. Utilisez le hachage cohérent quand la même clé doit atteindre le même serveur, comme des URL devant une couche de cache.
Le round robin DNS est-il un vrai répartiteur de charge ?
Il répartit le trafic, mais il ne vérifie pas la santé et ses réponses sont mises en cache pour le TTL de l'enregistrement, donc les clients continuent d'essayer une adresse morte. C'est correct pour une distribution grossière entre des sites qui ont chacun leur propre répartiteur de charge, et insuffisant comme seul mécanisme de basculement.
Ai-je besoin d'un répartiteur de charge si j'utilise un CDN ?
Le CDN répartit les visiteurs entre ses propres serveurs de périphérie et absorbe la plus grande partie du trafic depuis le cache. Si votre origine est un seul serveur, vous n'avez pas besoin d'un répartiteur pour la capacité, bien que l'origine reste un point de défaillance unique pour tout ce qui n'est pas mis en cache. Dès que vous exploitez deux serveurs d'origine ou plus, mettez un répartiteur de charge derrière l'adresse d'origine que le CDN interroge.