...

NGINX Worker Processes optimal konfigurieren für maximale Performance

Ich konfiguriere NGINX Worker so, dass worker_processes, worker_connections und worker_rlimit_nofile exakt zusammenpassen und Epoll im Event-Loop greift. Dadurch nutze ich CPU-Kerne effizient, skaliere gleichzeitige Verbindungen planbar und halte Latenzen bei Lastspitzen niedrig.

Zentrale Punkte

Die folgenden Kernaspekte geben dir sofortige Orientierung für eine belastbare NGINX-Worker-Konfiguration.

  • worker_processes an die Anzahl logischer Kerne koppeln, ideal mit „auto“.
  • worker_connections so setzen, dass reale Spitzen locker abgedeckt werden.
  • rlimit_nofile und OS-Limits passend zur Verbindungsmenge erhöhen.
  • epoll und multi_accept aktivieren, um den Event-Loop effizient zu nutzen.
  • Lasttests fahren und in kleinen Schritten feinjustieren.

NGINX-Architektur: Master und Worker verstehen

Ich trenne die Aufgaben von Master und Worker klar: Der Master lädt Konfigurationen, öffnet Sockets und startet Prozesse, während die Worker im Event-Loop Anfragen verarbeiten. Jeder Worker läuft eigenständig, reagiert auf Ereignisse und kann tausende Verbindungen verwalten, ohne Blockierungen zu erzeugen. Dieses Modell glänzt, wenn ich CPU-Kerne passend belege und den Event-Loop über epoll optimal anspreche. Ich beachte dabei, dass jeder zusätzliche Proxy-Hop Verbindungsressourcen beansprucht, was sich in den Limits widerspiegelt. Wer die Rollen versteht, entscheidet bewusst über Ressourcen und verhindert Engpässe frühzeitig.

Die drei Schlüssel-Direktiven richtig koppeln

Ich betrachte worker_processes, worker_connections und worker_rlimit_nofile nie isoliert, sondern als Einheit. Die Gesamtzahl möglicher Verbindungen ergibt sich aus Worker-Anzahl multipliziert mit Verbindungen je Worker; daraus leite ich Limits für File-Deskriptoren ab. Stimmen diese Stellschrauben nicht überein, stolpere ich über „too many open files“ oder sehe harte Timeouts. Für Hochlast brauche ich eine stimmige Kette: ausreichende Prozesse, großzügige Connections, sauber erhöhte rlimit_nofile und passende OS-Parameter. So verhindere ich, dass ein zu kleines Limit die gesamte Kapazität kastriert.

worker_processes: Anzahl gezielt wählen

Ich setze worker_processes in der Regel auf „auto“, damit NGINX die Anzahl logischer CPU-Kerne erkennt und jeden Kern nutzen kann. Ein Worker pro Kern vermeidet unnötige Kontextwechsel und verteilt Last sauber, was die Reaktionszeit planbar hält. Auf Maschinen mit sehr vielen Kernen teste ich bewusst auch geringere Worker-Zahlen, um Cache-Treffer und Core-Auslastung zu vergleichen. Zeigt die Metrik, dass Kerne überfüllt sind oder TLB-Misses steigen, passe ich die Worker-Zahl schrittweise an. Erst Messen, dann Ändern – so sichere ich mir belastbare Ergebnisse.

worker_connections: Verbindungen planbar erhöhen

Ich wähle die worker_connections abhängig von Zieltraffic und Protokollmix, häufig beginnend bei 2048 oder 4096. Für stark frequentierte APIs ziehe ich 8192 in Betracht, sofern OS-Limits und RAM dazu passen. Jede Erhöhung prüfe ich mit Lasttests, weil offene Verbindungen Speicher binden und Upstream-Verhalten beeinflussen. Wenn SSL-Handshakes oder große Uploads dominieren, orientiere ich mich eher an CPU- und I/O-Profilen, nicht nur an nackten Connection-Zahlen. So stelle ich sicher, dass die pro Worker definierte Kapazität auch real nutzbar bleibt.

worker_rlimit_nofile und OS-Limits synchronisieren

Ich sorge dafür, dass rlimit_nofile mindestens die rechnerische Gesamtkapazität abdeckt und häufig mit Reserve konfiguriert ist. Für Reverse-Proxy-Szenarien kalkuliere ich pro Client-Verbindung einen zweiten Deskriptor zum Upstream ein. Entsprechend setze ich rlimit_nofile gern doppelt so hoch wie die erwarteten gleichzeitigen Verbindungen. Die Kernel- und User-Limits (ulimit -n, fs.file-max) hebe ich so an, dass NGINX die Werte tatsächlich ziehen kann. Treten im Error-Log Meldungen zu offenen Dateien auf, erhöhe ich zügig und beobachte die Latenz unter Last erneut.

Events-Block: epoll und multi_accept wirksam einsetzen

Ich aktiviere im Events-Block epoll und setze multi_accept auf „on“, damit Worker wartende Verbindungen in einem Durchlauf annehmen. Epoll reduziert Overhead bei vielen gleichzeitigen Sockets und harmoniert mit dem nicht blockierenden NGINX-Design. Diese Schalter zahlen sich bei Traffic-Spitzen aus, weil ich die Annahmephase beschleunige und schneller zur eigentlichen Bearbeitung komme. Für Linux ist das mein Standard, den ich nur in seltenen Spezialfällen abändere. Wer tiefer einsteigen möchte, vergleicht das Event-Loop-Modell mit Threadpool vs. Event-Loop und zieht daraus Rückschlüsse für die eigene Umgebung.

CPU-Affinität: Worker an Kerne binden

Ich setze worker_cpu_affinity gezielt ein, wenn Workloads konstant und CPU-gebunden sind. Das Bindungsschema verteile ich über Bitmasken, um Kontextwechsel zu vermeiden und Cache-Lokalität zu fördern. Bei vier Kernen ordne ich die Masken so zu, dass jeder Worker einen eigenen Kern erhält. Anschließend prüfe ich Cache-Miss-Raten, Median-Latenzen und 99er-Perzentile, um den Effekt klar zu sehen. Nähere Erläuterungen zur Affinität und NUMA findest du kompakt auf CPU-Affinity Praxis, was beim Feintuning von Worker-Layouts hilft.

Kapazität planen: Headroom und Lasttests

Ich plane bei Verbindungen einen Puffer ein, der deutlich über den beobachteten Spitzen liegt, damit kurzzeitige Schübe nicht direkt Limits reißen. Verdoppele ich die Peak-Last als Ausgangspunkt, habe ich in vielen Szenarien einen soliden Spielraum. Bei stark schwankendem Traffic strecke ich den Puffer weiter, bis die 99er-Perzentile sauber verlaufen. Danach überprüfe ich Engpässe mit Tools wie wrk oder k6, beobachte Fehlerquoten und schaue mir offene Verbindungen im Status an. Erst wenn die Metriken konsistent sind, erhöhe oder senke ich gezielt einzelne Werte.

Konfiguration und Rechenbeispiele

Ich rechne die Verbindungskapazität aus Worker-Anzahl mal Verbindungen je Worker und setze daraus Limits abgeleitet höher an. Bei vier CPU-Kernen mit auto und 4096 Verbindungen pro Worker lande ich rechnerisch bei 16384 gleichzeitigen Verbindungen. In Proxy-Szenarien setze ich rlimit_nofile eher bei 32768 oder darüber, damit Upstream-Sockets inklusive sind. Für kleine Maschinen mit zwei Kernen genügen häufig 2048 Verbindungen pro Worker, sofern Upload- und TLS-Anteil moderat bleiben. Die folgende Tabelle hilft beim Einordnen der Startwerte:

CPU-Kerne worker_processes worker_connections (Start) Min. rlimit_nofile (Richtwert) Hinweis
2 auto (≈2) 2048 ≥ 4096 Reserve für TLS/Proxy einplanen
4 auto (≈4) 4096 ≥ 16384 Bei Proxy oft Faktor 2 bei FDs
8 auto (≈8) 4096–8192 ≥ 32768 Lasttest entscheidet über Erhöhung
16+ auto, ggf. weniger 8192+ ≥ 65535 Mit Affinität und Reserve testen

NGINX Worker und Upstreams: Szenarien richtig gewichten

Ich unterscheide statische Auslieferung, Reverse-Proxy-Betrieb und API-Gateway-Last, weil sie die Worker-Konfiguration unterschiedlich fordern. Statische Inhalte binden weniger Ressourcen, während TLS, Kompression und Upstream-Verbindungen CPU und FDs stärker beanspruchen. Je größer die SSL-Schlüssel und je mehr Handshakes, desto mehr profitiert die Einstellung „ein Worker pro Kern“. Große Uploads verschieben das Profil zu I/O, wodurch ich eher rlimit_nofile und Netzwerkpuffer in den Blick nehme. Bei spürbaren Wartezeiten auf Annahme oder Backend-Antworten hilft mir der Überblick zu Warteschlangen und Latenz, um Engstellen gezielt zu lösen.

Praxis-Workflow: Schritt für Schritt zum schnelleren Server

Ich starte mit einer Ist-Aufnahme aller relevanten Werte in der nginx.conf, prüfe CPU-Kerne, ulimit und Kernel-Parameter. Danach setze ich worker_processes auf auto, lege worker_connections beispielsweise auf 4096 und erhöhe rlimit_nofile großzugig. Im Events-Block aktiviere ich epoll sowie multi_accept und kontrolliere mit einem Reload die Logs. Es folgen Lasttests unter reproduzierbaren Bedingungen, bei denen ich Antwortzeiten, Fehlerraten und offene Verbindungen beobachte. In der Feinarbeit ändere ich stets nur eine Variable, dokumentiere jeden Schritt und prüfe Auswirkungen in den Metriken.

Hosting-Umgebung: Ressourcen, Kernel, Netzwerk

Ich achte auf ausreichend CPU-Kerne, genügend RAM, schnelle SSDs oder NVMe und einen aktuellen Linux-Kernel. Nur so greifen epoll, moderne TCP-Stacks und sinnvolle Offload-Features zuverlässig. Netzwerk-Parameter wie somaxconn und tcp_max_syn_backlog passe ich zur Zielmenge an Verbindungen an, um Annahme-Warteschlangen kurz zu halten. Ein Anbieter mit belastbarer I/O-Leistung und frei zugänglicher Systemkonfiguration zahlt sich dafür klar aus. In Vergleichen zeigt sich, dass Dienste mit konstanten Ressourcen NGINX-Spielräume deutlich vergrößern.

Keepalive-Strategie: Client- und Upstream-Verbindungen

Ich nutze Keepalive bewusst als Hebel für Kapazität und Latenz. Auf der Client-Seite setze ich keepalive_timeout nicht zu hoch, damit inaktive Sockets nicht unnötig worker_connections blockieren. Werte zwischen 10–30 s liefern mir oft einen guten Kompromiss aus Wiederverwendung und Ressourcenbindung. Mit keepalive_requests begrenze ich die Zahl der Requests pro Verbindung, um Langläufer zu kappen und Speicherdruck zu vermeiden. Auf der Upstream-Seite (Reverse-Proxy) halte ich persistente Verbindungen mit keepalive im upstream-Block warm, sodass Handshakes und TCP-Setup entfallen. Dabei skaliere ich die Zahl pro Backend konservativ zur Backend-Kapazität (max_conns), sonst drehe ich mir Warteschlangen beim Upstream selbst. Wichtig: Jeder Keepalive-Socket zählt als offene Verbindung und braucht FDs; das beziehe ich in rlimit_nofile und meine Headroom-Planung ein.

Listen-Optimierung: reuseport, backlog und Accept-Strategie

Ich verteile die Annahmelast gleichmäßig, indem ich SO_REUSEPORT aktiviere (listen … reuseport). Jeder Worker besitzt damit einen eigenen Accept-Queue, was „thundering herds“ reduziert und Hotspots vermeidet. In Kombination mit multi_accept beschleunige ich die Annahmephase spürbar. Den Listen-backlog (listen … backlog=) und die Kernel-Gegenstücke (somaxconn, tcp_max_syn_backlog) setze ich großzügig, damit Spitzen nicht am Socket-Eingang verpuffen. Die Option deferred verschiebt Accept bis Daten anliegen – bei vielen Kurzlebigen Requests kann das helfen, ansonsten vergleiche ich in Tests. Ob ich accept_mutex brauche, kläre ich im Benchmark: Mit reuseport ist er meist entbehrlich; ohne reuseport kann er die Fairness verbessern, kostet aber Koordination. Hier entscheide ich datengetrieben, nie aus dem Bauch.

Timeouts und Warteschlangen stabil einstellen

Ich setze Timeouts so, dass langsame Clients die Worker nicht verstopfen: client_header_timeout und client_body_timeout halte ich knapp genug, um Hänger zu vermeiden, aber großzügig genug für reale Nutzer. send_timeout schützt vor blockierten Antworten Richtung Client. Im Proxy-Kontext definiere ich proxy_connect_timeout, proxy_read_timeout und proxy_send_timeout stringent, damit hängende Backends nicht das Frontend lähmen. Bei Backends mit begrenzter Parallelität nutze ich queue im upstream-Block mit Timeout, um Spitzen abzufedern und 503 kontrolliert zu signalisieren, statt alle Worker auf wartende Upstream-Sockets zu binden. Zusätzlich stabilisiere ich mit limit_req (Burst/Delay) und limit_conn sensible Pfade, sodass einzelne Clients oder Bots nicht unverhältnismäßig Ressourcen ziehen.

Buffering, sendfile und AIO: I/O-Wege bewusst wählen

Ich setze sendfile für statische Dateien ein und kombiniere es mit tcp_nopush/tcp_nodelay je nach Workload, um Pakete effizient zu bündeln oder interaktive Latenzen zu drücken. Für große Dateien nutze ich directio ab einer Schwelle, damit Cache-Pollution ausbleibt und der Seiten-Cache nicht verdrängt wird. Im Proxy-Betrieb entscheide ich, ob proxy_buffering hilft (schnelle Übergabe an Client, entkoppelte Upstream-Lesung) oder ob ich bei Streaming-Lasten lieber proxy_request_buffering reduziere, um Uploads frühzeitig anzustoßen. Die Größen von proxy_buffers, proxy_buffer_size und large_client_header_buffers steuere ich bewusst, damit Speicherverbrauch pro Verbindung nicht explodiert. Für CPU-schonende Dateizugriffe erwäge ich aio (native oder threads), teste aber gründlich, weil sich Event-Loop- und I/O-Charakteristik gegenseitig beeinflussen.

HTTP/2/HTTP/3 und TLS: Auswirkungen auf Worker-Kapazität

Ich berücksichtige, dass HTTP/2 und HTTP/3 Verbindungsdynamik verändern: Viele Requests laufen als Streams über wenige TCP- bzw. QUIC-Verbindungen. Das senkt Verbindungsanzahl, erhöht aber CPU- und Speicherbedarf pro Verbindung (Multiplexing, Header-Kompression, TLS/QUIC). Meine worker_connections interpretiere ich daher nicht blind als „gleich viele Requests“. Ich beobachte concurrent streams je Verbindung und passe keepalive_timeout und ggf. http2_max_concurrent_streams an. Auf TLS-Seite gewinne ich mit Session-Resumption (Tickets/Cache) und OCSP-Stapling; so spare ich teure Handshakes ein und halte Latenzen niedrig. Die Kehrseite: Längere Keepalives binden FDs und RAM – also plane ich rlimit_nofile und Speicherkontingente mit realistischen Reserven. Für CPU-lastige Ciphers lohnt der Test mit Affinität und moderner Kryptografie-Beschleunigung.

Beobachtbarkeit: Status, Logs und Metriken

Ich schaffe Transparenz mit einem schlanken Status-Endpoint (z. B. stub_status), um aktive Verbindungen, Reading/Writing/Waiting und akzeptierte Requests zu sehen. In Logs halte ich das Rauschen klein: Eine kompakte log_format mit Zeit, Status, Upstream-Zeiten und Bytes reicht für die meisten Analysen. Bei sehr hohem QPS deaktiviere ich das Access-Log selektiv (location-basiert) oder puffere Logs asynchron, damit I/O nicht bremst. Das Error-Log setze ich auf warn oder error und wechsle nur für gezielte Analysen kurzzeitig auf debug. Laufend korreliere ich Latenzen (Median/95./99. Perzentil), offene Verbindungen, Backend-Fehlerquoten und CPU-Last je Worker – daraus leite ich Anpassungen der drei Schlüssel-Direktiven ab und erkenne frühzeitig Sättigungseffekte.

Container und virtuelle Umgebungen: Limits sauber durchreichen

Ich prüfe in Containern die cgroup-Grenzen für CPU, RAM und PIDs und gleiche sie mit NGINX-Settings ab. ulimit -n muss innerhalb des Containers hoch genug sein, sonst verpuffen meine rlimit_nofile-Anpassungen. Bei CPU-Quotas (z. B. 2 vCPUs) setze ich worker_processes entsprechend, damit das Scheduling nicht künstlich drängelt. Netzwerknah profitiere ich in „host“-Netzwerkmodi von geringerer Overhead-Latenz, während Overlays zusätzliche Hops bedeuten. Auf Multi-NUMA-Hosts achte ich auf Affinität und Speichersockel, damit Worker nicht quer über Nodes arbeiten. Gleiches gilt für IRQ- und RPS/XPS-Affinität: Stimmen die Pfade von NIC über IRQ bis zum Worker-Kern, sinken Latenzspitzen messbar.

Verbindungslebenszyklus: Ephemere Ports, TIME_WAIT und Reserves

Ich plane genügend ephemere Ports (ip_local_port_range), wenn NGINX als aktiver Client zu Upstreams agiert. Bei sehr hohem Verbindungsdurchsatz vermeide ich übermäßige Port-Fluktuation mit Upstream-Keepalive, wodurch TIME_WAIT-Stapel schrumpfen. An Kernel-Toggles für „Reuse“ greife ich nur vorsichtig; moderne Stacks optimieren vieles bereits intern. Stabiler ist es, die Verweildauer von Verbindungen über sinnvolle Keepalive- und Timeout-Werte zu steuern und reuseport für faire Verteilung zu nutzen. In der Kapazitätskalkulation berücksichtige ich neben Clients immer die Upstream-Seite – oft sind dort die FDs der eigentliche limitierende Faktor, nicht die Frontdoor.

Graceful Reloads und Deployments ohne Einbrüche

Ich nutze das Master/Worker-Modell für graceful reloads: Der Master lädt neue Configs, alte Worker fahren aus, während neue nahtlos übernehmen. Mit worker_shutdown_timeout gebe ich Requests Zeit, sauber zu enden, ohne Ressourcen zu blockieren. Zero-Downtime-Deployments auf dem Upstream koppel ich mit Health-Checks und proxy_next_upstream-Regeln, damit einzelne fehlernde Backends nicht die globale Latenz hochziehen. Bei Konfig-Änderungen ändere ich stets nur eine Stellschraube und verifiziere Effekte in Logs und Metriken – so vermeide ich Konfusionsfehler und halte die Performance reproduzierbar.

Knappe Zusammenfassung

Ich koppel worker_processes an die Kernanzahl (idealerweise auto), setze worker_connections passend zur Peak-Last und erhöhe rlimit_nofile samt OS-Limits großzügig. Im Events-Block nutze ich epoll und multi_accept, prüfe alles mit reproduzierbaren Lasttests und justiere anschließend in kleinen Schritten nach. Für Proxy-Workloads rechne ich zusätzliche Deskriptoren ein und teste CPU-Affinität, wenn Workloads konstant sind. Ein sauber konfigurierter Stack mit passendem Kernel, schneller I/O und sinnvollen Netzwerkparametern macht den Unterschied. So bringe ich NGINX verlässlich in den Leistungsbereich, den anspruchsvollen Seiten und APIs benötigen.

Aktuelle Artikel