Soft- und Hard-Limits gezielt für Deployment-Skripte steuern
ulimit steuert, wie viele Dateien, Prozesse und wie viel virtuellen Speicher ein Prozess nutzen darf, mit einem klaren Zusammenspiel aus Soft- und Hard-Limit. Wer die typische Ursache von too-many-open-files-Fehlern in Deployment-Skripten kennt, setzt Limits gezielt für die Skript-Laufzeit oder dauerhaft über systemd, statt Fehler erst in der Produktion zu entdecken.
Inhaltsverzeichnis
- 1. Was ulimit ist: Soft-Limits, Hard-Limits und ihr Zusammenspiel
- 2. ulimit -n: offene Dateideskriptoren und die Ursache von too many open files
- 3. ulimit -u: die maximale Prozessanzahl pro Nutzer begrenzen
- 4. ulimit -v: virtuellen Speicher pro Prozess begrenzen
- 5. Limits nur für die Skript-Laufzeit setzen: das Subshell-Pattern
- 6. Limits dauerhaft setzen: limits.conf im Vergleich zu systemd-Unit-Dateien
- 7. Typische Probleme in Deployment-Skripten durch zu niedrige Limits
- 8. Limits prüfen und diagnostizieren: ulimit -a und /proc/[pid]/limits
- 9. Best Practices für produktive Deployment-Skripte
- 10. Zusammenfassung
- 11. FAQ
1. Was ulimit ist: Soft-Limits, Hard-Limits und ihr Zusammenspiel
ulimit ist ein in Bash eingebautes Kommando, das die vom Kernel pro Prozess durchgesetzten Ressourcengrenzen liest und setzt, etwa die maximale Anzahl offener Dateien oder die maximale Prozessanzahl eines Benutzers. Jede dieser Grenzen existiert in Bash immer als Paar: ein Soft-Limit, das der aktuell wirksame, durchgesetzte Wert ist, und ein Hard-Limit, das die absolute Obergrenze markiert, bis zu der ein unprivilegierter Prozess das Soft-Limit selbst anheben darf.
Ein Prozess ohne besondere Rechte kann sein eigenes Soft-Limit jederzeit senken und bis maximal zum Hard-Limit wieder erhöhen, niemals aber über das Hard-Limit hinaus, und das Hard-Limit selbst kann ohne Root-Rechte nur gesenkt, nie erhöht werden. Dieses Zusammenspiel erklärt, warum ein Deployment-Skript, das versucht, ulimit -n 100000 zu setzen, mit einer Fehlermeldung scheitert, wenn das systemweite Hard-Limit niedriger liegt, selbst wenn das Skript mit vollen Root-Rechten läuft, solange kein systemweites Limit angehoben wurde.
2. ulimit -n: offene Dateideskriptoren und die Ursache von too many open files
ulimit -n begrenzt die Anzahl der Dateideskriptoren, die ein einzelner Prozess gleichzeitig offen halten darf, wobei jede geöffnete Datei, jeder Socket und jede Pipe jeweils einen Deskriptor beansprucht. Überschreitet ein Prozess dieses Limit, verweigert der Kernel jeden weiteren open()- oder socket()-Aufruf mit dem Fehler EMFILE, der sich in Anwendungslogs typischerweise als "Too many open files" zeigt.
In Deployment-Skripten trifft dieser Fehler besonders häufig Webserver oder Datenbankprozesse, die viele gleichzeitige Verbindungen offen halten und dabei an ein zu niedriges, oft aus einer veralteten Distributions-Voreinstellung von 1024 stammendes Soft-Limit stoßen. Die kurzfristige Lösung im Skript selbst ist ulimit -n 65536 vor dem eigentlichen Start des Dienstes, vorausgesetzt das Hard-Limit erlaubt diesen Wert, andernfalls muss zuerst das Hard-Limit systemweit angehoben werden.
# Check current limits before starting the service
echo "Soft limit (open files): $(ulimit -Sn)"
echo "Hard limit (open files): $(ulimit -Hn)"
# Raise the soft limit up to the hard limit for this process
ulimit -n 65536 || {
echo "Konnte Dateideskriptor-Limit nicht erhoehen" >&2
exit 1
}
exec ./my-webserver
3. ulimit -u: die maximale Prozessanzahl pro Nutzer begrenzen
ulimit -u legt fest, wie viele Prozesse und Threads ein Benutzer gleichzeitig starten darf, wobei Threads unter Linux aus Kernel-Sicht als eigene Einträge zählen, was dieses Limit bei stark multithreaded Anwendungen deutlich schneller erreicht, als der reine Prozessname vermuten lässt. Wird das Limit erreicht, schlägt jeder weitere fork()-Aufruf fehl, ein Deployment-Skript kann dann selbst keine weiteren Hintergrundprozesse mehr starten.
Besonders tückisch ist dieses Limit, wenn es unabhängig für einen Service-Account gesetzt wird, der mehrere parallele Worker-Prozesse startet: Ein zu niedriges ulimit -u führt dann nicht zu einem sofortigen, klaren Fehler beim Deployment selbst, sondern zu einem scheinbar zufälligen Scheitern einzelner Worker, sobald die Gesamtzahl aller laufenden Threads des Accounts das Limit überschreitet, oft erst unter Produktionslast sichtbar.
echo "Aktuelles Prozesslimit: $(ulimit -u)"
if (( $(ulimit -u) < 4096 )); then
echo "WARNUNG: Prozesslimit unter 4096, Worker koennten scheitern" >&2
fi
4. ulimit -v: virtuellen Speicher pro Prozess begrenzen
ulimit -v begrenzt den maximal reservierbaren virtuellen Speicher eines Prozesses in Kilobyte, nicht den tatsächlich genutzten physischen Speicher. Da moderne Anwendungen, insbesondere solche mit Java- oder Node.js-Laufzeit, häufig deutlich mehr virtuellen Adressraum reservieren als sie tatsächlich physisch nutzen, führt ein zu knapp bemessenes ulimit -v oft zu einem Absturz der Anwendung schon beim Start, lange bevor echter Speicherdruck entsteht.
Für Deployment-Skripte, die einen Prozess bewusst vor exzessivem Speicherverbrauch schützen wollen, ist ulimit -v deshalb meist das falsche Werkzeug, weil es virtuellen statt physischen Speicher begrenzt. Ein präziseres Werkzeug für dieses Ziel ist eine cgroup-basierte Speichergrenze über systemd, während ulimit -v in der Praxis vor allem dort sinnvoll bleibt, wo ein bewusst sehr restriktives Sandboxing für kleine, bekannte Skripte gewünscht ist.
# Deliberately restrictive sandbox for a small trusted script
( ulimit -v 262144 # 256 MB virtual memory
exec ./small-batch-job.sh )
5. Limits nur für die Skript-Laufzeit setzen: das Subshell-Pattern
ulimit-Aufrufe innerhalb eines laufenden Bash-Skripts wirken standardmäßig auf den aktuellen Prozess und werden an alle folgenden Kindprozesse vererbt, bleiben aber vollständig lokal auf die aktuelle Shell-Sitzung begrenzt und verschwinden, sobald das Skript beendet ist. Wer ein Limit nur für einen einzelnen Teilschritt eines größeren Skripts ändern will, sollte diesen Teilschritt deshalb bewusst in eine Subshell mit runden Klammern auslagern.
Innerhalb der Subshell gesetzte Limits gelten ausschließlich für diese Subshell und die von ihr gestarteten Kindprozesse, das umgebende Skript und alle danach folgenden Schritte behalten ihre ursprünglichen Limits unverändert. Dieses Muster ist besonders nützlich, wenn ein Deployment-Skript einen einzelnen, potenziell speicherhungrigen Build-Schritt bewusst begrenzen will, ohne die Limits für den Rest der Deployment-Pipeline zu beeinflussen.
echo "Vor der Subshell: $(ulimit -v)"
(
ulimit -v 1048576 # nur in dieser Subshell: 1 GB
./memory-intensive-build.sh
)
echo "Nach der Subshell: $(ulimit -v)" # unveraendert
6. Limits dauerhaft setzen: limits.conf im Vergleich zu systemd-Unit-Dateien
Ein im Skript per ulimit gesetztes Limit gilt immer nur für die aktuelle Prozessbaum-Instanz und geht mit dem Skriptende verloren. Für dauerhafte, systemweite oder benutzerspezifische Grenzen, die bei jedem neuen Login oder Dienststart automatisch gelten sollen, ist /etc/security/limits.conf der klassische Ort, an dem PAM-basierte Logins ihre Limits beziehen, konfigurierbar pro Benutzer, Gruppe oder mit einem Wildcard-Eintrag für alle.
Für Dienste, die über systemd gestartet werden, greift limits.conf jedoch häufig nicht, weil systemd-Dienste PAM-Login-Sessions umgehen. Der zuverlässigere Weg ist hier LimitNOFILE, LimitNPROC oder LimitAS direkt in der systemd-Unit-Datei des Dienstes zu setzen, weil systemd diese Direktiven unabhängig von limits.conf beim Start des Dienstes selbst durchsetzt und damit konsistent für jeden Neustart gilt, egal wer den Dienst auslöst.
7. Typische Probleme in Deployment-Skripten durch zu niedrige Limits
Das häufigste Symptom eines zu niedrigen Limits in produktiven Deployment-Skripten ist ein Dienst, der lokal beim Entwickeln problemlos startet, in der Produktionsumgebung aber sporadisch mit "Too many open files" oder "Resource temporarily unavailable" abstürzt, weil die Produktionsumgebung eine restriktivere Standardkonfiguration mitbringt als die lokale Entwicklungsmaschine, auf der oft großzügigere Distributions-Defaults gelten.
Ein zweites häufiges Problem entsteht, wenn ein Deployment-Skript Limits per ulimit im eigenen Prozess erhöht, den eigentlichen Dienst aber über systemd oder einen Prozessmanager startet, der eine eigene, unabhängige Umgebung mit eigenen Limits aufbaut. Die im Deployment-Skript gesetzten ulimit-Werte werden dann schlicht ignoriert, weil sie nur für den kurzlebigen Deployment-Prozess selbst galten, nicht für den tatsächlich langlebigen Dienstprozess.
8. Limits prüfen und diagnostizieren: ulimit -a und /proc/[pid]/limits
ulimit -a gibt eine vollständige Übersicht aller aktuell wirksamen Limits der laufenden Shell aus, praktisch für eine schnelle manuelle Prüfung während der Entwicklung eines Deployment-Skripts. Für die Diagnose eines bereits laufenden, fremden Prozesses ist dagegen /proc/[pid]/limits die richtige Quelle, weil sie sowohl Soft- als auch Hard-Limits für jede Ressource getrennt ausgibt, unabhängig davon, in welcher Shell oder ohne Shell der Prozess ursprünglich gestartet wurde.
Ein Diagnose-Skript, das prüfen soll, ob ein bereits laufender Produktionsdienst tatsächlich mit den erwarteten Limits gestartet wurde, liest deshalb /proc/[pid]/limits statt sich auf die Limits der eigenen, diagnostizierenden Shell zu verlassen, die typischerweise ganz andere Werte hat als der überwachte Zieldienst.
# Full overview in the current shell
ulimit -a
# Limits of a specific running process
pid=$(pgrep -f "my-webserver" | head -n1)
grep -E "open files|processes" "/proc/$pid/limits"
9. Best Practices für produktive Deployment-Skripte
Ein sauberes Deployment-Skript prüft benötigte Limits explizit, bevor der eigentliche Dienst gestartet wird, und bricht mit einer klaren Fehlermeldung ab, statt den Dienst mit unzureichenden Limits blind zu starten und den Fehler erst im Betrieb sichtbar werden zu lassen. Diese Prüfung sollte immer sowohl das Soft- als auch das Hard-Limit einbeziehen, weil nur das Hard-Limit verlässlich zeigt, wie weit sich das Soft-Limit ohne zusätzliche Rechte überhaupt anheben lässt.
Für langlebige, dauerhaft laufende Dienste gehören Limits konsequent in die systemd-Unit-Datei statt in ein einmalig ausgeführtes Deployment-Skript, weil systemd diese Limits bei jedem Neustart des Dienstes zuverlässig neu durchsetzt, unabhängig davon, wann und wie das Deployment-Skript zuletzt gelaufen ist. ulimit im Skript bleibt damit vor allem für kurzlebige Build- und Batch-Schritte das richtige Werkzeug.
| Limit | Flag | Typisches Problem | Empfehlung |
|---|---|---|---|
| Offene Dateien | ulimit -n | Too many open files bei vielen Verbindungen | Vor Dienststart prüfen, LimitNOFILE in systemd setzen |
| Prozesse/Threads | ulimit -u | fork() schlägt fehl bei vielen Workern | LimitNPROC in systemd, nicht nur im Deployment-Skript |
| Virtueller Speicher | ulimit -v | Absturz bei Java/Node trotz genug RAM | cgroup-Limits statt ulimit -v für physischen Speicher |
| Nur für Laufzeit | Subshell ( ) | Globale Limits versehentlich verändert | Kritische Schritte immer in Subshell kapseln |
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Shell-Skripte, die in der Produktion zuverlässig laufen?
Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.
Code-Review
ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.
Refactoring
Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.
CI-Integration
ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.
10. Zusammenfassung
ulimit in Bash: Das Wichtigste auf einen Blick
Soft vs. Hard
Soft-Limit ist der aktuell wirksame Wert, Hard-Limit die Obergrenze, bis zu der ein Prozess selbst erhöhen darf.
ulimit -n
Häufigste Fehlerquelle in Deployment-Skripten, Standardwert 1024 reicht für viele gleichzeitige Verbindungen nicht.
Subshell für Scope
Limits nur für einen Teilschritt setzen, ohne den Rest des Skripts zu beeinflussen, mit runden Klammern kapseln.
Dauerhaft via systemd
Für laufende Dienste gehören Limits in LimitNOFILE/LimitNPROC der Unit-Datei, nicht ins Deployment-Skript.