...

NGINX Worker Connections – Skalierung tausender Requests für maximale Hosting Performance

Ich skaliere nginx worker gezielt, um tausende gleichzeitige Requests mit geringer Latenz zu bedienen. Der Schlüssel liegt in einer abgestimmten Kombination aus worker_processes, worker_connections, Dateideskriptoren und Events.

Zentrale Punkte

  • Kapazität = worker_processes × worker_connections, bei Reverse Proxy oft durch Client- und Upstream-Verbindung verdoppelt.
  • Dateideskriptoren (worker_rlimit_nofile, ulimit) passend zur geplanten Verbindungslast anheben.
  • Events-Block mit epoll, multi_accept und Kernel-Backlogs auf hohe Last trimmen.
  • Monitoring via stub_status und Lasttests für iterative Anpassung.
  • Skalierung vertikal und horizontal kombinieren, Konfiguration entkoppeln.

NGINX-Architektur: Master, Worker und Events

NGINX setzt auf einen Master-Prozess, der mehrere Worker startet und diese effizient mit Events versorgt. Statt Threads pro Request verarbeitet jeder Worker zahlreiche Verbindungen nicht-blockierend über ein eventbasiertes Modell mit geringem Overhead. Die Direktive worker_processes lege ich auf auto, damit NGINX die CPU-Kerne ausnutzt und jede Einheit einen eigenen Worker erhält. So verteile ich eingehende Verbindungen besser und halte die Latenz unter Spitzenlast niedrig. Für tieferen Einstieg in die Planung der Prozesse verweise ich auf Worker-Prozesse optimieren, denn korrekte Parallelisierung bestimmt die machbare Verbindungskapazität. Entscheidend ist, dass worker_connections pro Worker sinnvoll dimensioniert sind, damit die Multiplikation mit den Prozessen die erwartete Spitzenlast deckt.

Kapazitätsformel: worker_processes × worker_connections

Die grobe Kapazität berechne ich mit worker_processes × worker_connections, wobei proxied Requests oft zwei Verbindungen pro Nutzerzugriff belegen und damit die effektive Zahl halbieren können. Viele Standard-Installationen starten mit 512 Verbindungen pro Worker, was für produktive Workloads häufig zu knapp ist. Praxisnahe Startwerte liegen typischerweise bei 1024 bis 4096 und hängen von Trafficprofil und Hardware ab. Ich plane mit Headroom, also mindestens Faktor zwei auf die gemessene Peak-Last, um Bursts sicher abzufedern. Wichtig bleibt die Validierung mit Tests und Live-Metriken, damit Zahlen nicht zu einer theoretischen Spielerei werden.

Szenario worker_processes worker_connections Theoretisch max. Effektiv (Proxy) FD pro Worker
Kleine Site 2 1024 2048 ~1024 ≥1024
API mittlere Last 4 2048 8192 ~4096 ≥2048
Shop Peak-Zeiten 8 4096 32768 ~16384 ≥4096

HTTP/1.1, HTTP/2 und TLS: Auswirkungen auf Worker und Latenz

Protokolle bestimmen das Verbindungsprofil. Mit HTTP/1.1 beobachte ich oft viele gleichzeitige TCP-Verbindungen pro Client, während HTTP/2 diese auf wenige, dafür stärker ausgelastete Streams bündelt. Das spart Dateideskriptoren, verlagert aber Last auf Puffer und Priorisierung. Unter TLS achte ich auf Session-Wiederverwendung, damit teure Handshakes nicht jede Anfrage ausbremsen. Ein gemeinsam genutzter Session-Cache und angemessene Timeouts reduzieren CPU-Spitzen. Zudem setze ich keepalive_requests nicht zu niedrig, damit langlebige Verbindungen ihre Vorteile ausspielen. Für HTTP/2 kalkuliere ich pro Connection höhere Concurrency und sorge für ausreichend große Sende-/Empfangspuffer, ohne Speicher zu verschwenden. Bei gemischtem Traffic plane ich konservativ und verifiziere die Auswirkungen pro Protokollvariante im Test.

Dateideskriptoren und ulimit richtig setzen

Jede Verbindung braucht mindestens einen Dateideskriptor, bei Reverse Proxy häufig zwei, weshalb niedrige ulimit-Werte harte Grenzen setzen. Ich erhöhe worker_rlimit_nofile so, dass worker_processes × worker_connections realisierbar werden und Reserven für Logs, Sockets und Caches bestehen. Systemweit passe ich limits.conf und fs.file-max an, damit das Betriebssystem die geplante Anzahl offener Dateien erlaubt und nicht vorzeitig bremst. Über ulimit -n sowie Systemd-Parameter (LimitNOFILE) prüfe ich, ob die Konfiguration persistiert und zu NGINX passt. Wer diese Stellschraube ignoriert, erlebt trotz hoher worker_connections plötzlich abgewiesene Verbindungen und steigende Latenzen.

Events-Block feinjustieren: epoll, multi_accept, Backlogs

Unter Linux nutze ich epoll, da dieser Mechanismus große Verbindungszahlen effizient über asynchrone Events handhabt. Mit multi_accept on akzeptiert ein Worker mehrere neue Verbindungen pro Ereignis, was Lastspitzen glättet und Annahmeverzögerungen senkt. Kernel-Parameter wie net.core.somaxconn und net.ipv4.tcp_max_syn_backlog erhöhe ich passend, damit Accept-Queues in Stürmen nicht überlaufen. TIME_WAIT-Optimierungen wie tcp_tw_reuse mindern Port-Engpässe und halten die Durchsatzkurve hoch. Für tiefergehende Hintergründe zu Nebenläufigkeit und Warteschlangen lohnt sich ein Blick auf Threadpool-Optimierung, auch wenn NGINX primär ereignisgesteuert arbeitet und dadurch sehr sparsam skaliert.

Listen-Sockets richtig verteilen: reuseport, backlog und accept_mutex

Bei sehr vielen gleichzeitigen Verbindungen skaliere ich den Annahmepfad aktiv. Mit reuseport erhält jeder Worker einen eigenen Listening-Socket; damit entfällt die Konkurrenz beim Accept und die Last verteilt sich sauber über alle Kerne. Den Listen-Backlog setze ich explizit, um kurze Stürme abzufangen. Der accept_mutex wird in diesem Setup nicht mehr benötigt. Ohne reuseport kann accept_mutex hingegen helfen, Herdeneffekte beim Accept zu dämpfen. Wichtig: Backlog-Größen in NGINX und Kernel (somaxconn) sollten zusammenpassen, sonst verpufft der Effekt.

events {
    use epoll;
    worker_connections 4096;
    # accept_mutex on;   # mit reuseport meist nicht nötig
}

server {
    listen 443 ssl http2 reuseport backlog=65535;
    # ...
}

Zusätzlich pinne ich Worker bei Bedarf an CPU-Kerne (worker_cpu_affinity), damit Cache-Linien und IRQ-Last stabil bleiben. In stark NUMA-geprägten Umgebungen reduziert das unnötige Querverkehr im Speicher.

Reverse Proxy, Upstreams und Keep-Alive

Als Reverse Proxy hält NGINX je Request oft zwei Verbindungen: eine zum Client und eine zum Backend, wodurch Kapazitätsplanung realistisch doppelt zählt. Ich aktiviere Keep-Alive sinnvoll, damit Upstream-Verbindungen wiederverwendbar bleiben und der Overhead pro Anfrage sinkt. So reduziere ich die Last auf PHP-FPM, Applikationsserver oder Microservices und gewinne freie Slots für neue User-Sessions. Die Balance zwischen Timeouts, Leerlaufdauer und Wiederverwendung bestimmt, wie sauber Verbindungen recycelt werden. Wer Grundlagen dazu nachlesen möchte, findet in Persistente Verbindungen praxisnahe Hinweise zur Auslastung und besseren Netzwerk-Nutzung.

Upstream-Pools, Timeouts und Wiederholversuche

Damit Worker nicht auf lahme Backends warten, pflege ich schlanke Timeouts und wohldosierte Retries. Upstream-Keepalive-Pools halte ich ausreichend groß, damit Verbindungen warm bleiben, aber nicht so groß, dass inaktive FDs Speicher und Slots binden. Ich begrenze Retries auf wenige Versuche und schalte nur bei klaren Transportfehlern um – so verhindere ich Thundering-Herd-Effekte bei kurzen Backend-Aussetzern.

upstream app_backend {
    server 10.0.0.11:8080 max_fails=3 fail_timeout=10s;
    server 10.0.0.12:8080 max_fails=3 fail_timeout=10s;
    keepalive 64;  # wiederverwendbare Upstream-Verbindungen
}

server {
    location / {
        proxy_pass              http://app_backend;
        proxy_connect_timeout   2s;
        proxy_read_timeout      15s;
        proxy_send_timeout      15s;
        proxy_next_upstream     error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}

Gleichzeitig reguliere ich Keep-Alive-Parameter (Timeouts, Requests pro Verbindung), um Ressourcen von selten aktiven Clients zügig wieder freizugeben.

Skalierung sinnvoll planen: vertikal und horizontal kombinieren

Für hohe Trafficwerte ziehe ich vertikale und horizontale Skalierung gemeinsam in Betracht. Vertikal skaliere ich durch mehr CPU-Kerne, RAM, schnelle SSDs und optimierte Netzwerkkonfiguration, damit jeder Worker flüssig arbeitet. Horizontal erweitere ich mit stateless NGINX-Knoten, zentral verwalteter Konfiguration und verteiltem Logging, damit die Gesamtkapazität linear wächst. Caches vor Ort und wohldefinierte Policies per Maps oder API machen Änderungen schnell ausrollbar. Diese Trennung reduziert Seiteneffekte und hilft, neue Trafficmuster ohne Umbauten an jedem Node zu bedienen.

Hosting-Perspektive: Latenz, Fehlerraten und Erlebnis

Zu knappe worker_connections führen zu abgewiesenen Verbindungen, Timeouts und schlechter User-Experience. Dynamische Anwendungen wie CMS oder Shops spüren das sofort, weil ein Seitenaufruf mehrere Backend-Anfragen erzeugt und Slots schneller knapp werden. Deshalb starte ich mit moderaten Werten wie 1024 oder 2048 pro Worker und erhöhe diese schrittweise anhand echter Messwerte. Parallel halte ich die Upstream-Dienste leistungsfähig und sorge für ausreichend Dateideskriptoren, damit keine künstlichen Limits greifen. In Benchmarks zeigt sich, dass sorgfältig abgestimmte Plattformen hier echte Vorteile liefern und Spitzenverkehr verlässlich abfangen.

Speicher, Buffering und I/O-Pfade

Jede Connection belegt Arbeitsspeicher für Metadaten und Puffer. Ich dimensioniere proxy_buffers, client_body_buffer_size und large_client_header_buffers so, dass typische Requests passen, ohne bei Ausreißern pauschal zu viel RAM zu binden. Für statische Inhalte beschleunigen sendfile und tcp_nopush die Auslieferung, während tcp_nodelay für Latenz-kritische kleine Antworten wichtig bleibt. Liegen Assets auf langsamerem Storage, hilft aio threads plus thread_pool, Blocking-Effekte abzufedern. Mit open_file_cache reduziere ich Dateizugriffe und stat()-Aufrufe, beachte aber den zusätzlichen FD-Bedarf. Logs schreibe ich gepuffert (access_log … buffer=… flush=…), damit I/O-Spitzen die Antwortzeiten nicht beeinflussen.

Sicherheit und TLS-Performance im Gleichgewicht

TLS-Handshakes sind CPU-intensiv. Ich kombiniere Session-Wiederverwendung mit moderaten Schlüsselparametern und aktiviere stapelbare Optimierungen wie Session-Caches und Tickets, sofern sie betrieblich passen. Der Sweet-Spot aus Sicherheit und Performance hält Latenzen stabil, ohne an Cipher-Qualität zu sparen. Unter höherer Last beobachte ich 95./99.-Perzentile gesondert, da TLS-Spitzen sich sonst hinter Durchschnittswerten verstecken. HTTP/2 entlastet Verbindungszahlen, verlangt aber Sorgfalt bei Flusskontrolle und Header-Kompression, um CPU- und Speicherprofile im Griff zu behalten.

Resilienz unter Überlast: Limits und sanftes Abwerfen

Um Latenz zu wahren, ist gezieltes Shaping unter Peak-Last unverzichtbar. Mit limit_conn begrenze ich parallele Verbindungen pro Schlüssel (z. B. IP oder Session), limit_req drosselt Burst-Last und schützt Backends vor synchronen Stürmen. Kritische Endpunkte isoliere ich mit strengeren Regeln als statische Assets. Fällt Last kurzfristig an, liefere ich wohldefinierte 429/503 mit Retry-After, statt alle Anfragen gleichmäßig verhungern zu lassen. lingernde Verbindungen (lingering_close) halte ich an, um Ressourcen kontrolliert freizugeben und Slowloris-Muster zu entkräften. Dieses aktive Sheddern hält die p95/p99-Latenz im grünen Bereich, selbst wenn die Gesamtnachfrage zeitweise über der Nennkapazität liegt.

Container- und Systemintegration: Limits dort heben, wo sie entstehen

In Containern gelten oft engere Grenzen. Ich prüfe cgroup-Limits (CPU, RAM), setze ulimit -n innerhalb des Containers passend und verankere LimitNOFILE in der Service-Definition. sysctl-Parameter wie somaxconn und tcp_max_syn_backlog müssen auf dem Host wirksam werden; Namespaces isolieren diese Einstellungen nicht immer transparent. Auf orchestrierten Plattformen plane ich Kapazität pro Pod/Node, pinne Worker an zugewiesene Kerne und achte auf stabile Netzpfade (z. B. keine unnötigen NAT-Hops), damit die Latenzkurve ruhig bleibt. Rolling-Updates begleite ich mit worker_shutdown_timeout, damit bestehende Verbindungen sauber auslaufen.

Monitoring und iterative Optimierung

Ohne Sichtbarkeit bleiben Tuning-Schritte ein Risiko. Ich aktiviere stub_status oder Alternativen, um aktive Verbindungen, Annahmeraten und Abweisungen kontinuierlich zu beobachten. In Lasttests simuliere ich realistische Zugriffsmuster und identifiziere Bottlenecks bei Accept-Queues, Upstream-Latenzen oder CPU-Sättigung. Danach passe ich worker_connections, Prozesse, File-Limits und TCP-Parameter behutsam an und verifiziere die Wirkung erneut. Dieser Kreislauf hält die Plattform verlässlich und verhindert Überraschungen zu ungünstigen Zeiten.

Beispielkonfiguration und Rechenweg

Angenommen, ich erwarte zu Spitzenzeiten 2000 gleichzeitige in-flight Requests und nutze Reverse Proxy, dann kalkuliere ich grob 4000 Verbindungs-Slots plus Puffer. Läuft NGINX auf vier CPU-Kernen, starte ich etwa mit worker_processes auto und worker_connections 1000 bis 2000 pro Worker. Den File-Descriptor-Limit setze ich pro Worker ausreichend hoch, damit Verbindungen, Logs und interne Sockets genügend Platz haben. Den Events-Block stelle ich auf epoll, aktiviere multi_accept und erhöhe Kernel-Backlogs passend zu meinem Peak-Traffic. Ein minimalistischer Ausschnitt kann so aussehen, den ich anschließend mit Benchmarks fein abstimme:

worker_processes  auto;
worker_rlimit_nofile  65535;

events {
    use epoll;
    worker_connections  2048;
    multi_accept        on;
}

http {
    keepalive_timeout   65;
    sendfile            on;
    # weitere Proxy-/Cache-Optionen ...
}

Erweitert ergänze ich Listen- und Upstream-Optimierungen, um Annahme- und Backendpfad unter Last abzurunden:

events {
    use epoll;
    worker_connections 4096;
    # worker_cpu_affinity auto;  # bei Bedarf Kerne fest zuweisen
}

http {
    # TLS-/Session-Optimierungen exemplarisch
    ssl_session_cache    shared:SSL:50m;
    ssl_session_timeout  1h;

    upstream app_backend {
        server 10.0.0.11:8080;
        server 10.0.0.12:8080;
        keepalive 64;
    }

    server {
        listen 443 ssl http2 reuseport backlog=65535;

        location / {
            proxy_pass                http://app_backend;
            proxy_connect_timeout     2s;
            proxy_read_timeout        15s;
            proxy_next_upstream       error timeout http_502 http_503;
            proxy_next_upstream_tries 2;
        }
    }
}

Kurz zusammengefasst: Handfeste Richtwerte

Ich passe worker_processes an die CPU-Kerne an und starte worker_connections typischerweise zwischen 1024 und 4096. Bei Reverse Proxy plane ich zwei Verbindungen pro Request und halte mindestens den doppelten Headroom zur gemessenen Peak-Last. worker_rlimit_nofile sowie systemweite Limits setze ich hoch genug, damit die Zahlen aus der nginx.conf real nutzbar bleiben. Den Events-Block trimme ich auf epoll und multi_accept, während Kernel-Backlogs kurze Traffic-Stürme abfedern. Mit Monitoring und schrittweisen Anpassungen forme ich daraus eine zuverlässige Traffic-Engine, die wachsende Zugriffszahlen sauber trägt.

Aktuelle Artikel