📍 Article principal du parcours : Nginx : reverse proxy, HTTPS et configuration de A à Z
Cet article fait partie du parcours Nginx. Pour la vue d’ensemble, lisez d’abord le guide principal.
Une seule instance de l’API Colis Express tient tant que le trafic reste modeste. Mais le jour d’une grosse opération de livraison, une instance sature, et si elle tombe, tout le service tombe avec elle. La parade est double : faire tourner plusieurs instances identiques de l’API, et placer Nginx devant pour répartir les requêtes entre elles. Si l’une flanche, les autres prennent le relais sans que le visiteur s’en aperçoive. Dans ce tutoriel, vous lancez deux instances de l’API et vous configurez Nginx pour équilibrer la charge entre elles, avec gestion des pannes.
🎯 Ce que vous allez apprendre
- Déclarer un groupe de backends avec un bloc
upstream. - Choisir un algorithme de répartition : tourniquet, moins de connexions, ou par IP cliente.
- Pondérer les serveurs selon leur puissance.
- Détecter et écarter automatiquement un backend en panne.
- Réutiliser les connexions vers les backends pour gagner en performance.
🛠️ Ce que vous allez construire
Vous aurez deux instances de l’API tournant sur deux ports locaux, regroupées dans un upstream, et Nginx répartira le trafic entre elles. Vous vérifierez que la coupure d’une instance n’interrompt pas le service, et vous saurez ajuster l’algorithme selon la nature de votre application.
Prérequis
- Nginx en reverse proxy devant une API (voir Mettre une API Node.js derrière un reverse proxy Nginx).
- De quoi lancer plusieurs instances de l’application (deux processus Node sur deux ports suffisent).
- Niveau intermédiaire à avancé. Test express : si vous savez écrire un
location /api/avecproxy_pass, vous êtes prêt. - ⏱️ Temps estimé : ~35 minutes.
Étape 1 — Lancer plusieurs instances de l’API
Le load balancing suppose plusieurs cibles identiques. Faisons écouter deux instances de l’API sur deux ports locaux distincts. Pour les distinguer dans nos tests, chacune annonce son port dans sa réponse.
// api.js — lit le port depuis l'environnement
const http = require("http");
const PORT = process.env.PORT || 3000;
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ service: "colis-express", servi_par: Number(PORT) }));
}).listen(PORT, "127.0.0.1");
Lancez deux instances dans deux terminaux (ou via deux services systemd) :
PORT=3001 node api.js # instance 1
PORT=3002 node api.js # instance 2
Le champ servi_par nous permettra de voir, requête après requête, quelle instance a répondu — la preuve visible que la répartition fonctionne. En production, ces instances seraient gérées par systemd ou un gestionnaire de processus, et pourraient même vivre sur des machines différentes ; le principe côté Nginx reste identique.
✅ Point d’étape —
curl 127.0.0.1:3001etcurl 127.0.0.1:3002renvoient chacun leur port dansservi_par. Les deux instances tournent.
Étape 2 — Déclarer le bloc upstream
Le bloc upstream regroupe les backends sous un nom logique, qu’on utilise ensuite dans proxy_pass à la place d’une adresse unique. On le place dans le bloc http (par exemple dans un fichier de conf.d/), au-dessus du bloc server.
upstream colis_api {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
Puis, dans le location de l’API, on relaie vers ce groupe :
location /api/ {
proxy_pass http://colis_api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Sans préciser d’algorithme, Nginx applique le tourniquet (round-robin) : il envoie la première requête à l’instance 3001, la suivante à 3002, et ainsi de suite à tour de rôle. C’est l’algorithme par défaut, simple et efficace quand toutes les requêtes se valent. Validez et rechargez avec nginx -t && sudo systemctl reload nginx.
✅ Point d’étape — Plusieurs appels à
https://colis-express.net/api/font alternerservi_parentre 3001 et 3002. La répartition est active.
Étape 3 — Choisir le bon algorithme
Le tourniquet ne convient pas à tous les cas. Nginx propose plusieurs méthodes, à choisir selon la nature de votre application. Le bon choix dépend d’une question : vos requêtes durent-elles toutes le même temps, et votre application garde-t-elle un état par utilisateur ?
upstream colis_api {
least_conn; # envoie au backend le moins chargé
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
least_conn dirige chaque requête vers le backend qui a le moins de connexions actives. C’est le meilleur choix quand la durée des requêtes varie beaucoup : il évite qu’une instance accumule les requêtes longues pendant qu’une autre chôme. Une troisième option, ip_hash, envoie toujours un même client (identifié par son IP) vers le même backend — utile quand l’application garde une session en mémoire locale plutôt que dans un stockage partagé. Cela dit, la pratique moderne préfère rendre l’application sans état (sessions dans une base ou un cache partagé), ce qui laisse libre de répartir comme on veut. À retenir : least_conn par défaut pour des requêtes hétérogènes, le tourniquet pour des requêtes homogènes, ip_hash seulement si l’état local l’impose.
Étape 4 — Pondérer et gérer les pannes
Tous les serveurs ne sont pas égaux : si une instance tourne sur une machine plus puissante, elle peut encaisser davantage. Le paramètre weight ajuste la part de trafic. Surtout, on veut qu’un backend en panne soit écarté automatiquement, sans erreur visible pour l’utilisateur.
upstream colis_api {
server 127.0.0.1:3001 weight=2 max_fails=3 fail_timeout=15s;
server 127.0.0.1:3002 weight=1 max_fails=3 fail_timeout=15s;
server 127.0.0.1:3003 backup;
}
weight=2 sur la première instance lui envoie deux fois plus de requêtes qu’à la seconde. max_fails=3 et fail_timeout=15s forment le mécanisme de santé passif : si un backend échoue 3 fois en 15 secondes, Nginx le considère indisponible et cesse de lui envoyer du trafic pendant 15 secondes, avant de retenter. Le serveur marqué backup ne reçoit rien tant que les autres répondent ; il n’entre en jeu que si tous les serveurs principaux sont tombés — une réserve de secours. Ce contrôle de santé est dit « passif » car il déduit l’état des backends de leurs réponses aux vraies requêtes ; les contrôles « actifs » (sondes périodiques dédiées) existent mais relèvent de la version commerciale ou de modules tiers.
✅ Point d’étape — Coupez l’instance 3002 (
Ctrl+C) : après quelques requêtes, tout le trafic bascule sur 3001 sans erreur côté visiteur. Relancez-la : elle réintègre la rotation.
Étape 5 — Réutiliser les connexions backend
Par défaut, Nginx ouvre une nouvelle connexion vers le backend à chaque requête, puis la ferme. Sous forte charge, cet va-et-vient coûte cher. La directive keepalive maintient un pool de connexions ouvertes vers les backends, prêtes à resservir.
upstream colis_api {
least_conn;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
keepalive 32;
}
Pour que keepalive prenne effet, il faut aussi, dans le location, parler au backend en HTTP/1.1 et vider l’en-tête Connection :
location /api/ {
proxy_pass http://colis_api/;
proxy_http_version 1.1;
proxy_set_header Connection "";
# ... les autres proxy_set_header ...
}
Le keepalive 32; garde jusqu’à 32 connexions inactives par processus de travail. proxy_http_version 1.1; et proxy_set_header Connection ""; sont indispensables : sans eux, Nginx ferme la connexion après chaque requête et le pool ne sert à rien. Ce réglage réduit la latence et la charge sous trafic soutenu — exactement ce qu’on vise en répartissant la charge.
Étape 6 — Vérifier la répartition sous charge
Configurer la répartition, c’est bien ; prouver qu’elle équilibre vraiment, c’est mieux. Comme chaque instance annonce son port dans le champ servi_par, on peut envoyer une série de requêtes et compter qui répond. Une petite boucle suffit à rendre la répartition visible.
for i in $(seq 1 10); do
curl -s https://colis-express.net/api/ | grep -o '"servi_par":[0-9]*'
done
Avec le tourniquet, vous verrez alterner 3001 et 3002 de façon régulière ; avec une pondération weight=2 sur la première, elle apparaîtra environ deux fois plus souvent. C’est la confirmation directe que le trafic se répartit comme prévu. Pour aller plus loin et mesurer le comportement sous une charge réelle, des outils de test comme ab (Apache Bench) ou hey envoient des centaines de requêtes concurrentes et rapportent le débit et les temps de réponse — utile pour comparer un seul backend à deux backends et chiffrer le gain.
Un dernier point sur la persistance de session. On a vu ip_hash ; il existe aussi hash, plus souple, qui répartit selon une clé de votre choix — par exemple un identifiant présent dans l’URL. Mais répétons la règle qui fait gagner du temps à long terme : plutôt que de coller un client à un backend, rendez l’application sans état en externalisant les sessions (base de données, cache partagé). Vous gagnez alors la liberté de répartir comme vous voulez, d’ajouter ou retirer une instance sans casser les sessions, et de survivre à la perte d’un backend sans déconnecter personne. La persistance par ip_hash ou hash reste un palliatif pour le code existant qu’on ne peut pas encore rendre sans état.
✅ Point d’étape — La boucle de test montre une alternance nette entre les backends, conforme à l’algorithme et aux poids choisis. La répartition est prouvée, pas seulement configurée.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| Les sessions utilisateur sautent au hasard | État stocké localement + tourniquet | Rendre l’app sans état, ou utiliser ip_hash |
| Une instance morte renvoie encore des erreurs | max_fails/fail_timeout non réglés |
Ajouter ces paramètres pour l’éviction automatique |
keepalive sans effet |
Version HTTP/1.0 ou en-tête Connection non vidé |
Ajouter proxy_http_version 1.1; et Connection "" |
| Charge déséquilibrée malgré le tourniquet | Requêtes de durées très inégales | Passer à least_conn |
host not found in upstream |
Nom d’upstream mal orthographié dans proxy_pass |
Vérifier la correspondance exacte des noms |
🌍 Adaptation au contexte ouest-africain
On n’a pas toujours les moyens de plusieurs gros serveurs, et c’est justement là que le load balancing devient malin. Sur un même VPS multi-cœurs, faire tourner deux ou trois instances de l’API derrière Nginx exploite tous les cœurs disponibles — un seul processus Node n’en utilise qu’un. Vous gagnez en capacité sans louer une seconde machine. Et le jour où le trafic justifie un deuxième VPS, il suffit d’ajouter une ligne server pointant vers son IP privée dans l’upstream : l’architecture grandit sans être repensée. La tolérance aux pannes, elle, vaut de l’or quand l’hébergement n’est pas toujours d’une stabilité parfaite : une instance qui redémarre ne coupe pas le service.
✅ Récapitulatif
Vous savez maintenant regrouper des backends dans un upstream, répartir le trafic selon l’algorithme adapté, pondérer les serveurs, écarter automatiquement une instance en panne, et réutiliser les connexions pour la performance. Votre service ne dépend plus d’une instance unique. Il ne reste qu’une étape, et non des moindres, pour parler de production : le durcissement de sécurité.
🧾 Aide-mémoire
| Directive | Rôle |
|---|---|
upstream nom { server ...; } |
Regrouper des backends |
| (défaut) round-robin | Répartition à tour de rôle |
least_conn; |
Vers le backend le moins chargé |
ip_hash; |
Un client toujours vers le même backend |
max_fails / fail_timeout |
Éviction automatique d’un backend en panne |
keepalive 32; |
Pool de connexions réutilisées vers les backends |
💪 À vous de jouer
Configurez une page de maintenance : si tous les backends sont indisponibles, au lieu d’une erreur 502 brute, servez une page statique « Service temporairement indisponible ». Indice : error_page 502 503 504 et un location interne.
Voir une solution
location /api/ {
proxy_pass http://colis_api/;
proxy_intercept_errors on;
error_page 502 503 504 = /maintenance.html;
}
location = /maintenance.html {
root /var/www/colis-express/html;
internal;
}
proxy_intercept_errors on; autorise Nginx à remplacer l’erreur du backend par sa propre page. error_page redirige vers une page statique servie localement, et internal; empêche d’y accéder directement par URL. L’utilisateur voit un message soigné plutôt qu’une erreur technique.
Tutoriels frères
- Durcir Nginx : sécurité TLS, en-têtes et rate limiting — la dernière étape avant la production.
- Cache et compression Nginx pour accélérer un site — soulager les backends en amont de la répartition.
Pour aller plus loin
- 🔝 Retour au guide principal : Nginx : reverse proxy, HTTPS et configuration de A à Z
- Documentation officielle : Guide du load balancing HTTP et module
upstream. - Étape suivante du parcours : durcir la configuration pour la production.
FAQ
Le load balancing nécessite-t-il plusieurs serveurs ?
Non. On peut répartir entre plusieurs instances d’une application sur une même machine — c’est même un excellent moyen d’exploiter tous les cœurs d’un VPS, puisqu’un processus Node n’en utilise qu’un. Plusieurs machines deviennent utiles ensuite, pour la résilience et la capacité.
Quelle différence entre tourniquet et least_conn ?
Le tourniquet distribue à tour de rôle sans regarder la charge ; least_conn choisit le backend ayant le moins de connexions actives. Pour des requêtes de durées variables, least_conn équilibre mieux et évite qu’une instance soit engorgée.
Comment garder les sessions utilisateur avec plusieurs backends ?
La meilleure approche est de rendre l’application sans état en stockant les sessions dans une base ou un cache partagé (Redis, base de données). À défaut, ip_hash colle un client à un backend, mais c’est un palliatif qui supporte mal la perte d’une instance.
Le load balancing gère-t-il aussi le basculement (failover) ?
Oui, c’est inhérent à la répartition. Avec max_fails et fail_timeout, un backend qui cesse de répondre est écarté automatiquement, et le serveur marqué backup prend le relais si tous les principaux tombent. Vous obtenez donc la haute disponibilité et la montée en charge avec la même configuration, sans outil supplémentaire — un excellent rapport bénéfice/effort pour un petit déploiement.