Design wechseln

Nginx Lastverteilung in der Praxis: upstream-Konfiguration und Health Checks

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

Das Handy vibriert ununterbrochen. Im Monitoring leuchtet backend1 rot – ein App-Server ist ausgefallen.

Beim großen November-Sale hatten wir nur zwei Backend-Server. In der Nginx-Konfiguration stand upstream backend { server backend1; server backend2; } – symmetrisch, aber irreführend. backend1 trug etwa 70 % des Traffics, weil es in der Datei an erster Stelle stand und der Default round-robin ohne Gewichtung und ohne Health Check lief.

Als backend1 ausfiel, gingen Bestellanfragen weiter an den toten Server. Nginx wusste nichts vom Ausfall und leitete weiter. Nutzer sahen 500-Fehler. Erst nach 15 Minuten manuellem Entfernen aus dem upstream war Ruhe.

Danach haben wir nachgerüstet: Nginx upstream ist mehr als eine Serverliste. Gewichtung, Health Checks und Failover gehören in Produktion dazu. Dieser Artikel fasst unsere Fehler und die spätere Konfiguration zusammen – inklusive aktiver Health Checks mit Open-Source-Nginx.

1. upstream-Grundlagen: vom Einzelserver zum Cluster

Der upstream-Block bündelt mehrere Server zu einer logischen Gruppe. Die verfügbaren Parameter sind reichhaltiger, als viele annehmen.

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;
}

Zeile für Zeile:

zone backend 64k: Shared-Memory-Bereich. Worker-Prozesse teilen Backend-Status (wer erreichbar ist, wer ausgefallen). 64k ist ein Startwert; bei vielen Servern vergrößern. Ohne zone verwaltet jeder Worker seinen eigenen Status – ein häufiger Fehler.

weight=3: Gewichtung. backend1 hat 3, backend2 standardmäßig 1. Von vier Anfragen gehen drei an backend1, eine an backend2. Sinnvoll bei unterschiedlicher Hardware – z. B. backend1 mit 8 Kernen/16 GB, backend2 mit 4 Kernen/8 GB.

max_fails=2: Schwellwert für Fehlversuche. Innerhalb des fail_timeout-Fensters markiert Nginx den Server nach zwei Fehlschlägen als ungesund. Default 1 ist zu empfindlich – ein Netzwerk-Hiccup reicht. In Produktion 2 oder 3 setzen.

fail_timeout=30s: Doppelte Bedeutung. Erstens das Zeitfenster für Fehlzählung; zweitens die Wartezeit, bis Nginx erneut verbindet, nachdem der Server als down markiert wurde. Default 10s reicht für langsam startende Dienste oft nicht.

backup: Reserve-Server. Er wird erst genutzt, wenn alle primären Server ausfallen – z. B. schwächerer Server als Fallback.

Mit down markieren Sie einen Server manuell offline, typisch für Wartung:

server backend3.example.com down;  # vorübergehend offline

In der Praxis wird zone oft vergessen. Dann sieht ein Worker einen Ausfall, andere senden weiter – mit zone ist der Status synchron.

2. Fünf Lastverteilungsstrategien: wann welche?

Reicht round-robin immer? Kommt darauf an.

Viele Projekte laufen jahrelang mit Default-Round-Robin ohne Probleme. Bei WebSocket-Long-Living-Connections, Warenkorb-Sessions oder Cache-Penetration stößt die Default-Strategie an Grenzen. Orientierung:

SzenarioEmpfohlene StrategieBegründung
Zustandslose APIround-robinGleichmäßige Verteilung, keine Besonderheiten
WebSocket-Dienstleast_connVerbindungszahl in Echtzeit, keine Überlastung einzelner Knoten
E-Commerce-Warenkorbip_hashGleicher Nutzer immer auf demselben Server
Cache-Proxyhash key=$uriWeniger Cache-Invalidierung und -Penetration
TestumgebungrandomSchnelle Validierung, einfache Konfiguration

round-robin (Standard)

Ohne Extra-Konfiguration rotiert Nginx sequenziell:

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

Für zustandslose Dienste. Jede Anfrage steht für sich – typisch für REST-APIs.

least_conn (wenigste Verbindungen)

Anfragen gehen an den Server mit den wenigsten aktiven Verbindungen:

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

Klassisch für WebSocket: Eine Long-Living-Connection pro Nutzer, stark schwankende Verbindungszahlen. Round-robin kann einen Knoten mit vielen offenen Verbindungen weiter belasten; least_conn wählt den entlastetsten Server.

ip_hash (IP-Hash)

Hash aus Client-IP – gleiche IP landet immer auf demselben Server:

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

Für Session-Konsistenz. Warenkorb auf cart1, nächste Anfrage auf cart2 ohne verteilte Session – Daten weg. ip_hash löst das.

Einschränkung: Fällt ein Server aus, werden betroffene IPs neu gemappt – Sessions gehen verloren. Daher nur bei unkritischen Sessions oder mit Shared-Session-Storage.

hash (Konsistenter Hash)

Eigener Hash-Key, optional konsistenter Hash:

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

Ideal für Cache-Proxies. $uri als Key; consistent sorgt dafür, dass beim Hinzufügen/Entfernen von Servern nur ein Teil der Keys umzieht – Cache-Trefferquote bleibt stabil.

random

Zufällige Verteilung:

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

Für Tests ausreichend. In Produktion wegen fehlender Steuerung unüblich.

Praxis: Die meisten Web-Apps kommen mit round-robin oder least_conn aus. ip_hash und hash sind Speziallösungen – nicht der Einsatz um des Einsatzes willen.

3. Passive Health Checks: max_fails und fail_timeout

„Passiv“ heißt: Nginx sendet keine Sondierungen, sondern wertet echte Anfragen aus – wie indirektes Beobachten statt aktiv nachfragen.

max_fails und fail_timeout konfigurieren diesen Mechanismus:

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 definiert Fehler: error (Verbindung), timeout, HTTP 500–504. Nginx leitet weiter und zählt einen Fehler für den aktuellen Server.

Drei Fehler in 30 Sekunden → Server als down. 30 Sekunden lang keine Anfragen dorthin, dann ein erneuter Versuch.

Nachteil: langsame Reaktion. Erst nach drei echten Nutzerfehlern wird abgeschaltet – drei Nutzer sehen bereits Fehler.

Extremfall: Server startet noch; max_fails=1 kann ihn sofort ausschließen und dauerhaft draußen halten.

Empfehlung:

  • max_fails auf 2 oder 3 – kurze Netzstörungen tolerieren
  • fail_timeout mindestens 30s – Erholung ermöglichen
  • proxy_next_upstream vollständig konfigurieren

Vorteil: einfach, in Open-Source-Nginx enthalten. Nachteil: Nutzertraffic als Trigger. Für schnellere Erkennung: aktive Health Checks.

4. Aktive Health Checks: NGINX Plus und Open-Source-Alternativen

Aktive Checks: Nginx sendet regelmäßig Sondierungen (z. B. GET /health) und entfernt fehlerhafte Server, bevor Nutzer betroffen sind.

Offiziell: NGINX Plus – etwa 3.675 USD/Instanz/Jahr. Zehn Instanzen: 36.750 USD/Jahr. Für viele Teams teuer.

Open Source: nginx_upstream_check_module (Taobao-Team). Nginx muss mit Modul kompiliert werden, Funktionsumfang ist stark:

MerkmalNGINX Plusnginx_upstream_check_module
Preis3.675 USD/JahrOpen Source, kostenlos
HTTP-CheckJaJa
TCP-CheckJaJa
MySQL-CheckNeinJa
FastCGI-CheckNeinJa
StatusseiteJaJa (check_status)

MySQL- und FastCGI-Checks fehlen in NGINX Plus – für PHP-FPM- oder MySQL-Backends oft die bessere Wahl.

Beispiel nginx_upstream_check_module

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;
  }
}

Parameter:

  • interval=3000: Sondierung alle 3 Sekunden
  • rise=2: zwei Erfolge in Folge → gesund (Startphase abfedern)
  • fall=5: fünf Fehler in Folge → ungesund (kurze Timeouts tolerieren)
  • timeout=1000: Timeout 1 Sekunde
  • type=http: HTTP-Sondierung (auch tcp, ssl_hello, mysql, ajp, fastcgi)

check_http_send: Inhalt der Sondierung – hier GET /health. Backend muss /health mit 200 oder 3xx beantworten.

check_http_expect_alive: Gesunde Statuscodes – http_2xx und http_3xx.

check_status: Monitoring-Endpoint; json für Prometheus oder Zabbix. allow/deny ist Pflicht – nicht öffentlich exponieren.

Modul-Installation

Kompilierung nötig:

# Modul-Quellcode
git clone https://github.com/yaoweibin/nginx_upstream_check_module.git

# Nginx-Quellcode
wget http://nginx.org/download/nginx-1.24.0.tar.gz
tar -zxvf nginx-1.24.0.tar.gz

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

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

Mit Docker: eigenes Image bauen oder Community-Images nutzen.

5. Produktion: Sicherheit und Monitoring

Drei Prinzipien, die oft übersehen werden:

Sicherheit: drei Punkte

1. check_status absichern

Die Seite verrät Backend-Liste und Gesundheitszustand – Topologie und Schwachstellen für Angreifer.

location /upstream_status {
  check_status json;
  allow 127.0.0.1;       # lokal
  allow 10.0.0.0/8;      # internes Netz
  deny all;
}

Strenger: nur definierte Monitoring-IPs.

2. Separater Health-Check-Port

Sondierungen alle 3–5 Sekunden auf dem Business-Port füllen Logs mit /health-Einträgen.

Zwei Ports: Business (z. B. 8080) und Health (z. B. 8888) – letzterer nur Statuscode, kein Business, idealerweise ohne Logging.

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

port=8888 wählt den Sondierungsport.

3. Keine sensiblen Daten in /health

Nur Statuscode – keine Version, Konfiguration oder Speichernutzung.

# Backend-Beispiel
@app.route('/health')
def health():
    return '', 200   # nur Statuscode

Monitoring: JSON-Ausgabe

check_status unterstützt mehrere Formate; json passt zu Monitoring-Stacks:

curl http://127.0.0.1/upstream_status

Beispielausgabe:

{
  "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 zählt Konfigurationsänderungen. Nach upstream-Reload steigt der Wert – Skripte können prüfen, ob die neue Konfiguration aktiv ist.

Parameter-Tuning

interval nicht unter 3000 ms

Zu hohe Frequenz belastet Backends. 3–5 Sekunden reichen; Fehlererkennung bleibt im Sekundenbereich.

rise und fall ausbalancieren

  • rise zu klein (z. B. 1): Startphase oszilliert zwischen gesund/ungesund
  • fall zu klein (z. B. 1): ein Netz-Hiccup reicht zum Abschalten

Praxiswerte: rise=2, fall=3 oder fall=5.

Vollständige Produktionskonfiguration

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;
  }
}

Diese Konfiguration bietet:

  • zone für Worker-Synchronisation
  • aktive Checks alle 5 Sekunden auf dediziertem Port
  • Statusseite nur im internen Netz
  • proxy_next_upstream für Failover bei Backend-Fehlern

Fazit

Zurück zum November-Vorfall: Wir ergänzten zone, max_fails/fail_timeout und später nginx_upstream_check_module. Bei Serverausfall erkennt Nginx den Fehler in etwa 5 Sekunden – Nutzer sehen kaum noch Fehlerseiten.

Strategie in einem Satz: zustandslos → round-robin oder least_conn; zustandsbehaftet → ip_hash oder hash. Health Checks in Produktion sind Pflicht; Open Source: nginx_upstream_check_module; check_status unbedingt absichern.

Läuft Ihr Nginx noch mit Default-Round-Robin ohne Checks, starten Sie mit passivem Check (max_fails + fail_timeout) – minimaler Aufwand, schneller Effekt. Danach aktiv upgraden. Erst in der Testumgebung validieren, dann Produktion.

FAQ

Wozu dient die zone-Konfiguration in Nginx upstream?
zone legt einen Shared-Memory-Bereich an, in dem mehrere Worker-Prozesse den Backend-Status teilen. Ohne zone pflegt jeder Worker seinen eigenen Status – ein Worker kann einen Server als ausgefallen markieren, während andere weiterhin Anfragen dorthin senden.
Was ist der Unterschied zwischen passivem und aktivem Health Check?
Passive Checks bewerten den Server anhand echter Anfragen – sie brauchen Nutzertraffic und reagieren langsam. Aktive Checks senden regelmäßige Sondierungen unabhängig von Nutzeranfragen, erkennen Ausfälle schneller und nehmen fehlerhafte Server ab, bevor Nutzer betroffen sind.
nginx_upstream_check_module oder NGINX Plus – was ist besser?
Beide haben Stärken: NGINX Plus bietet offiziellen Support und vollständige Dokumentation; nginx_upstream_check_module ist Open Source und unterstützt MySQL-/FastCGI-Checks (NGINX Plus nicht). Bei knappem Budget empfiehlt sich das Open-Source-Modul; bei Bedarf an Stabilität und Herstellersupport NGINX Plus.
Wann round-robin, wann least_conn?
Für zustandslose API-Dienste reicht round-robin. Bei WebSocket und anderen Long-Living-Connections ist least_conn besser: Es beobachtet die Verbindungszahl in Echtzeit und leitet neue Anfragen an den am wenigsten belasteten Server.
Wie setzt man rise und fall bei Health Checks?
rise steuert, wie viele aufeinanderfolgende Erfolge nötig sind, bevor ein Server als gesund gilt – empfohlen: 2, um Schwingungen nach dem Start zu vermeiden. fall legt fest, wie viele Fehlschläge bis zur Markierung als ungesund führen – 3 oder 5 tolerieren kurzzeitige Störungen. Zu empfindliche Werte führen zu Fehlalarmen.
Warum muss die check_status-Seite geschützt werden?
Die Statusseite zeigt Backend-Liste und Gesundheitszustand. Angreifer sehen damit die interne Topologie und erkennen, welche Server gerade ausfallen – ein ideales Angriffsfenster. Zugriff nur aus dem internen Netz oder von definierten Monitoring-Servern erlauben.

7 Min. Lesezeit · Veröffentlicht am: 27. Apr. 2026 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog