Upstream dynamique Nginx : découverte de services en temps réel avec Lua

Alerte en production.
Un conteneur Docker redémarre. L’IP change. nginx.conf contient encore l’ancienne adresse.
Il faut se lever, modifier la config à la main, lancer nginx -s reload. Le QPS en ligne vacille, la courbe de monitoring creuse un trou. Avec un peu de chance, retour à la normale en quelques secondes. Sinon, les appels des utilisateurs arrivent.
Ça m’est arrivé plus d’une fois. À chaque fois la même question : Nginx ne peut-il pas découvrir les backends tout seul ? Comme Consul — l’IP change, la config se met à jour, sans se lever la nuit pour patcher nginx.conf ?
OpenResty le permet depuis longtemps. Ses scripts Lua modifient la config upstream à l’exécution, sans reload. Cloudflare fonctionne ainsi : leurs nœuds edge CDN s’appuient sur ce mécanisme pour router le trafic dynamiquement.
Cet article explique comment mettre en place un upstream dynamique en trois couches (ngx.balancer + lua-resty-balancer + health check), compare deux bibliothèques de health check courantes, et fournit du code complet pour Consul, Nacos et etcd. À la fin, vous pourrez déployer l’ensemble en production.
Pourquoi un upstream dynamique
La config upstream de Nginx est statique. Les adresses server dans nginx.conf sont chargées au démarrage ; pour les changer ensuite, il faut reload.
En conteneurs, c’est pénible. Un conteneur Docker redémarre, l’IP change. Un Pod K8s est reschedulé, l’IP change aussi. Impossible de modifier nginx.conf à chaque fois. L’équipe technique de zbj.com a vécu ce piège : config manuelle, puis templates, puis Consul — poussés par ce problème.
On vous dira NGINX Plus, la version commerciale avec upstream dynamique. Oui, mais plusieurs dizaines de milliers de dollars par an, code non open source. En cas de bug, vous attendez le correctif officiel. Pour la plupart des équipes, ce n’est pas le bon choix.
OpenResty ouvre une autre voie. Il embarque LuaJIT dans Nginx : des scripts Lua modifient upstream à l’exécution, sans reload, même pendant le traitement des requêtes.
Le levier clé est balancer_by_lua_block. Nginx y choisit le serveur upstream ; votre Lua décide où envoyer la requête. La liste des IP peut vivre en mémoire partagée, Redis ou Consul. Backend down → retrait automatique. Nouveau service → découverte automatique.
Cas d’usage fréquents :
- Passerelle d’entrée K8s : IP de Pod changeantes, Nginx en Ingress doit les suivre
- Déploiement canary microservices : anciennes et nouvelles versions, routage dynamique par en-tête ou cookie
- Retrait automatique des pannes : backend lent ou down, sondes actives et retrait du pool
- Routage inter-datacenter : choix du datacenter le plus proche selon géolocalisation ou latence
Les nœuds edge CDN de Cloudflare reposent sur ce mécanisme. Des centaines de nœuds, des millions de requêtes par seconde, le trafic piloté par OpenResty. Une partie de l’implémentation est open source sur GitHub.
Architecture en trois couches et composants clés
L’upstream dynamique OpenResty repose sur trois couches :
┌─────────────────────────────────────────┐
│ Couche 3 : health check │
│ lua-resty-healthcheck │
│ - sondes actives des backends │
│ - mise à jour de l'état en shm │
└──────────────┬──────────────────────────┘
│ sync d'état
┌──────────────▼──────────────────────────┐
│ Couche 2 : algorithmes de LB │
│ lua-resty-balancer │
│ - resty.roundrobin (round-robin) │
│ - resty.chash (hash consistant) │
│ - lit la liste saine en shm │
└──────────────┬──────────────────────────┘
│ choix
┌──────────────▼──────────────────────────┐
│ Couche 1 : API bas niveau │
│ ngx.balancer │
│ - set_current_peer(host, port) │
│ - get_last_failure() │
│ - set_more_tries(n) │
│ - appelé en phase balancer_by_lua │
└─────────────────────────────────────────┘
ngx.balancer : API bas niveau
Couche la plus proche du noyau Nginx. Le module ngx.balancer expose trois API :
- set_current_peer(host, port) : cible backend pour cette requête
- get_last_failure() : infos d’échec de la dernière tentative (retry)
- set_more_tries(n) : nombre de tentatives supplémentaires
Ces API s’appellent uniquement dans balancer_by_lua_block, phase où Nginx choisit l’upstream — votre Lua prend alors la décision de routage.
Exemple minimal :
upstream backend {
server 0.0.0.1; # adresse placeholder, server obligatoire
balancer_by_lua_block {
local balancer = require "ngx.balancer"
-- choix dynamique du backend
local host = "192.168.1.10"
local port = 8080
local ok, err = balancer.set_current_peer(host, port)
if not ok then
ngx.log(ngx.ERR, "failed to set peer: ", err)
return ngx.exit(500)
end
}
}
Note : server 0.0.0.1 est un placeholder. Nginx exige au moins une directive server dans upstream ; le vrai backend est choisi en Lua, cette adresse n’est jamais contactée.
lua-resty-balancer : algorithmes de répartition
Utiliser ngx.balancer seul est rudimentaire : round-robin, hash et liste des backends à coder vous-même. lua-resty-balancer encapsule ces algorithmes.
Deux équilibreurs :
- resty.roundrobin : round-robin entre les backends
- resty.chash : hash consistant, même client → même backend (affinité de session)
Initialisation dans init_worker_by_lua_block :
init_worker_by_lua_block {
local roundrobin = require "resty.roundrobin"
local chash = require "resty.chash"
-- liste des backends (Consul/Nacos en dynamique)
local servers = {
{ "192.168.1.10", 8080, weight = 10 },
{ "192.168.1.11", 8080, weight = 5 },
{ "192.168.1.12", 8080, weight = 3 },
}
-- équilibreur round-robin
local rr_upstream = roundrobin:new(servers)
-- stockage en shm pour la phase balancer
local shared_dict = ngx.shared.upstreams
shared_dict:set("backend_rr", rr_upstream)
}
Usage dans balancer_by_lua_block :
upstream backend {
server 0.0.0.1;
balancer_by_lua_block {
local shared_dict = ngx.shared.upstreams
local rr_upstream = shared_dict:get("backend_rr")
-- prochain serveur
local host, port = rr_upstream:select()
local balancer = require "ngx.balancer"
balancer.set_current_peer(host, port)
}
}
Phases d’exécution
Nginx suit un ordre strict de phases. Pour placer le Lua au bon endroit :
1. init_by_lua_block → démarrage du master
2. init_worker_by_lua → démarrage de chaque worker
3. ssl_certificate_by_lua → handshake SSL
4. set_by_lua → affectation de variables
5. rewrite_by_lua → réécriture d'URL
6. access_by_lua → contrôle d'accès
7. balancer_by_lua → choix upstream (cœur)
8. header_filter_by_lua → en-têtes de réponse
9. body_filter_by_lua → corps de réponse
10. log_by_lua → journalisation
balancer_by_lua_block est en phase 7. La requête n’est pas encore proxifiée ; vous choisissez la cible. En retry, get_last_failure() indique pourquoi la dernière tentative a échoué, pour en choisir une autre.
Comparaison des implémentations de health check
La dernière couche : le health check. Un backend peut tomber à tout moment ; mieux vaut sonder activement que découvrir passivement à chaque échec.
Deux options courantes : lua-resty-upstream-healthcheck (officiel) et lua-resty-healthcheck (enrichi). Après avoir pris des claques, je recommande fortement le second.
lua-resty-upstream-healthcheck : solution officielle
Bibliothèque maintenue par OpenResty. Checks actifs : requêtes HTTP périodiques en arrière-plan.
Exemple :
-- shm dans le bloc http de nginx.conf
lua_shared_dict healthcheck 1m;
-- lancement en init_worker_by_lua_block
init_worker_by_lua_block {
local hc = require "resty.upstream.healthcheck"
local ok, err = hc.spawn_checker{
shm = "healthcheck", -- nom du shm
upstream = "backend", -- nom upstream
type = "http", -- http ou tcp
-- requête de health check
http_req = "GET /health HTTP/1.0\r\nHost: backend\r\n\r\n",
interval = 2000, -- intervalle : 2000 ms (2 s)
timeout = 1000, -- timeout par sonde : 1 s
fall = 3, -- 3 échecs consécutifs → down
rise = 2, -- 2 succès consécutifs → up
valid_statuses = { 200, 302 }, -- codes HTTP considérés sains
}
if not ok then
ngx.log(ngx.ERR, "failed to spawn health checker: ", err)
end
}
Toutes les 2 s, la lib interroge /health sur chaque backend. Trois échecs consécutifs → marqué down, exclu du LB. Deux succès consécutifs → remis up.
L’état vit dans le shm configuré (lua_shared_dict healthcheck). En balancer_by_lua_block, vous lisez cet état avant de choisir un backend.
lua-resty-healthcheck : recommandé en production
La lib officielle fonctionne, mais manque de checks passifs (ajustement selon les échecs réels) et a des bugs sur certains cas limites.
lua-resty-healthcheck est la version communautaire enrichie :
- Checks actifs : sondes HTTP/TCP planifiées
- Checks passifs : ajustement via les échecs en
balancer_by_lua_block - Config flexible : logique custom, callbacks
- Plus stable : validé à grande échelle (Apache APISIX, etc.)
Exemple :
-- shm requis
lua_shared_dict healthcheck 2m;
init_worker_by_lua_block {
local healthcheck = require "resty.healthcheck"
local checker = healthcheck.new({
name = "backend_checker",
shm_name = "healthcheck",
checks = {
active = {
type = "http",
http_path = "/health",
healthy = {
interval = 2, -- sonde toutes les 2 s
successes = 2, -- 2 succès → up
},
unhealthy = {
interval = 1, -- plus fréquent une fois down
tcp_failures = 1, -- échec TCP → down immédiat
http_failures = 3, -- 3 échecs HTTP → down
},
},
passive = {
healthy = {
successes = 3, -- 3 requêtes OK → up
},
unhealthy = {
tcp_failures = 2, -- 2 échecs TCP → down
http_failures = 3, -- 3 échecs HTTP → down
},
},
},
})
-- backends à surveiller
checker:add_target("192.168.1.10", 8080, "backend", true)
checker:add_target("192.168.1.11", 8080, "backend", true)
checker:add_target("192.168.1.12", 8080, "backend", true)
}
Les checks passifs aident quand les sondes actives ne voient rien mais le trafic réel échoue en masse — retrait plus rapide en incident.
Comparaison des deux approches
| Critère | lua-resty-upstream-healthcheck | lua-resty-healthcheck |
|---|---|---|
| Mainteneur | OpenResty officiel | Communauté (validé APISIX) |
| Check actif | Oui | Oui |
| Check passif | Non | Oui |
| Flexibilité | Faible | Élevée (callbacks, logique custom) |
| Stabilité prod | Moyenne (bugs connus) | Élevée (validation à grande échelle) |
| Documentation | Officielle | Détaillée, avec exemples |
| Recommandation | OK pour débuter | Recommandé en prod |
Au départ j’utilisais la lib officielle. En prod, un backend renvoyait HTTP 200 avec un corps d’erreur (panne interne) — la lib officielle le voyait comme sain. Avec lua-resty-healthcheck, j’ai customisé la logique pour parser le corps ; problème résolu.
Mon conseil : partez directement sur lua-resty-healthcheck. Code plus clair ; APISIX s’en sert — inspirez-vous de sa config health check.
Intégration service discovery en pratique
Le health check répond à « que faire si le backend tombe ? ». Question amont : d’où vient la liste des backends ?
En conteneurs, les IP changent souvent. Pas de liste en dur : un registre de services indique qui tourne.
Trois options courantes : Consul, Nacos, etcd. Code d’intégration pour chacune.
Intégration Consul : la plus mature
Consul (HashiCorp) : enregistrement, health check, KV — très répandu en microservices.
Principe Nginx : timer en arrière-plan qui interroge l’API Consul et met à jour le shm.
Implémentation complète :
-- shm pour la liste de services
lua_shared_dict upstream_servers 5m;
-- pull périodique depuis Consul
init_worker_by_lua_block {
local timer = require "ngx.timer"
local http = require "resty.http"
local cjson = require "cjson.safe"
-- API Consul
local consul_host = "consul.service.consul"
local consul_port = 8500
local service_name = "backend"
local function update_upstream(premature)
if premature then return end
local httpc = http.new()
httpc:set_timeout(1000) -- timeout 1 s
-- Catalog API
local res, err = httpc:request_uri(
"http://" .. consul_host .. ":" .. consul_port ..
"/v1/catalog/service/" .. service_name,
{
method = "GET",
headers = { Accept = "application/json" }
}
)
if not res then
ngx.log(ngx.ERR, "failed to query consul: ", err)
return
end
local services = cjson.decode(res.body)
if not services or #services == 0 then
ngx.log(ngx.WARN, "no backend services found in consul")
return
end
local servers = {}
for _, svc in ipairs(services) do
-- Address et ServicePort ; seuls les services sains (checks Consul)
servers[#servers + 1] = {
svc.ServiceAddress or svc.Address,
svc.ServicePort,
weight = 10 -- poids par défaut
}
end
local shared_dict = ngx.shared.upstream_servers
local packed = cjson.encode(servers)
shared_dict:set("backend_servers", packed)
ngx.log(ngx.INFO, "updated upstream servers: ", #servers, " instances")
end
timer.every(5, update_upstream)
update_upstream(false)
}
Lecture en balancer_by_lua_block :
upstream backend {
server 0.0.0.1;
balancer_by_lua_block {
local cjson = require "cjson.safe"
local roundrobin = require "resty.roundrobin"
local shared_dict = ngx.shared.upstream_servers
local packed = shared_dict:get("backend_servers")
if not packed then
ngx.log(ngx.ERR, "no upstream servers available")
return ngx.exit(503)
end
local servers = cjson.decode(packed)
local rr = roundrobin:new(servers)
local host, port = rr:select()
local balancer = require "ngx.balancer"
local ok, err = balancer.set_current_peer(host, port)
if not ok then
ngx.log(ngx.ERR, "failed to set peer: ", err)
return ngx.exit(500)
end
}
}
Atout Consul : health check natif à l’enregistrement. L’API Catalog ne renvoie que les instances saines — Nginx reçoit une liste déjà filtrée.
Intégration Nacos : courante en Chine
Nacos (Alibaba) : discovery et config ; défaut de Spring Cloud Alibaba.
API proche de Consul, format légèrement différent.
Code d’intégration :
lua_shared_dict upstream_servers 5m;
init_worker_by_lua_block {
local timer = require "ngx.timer"
local http = require "resty.http"
local cjson = require "cjson.safe"
local nacos_host = "nacos.service.nacos"
local nacos_port = 8848
local namespace_id = "public"
local service_name = "backend-service"
local group_name = "DEFAULT_GROUP"
local function update_from_nacos(premature)
if premature then return end
local httpc = http.new()
httpc:set_timeout(2000)
local url = "http://" .. nacos_host .. ":" .. nacos_port ..
"/nacos/v1/ns/instance/list?serviceName=" .. service_name ..
"&groupName=" .. group_name ..
"&namespaceId=" .. namespace_id
local res, err = httpc:request_uri(url, { method = "GET" })
if not res then
ngx.log(ngx.ERR, "failed to query nacos: ", err)
return
end
local data = cjson.decode(res.body)
if not data or not data.hosts then
ngx.log(ngx.WARN, "no instances found in nacos")
return
end
local servers = {}
for _, instance in ipairs(data.hosts) do
if instance.healthy then
servers[#servers + 1] = {
instance.ip,
instance.port,
weight = instance.weight or 10
}
end
end
local shared_dict = ngx.shared.upstream_servers
shared_dict:set("backend_servers", cjson.encode(servers))
ngx.log(ngx.INFO, "updated from nacos: ", #servers, " instances")
end
timer.every(5, update_from_nacos)
update_from_nacos(false)
}
Nacos permet d’ajuster les poids dynamiquement depuis la console ; au prochain pull, Nginx adapte la répartition — utile en canary (nouvelle version à faible trafic, montée progressive).
Intégration etcd : solution légère
etcd (CoreOS) : KV distribué, état de cluster K8s. Si vos enregistrements vivent dans etcd, lecture directe.
Code d’intégration :
lua_shared_dict upstream_servers 5m;
init_worker_by_lua_block {
local timer = require "ngx.timer"
local http = require "resty.http"
local cjson = require "cjson.safe"
local etcd_host = "etcd.service.etcd"
local etcd_port = 2379
local service_key = "/services/backend"
local function update_from_etcd(premature)
if premature then return end
local httpc = http.new()
httpc:set_timeout(1000)
local url = "http://" .. etcd_host .. ":" .. etcd_port .. "/v3/kv/range"
local body = cjson.encode({ key = service_key, range_end = service_key .. "/" })
local res, err = httpc:request_uri(url, {
method = "POST",
body = body,
headers = { ["Content-Type"] = "application/json" }
})
if not res then
ngx.log(ngx.ERR, "failed to query etcd: ", err)
return
end
local data = cjson.decode(res.body)
if not data or not data.kvs then
ngx.log(ngx.WARN, "no services found in etcd")
return
end
local servers = {}
for _, kv in ipairs(data.kvs) do
local value = ngx.decode_base64(kv.value)
local instance = cjson.decode(value)
if instance and instance.healthy then
servers[#servers + 1] = {
instance.host,
instance.port,
weight = instance.weight or 10
}
end
end
local shared_dict = ngx.shared.upstream_servers
shared_dict:set("backend_servers", cjson.encode(servers))
end
timer.every(5, update_from_etcd)
update_from_etcd(false)
}
etcd est simple et léger, sans écosystème discovery complet de Consul/Nacos — il faut concevoir l’enregistrement. Sur K8s, intégration naturelle.
Comparaison des trois approches
| Critère | Consul | Nacos | etcd |
|---|---|---|---|
| Health check natif | Oui (HTTP/TCP) | Oui | Non (à construire) |
| Ajustement des poids | Oui | Oui (UI) | À implémenter |
| Intégration Spring Cloud | Oui | Intégration par défaut | Config supplémentaire |
| Console | Web UI | Web UI (riche) | Non (tiers) |
| Gestion de config | Oui (KV) | Oui (plus complet) | Oui |
| Communauté (Chine) | Moyenne | Forte | Forte (écosystème K8s) |
| Cas d’usage | Microservices généralistes | Spring Cloud Alibaba | Environnement K8s |
Mon choix : Spring Cloud → Nacos. K8s → etcd pratique. Plateforme discovery autonome → Consul le plus mature.
Scénarios pratiques et optimisation des performances
Architecture en place, discovery intégrée. Quelques cas typiques.
Scénario 1 : passerelle d’entrée Kubernetes
Cycle de vie court des Pods : scale, IP changeantes. Un upstream statique ne tient pas.
OpenResty peut suivre les Pods :
- Timer en
init_worker_by_lua_block, toutes les 5 s : API K8s ou CoreDNS - Parser la liste d’IP des Pods du service
- Mettre à jour le shm
balancer_by_lua_blockrépartit sur cette liste
Exemple d’appel API K8s :
local function watch_k8s_services(premature)
local httpc = http.new()
local url = "https://kubernetes.default/api/v1/namespaces/default/endpoints/backend-service"
local token_file = "/var/run/secrets/kubernetes.io/serviceaccount/token"
local token = read_file(token_file)
local res, err = httpc:request_uri(url, {
headers = {
Authorization = "Bearer " .. token
}
})
if res then
local endpoints = cjson.decode(res.body)
-- subsets contient adresses et ports des Pods
-- parser et stocker en shm...
end
end
timer.every(5, watch_k8s_services)
En prod, NGINX Ingress Controller ou Traefik encapsulent souvent cette logique. Pour des règles custom (canary, routage avancé), OpenResty maison reste plus flexible.
Scénario 2 : déploiement canary
Nouvelle version : 90 % trafic ancienne, 10 % nouvelle ; montée progressive sur une semaine.
OpenResty : routage par en-tête + poids dynamiques :
upstream backend {
server 0.0.0.1;
balancer_by_lua_block {
local cjson = require "cjson.safe"
local chash = require "resty.chash"
local shared_dict = ngx.shared.upstream_servers
local old_servers = cjson.decode(shared_dict:get("old_version"))
local new_servers = cjson.decode(shared_dict:get("new_version"))
local version_header = ngx.req.get_headers()["X-Version"]
if version_header == "new" then
local host, port = select_random(new_servers)
balancer.set_current_peer(host, port)
else
local rand = math.random()
if rand < 0.1 then
local host, port = select_random(new_servers)
balancer.set_current_peer(host, port)
else
local host, port = select_random(old_servers)
balancer.set_current_peer(host, port)
end
end
}
}
Les ratios peuvent vivre en shm ou Redis ; une API admin les met à jour : POST /admin/traffic-weight { "old": 90, "new": 10 }.
Scénario 3 : retrait automatique des pannes
Backend down soudainement : Nginx doit arrêter d’y envoyer du trafic.
lua-resty-healthcheck sonde en continu ; trois échecs consécutifs → down.
En balancer_by_lua_block, filtrer par état :
balancer_by_lua_block {
local checker = ngx.shared.healthcheck
local servers = get_all_servers()
local healthy_servers = {}
for _, srv in ipairs(servers) do
local key = srv[1] .. ":" .. srv[2]
if checker:get(key) == "up" then
healthy_servers[#healthy_servers + 1] = srv
end
end
if #healthy_servers == 0 then
return ngx.exit(503)
end
local rr = roundrobin:new(healthy_servers)
local host, port = rr:select()
balancer.set_current_peer(host, port)
}
Retry : si la requête proxifiée échoue, tenter un autre backend plutôt qu’un 500 immédiat.
balancer.set_more_tries(2)
local last_failure = balancer.get_last_failure()
if last_failure then
ngx.log(ngx.WARN, "request failed: ", last_failure.type, " to ", last_failure.host)
-- le check passif enregistre l'échec
-- prochain choix : skip ce serveur
end
Conseils de réglage perf
Coût : sondes health check, appels API discovery. Mal réglé, latence globale impactée.
Retours d’expérience :
-
Intervalle de sonde : 2–10 s. Trop court = charge ; trop long = détection lente. Haut QPS → 2 s ; faible trafic → 5 s.
-
Taille du shm :
lua_shared_dict healthcheck≥ 1 Mo. ~100 Ko par upstream. Dix upstreams → 2 Mo prudent. -
Pool keepalive : réduit le coût de connexion :
upstream backend {
server 0.0.0.1;
keepalive 64;
}
-
Checks asynchrones : ngx.timer n bloque pas les requêtes ; beaucoup de backends → espacer les sondes.
-
Cache d’état : résultat discovery en cache 5 s pour limiter les appels Consul/Nacos — délai acceptable en général.
Exemple de config prod complète :
lua_shared_dict healthcheck 2m;
lua_shared_dict upstream_servers 5m;
http {
keepalive_timeout 60s;
keepalive_requests 100;
init_worker_by_lua_block {
local healthcheck = require "resty.healthcheck"
local checker = healthcheck.new({
shm_name = "healthcheck",
checks = {
active = {
interval = 2,
healthy = { successes = 2 },
unhealthy = { tcp_failures = 1, http_failures = 3 }
},
passive = {
healthy = { successes = 3 },
unhealthy = { tcp_failures = 2, http_failures = 3 }
}
}
})
timer.every(5, update_upstream_from_consul)
}
upstream backend {
server 0.0.0.1;
keepalive 64;
balancer_by_lua_block {
local host, port = select_healthy_backend()
balancer.set_current_peer(host, port)
balancer.set_more_tries(2)
}
}
}
En prod chez nous, six mois à ~5000 req/s, latence stable sous 50 ms. La clé : paramètres équilibrés, ni trop agressifs ni trop laxistes.
Conclusion
L’upstream dynamique en trois couches : ngx.balancer pour la base, lua-resty-balancer pour les algorithmes, lua-resty-healthcheck pour la santé. Assemblés, choix de backend à l’exécution sans reload Nginx.
Discovery : Consul le plus mature, Nacos pour Spring Cloud, etcd pour K8s. Choisissez selon votre stack, pas la « meilleure » solution abstraite.
À tester : commencez par lua-resty-healthcheck. Voyez un backend tomber, être retiré, revenir, être réintégré — puis branchez la discovery. Le balancer.lua d’APISIX fait ~400 lignes : réutilisez-le plutôt que tout réécrire.
L’idée : rendre Nginx « vivant ». Config statique → perception dynamique. Fin des nuits à patcher nginx.conf.
Mettre en place un upstream dynamique Nginx
Architecture OpenResty en trois couches pour discovery dynamique et health check
⏱️ Estimated time: 120 min
- 1
Step 1: Installer les modules requis
Installer OpenResty et les bibliothèques Lua :
• OpenResty (inclut ngx.balancer)
• lua-resty-balancer (algorithmes de LB)
• lua-resty-healthcheck (health check)
• lua-resty-http (client HTTP pour les API discovery) - 2
Step 2: Configurer la mémoire partagée
Dans le bloc http de nginx.conf :
```nginx
lua_shared_dict healthcheck 2m;
lua_shared_dict upstream_servers 5m;
```
• healthcheck : état des checks (~100 Ko par upstream)
• upstream_servers : liste des services (Consul/Nacos/etcd) - 3
Step 3: Implémenter le health check
Lancer en init_worker_by_lua_block :
```lua
local healthcheck = require "resty.healthcheck"
local checker = healthcheck.new({
shm_name = "healthcheck",
checks = {
active = {
type = "http",
http_path = "/health",
interval = 2,
healthy = { successes = 2 },
unhealthy = { http_failures = 3 }
}
}
})
```
• active : sonde HTTP toutes les 2 s
• unhealthy : 3 échecs consécutifs → down - 4
Step 4: Intégrer la discovery
Choisir une option :
• Consul : API /v1/catalog/service/{name}
• Nacos : API /nacos/v1/ns/instance/list
• etcd : API /v3/kv/range
Utiliser ngx.timer.every pour mettre à jour la liste toutes les 5 s en shm. - 5
Step 5: Configurer l'upstream dynamique
Dans le bloc upstream, balancer_by_lua_block :
```nginx
upstream backend {
server 0.0.0.1; # placeholder
keepalive 64;
balancer_by_lua_block {
local servers = get_healthy_servers()
local rr = roundrobin:new(servers)
local host, port = rr:select()
balancer.set_current_peer(host, port)
balancer.set_more_tries(2)
}
}
```
• server 0.0.0.1 : placeholder, backends choisis en Lua
• keepalive 64 : pool de 64 connexions
• set_more_tries(2) : jusqu'à 2 retries - 6
Step 6: Tests et réglage
Valider en environnement de test :
• Health check : arrêter un backend, vérifier le retrait automatique
• Discovery : redémarrer un conteneur, vérifier la mise à jour d'IP
• Perf : wrk ou ab pour QPS et latence
• Paramètres : intervalle de sonde (2–10 s), pool (64–128), retries (2–3)
FAQ
Quelle différence entre lua-resty-upstream-healthcheck et lua-resty-healthcheck ?
Comment mettre à jour upstream dynamiquement sans reload Nginx ?
Consul, Nacos ou etcd pour la discovery ?
• Consul : le plus mature, complet, microservices généralistes
• Nacos : intégration par défaut Spring Cloud Alibaba, console riche
• etcd : léger, natif K8s, adapté aux environnements Kubernetes
L'upstream dynamique impacte-t-il les performances ?
Discovery dynamique dans Kubernetes ?
Quel intervalle pour les sondes de health check ?
15 min de lecture · Publié le: 7 mai 2026 · Mis à jour le: 27 juil. 2026
Guide pratique Nginx
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Équilibrage de charge Nginx en pratique : upstream et contrôles de santé
Guide détaillé de la configuration upstream Nginx : pondération, choix parmi cinq stratégies, contrôles passifs et actifs, et bonnes pratiques de sécurité en production.
Partie 4 sur 6
Suivant
Réglage des performances Nginx en pratique : gzip, cache et pools de connexions
Guide détaillé du réglage Nginx : compression gzip (-60 à -80 % du volume), stratégie proxy_cache (95 % de hits), worker_connections et keepalive upstream pour multiplier la concurrence par 3 à 4.
Partie 6 sur 6



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire