Changer le thème

Équilibrage de charge Nginx en pratique : upstream et contrôles de santé

Easton editorial illustration: large Nginx upstream distributor steering weighted requests toward three servers with health indicators

Le téléphone vibre sans arrêt. Sur le tableau de bord, la ligne de backend1 est toute rouge — un seul serveur applicatif est tombé.

Lors du Double 11 de cette année-là, nous n’avions que deux backends. La config Nginx disait upstream backend { server backend1; server backend2; } — symétrique en apparence. Mais backend1 prenait ~70 % du trafic : premier dans le fichier, round-robin par défaut, sans poids ni contrôle de santé.

Quand backend1 est tombé, les commandes continuaient d’y être envoyées. Nginx ne savait pas qu’il était mort. Résultat côté utilisateur : pages 500. Il a fallu 15 minutes pour que l’ops le retire manuellement de l’upstream.

Depuis, j’ai creusé le sujet : l’upstream Nginx ne se résume pas à une liste d’adresses. Pondération, santé, bascule — c’est ce dont la prod a besoin. Cet article rassemble les pièges vécus et les réglages retenus, y compris le contrôle actif avec Nginx open source.

1. Configuration upstream de base : du mono-serveur au cluster

Le bloc upstream regroupe plusieurs machines en un groupe logique. Les paramètres sont plus riches qu’on ne le croit souvent.

upstream backend {
  zone backend 64k;
  server backend1.example.com weight=3 max_fails=2 fail_timeout=30s;
  server backend2.example.com;
  server backup1.example.com backup;
}

Ligne par ligne :

zone backend 64k : mémoire partagée entre workers pour l’état des backends (qui est up, qui est down). 64k pour démarrer ; augmentez si vous avez beaucoup de serveurs. Sans zone, chaque worker vit sa vie — source classique d’incohérence.

weight=3 : backend1 reçoit trois parts sur quatre, backend2 une (défaut 1). Utile si les machines ne sont pas homogènes — ex. 8 cœurs / 16 Go vs 4 cœurs / 8 Go.

max_fails=2 : dans la fenêtre fail_timeout, deux échecs marquent le serveur indisponible. La valeur par défaut 1 est trop nerveuse (un micro-blocage réseau suffit). En prod : 2 ou 3.

fail_timeout=30s : double rôle — fenêtre de comptage des échecs (30 s) et délai avant nouvel essai après marquage down. 10 s par défaut peut être court pour des services à démarrage lent.

backup : ne reçoit du trafic que si tous les principaux sont down. Idéal pour une machine plus légère en secours.

down marque un serveur hors ligne pour la maintenance :

server backend3.example.com down;  # maintenance temporaire

Beaucoup oublient zone : un worker voit la panne, les autres non. Avec zone, l’état est aligné.

2. Cinq stratégies d’équilibrage : laquelle choisir ?

Le round-robin par défaut suffit-il ? Parfois oui.

J’ai vu des projets tourner des années en round-robin sans souci. WebSocket, panier e-commerce, cache : là, le défaut coince. Tableau de choix :

ScénarioStratégieRaison
API sans étatround-robinRépartition simple
WebSocketleast_connÉvite la surcharge sur une machine
Panier e-commerceip_hashMême client → même serveur
Proxy cachehash key=$uriLimite invalidation et trous de cache
Environnement de testrandomRapide à configurer

round-robin (par défaut)

Sans directive, alternance séquentielle :

upstream backend {
  server backend1.example.com;
  server backend2.example.com;
  server backend3.example.com;
}

Adapté aux services sans état — la plupart des API REST.

least_conn (moins de connexions)

Envoie vers le serveur avec le moins de connexions actives :

upstream websocket_app {
  least_conn;
  server ws1.example.com:8080;
  server ws2.example.com:8080;
}

Typique WebSocket : une connexion longue par utilisateur. Le round-robin peut empiler les longues connexions sur une machine ; least_conn rééquilibre en temps réel.

ip_hash

Hash de l’IP client : même IP → même serveur :

upstream shopping_cart {
  ip_hash;
  server cart1.example.com;
  server cart2.example.com;
}

Session locale : articles ajoutés sur cart1, requête suivante sur cart2 sans session distribuée = panier vide. ip_hash corrige cela.

Limite : si un serveur tombe, les clients hashés dessus sont réassignés — perte de session locale. À coupler avec stockage session partagé, ou pour données peu critiques.

hash (hachage cohérent)

Clé personnalisée, algorithme cohérent :

upstream cache_proxy {
  hash $uri consistent;
  server cache1.example.com;
  server cache2.example.com;
}

Proxy cache : $uri comme clé ; consistent limite le remapping quand on ajoute/retire un nœud — meilleur taux de hit.

random

Répartition aléatoire :

upstream test_backend {
  random;
  server test1.example.com;
  server test2.example.com;
}

OK en test. Peu de contrôle en production.

En pratique : round-robin ou least_conn couvrent la majorité des apps web. ip_hash et hash répondent à des cas précis — pas pour « faire joli ».

3. Contrôle passif : max_fails et fail_timeout

« Passif » : pas de sonde dédiée ; Nginx déduit la santé des requêtes réelles — comme juger un voisin à ses allers-retours, pas en frappant à sa porte.

upstream backend {
  server backend1.example.com max_fails=3 fail_timeout=30s;
  server backend2.example.com max_fails=3 fail_timeout=30s;
}

location / {
  proxy_pass http://backend;
  proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
}

proxy_next_upstream définit l’échec : erreur de connexion, timeout, 5xx. Nginx réessaie sur le suivant et incrémente le compteur d’échecs.

Trois échecs en 30 s → serveur marqué down, plus de trafic pendant 30 s, puis un essai de reprise.

Limite : lenteur. Il faut des échecs utilisateurs réels — trois clients mal servis avant retrait.

Pire au redémarrage : max_fails=1 peut exclure un serveur encore en init.

Recommandations :

  • max_fails à 2 ou 3
  • fail_timeout ≥ 30 s
  • proxy_next_upstream avec la liste complète des codes

Avantage : natif, simple. Inconvénient : l’utilisateur paie le premier échec. Pour anticiper, passez à l’actif.

4. Contrôle actif : NGINX Plus et alternative open source

Logique : sondes périodiques (ex. GET /health), retrait avant que le trafic client échoue.

Officiel : NGINX Plus, ~3 675 $/instance/an — dix instances ≈ 36 750 $/an, cher pour beaucoup d’équipes.

Open source : nginx_upstream_check_module (équipe Taobao). Recompilation Nginx requise, fonctionnalités solides :

FonctionnalitéNGINX Plusnginx_upstream_check_module
Prix3 675 $/anGratuit (open source)
Sonde HTTPOuiOui
Sonde TCPOuiOui
Sonde MySQLNonOui
Sonde FastCGINonOui
Page d’étatOuiOui (check_status)

MySQL et FastCGI manquent chez Plus — utile pour PHP-FPM ou MySQL.

Exemple de configuration

upstream backend {
  server backend1.example.com:8080;
  server backend2.example.com:8080;

  check interval=3000 rise=2 fall=5 timeout=1000 type=http;
  check_http_send "GET /health HTTP/1.0\r\n\r\n";
  check_http_expect_alive http_2xx http_3xx;
}

server {
  location / {
    proxy_pass http://backend;
  }

  location /upstream_status {
    check_status json;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
  }
}

Paramètres :

  • interval=3000 : sonde toutes les 3 s
  • rise=2 : deux succès consécutifs → healthy
  • fall=5 : cinq échecs → down
  • timeout=1000 : timeout sonde 1 s
  • type=http : aussi tcp, ssl_hello, mysql, ajp, fastcgi

check_http_send : contenu de la sonde (GET /health). Le backend doit répondre 2xx/3xx.

check_http_expect_alive : codes considérés sains.

check_status : JSON pour Prometheus/Zabbix — protégez l’URL (allow/deny).

Installation du module

# Télécharger le module
git clone https://github.com/yaoweibin/nginx_upstream_check_module.git

# Télécharger les sources Nginx
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar -zxvf nginx-1.24.0.tar.gz

# Patch (selon version)
cd nginx-1.24.0
patch -p1 < ../nginx_upstream_check_module/check_1.20.1+.patch

# Compilation
./configure --add-module=../nginx_upstream_check_module
make && make install

Sous Docker : image maison ou image communautaire avec le module.

5. Production : sécurité et supervision

Trois principes qui évitent les erreurs courantes :

Sécurité : trois points

1. Restreindre check_status

La page expose la liste des backends et leur état — topologie interne et cibles affaiblies visibles de l’extérieur.

location /upstream_status {
  check_status json;
  allow 127.0.0.1;       # local
  allow 10.0.0.0/8;      # intranet
  deny all;
}

Ou uniquement les IP de votre stack de monitoring.

2. Port dédié aux sondes

Sondes toutes les 3-5 s sur le port métier → logs /health saturés.

Deux ports : métier (8080) et santé (8888) — réponse minimale, sans log métier.

check interval=5000 rise=2 fall=3 timeout=2000 type=http port=8888;

3. /health sans données sensibles

Statut HTTP suffit — pas de version, config ou mémoire.

# Exemple backend
@app.route('/health')
def health():
    return '', 200   # code seulement

Intégration monitoring : sortie JSON

curl http://127.0.0.1/upstream_status

Exemple :

{
  "servers": {
    "total": 3,
    "generation": 12,
    "server": [
      {"index": 0, "name": "10.0.0.1:8080", "status": "up", "rise": 5, "fall": 0, "type": "http"},
      {"index": 1, "name": "10.0.0.2:8080", "status": "up", "rise": 3, "fall": 0, "type": "http"},
      {"index": 2, "name": "10.0.0.3:8080", "status": "down", "rise": 0, "fall": 5, "type": "http"}
    ]
  }
}

generation compte les reload de config upstream — utile pour vérifier qu’un déploiement a bien été pris en compte.

Réglage des seuils

interval ≥ 3000 ms

3-5 s suffisent ; détection en secondes, charge backend raisonnable.

rise / fall

  • rise trop bas (1) → oscillations au boot
  • fall trop bas (1) → retrait sur un pic réseau

Valeurs éprouvées : rise=2, fall=3 ou 5.

Configuration production complète

upstream web_app {
  zone web_app 64k;
  server 10.0.0.1:8080 weight=3;
  server 10.0.0.2:8080;
  server 10.0.0.3:8080 backup;

  check interval=5000 rise=2 fall=3 timeout=2000 type=http port=8888;
  check_http_send "GET /health HTTP/1.1\r\nHost: app.example.com\r\n\r\n";
  check_http_expect_alive http_2xx;
}

server {
  listen 80;
  server_name app.example.com;

  location / {
    proxy_pass http://web_app;
    proxy_set_header Host $host;
    proxy_next_upstream error timeout http_502 http_503 http_504;
  }

  location /upstream_status {
    check_status json;
    allow 127.0.0.1;
    allow 10.0.0.0/8;
    deny all;
  }
}

Récap : zone pour les workers ; sonde active toutes les 5 s sur port dédié ; check_status intranet only ; proxy_next_upstream pour basculer hors serveur down.

Conclusion

Après l’incident Double 11 : zone, max_fails/fail_timeout, puis nginx_upstream_check_module. Panne détectée et retirée en ~5 s — peu d’erreurs côté client.

Stratégies : sans état → round-robin ou least_conn ; avec état → ip_hash ou hash. Santé : obligatoire en prod ; module open source + accès verrouillé sur check_status.

Encore en round-robin pur sans santé ? Commencez par le passif (max_fails + fail_timeout) — changement minimal, effet rapide. Puis activez l’actif après validation en test.

FAQ

À quoi sert la directive zone dans un upstream Nginx ?
zone crée une zone mémoire partagée pour que les workers partagent l'état des backends. Sans zone, chaque worker garde son propre état : l'un peut marquer un serveur en panne pendant que les autres continuent d'y envoyer du trafic.
Quelle différence entre contrôle de santé passif et actif ?
Le passif observe les succès/échecs des requêtes réelles ; il dépend du trafic utilisateur et détecte tard. L'actif envoie des sondes périodiques, sans attendre un client, retire plus tôt les serveurs défaillants.
nginx_upstream_check_module ou NGINX Plus ?
NGINX Plus offre support officiel et documentation complète ; le module open source est gratuit et ajoute des sondes MySQL/FastCGI (absentes de Plus). Budget serré → module ; stabilité et support officiel → Plus.
round-robin ou least_conn ?
API sans état : round-robin suffit. WebSocket et longues connexions : least_conn suit le nombre de connexions et oriente vers le serveur le moins chargé.
Comment régler rise et fall ?
rise : succès consécutifs avant « healthy » — souvent 2 pour éviter les oscillations au démarrage. fall : échecs avant « down » — 3 ou 5 pour tolérer les pics. Trop sensible → faux positifs.
Pourquoi restreindre l'accès à check_status ?
La page liste les backends et leur état : fuite de topologie interne et indication des cibles affaiblies. Limitez à l'intranet ou aux IP de supervision.

7 min de lecture · Publié le: 27 avr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog