Nginx dynamischer Upstream: Echtzeit-Service-Discovery mit Lua

Alarm in der Produktionsumgebung.
Ein Docker-Container wurde neu gestartet. Die IP hat sich geändert. In der nginx.conf steht noch die alte Adresse.
Sie müssen aufstehen, die Konfiguration manuell anpassen und nginx -s reload ausführen. Die QPS-Kurve zuckt kurz, im Monitoring entsteht ein Loch. Mit etwas Glück erholt sich alles in wenigen Sekunden. Ohne Glück kommen Beschwerden von Nutzern.
Das ist mir mehr als einmal passiert. Jedes Mal die gleiche Frage: Kann Nginx Backend-Services nicht selbst entdecken – wie Consul, das bei IP-Wechseln automatisch aktualisiert, ohne dass man mitten in der Nacht die Konfiguration anfassen muss?
OpenResty kann das seit Langem. Mit Lua-Skripten lassen sich Upstream-Konfigurationen zur Laufzeit ändern – ganz ohne Reload. Cloudflare nutzt genau diesen Mechanismus; ihre CDN-Edge-Knoten steuern den Traffic dynamisch darüber.
Dieser Artikel zeigt, wie Sie mit einer Drei-Schichten-Architektur (ngx.balancer + lua-resty-balancer + Health Check) dynamische Upstreams aufbauen, die beiden gängigen Health-Check-Bibliotheken vergleichen und vollständigen Integrationscode für Consul, Nacos und etcd liefern. Nach dem Lesen können Sie das Setup in Produktion deployen.
Warum dynamische Upstreams nötig sind
Die Upstream-Konfiguration von Nginx ist statisch. Die server-Adressen in der nginx.conf werden beim Start einmal geladen – Änderungen erfordern ein Reload.
In Container-Umgebungen ist das mühsam. Docker-Container starten neu, die IP ändert sich. K8s-Pods werden umgeplant, die IP ändert sich ebenfalls. Sie können die nginx.conf nicht bei jedem Vorgang manuell anpassen. Technische Teams haben genau dieses Problem erlebt – von manueller Konfiguration über Template-Rendering bis hin zu Consul-basierter dynamischer Service-Discovery.
Manche schlagen NGINX Plus vor – die kommerzielle Variante unterstützt dynamische Upstreams. Stimmt, aber die Lizenz kostet Zehntausende Dollar pro Jahr, und der Code ist nicht Open Source. Bei Problemen warten Sie auf den Hersteller. Für die meisten Teams ist das keine gute Wahl.
OpenResty bietet einen anderen Weg. Es integriert die LuaJIT-VM in Nginx; mit Lua-Skripten ändern Sie Upstream-Konfigurationen zur Laufzeit – ohne Reload, sogar mitten in der Request-Verarbeitung.
Der Schlüssel ist balancer_by_lua_block. In der Phase, in der Nginx den Upstream-Server wählt, übernehmen Sie mit Lua die Routing-Entscheidung. Backend-IP-Listen können im Shared Memory, in Redis oder in Consul liegen. Fällt ein Backend aus, entfernt Lua es automatisch. Kommt ein neuer Service online, erkennt Lua ihn automatisch.
Typische Einsatzszenarien:
- K8s-Ingress-Gateway: Pod-IPs ändern sich häufig; Nginx als Ingress muss dynamisch reagieren
- Microservice-Canary-Releases: Alte und neue Versionen parallel; Routing nach Header oder Cookie
- Automatisches Aussondern bei Fehlern: Langsame oder ausgefallene Backends werden aktiv erkannt und aus dem Pool entfernt
- Cross-DC-Scheduling: Dynamische Wahl des nächsten Rechenzentrums nach Geo-Lage oder Latenz
Cloudflares CDN-Edge-Knoten basieren auf diesem Mechanismus. Hunderte Knoten weltweit, Millionen Requests pro Sekunde – alles über OpenResty gesteuert. Teile der Implementierung sind auf GitHub einsehbar.
Drei-Schichten-Architektur und Kernkomponenten
Dynamische Upstreams bei OpenResty sind kein Einzeltrick, sondern ein Zusammenspiel dreier Schichten:
┌─────────────────────────────────────────┐
│ Schicht 3: Health Check │
│ lua-resty-healthcheck │
│ - Aktive Prüfung der Backend-Verfügbarkeit │
│ - Upstream-Status im Shared Memory aktualisieren │
└──────────────┬──────────────────────────┘
│ Status-Synchronisation
┌──────────────▼──────────────────────────┐
│ Schicht 2: Load-Balancing-Algorithmus │
│ lua-resty-balancer │
│ - resty.roundrobin (Round Robin) │
│ - resty.chash (Consistent Hash) │
│ - Gesunde Backend-Liste aus Shared Memory │
└──────────────┬──────────────────────────┘
│ Auswahlergebnis
┌──────────────▼──────────────────────────┐
│ Schicht 1: Low-Level-API │
│ ngx.balancer │
│ - set_current_peer(host, port) │
│ - get_last_failure() │
│ - set_more_tries(n) │
│ - Wird in der balancer_by_lua-Phase aufgerufen │
└─────────────────────────────────────────┘
ngx.balancer: Low-Level-API
Diese Schicht liegt am nächsten am Nginx-Kern. Das Modul ngx.balancer stellt drei zentrale APIs bereit:
- set_current_peer(host, port): Legt fest, wohin der aktuelle Request weitergeleitet wird
- get_last_failure(): Liefert Fehlerinformationen des letzten Versuchs (für Retry-Logik)
- set_more_tries(n): Setzt zusätzliche Retry-Versuche
Diese APIs müssen in balancer_by_lua_block aufgerufen werden. In dieser Phase wählt Nginx den Upstream-Server – Ihr Lua-Code übernimmt die Routing-Entscheidung vollständig.
Minimales Beispiel:
upstream backend {
server 0.0.0.1; # Platzhalter – mindestens eine server-Direktive erforderlich
balancer_by_lua_block {
local balancer = require "ngx.balancer"
-- Backend dynamisch wählen
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
}
}
Hinweis: server 0.0.0.1 ist ein Platzhalter. Nginx verlangt mindestens eine server-Direktive im upstream-Block; das tatsächliche Backend wählen Sie per Lua – diese Adresse wird nie angesprochen.
lua-resty-balancer: Load-Balancing-Algorithmen
Direkt mit ngx.balancer zu arbeiten ist mühsam: Round Robin, Hashing und Backend-Listen müssen Sie selbst implementieren. lua-resty-balancer kapselt diese Algorithmen.
Zwei Load Balancer stehen bereit:
- resty.roundrobin: Round Robin – Backends der Reihe nach
- resty.chash: Consistent Hash – gleicher Client landet immer auf demselben Backend (Session-Affinität)
Initialisierung in init_worker_by_lua_block:
init_worker_by_lua_block {
local roundrobin = require "resty.roundrobin"
local chash = require "resty.chash"
-- Backend-Liste (dynamisch aus Consul/Nacos)
local servers = {
{ "192.168.1.10", 8080, weight = 10 },
{ "192.168.1.11", 8080, weight = 5 },
{ "192.168.1.12", 8080, weight = 3 },
}
-- Round-Robin-Load-Balancer erstellen
local rr_upstream = roundrobin:new(servers)
-- In Shared Memory speichern, balancer-Phase liest daraus
local shared_dict = ngx.shared.upstreams
shared_dict:set("backend_rr", rr_upstream)
}
Verwendung in 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")
-- Nächsten Server wählen
local host, port = rr_upstream:select()
local balancer = require "ngx.balancer"
balancer.set_current_peer(host, port)
}
}
Nginx-Phasen im Detail
Nginx verarbeitet Requests in einer festen Phasenfolge. Nur wer die Phasen kennt, platziert Lua-Code richtig:
1. init_by_lua_block → Beim Start des Master-Prozesses
2. init_worker_by_lua → Beim Start jedes Worker-Prozesses
3. ssl_certificate_by_lua → SSL-Handshake
4. set_by_lua → Variablenzuweisung
5. rewrite_by_lua → URL-Rewrite
6. access_by_lua → Zugriffskontrolle
7. balancer_by_lua → Upstream-Server wählen (Kern)
8. header_filter_by_lua → Response-Header
9. body_filter_by_lua → Response-Body
10. log_by_lua → Logging
balancer_by_lua_block liegt in Phase 7. Der Request ist noch nicht weitergeleitet – Sie entscheiden, wohin er geht. Bei Retries (Backend-Fehler) sagt get_last_failure(), warum der letzte Versuch scheiterte – Grundlage für die Wahl eines anderen Backends.
Health-Check-Implementierung im Vergleich
Die dritte Schicht dynamischer Upstreams ist der Health Check. Backends können jederzeit ausfallen – Sie sollten proaktiv prüfen, statt erst bei fehlgeschlagenen Requests zu reagieren.
In der OpenResty-Community gibt es zwei gängige Lösungen: die offizielle lua-resty-upstream-healthcheck und die ausgereiftere lua-resty-healthcheck. Nach eigener Erfahrung empfehle ich Letztere.
lua-resty-upstream-healthcheck: Offizielle Lösung
Vom OpenResty-Team gepflegt. Bietet aktive Checks – im Hintergrund werden periodisch HTTP-Requests an Backends gesendet.
Konfigurationsbeispiel:
-- Shared Memory im http-Block der nginx.conf
lua_shared_dict healthcheck 1m;
-- Health Check in init_worker_by_lua_block starten
init_worker_by_lua_block {
local hc = require "resty.upstream.healthcheck"
local ok, err = hc.spawn_checker{
shm = "healthcheck", -- Name des Shared Memory
upstream = "backend", -- Upstream-Name
type = "http", -- Prüftyp (http oder tcp)
-- Inhalt des Health-Check-Requests
http_req = "GET /health HTTP/1.0\r\nHost: backend\r\n\r\n",
interval = 2000, -- Intervall: 2000 ms (2 Sekunden)
timeout = 1000, -- Timeout pro Probe: 1 Sekunde
fall = 3, -- 3 aufeinanderfolgende Fehler → down
rise = 2, -- 2 aufeinanderfolgende Erfolge → up
valid_statuses = { 200, 302 }, -- Als erfolgreich geltende HTTP-Statuscodes
}
if not ok then
ngx.log(ngx.ERR, "failed to spawn health checker: ", err)
end
}
Alle 2 Sekunden sendet die Bibliothek einen Request an /health jedes Backends. Nach 3 Fehlern in Folge wird der Server als down markiert und vom Load Balancer ausgeschlossen. Nach Wiederherstellung braucht es 2 Erfolge in Folge für up.
Statusdaten liegen im konfigurierten Shared Memory (lua_shared_dict healthcheck). In balancer_by_lua_block können Sie den Status lesen und Backends gezielt ein- oder ausschließen.
lua-resty-healthcheck: Produktionsempfehlung
Die offizielle Bibliothek funktioniert, ist aber begrenzt: nur aktive Checks, keine passiven (basierend auf echten Request-Fehlern). In Randfällen gibt es bekannte Bugs.
lua-resty-healthcheck ist die Community-Variante mit mehr Funktionen:
- Aktive Checks: Periodische HTTP/TCP-Probes
- Passive Checks: Statusanpassung aus Fehlerinfos in
balancer_by_lua_block - Flexiblere Konfiguration: Custom-Logik, Callbacks
- Stabiler: In Apache APISIX und anderen Projekten großflächig erprobt
Konfigurationsbeispiel:
-- Ebenfalls Shared Memory erforderlich
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, -- Alle 2 Sekunden prüfen
successes = 2, -- 2 Erfolge in Folge → up
},
unhealthy = {
interval = 1, -- Nach down häufiger prüfen
tcp_failures = 1, -- TCP-Fehler → sofort down
http_failures = 3, -- 3 HTTP-Fehler → down
},
},
passive = {
healthy = {
successes = 3, -- 3 erfolgreiche echte Requests → up
},
unhealthy = {
tcp_failures = 2, -- 2 TCP-Fehler → down
http_failures = 3, -- 3 HTTP-Fehler → down
},
},
},
})
-- Backends zum Prüfen hinzufügen
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)
}
Passive Checks sind mächtig: Selbst wenn aktive Probes nichts finden, markiert die Bibliothek ein Backend als down, wenn viele echte Requests scheitern – schnellere Reaktion auf plötzliche Ausfälle.
Vergleich der beiden Ansätze
| Kriterium | lua-resty-upstream-healthcheck | lua-resty-healthcheck |
|---|---|---|
| Maintainer | OpenResty offiziell | Community (APISIX-validiert) |
| Aktive Checks | Ja | Ja |
| Passive Checks | Nein | Ja |
| Konfigurationsflexibilität | Gering | Hoch (Callbacks, Custom-Logik) |
| Produktionsstabilität | Mittel (bekannte Bugs) | Hoch (großflächig validiert) |
| Dokumentation | Offiziell | Ausführlich, mit Beispielen |
| Empfehlung | Für Einstieg | Produktionsempfehlung |
Anfangs nutzte ich die offizielle Bibliothek. In Produktion trat ein Problem auf: Ein Backend lieferte HTTP 200, aber der Body enthielt Fehler – intern defekt, äußerlich „gesund“. Die offizielle Bibliothek erkannte das nicht. Mit lua-resty-healthcheck und custom Check-Logik (Body parsen) war das Problem gelöst.
Meine Empfehlung: Direkt lua-resty-healthcheck verwenden. Der Code ist klarer; Apache APISIX baut darauf auf – deren Health-Check-Konfiguration ist eine gute Referenz.
Service-Discovery in der Praxis
Health Checks beantworten: Was passiert, wenn ein Backend ausfällt? Die Vorfrage: Woher kommt die Backend-Liste?
In Container-Umgebungen ändern sich Backend-IPs ständig. Hardcoding in der Konfiguration scheitert. Sie brauchen ein Service-Register, das Nginx mitteilt, welche Services laufen.
Drei gängige Optionen: Consul, Nacos, etcd – mit Integrationscode für jede.
Consul-Integration: Die reifste Lösung
Consul von HashiCorp ist weit verbreitet in Microservice-Architekturen: Registrierung, Health Checks, KV-Storage.
Ansatz für Nginx: Periodisch die Consul-API abfragen und die Liste im Shared Memory aktualisieren.
Vollständiger Code:
-- Shared Memory für Service-Liste
lua_shared_dict upstream_servers 5m;
-- Periodisch Service-Liste von Consul laden
init_worker_by_lua_block {
local timer = require "ngx.timer"
local http = require "resty.http"
local cjson = require "cjson.safe"
-- Consul Service-Discovery-API
local consul_host = "consul.service.consul"
local consul_port = 8500
local service_name = "backend"
-- Funktion zum Aktualisieren der Service-Liste
local function update_upstream(premature)
if premature then return end
local httpc = http.new()
httpc:set_timeout(1000) -- 1 Sekunde Timeout
-- Consul 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
-- Consul-Antwort parsen
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
-- Backend-Liste aufbauen
local servers = {}
for _, svc in ipairs(services) do
-- Consul liefert Address und ServicePort
-- Nur gesunde Services werden zurückgegeben (Consul Health Check)
servers[#servers + 1] = {
svc.ServiceAddress or svc.Address,
svc.ServicePort,
weight = 10 -- Standard-Gewicht
}
end
-- In Shared Memory speichern
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
-- Alle 5 Sekunden aktualisieren
timer.every(5, update_upstream)
-- Beim Start sofort einmal ausführen
update_upstream(false)
}
In balancer_by_lua_block die Daten lesen:
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
-- Service-Liste aus Shared Memory
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)
-- Round-Robin-Load-Balancer
local rr = roundrobin:new(servers)
local host, port = rr:select()
-- Backend setzen
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
}
}
Vorteil: Consul bringt eigene Health Checks mit. Bei der Registrierung konfigurieren Sie HTTP-Checks; die Catalog-API liefert nur gesunde Instanzen – Nginx erhält bereits gefilterte Listen.
Nacos-Integration: Häufig in China
Nacos ist Alibabas Open-Source-Plattform für Service-Discovery und Konfiguration; Spring Cloud Alibaba nutzt es standardmäßig.
Die API ähnelt Consul, das Format weicht leicht ab.
Integrationscode:
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"
-- Nacos-Konfiguration
local nacos_host = "nacos.service.nacos"
local nacos_port = 8848
local namespace_id = "public" -- Nacos-Namespace
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)
-- Nacos Service-Discovery-API
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
-- hosts enthält Instanzliste
local servers = {}
for _, instance in ipairs(data.hosts) do
-- Nur healthy=true verwenden
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)
}
Besonderheit von Nacos: dynamische Gewichtsanpassung. Gewicht einer Instanz in der Konsole ändern – beim nächsten Pull passt Nginx die Traffic-Verteilung an. Ideal für Canary-Releases: neue Version zunächst mit wenig Traffic, schrittweise hochfahren.
etcd-Integration: Leichtgewichtige Option
etcd ist der verteilte KV-Store von CoreOS; Kubernetes speichert damit Cluster-Status. Liegen Registrierungsdaten in etcd, kann Nginx direkt daraus lesen.
Integrationscode:
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"
-- etcd-Konfiguration
local etcd_host = "etcd.service.etcd"
local etcd_port = 2379
-- Key für Service-Registrierung (eigenes Format)
local service_key = "/services/backend"
local function update_from_etcd(premature)
if premature then return end
local httpc = http.new()
httpc:set_timeout(1000)
-- etcd V3 API (POST erforderlich)
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
-- Key-Value-Paare parsen
local servers = {}
for _, kv in ipairs(data.kvs) do
-- kv.value ist Instanzinfo (Base64)
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)
}
Vorteil von etcd: einfach und leicht. Kein vollständiges Service-Discovery-Ökosystem wie bei Consul oder Nacos – Registrierung müssen Sie selbst designen. Wer Kubernetes nutzt, profitiert von der nativen Integration.
Vergleich der drei Optionen
| Kriterium | Consul | Nacos | etcd |
|---|---|---|---|
| Native Health Checks | Ja (HTTP/TCP) | Ja | Nein (selbst bauen) |
| Dynamische Gewichte | Ja | Ja (UI) | Selbst implementieren |
| Spring Cloud | Unterstützt | Standard | Zusatzkonfiguration |
| Konsole | Web-UI | Web-UI (ausgereifter) | Keine (Third-Party) |
| Config Management | Ja (KV) | Ja (mächtiger) | Ja |
| Community (China) | Mittel | Hoch | Hoch (K8s-Ökosystem) |
| Einsatz | Allgemeine Microservices | Spring Cloud Alibaba | K8s-Umgebung |
Meine Wahl: Spring Cloud → Nacos. Kubernetes → etcd. Unabhängige, vollständige Service-Discovery → Consul.
Praxis-Szenarien und Performance-Tuning
Architektur und Service Discovery stehen. Nun typische Szenarien und Optimierung.
Szenario 1: Kubernetes-Ingress-Gateway
Pod-Lebenszyklen in K8s sind kurz. Skalierung, Shrinking und Upgrades erzeugen neue Pods mit neuen IPs. Statische Upstreams sind untauglich.
OpenResty kann Pod-Änderungen dynamisch erfassen:
- Timer in
init_worker_by_lua_block, alle 5 Sekunden K8s-API oder CoreDNS - Pod-IP-Liste des Services parsen
- Shared Memory aktualisieren
balancer_by_lua_blockverteilt Last auf Pod-Liste
K8s-API-Beispiel:
local function watch_k8s_services(premature)
local httpc = http.new()
-- K8s API: Endpoints eines Services (= Pod-IPs)
local url = "https://kubernetes.default/api/v1/namespaces/default/endpoints/backend-service"
-- K8s API braucht Auth – Token aus ServiceAccount
local token_file = "/var/run/secrets/kubernetes.io/serviceaccount/token"
local token = read_file(token_file) -- eigene Hilfsfunktion
local res, err = httpc:request_uri(url, {
headers = {
Authorization = "Bearer " .. token
}
})
if res then
local endpoints = cjson.decode(res.body)
-- endpoints.subsets enthält Adressen und Ports
-- Parsen und in Shared Memory speichern...
end
end
timer.every(5, watch_k8s_services)
In Produktion nutzen viele K8s-Ingress-Controller (NGINX Ingress, Traefik) mit eingebauter Logik. Bei speziellen Anforderungen (Custom-Routing, Canary-Strategien) ist eigener OpenResty-Code flexibler.
Szenario 2: Canary-Release
Neue Service-Version: 90 % alter Traffic, 10 % neue Version. Nach einer Woche schrittweise 50 %, dann 100 %.
OpenResty: Header-Routing plus dynamische Gewichte:
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
-- Alte und neue Version aus Shared Memory
local old_servers = cjson.decode(shared_dict:get("old_version"))
local new_servers = cjson.decode(shared_dict:get("new_version"))
-- Canary: nach Request-Header routen
local version_header = ngx.req.get_headers()["X-Version"]
if version_header == "new" then
-- Erzwingen: neue Version (für Tester)
local host, port = select_random(new_servers)
balancer.set_current_peer(host, port)
else
-- Gewichtet zufällig: 90 % alt, 10 % neu
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
}
}
Gewichte in Shared Memory oder Redis; Ops passt sie per Management-API an, z. B. POST /admin/traffic-weight { "old": 90, "new": 10 }.
Szenario 3: Automatisches Aussondern bei Fehlern
Backend fällt plötzlich aus – Nginx soll schnell reagieren und keinen Traffic mehr dorthin senden.
lua-resty-healthcheck prüft im Hintergrund; nach 3 Fehlern in Folge → down.
In balancer_by_lua_block zuerst Gesundheit prüfen:
balancer_by_lua_block {
local checker = ngx.shared.healthcheck
local servers = get_all_servers() -- aus Service Discovery
-- Ungesunde Backends filtern
local healthy_servers = {}
for _, srv in ipairs(servers) do
local key = srv[1] .. ":" .. srv[2]
if checker:get(key) == "up" then -- Health-Status abfragen
healthy_servers[#healthy_servers + 1] = srv
end
end
if #healthy_servers == 0 then
return ngx.exit(503) -- alle Backends down
end
-- Aus gesunder Liste wählen
local rr = roundrobin:new(healthy_servers)
local host, port = rr:select()
balancer.set_current_peer(host, port)
}
Retry-Logik ist wichtig: Schlägt ein Versuch fehl, anderes Backend probieren statt sofort 500 an den Client.
-- Retry-Anzahl setzen
balancer.set_more_tries(2)
-- Bei Fehler protokollieren und erneut versuchen
local last_failure = balancer.get_last_failure()
if last_failure then
ngx.log(ngx.WARN, "request failed: ", last_failure.type, " to ", last_failure.host)
-- Passiver Health Check zeichnet Fehler auf
-- Nächste Wahl überspringt diesen Server
end
Performance-Tuning
Dynamische Mechanismen haben Overhead: Health-Check-Probes, Remote-API-Abfragen. Falsche Konfiguration bremst die Antwortzeit.
Praxiserfahrung:
-
Probe-Intervall: 2–10 Sekunden empfohlen. Zu kurz kostet Ressourcen, zu lang verzögert Fehlererkennung. Hohe Last: 2 s; niedrige Last: 5 s.
-
Shared-Memory-Größe:
lua_shared_dict healthcheckmindestens 1 MB. Pro Upstream ca. 100 KB. Bei 10 Upstreams: 2 MB sicher. -
Keepalive-Connection-Pool: Keepalive am Backend reduziert Verbindungsaufbau:
upstream backend {
server 0.0.0.1;
keepalive 64; -- 64 Verbindungen im Pool
}
-
Asynchrone Health Checks: ngx.timer läuft asynchron und blockiert Requests nicht. Probes verbrauchen dennoch HTTP-Verbindungen – bei vielen Backends Intervall reduzieren.
-
Status-Cache: Service-Discovery-Ergebnisse 5 Sekunden cachen, häufige Consul/Nacos-Calls vermeiden. 5 s Verzögerung ist meist akzeptabel.
Vollständiges Produktionsbeispiel:
# Shared Memory
lua_shared_dict healthcheck 2m;
lua_shared_dict upstream_servers 5m;
# http-Block
http {
# Connection Pool
keepalive_timeout 60s;
keepalive_requests 100;
init_worker_by_lua_block {
-- Health Check (2 s Intervall)
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 }
}
}
})
-- Service Discovery (5 s Update)
timer.every(5, update_upstream_from_consul)
}
upstream backend {
server 0.0.0.1;
keepalive 64; -- Connection Pool
balancer_by_lua_block {
-- Gesundes Backend wählen
local host, port = select_healthy_backend()
balancer.set_current_peer(host, port)
balancer.set_more_tries(2) -- max. 2 Retries
}
}
}
Diese Konfiguration lief bei uns ein halbes Jahr in Produktion: 5000 Requests/Sekunde, Antwortzeiten stabil unter 50 ms. Entscheidend sind passende Parameter – weder zu aggressiv noch zu konservativ.
Fazit
Dynamische Upstreams in drei Schichten: ngx.balancer liefert die Low-Level-API, lua-resty-balancer kapselt Algorithmen, lua-resty-healthcheck übernimmt Health Checks. Zusammen wählen Sie Backends zur Laufzeit – ohne Nginx-Reload.
Service Discovery: Consul am reifsten, Nacos für Spring Cloud, etcd für K8s. Wählen Sie nach Ihrem Stack, nicht nach einem theoretischen „Best Case“.
Probieren Sie es aus: Starten Sie mit lua-resty-healthcheck. Beobachten Sie Ausfall, automatisches Aussondern, Wiederherstellung, automatisches Zurücknehmen. Wenn der Ablauf sitzt, Service Discovery integrieren. Apache APISIX’ balancer.lua hat nur 400 Zeilen – gute Referenz, nicht bei null anfangen.
Der Kern: Nginx wird lebendig. Statische Konfiguration wird dynamische Wahrnehmung – Nächte mit manuellen Config-Änderungen können vorbei sein.
Nginx dynamischen Upstream implementieren
OpenResty Drei-Schichten-Architektur für dynamische Service-Discovery und Health Checks
⏱️ Estimated time: 120 min
- 1
Step 1: Abhängigkeiten installieren
OpenResty und benötigte Lua-Bibliotheken installieren:
• OpenResty (inkl. ngx.balancer)
• lua-resty-balancer (Load-Balancing-Algorithmen)
• lua-resty-healthcheck (Health Check)
• lua-resty-http (HTTP-Client für Service-Discovery-APIs) - 2
Step 2: Shared Memory konfigurieren
Im http-Block der nginx.conf:
```nginx
lua_shared_dict healthcheck 2m;
lua_shared_dict upstream_servers 5m;
```
• healthcheck: Health-Check-Status (ca. 100 KB pro Upstream)
• upstream_servers: Service-Liste (aus Consul/Nacos/etcd) - 3
Step 3: Health Check implementieren
In init_worker_by_lua_block starten:
```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: alle 2 Sekunden HTTP-Probe
• unhealthy: 3 Fehler in Folge → down - 4
Step 4: Service Discovery integrieren
Eine Option wählen:
• Consul: /v1/catalog/service/{name} API
• Nacos: /nacos/v1/ns/instance/list API
• etcd: /v3/kv/range API
Mit ngx.timer.every alle 5 Sekunden aktualisieren und in Shared Memory speichern. - 5
Step 5: Dynamischen Upstream konfigurieren
balancer_by_lua_block im upstream-Block:
```nginx
upstream backend {
server 0.0.0.1; # Platzhalter
keepalive 64; # Connection Pool
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 ist Platzhalter – echtes Backend wählt Lua
• keepalive 64 hält 64 Verbindungen im Pool
• set_more_tries(2) max. 2 Retries - 6
Step 6: Testen und tunen
In Testumgebung deployen und prüfen:
• Health Check: Backend stoppen – entfernt Nginx es automatisch?
• Service Discovery: Container neu starten – aktualisiert sich die IP?
• Performance: wrk oder ab für QPS und Latenz
• Parameter: Probe-Intervall (2–10 s), Pool-Größe (64–128), Retries (2–3)
FAQ
Was ist der Unterschied zwischen lua-resty-upstream-healthcheck und lua-resty-healthcheck?
Wie aktualisiere ich Upstreams in Nginx dynamisch ohne Reload?
Consul, Nacos oder etcd für Service Discovery?
• Consul: am reifsten, vollständig, für allgemeine Microservices
• Nacos: Standard in Spring Cloud Alibaba, gute Konsole
• etcd: leichtgewichtig, native K8s-Integration
Beeinträchtigt dynamischer Upstream die Performance?
Dynamische Service Discovery in Kubernetes?
Welches Intervall für Health-Check-Probes?
15 Min. Lesezeit · Veröffentlicht am: 7. Mai 2026 · Aktualisiert am: 14. Juli 2026
Nginx Praxisleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
Nginx Lastverteilung in der Praxis: upstream-Konfiguration und Health Checks
Ausführliche Anleitung zur Nginx-upstream-Lastverteilung: Gewichtung, fünf Strategien, passive und aktive Health Checks sowie Sicherheits-Best-Practices für Produktionsumgebungen
Teil 4 von 6
Nächster
Nginx Performance-Tuning in der Praxis: gzip, Cache und Connection-Pool-Konfiguration
Kernkonfigurationen für Nginx Performance-Tuning: gzip-Komprimierung reduziert 60–80 % des Übertragungsvolumens, proxy_cache-Strategien erreichen 95 % Hit-Rate, worker_connections und Connection-Pool steigern die Parallelität um das 3–4-Fache
Teil 6 von 6
Ähnliche Beiträge
Nginx Reverse Proxy: upstream, Buffer und Timeouts im Überblick

Nginx Reverse Proxy: upstream, Buffer und Timeouts im Überblick
Nginx Performance-Tuning: gzip, Cache und Connection-Pool-Konfiguration


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen