...

File Descriptor Limit Server: Grenzen im Hosting optimieren

Ich zeige, wie das File Descriptor Limit auf dem Server Verbindungen, Dateien und Sockets begrenzt und so die Leistungsfähigkeit bestimmt. Mit klaren Schritten erhöhe ich Limits, messe den Bedarf und verhindere EMFILE-Fehler, bevor Dienste unter Last ausfallen.

Zentrale Punkte

Damit du schnell handeln kannst, fasse ich die wichtigsten Hebel zum Optimieren der FD-Limits kompakt zusammen:

  • Ursache: Jeder Socket, jede Datei, jede DB-Connection verbraucht FDs.
  • Symptome: HTTP 500, EMFILE-Meldungen, blockierte I/O, Dienstabstürze.
  • Messung: ulimit, /proc/limits, file-max und lsof geben Klarheit.
  • Optimierung: Limits in limits.conf, systemd und sysctl gezielt anheben.
  • Sicherheit: Hohe Limits mit Rate-Limiting und Monitoring flankieren.

Was sind File Descriptors und warum Limits zählen

Ein File Descriptor ist ein einfacher ganzzahliger Bezeichner, den der Kernel nutzt, um geöffnete Dateien, Sockets, Pipes oder Geräte per Prozess zu referenzieren. Jeder Prozess hält ein Soft- und ein Hard-Limit, systemweit existiert zusätzlich ein globales Maximum, das alle Prozesse zusammen begrenzt und so Knappheit verhindern soll. Standardmäßig stehen pro Prozess oft nur 1024 FDs bereit, was bei High-Traffic-Websites, API-Gateways oder Chat-Backends schnell eng wird und so Lastspitzen verschärft. Erreicht ein Prozess sein Limit, schlagen neue Verbindungen fehl, Worker können keine Dateien mehr öffnen, und Logs füllen sich mit EMFILE, was die Antwortzeiten verlängert. Besonders kritisch wird es bei Setups, die pro Request mehrere Handles belegen: PHP-FPM, Cache-Backends, Logfiles und Reverse-Proxies akkumulieren FDs, bis die Grenzen blockieren.

Symptome erkennen und messen

Erste Anzeichen eines zu engen Limits siehst du oft als HTTP-500 ohne klare Ursache, zähe Responses oder sporadische Neustarts einzelner Dienste. Typische Logeinträge wie „Too many open files“ deuten auf EMFILE hin und signalisieren unmittelbaren Handlungsbedarf. Ich prüfe zuerst prozessbezogene Grenzen und den aktuellen Verbrauch, um zwischen lokalem Engpass und systemweitem Problem zu unterscheiden und so die Ursache zu präzisieren. Für einen strukturierten Einstieg eignet sich dieser kompakte Server-Ulimits Guide, wenn du die Stellschrauben kurz überblicken willst und Schritte planst. Danach messe ich mit lsof, wie viele Deskriptoren ein Prozess wirklich hält, denn Messwerte schlagen Annahmen, sobald Lastprofile wechseln.

# Soft- und Hard-Limit der aktuellen Shell
ulimit -n
ulimit -Hn

# Limits eines Prozesses prüfen
cat /proc/<pid>/limits | grep "open files"

# Gesamtzustand des Systems
cat /proc/sys/fs/file-nr   # geöffnet | frei | maximum
cat /proc/sys/fs/file-max  # globales Maximum

# Verbrauch pro Prozess grob schätzen
lsof -p <pid> | wc -l

Limits prüfen und Kennzahlen interpretieren

Ich unterscheide strikt zwischen prozessbezogenen und globalen Grenzen, damit ich Engpässe gezielt beseitige statt nur zu verschieben. Das Hard-Limit setzt die Oberkante für Erhöhungen in Sessions, während fs.file-max und fs.nr_open den globalen Rahmen festlegen und somit die Kapazität des Hosts beschreiben. Bewährt hat sich die Faustregel, pro Prozess mindestens 65535 FDs zuzulassen, sofern RAM und Workload dies tragen und du die Last kennst. Parallel achte ich darauf, dass die Summe aktiver Worker, Child-Prozesse und Netzwerkverbindungen in realistischen Hochlastszenarien innerhalb der globalen Rahmenwerte bleibt. Eine klare Sicht auf diese Zahlen verhindert blindes Erhöhen ohne Plan und hält die Systemintegrität unter Druck.

Befehl/Pfad Funktion Worauf achten
ulimit -n / -Hn Soft-/Hard-Limit der aktuellen Session Hard-Limit setzt Obergrenze für Anhebung
/proc/<pid>/limits Per-Prozess-Grenzen und offene Dateien Kritisch bei Daemons wie nginx/php-fpm
/proc/sys/fs/file-max Globales Maximum aller FDs Muss zur Prozesssumme und RAM passen
/proc/sys/fs/file-nr Geöffnet, frei, Maximum in Zahlen Trend bei Lasttests und Peaks prüfen
lsof Zeigt offene Handles Je Worker/Thread verbrauchte FDs messen

FD-Limits temporär und dauerhaft anpassen

Für schnelle Tests setze ich per ulimit ein höheres Soft-Limit, bevor ich dauerhafte Regeln hinterlege und Dienste neu starte. Danach schreibe ich die passenden Einträge in /etc/security/limits.conf, ergänze systemd-Overrides und verifiziere die Änderung mit einem gezielten Lasttest. Wichtig: Der Dienst-User muss stimmen, sonst bleibt die Erhöhung wirkungslos und das Problem taucht unter Last wieder auf. Zusätzlich passe ich das globale Limit an, damit viele Worker-Prozesse das Systemlimit nicht gemeinsam auflaufen lassen. Erst wenn Prozess- und Systemseite zusammenpassen, hält die Konfiguration echten Hochlastsszenarien stand und vermeidet EMFILE.

# Temporär (bis zur Abmeldung/Neustart)
ulimit -n 65535

# Systemweit (bis zum Neustart oder dauerhaft per sysctl.conf)
sudo sysctl -w fs.file-max=2097152

# Dauerhaft (Beispiel für Webserver-User)
echo -e "www-data soft nofile 65535\nwww-data hard nofile 65535\n* soft nofile 65535\n* hard nofile 65535" | sudo tee -a /etc/security/limits.conf

# systemd-Services (z. B. nginx)
sudo mkdir -p /etc/systemd/system/nginx.service.d
cat <<'EOF' | sudo tee /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=65536
EOF
sudo systemctl daemon-reload && sudo systemctl restart nginx

Kernel-Parameter richtig setzen

Ich validiere fs.file-max und fs.nr_open gemeinsam, damit der Kernel genug Puffer für Spitzenlast bereitstellt. Erhöht man nur das per-Prozess-Limit, stößt man sonst am globalen Limit an und verlagert den Engpass auf Systemebene. Sinnvoll ist ein Abstand zwischen typischer Peak-Last und den globalen Werten, sodass Reserven für Wartungsfenster oder Burst-Traffic bestehen und Risikospitzen abgedämpft werden. Details zur systemweiten Abstimmung findest du im Artikel zu Kernel-Tuning, den ich bei tiefgreifenden OS-Anpassungen gern als Checkliste nutze. Nach Änderungen lade ich die Parameter neu, prüfe erneut file-nr und verifiziere, dass alle Dienste mit den neuen Limits neu gestartet wurden und die Werte übernehmen.

# Dauerhafte Kernel-Parameter
sudo bash -c 'cat >> /etc/sysctl.d/99-ulimits.conf <<EOF
fs.file-max = 2097152
fs.nr_open  = 2097152
EOF'
sudo sysctl --system

# Kontrolle
cat /proc/sys/fs/file-max
cat /proc/sys/fs/nr_open || sysctl fs.nr_open

Kapazitätsplanung und Architektur

Eine realistische Kapazitätsplanung startet mit gemessenen Profilen pro Request: Wie viele FDs brauchen Webserver, App-Layer, Datenbank und Cache gemeinsam. Aus diesen Zahlen leite ich pro Host die Summe gleichzeitig geöffneter Handles ab und plane Puffer für Peaks ein. Rechne konservativ: Logrotation, zusätzliche Sockets, temporäre Dateien und Backup-Jobs erhöhen den Bedarf und fressen Reserven. Ich achte darauf, dass horizontale Skalierung mit Load-Balancern die FD-Last pro Knoten senkt, was Ausfalltoleranz und Änderungsfenster vereinfacht. Erst mit klaren Grenzwerten pro Tier kannst du Limits gezielt setzen und Kapazität sinnvoll zwischen Diensten aufteilen.

Webserver- und Datenbank-Feinabstimmung

Bei Webservern halte ich mich an die Regel Threads*4 kleiner als das FD-Limit, damit Reserven für Upstream-Verbindungen, temporäre Dateien und Logs bestehen. Für nginx und Apache beziehe ich Keep-Alive, offene Access- und Error-Logs sowie Upstream-Sockets in die Kalkulation ein und sichere mir so Puffer. Datenbanken wie MariaDB oder PostgreSQL öffnen viele Sockets gegen Applikationen, Replikation und Monitoring; deren Limits müssen zum Connection-Pool und zu Spitzen im Traffic passen. Caches (Redis, Memcached) senken DB-Last, reduzieren aber nicht zwangsläufig die FD-Zahl, sofern viele Clients parallel anfragen und Verbindungen halten. Ich plane deshalb koordinierte Limits entlang der Kette: Frontend, Upstream, DB, Cache und Message-Queues, damit nirgendwo die erste harte Schranke greift.

# Beispiel: nginx systemd-Limit und Worker
LimitNOFILE=65536   # systemd
worker_processes auto;
worker_connections 4096;  # 4096 * Worker <= 65536

# Beispiel: PostgreSQL
max_connections = 1000     # FD-Bedarf ~ 1-2 pro Connection + Dateien/Logs

WordPress und PHP-Stacks effizient halten

WordPress-Instanzen mit vielen Plugins öffnen mehr Dateien, mehr Netzwerkverbindungen und mehr Logs. Ich senke die Zahl unnötiger Includes per OPCache, entlaste Datenbanken mit Redis Object Cache und lagere statische Assets über ein CDN aus, um Dateizugriffe und Verbindungen zu reduzieren. Gleichzeitig erhöhe ich gezielt die Limits für php-fpm und den Webserver, damit Peaks bei Cronjobs, Crawlern oder Shop-Checkouts keine harten Abbrüche erzeugen. Ein sauberer Umgang mit Error-Logs und Rotationen verhindert, dass Logwriter ins Leere laufen und neue Dateien nicht öffnen dürfen. So kombiniere ich Verbrauchsreduktion und Limitanhebung, damit der Stack unter Last leistbar bleibt und Durchsatz hält.

Container und Cloud-Umgebungen

In Docker und Kubernetes erben Prozesse oft die FD-Limits des Nodes, weshalb ich zuerst die Host-Parameter und anschließend die Service-Definitionen prüfe. Für systemd-nspawn oder containerd gelten analoge Prinzipien, doch die Umsetzung erfolgt in Unit-Files, PodSpecs oder Daemon-Konfigurationen mit Overrides. Ich dokumentiere die Limits als Code (IaC) und halte sie über Playbooks konsistent, damit neue Nodes identische Grenzen mitbringen. Bei Kubernetes prüfe ich zusätzlich SecurityContexts und setze erforderliche Capabilities, damit systemseitige Limits greifen. Wichtig bleibt am Ende die Messung im Cluster, denn Scheduling, Autoscaling und Rolling Updates verändern die Verteilung offener Handles und testen deine Puffer.

# Beispiel: systemd in Container-Hosts
cat <<'EOF' | sudo tee /etc/systemd/system/myapp.service.d/limits.conf
[Service]
LimitNOFILE=65536
EOF

# Kubernetes: podSpec (Container-Image muss ulimit respektieren)
# Hinweis: rlimit-Settings sind je nach Runtime/OS unterschiedlich zu setzen

Sicherheit, Rate-Limiting und Monitoring

Hohe Limits geben Luft, doch sie vergrößern die Angriffsfläche bei Floods, daher begrenze ich Anfragen an der Edge mit Rate-Limiting und setze Connection Limits im Webserver. Eine Web Application Firewall und sinnvolle Timeouts verhindern, dass Idle-Verbindungen die FDs dauerhaft binden. Für wiederkehrende Tests nutze ich reproduzierbare Lastprofile und beobachte mit Prometheus, Netdata oder Nagios konsistent die Metriken rund um offene Dateien und Sockets. Je nach Workload korrigiere ich Grenzwerte schrittweise, statt sie sprunghaft zu erhöhen, damit Auswirkungen messbar bleiben und Rückbau leicht fällt. Wer tiefer in Grenzwerte auf Verbindungsseite einsteigen will, findet Orientierung über diesen kompakten Beitrag zu Connection Limits, der mir bei Netzwerkgrenzen als Leitfaden dient.

Fehlersuche bei EMFILE: strukturierter Ablauf

Ich starte mit einem Blick ins Journal und in die Dienst-Logs, um Zeitpunkt und Häufigkeit der Fehler sauber einzugrenzen. Danach prüfe ich mit lsof den Top-Verbrauch pro Prozess und identifiziere Muster wie Leaks, wachsendes Logging oder ungewöhnliche Socket-Typen. Als Nächstes vergleiche ich gesetzte Limits mit realen Peaks und erhöhe zuerst temporär, damit ich Ursache und Wirkung in einem kontrollierten Test validiere und daraus Dauereinstellungen ableite. Findet sich ein Leak, patche oder rolle ich die Komponente zurück, denn höhere Limits kaschieren nur Symptome und verschieben das Problem. Zuletzt dokumentiere ich die Korrektur, setze Alarme und plane einen erneuten Lasttest, damit die Lösung Bestand zeigt und Vertrauen schafft.

Ressourcenkosten hoher FD-Limits realistisch bewerten

Jede offene Datei oder jeder Socket kostet Kernel-Speicher. Plane deshalb den RAM-Fußabdruck mit ein: Pro FD fallen je nach Kernel-Version und Architektur grob einige hundert Bytes bis wenige Kilobyte an (inklusive VFS-/Socket-Strukturen). Bei hunderttausenden FDs summiert sich das. Ich dimensioniere globales file-max so, dass im Worst Case noch ausreichender Pagecache und Arbeitsspeicher für Anwendungen frei bleibt. Ein einfacher Gegencheck erfolgt über vmstat, free und den Trend offener FDs über file-nr während eines Spitzenlasttests. Ziel ist eine Konfiguration, die bei Peak-Last weder in Swap kippt noch übermäßige Reclaim- oder OOM-Aktivität triggert.

Distribution- und Startpfad-Fallen (PAM, systemd, Cron)

Ob Limits greifen, hängt vom Startpfad ab. PAM-basierte Logins (ssh, su, login) lesen /etc/security/limits.conf, systemd-Services hingegen nutzen primär ihre Unit-Parameter (LimitNOFILE) und nicht zwingend PAM. Cron/at können eigene Kontexte haben. Ich validiere daher pro Dienst:

  • Wie startet der Prozess? (systemctl status, ps -ef)
  • Welche Limits sieht er wirklich? (cat /proc/<pid>/limits)
  • Greift PAM? (PAM-Module in /etc/pam.d/* prüfen)
  • Existieren systemweite Defaults? (systemd: DefaultLimitNOFILE in system.conf)

So verhindere ich, dass Anwendungen je nach Startweg unterschiedliche FD-Limits erhalten und unter Last inkonsistent reagieren.

Praxisdimensionierung mit Rechenbeispielen

Ich rechne von den Workern und Verbindungsprofilen aus rückwärts zur nötigen FD-Kapazität:

  • nginx mit 8 Workern à 4000 Verbindungen: ~32000 Verbindungen. Pro aktiver Verbindung reserviert nginx in der Regel 1 FD; plus Upstream (Keep-Alive) und Logs addieren ~10–20% Puffer. Ergebnis: ~38000 FDs allein für nginx.
  • php-fpm mit 150 Children, je Child typische 20–40 FDs (Includes, Sockets, Logs): konservativ 6000 FDs.
  • Redis/DB-Clients: 200 parallele Verbindungen, je 1–2 FDs: ~400 FDs.

Summiert pro Host: ~44k FDs. Ich setze LimitNOFILE für nginx auf 65536, php-fpm analog, und plane global fs.file-max so, dass alle Dienste plus Reserve (x1,5–x2) hineinpassen. Für mehrere stark ausgelastete Instanzen pro Host skaliere global auf 1–2 Millionen FDs, wenn RAM und I/O-Pfade das hergeben.

Tiefere Diagnose: Leaks und Hotspots finden

Wenn FDs kontinuierlich steigen, suche ich mit zielgerichteten Werkzeugen nach der Ursache:

# Offene Handles gruppiert nach Typ
lsof -p <pid> | awk '{print $5}' | sort | uniq -c | sort -nr

# Nur Sockets eines Prozesses
lsof -Pan -p <pid> -i

# Welche Dateien wachsen (Logs, Temp)
lsof +L1            # gelöschte, aber noch geöffnete Dateien
ls -l /proc/<pid>/fd

# Syscall-Sicht: wer öffnet dauernd?
strace -f -p <pid> -e trace=open,openat,close,socket,accept,accept4 -s 0

Besonders tückisch sind gelöschte Logs, die weiter offen sind: Sie belegen Platz und FDs, tauchen aber im Filesystem nicht mehr auf. Neustart oder ein explizites Reopen (z. B. bei nginx via USR1) löst das sauber. Auch falsch konfigurierte Watcher/Exporter können stetig neue Sockets öffnen – hier helfen Rate-Limits und Pooling.

Inotify, epoll und EMFILE sauber abgrenzen

Nicht jede Ressourcengrenze heißt FD-Limit. In Entwicklungs- und CI-Umgebungen schlagen Builds oft mit ENOSPC in Bezug auf Inotify fehl (Watcher-Grenzen). Ich prüfe und setze ergänzend:

# Inotify-Grenzen (nutzerweit und Instanzen)
sysctl fs.inotify.max_user_watches
sysctl fs.inotify.max_user_instances

# Beispielhafte Erhöhung
sudo sysctl -w fs.inotify.max_user_watches=524288
sudo sysctl -w fs.inotify.max_user_instances=1024

Während epoll intern mit FDs arbeitet, ist die eigentliche Engstelle bei massiven Long-Lived-Verbindungen oft das FD-Limit selbst. Deshalb korreliere ich epoll-/Event-Loop-Daten (z. B. aktive Handles) mit file-nr und prozessbezogenem Verbrauch.

Sprach- und Runtime-Besonderheiten (Java, Node.js, Go, Python)

Runtimes gehen unterschiedlich mit FDs um:

  • Java/Netty: Viele NIO-Channels pro Prozess, Logging-Frameworks halten File-Appender offen. Ich setze großzügige Limits und rotiere Logs mit Reopen-Strategie statt Close/Replace.
  • Node.js: EMFILE tritt schnell bei File-System-Heavy Workloads auf (z. B. Watcher, Build-Pipelines). Ich reguliere parallele fs-Operationen, erhöhe Limits und setze Backoff/Retry-Strategien.
  • Go: Hohe Parallelität durch Goroutines kann viele Sockets öffnen. Ich begrenze Dial- und Response-Header-Timeouts und prüfe, ob Verbindungen sauber geschlossen werden (IdleConnTimeout).
  • Python/uWSGI/Gunicorn: Worker/Thread-Modelle verbrauchen FDs für Logs, Sockets und temporäre Dateien; ich harmonisiere Worker-Zahl, Thread-Pools und nofile-Limits.

Gemeinsam ist allen: Ohne koordinierte Logrotation und verlässliches Connection-Management steigen FDs schleichend an.

Container konkret: Docker- und Kubernetes-Settings

Damit Container die gewünschten Limits auch wirklich sehen, setze ich sie konsistent entlang der Kette:

  • Docker-Run: mit –ulimit nofile=65535:65535 starten oder per Daemon-Vorgabe (z. B. Default-Ulimits) setzen.
  • Images: Startskripte sollten kein restriktives ulimit zurücksetzen.
  • Kubernetes: Je nach Runtime (containerd, cri-o) greifen rlimit-Einstellungen unterschiedlich. Ich teste im Pod via cat /proc/self/limits und passe Node-/Runtime-Defaults an, wenn Pod-Spezifika nicht ausreichen.

Besonders in Multi-Tenant-Umgebungen sichere ich die Gesamtsumme gegen fs.file-max ab und isoliere Noisy-Neighbor-Effekte durch getrennte Nodesets oder PodBudgets, damit einzelne Deployments nicht die Host-reservierten FDs aufzehren.

Monitoring-Metriken und Alarme präzisieren

Ich beobachte neben file-nr und file-max auch per-Prozess-FDs und Trendlinien:

  • Systemweit: Allocated vs. Maximum, Rate of Change, Verhältnis Peak/Maximum.
  • Pro Prozess: FDs je Worker/Thread, Top-N Prozesse, Anomalien in der Nacht (Batch/Jobs).
  • Qualitativ: HTTP-Fehlerraten, Queue-Längen, Accept-/Handshake-Fehler synchron zum FD-Trend.

Alerts setze ich mehrstufig: Warnung bei 70–80%, Kritisch ab 90% des konfigurierten Limits, plus Leckage-Erkennung über steigende 7‑Tage-Trends. So reagiere ich rechtzeitig, bevor harte Schranken greifen.

Runbook für den Notfall

Wenn EMFILE akut zuschlägt, handle ich in klaren Schritten:

  1. Top-Verbraucher identifizieren (lsof, /proc/<pid>/fd, Journaleinträge).
  2. Temporär Soft-Limit erhöhen (ulimit in Session oder LimitNOFILE-Override) und Dienst neu starten.
  3. Falls Logs der Grund sind: Rotation stoppen, Reopen triggern, Log-Level reduzieren.
  4. Burst-Traffic an der Edge drosseln (Rate-Limits/Connection-Limits erhöhen oder verschärfen – je nach Lage).
  5. Root Cause fixen (Leak, zu aggressive Parallelität, fehlende Timeouts) und dauerhafte Limits nachziehen.

Wichtig ist die Nachbereitung: Dokumentation, wiederholte Lasttests, und Alarme so schärfen, dass dieselbe Kette frühzeitig erkannt wird.

Kurz zusammengefasst

Mit erhöhten FD-Limits, sauber gesetzten Kernel-Parametern und einer getesteten Architektur verschaffe ich Diensten Spielraum unter Hochlast. Ich messe zuerst, setze dann angemessene Grenzwerte, verifiziere mit Lasttests und sichere das Ergebnis mit Rate-Limiting, Monitoring und klaren Regeln.

Aktuelle Artikel