Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowSave products you love by clicking the heart icon.
Ein praktischer Leitfaden zur Skalierung von Nextcloud, OpenDesk und ähnlichen selbstgehosteten Cloud-Plattformen über vier Wachstumsstufen hinweg, inklusive konkreter Konfigurationen und Architekturmuster.
Sie kennen die Symptome:
vm.swappiness zu aggressiv eingestellt istkernel.shm* nicht auf den Shared Memory abgestimmt istModerne Infrastruktur-Layer führen unzählige einstellbare Parameter ein: Kernel-Parameter, Docker-Daemon-Einstellungen, Container-Limits, Kubernetes-Ressourcenrichtlinien und workload-spezifische Anpassungen. Diese korrekt zu konfigurieren, macht den Unterschied zwischen einem System, das die 10-fache Last bewältigt, und einem, das bei Skalierung zusammenbricht.
Dieser Guide kodifiziert die Tuning-Patterns aus der Produktionserfahrung, organisiert nach Isolation-Layer und ergänzt durch workload-spezifische Cheat-Sheets.
Ressourcensteuerungen operieren auf verschiedenen Ebenen, jede mit eigenen Parametern und Trade-offs:
Jede Ebene kann die darüber liegende Ebene überschreiben, aber Standardwerte fließen fehlerhaft nach unten, wenn sie nicht bewusst konfiguriert werden.
| Parameter | Standard | Produktion | Zweck |
|---|---|---|---|
net.core.somaxconn | 128 | 4096 | Max. ausstehende SYN-Queue |
net.core.netdev_max_backlog | 1000 | 5000 | Max. Pakete im NIC-Backlog |
net.ipv4.tcp_max_syn_backlog | 1024 | 8192 | Max. SYN-Requests |
net.ipv4.tcp_tw_reuse | 0 | 1 | TIME_WAIT Sockets wiederverwenden |
net.ipv4.ip_local_port_range | 32768-60999 | 1024-65535 | Ephemeral Port Range |
net.ipv4.tcp_fin_timeout | 60 | 30 | FIN-Timeout in Sekunden |
net.ipv4.tcp_keepalive_time | 7200 | 600 | Keepalive Idle in Sekunden |
net.ipv4.tcp_keepalive_intvl | 75 | 30 | Keepalive Probe Intervall |
net.ipv4.tcp_keepalive_probes | 9 | 5 | Keepalive Probe Count |
# /etc/sysctl.d/99-production.conf
net.core.somaxconn = 4096
net.core.netdev_max_backlog = 5000
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
# Security
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
Sofort anwenden:
sudo sysctl -p /etc/sysctl.d/99-production.conf
| Parameter | Standard | Produktion | Benchmark-Leitfaden |
|---|---|---|---|
vm.swappiness | 60 | 1-10 (DB), 30 (App) | Niedriger für speichersensitive Workloads |
vm.overcommit_memory | 0 | 1 (K8s Nodes) | Allocator-Overcommit erlauben (kubelet berechnet neu) |
vm.overcommit_ratio | 50 | 100 | Prozentsatz von RAM+Swap für Overcommit |
vm.dirty_ratio | 30 | 20 | % Speicher, ab dem Dirty Pages blockieren |
vm.dirty_background_ratio | 10 | 5 | % Speicher, ab dem Background Writeback startet |
vm.min_free_kbytes | 65536 | 1-2% des RAMs | Notfallreserve gegen OOM |
# /etc/sysctl.d/99-memory.conf
vm.swappiness = 10 # Don't swap unless critical
vm.overcommit_memory = 1 # K8s nodes expect this
vm.overcommit_ratio = 100 # Full overcommit
vm.dirty_ratio = 20 # Start blocking writes at 20%
vm.dirty_background_ratio = 5 # Background writeback at 5%
vm.min_free_kbytes = 1048576 # 1GB reserve on 64GB node
# /etc/sysctl.d/99-fs.conf
fs.file-max = 2097152 # Global open file limit
fs.inotify.max_user_instances = 1024
fs.inotify.max_user_watches = 819200 # For etcd, collectd
# /etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576
* soft nproc 65535
* hard nproc 65535
root soft nofile 1048576
root hard nofile 1048576
{
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
},
"default-ulimits": {
"nofile": { "Name": "nofile", "Hard": 1048576, "Soft": 65535 },
"nproc": { "Name": "nproc", "Hard": 65535, "Soft": 4096 }
},
"live-restore": true,
"userns-remap": "default",
"no-new-privileges": false,
"max-concurrent-downloads": 10,
"max-concurrent-uploads": 10,
"data-root": "/var/lib/docker"
}
Die Standard-Ressourcenlimits von Docker sind großzügig bemessen, können jedoch zu „Noisy Neighbors“ führen:
| Ressource | Standard | Empfohlen | Wann zu überschreiben |
|---|---|---|---|
--pids-limit | (keine) | 100-500 pro Container | Apps mit hoher Fork-Tendenz benötigen mehr |
--memory-swap | unbegrenzt | memory × 2 | Mit --memory-swap -1 deaktivieren, um Swapping auf die Festplatte bei latenzsensitiven DBs zu verhindern |
--memory-reservation | entspricht Limit | 70-80 % des Limits | Verhindert „Burst then Die“-Muster |
--kernel-memory | unbegrenzt | 100-200Mi | Steuert nicht-residente Seitentabellen; niedrige Standardwerte führen bei hohen Verbindungszahlen zu Container-Abstürzen |
security-opt no-new-privileges | false | true (empfohlen) | Blockiert setuid Escalation |
docker run -d \
--name myapp \
--memory=512m \
--memory-reservation=400m \
--memory-swap=-1 \
--pids-limit=200 \
--ulimit nofile=65535:65535 \
--ulimit nproc=4096:8192 \
--security-opt no-new-privileges \
--security-opt seccomp=default.json \
myapp:latest
Kubernetes weist QoS-Klassen basierend auf requests und limits zu:
| QoS-Klasse | Anforderungen | Eviction-Priorität |
|---|---|---|
| Guaranteed | requests.cpu == limits.cpu <br/> requests.memory == limits.memory | Zuletzt evakuiert |
| Burstable | Beliebige requests gesetzt <br/> requests != limits | Mittel |
| BestEffort | Keine requests <br/> Keine limits | Zuerst evakuiert |
# Guaranteed (best for DBs, latency-critical services)
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "2"
memory: "8Gi"
# Burstable (typical for stateless services)
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2"
memory: "4Gi"
# ❌ BestEffort (avoid in production)
resources: {} # or omit entirely
Produktionsregel: Setzen Sie immer requests. Weisen Sie zumindest requests.memory = limits.memory / 2 für Workloads zu, die nicht latenzkritisch sind.
Unter cgroup v2 ist das memory.swap-Accounting aktiviert. Stellen Sie die Kubelet-Konfiguration sicher:
# /var/lib/kubelet/config.yaml
memorySwap:
swapBehavior: NoSwap # NoSwap, LimitedSwap, UnlimitedSwap
limits.memory wird für Swap ignoriertapiVersion: v1
kind: Pod
metadata:
name: hugepages-pod
spec:
containers:
- name: app
image: myapp:latest
resources:
limits:
Hugepages-2Mi: 512Mi # 256 × 2Mi pages
memory: "1Gi"
volumeMounts:
- mountPath: /hugepages
name: hugepage
volumes:
- name: hugepage
emptyDir:
medium: HugePages
Use for: databases (PostgreSQL, MySQL), DPDK applications, in-memory caches.
# /var/lib/kubelet/config.yaml
cpuManagerPolicy: static # none (Standard), static
cpuManagerReconcilePeriod: 5s
requests.cpu = integer cores get exclusive cores. Ideal for low-latency workloads.# Pod, der exklusive CPU 0-1 anfordert
resources:
requests:
cpu: "2" # Muss eine Ganzzahl sein
limits:
cpu: "2"
# /var/lib/kubelet/config.yaml
topologyManagerPolicy: best-effort # none, best-effort, restricted, single-numa-node
# Pods, die ausgerichtete Ressourcen anfordern
resources:
requests:
cpu: "4"
memory: "8Gi"
cpu: "4" # Topology Manager richtet dies auf demselben NUMA-Knoten aus
devices.kubernetes.com/mock: "1" # Device Affinity
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: production
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4"
memory: "8Gi"
min:
cpu: "50m"
memory: "64Mi"
maxLimitRequestRatio:
cpu: "10"
memory: "4"
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-tuning
data:
# Postgres erwartet Shared-Memory-Segmente, die mit dem Kernel abgestimmt sind
# Passen Sie kernel.shm* an, wenn Sie auf einer Bare-Metal-VM bereitstellen
# /etc/sysctl.d/99-postgres.conf:
# kernel.shmmax = 4294967296 # 4GB
# kernel.shmall = 1048576 # 4GB pages
# kernel.sem = 250 32000 100 128
postgresql.conf: |
shared_buffers = 4GB # 25% des RAM
effective_cache_size = 12GB # 75% des RAM
work_mem = 64MB # Sortierspeicher pro Operation
maintenance_work_mem = 1GB
max_connections = 200
wal_buffers = 128MB
checkpoint_completion_target = 0.9
---
apiVersion: v1
kind: Pod
metadata:
name: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "16Gi" # OOM-Ziel ist 2× requests
securityContext:
fsGroup: 999 # Behebt Berechtigungsprobleme auf /var/run/postgresql
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: data
- mountPath: /var/run/postgresql
name: run
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-pvc
- name: run
emptyDir: {}
resources:
requests:
cpu: "500m"
memory: "4Gi"
limits:
cpu: "2"
memory: "8Gi"
# In redis.conf:
# maxmemory 6gb
# maxmemory-policy allkeys-lru
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "1"
memory: "512Mi"
# In nginx.conf:
# worker_processes auto;
# worker_connections 4096; # Offene Datei-Limits beachten
# worker_rlimit_nofile 65535;
# multi_accept on;
# access_log off; # Bei hohem Durchsatz an syslog leiten
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "8"
memory: "32Gi"
# In server.properties:
# log.dirs = /kafka/data
# num.network.threads = 8
# num.io.threads = 16
# socket.send.buffer.bytes = 102400
# socket.receive.buffer.bytes = 102400
# log.flush.interval.messages = 10000
# log.flush.interval.ms = 1000
# num Partitions: 2× Broker-Anzahl
resources:
requests:
cpu: "2"
memory: "8Gi"
limits:
cpu: "4"
memory: "16Gi"
# In jvm.options:
# -Xms8g
# -Xmx8g # Heap darf 50 % des Pod-Speichers nicht überschreiten
# In elasticsearch.yml:
# bootstrap.memory_lock: true
# indices.queries.cache.size: 10%
# indices.fielddata.cache.size: 20%
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
# JVM args:
# -Xms512m # Start mit 512MB Heap
# -Xmx1536m # Heap auf 50 % des Pod-Speichers begrenzen (Non-Heap Overhead)
# -XX:+UseG1GC
# -XX:MaxGCPauseMillis=200
# -XX:+PrintGCDetails
# -XX:+PrintGCDateStamps
# -Xloggc:/logs/gc.log
apiVersion: v1
kind: Pod
metadata:
name: ml-worker
spec:
runtimeClassName: nvidia # Für NVIDIA GPU-Unterstützung
containers:
- name: ml-app
image: pytorch:24.01-cuda12.1
resources:
requests:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: 1
limits:
cpu: "8"
memory: "32Gi"
nvidia.com/gpu: 1
env:
- name: CUDA_VISIBLE_DEVICES
value: "0"
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Falls benötigt
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
ephemeral-storage: "1Gi"
volumeMounts:
- name: tmp
mountPath: /tmp
- name: run
mountPath: /var/run
volumes:
- name: tmp
emptyDir:
sizeLimit: "100Mi"
- name: run
emptyDir:
sizeLimit: "50Mi"
The RuntimeDefault profile blocks dangerous syscalls:
kexec_load, module_load, delete_module (kernel persistence)ptrace, process_vm_readv/writev (process manipulation)mount, umount, pivot_root (filesystem remapping)clock_settime, settimeofday (system time manipulation)bpf (unprivileged BPF is blocked by kernel.unprivileged_bpf_disabled=1)Use custom profiles only when required (e.g., some FUSE workloads).
# Netzwerk
sysctl net.core.somaxconn net.ipv4.tcp_tw_reuse net.ipv4.ip_local_port_range
# Speicher
sysctl vm.swappiness vm.dirty_ratio vm.min_free_kbytes
# Dateisystem
sysctl fs.file-max fs.inotify.max_user_watches
# Pro Knoten
cat /proc/sys/vm/swappiness
cat /proc/sys/net/core/somaxconn
# Den cgroup-Pfad eines Containers finden
docker inspect --format '{{.State.Pid}}' myapp | xargs cat /proc/$$/cgroup
# Limits prüfen
cat /sys/fs/cgroup/memory.max # Memory-Limit
cat /sys/fs/cgroup/cpu.max # CPU-Quota (z. B. "200000 100000")
cat /sys/fs/cgroup/pids.max # PID-Limit
cat /sys/fs/cgroup/memory.swap.max # Swap-Limit
kubectl get pod -o jsonpath='{.metadata.name}{"\t"}{.status.qosClass}{"\n"}'
# Guaranteed? Blick in .spec.containers[].resources
kubectl get pod -o json | jq '.items[] | select(.metadata.name=="myapp") |
{
name: .metadata.name,
qos: .status.qosClass,
resources: .spec.containers[].resources
}'
# Aus Prometheus
rate(container_cpu_cfs_throttled_periods_total[5m]) / rate(container_cpu_cfs_periods_total[5m])
# Alert, wenn > 0,5 (50 % throttled)
# fd-Count pro Container prüfen
find /proc/*/fd 2>/dev/null | xargs -I{} sh -c 'echo $(dirname $(dirname {})): $(ls {}/fd | wc -l)' | sort -rn | head
# Oder cgroup verwenden
cat /proc/{PID}/limits | grep "Max open files"
# Auf dem Host
dmesg | grep -i "killed process"
# Auf K8s
kubectl get pod -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}{end}' | grep OOMKilled
requests on All Pods# Schlecht
resources:
limits:
memory: "512Mi"
cpu: "1"
# wird zu BestEffort, wenn requests nicht gesetzt sind
Result: First to evict; unpredictable scheduling; node resource exhaustion.
kernel.shmmax Too Low# PostgreSQL startet nicht:
# FATAL: could not create shared memory segment: Invalid argument
Fix: Adjust kernel parameters for database pods:
# /etc/sysctl.d/99-database.conf
kernel.shmmax = 68719476736 # 64GB
kernel.shmall = 16777216 # 64GB / 4096 (page size)
--pids-limit Not Set# Eine Fork-Bombe in einem Container kann den gesamten Node zum Absturz bringen
Fix: Set limits on daemon or container:
{
"default-pids-limit": 200
}
Or per-container:
docker run --pids-limit 200 myapp
# Standard: limits.memory=512Mi, memory-swap=unlimited
# → Container kann auf die Disk swappen (langsam) und den Host-Swap erschöpfen
--memory-swap=-1, um Swapping zu deaktivieren, oder konfigurieren Sie den kubelet memorySwap.swapBehavior: NoSwap.runAsNonRoot + fsGroup Mismatch# PostgreSQL sleeps at startup, can't read/write /var/run/postgresql
Fix: Setzen Sie fsGroup so, dass es mit dem DB-Benutzer übereinstimmt:
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
# Kubernetes operations break when clocks are set back in time
Fix: Verwenden Sie NTP mit maxslewrate oder makestep, um große Sprünge nach vorne zu erlauben, aber Sprünge zurück abzulehnen:
# /etc/chrony.conf
maxslewrate 1000 # Allow 1000x faster clock catch-up
makestep 1.0 -1 # Step clock if offset > 1 second (forward only)
# Security review fails: "RuntimeDefault not set"
Fix: Profil explizit setzen:
podSecurityContext:
seccompProfile:
type: RuntimeDefault
# Alert: Pod Memory Usage Nearing Limit
- alert: PodMemoryHigh
expr: container_memory_usage_bytes / container_spec_memory_limit_bytes > 0.9
for: 10m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} memory > 90% of limit"
# Alert: OOM Kill
- alert: PodOOMKilling
expr: rate(kube_pod_container_status_restarts_total{reason="OOMKilled"}[5m]) > 0
for: 0m
annotations:
summary: "Container {{ $labels.container }} in pod {{ $labels.pod }} OOM killed"
RCA:
limits.memory angemessen ist (ist die App speicherintensiv?)kubectl logs ... + heap Dump (JVM) oder ps + pmap (native)requests.memory und limits.memory erhöhen# Alert: High CPU Throttling
- alert: HighCPUThrottling
expr: |
rate(container_cpu_cfs_throttled_periods_total[5m]) /
rate(container_cpu_cfs_periods_total[5m]) > 0.5
for: 10m
annotations:
summary: "Container {{ $labels.container }} throttled > 50%"
RCA:
limits.cpu erhöhen)nodeSelector auf dedizierte Nodes verschiebenstatic Policy für garantierte exklusive Kerne in Betracht ziehen# Alert: TCP Listen Drops / SYN Flood
- alert: TCPListenDrops
expr: rate(netstat_Tcp_ListenDrops[5m]) > 10
for: 5m
annotations:
summary: "High TCP listen drops (increase net.core.somaxconn?)"
RCA:
net.core.somaxconn auf dem Node erhöhennet.ipv4.tcp_max_syn_backlog optimierensomaxconn=4096, tcp_max_syn_backlog=8192, tw_reuse=1swappiness=10 (DB), dirty_ratio=20, min_free_kbytes=1-2%file-max=2097152, inotify.max_user_watches=819200kexec_load_disabled=1, unprivileged_bpf_disabled=1, yama.ptrace_scope=2default-ulimits gesetzt (nofile, nproc)userns-remap=default (UID remap)live-restore=true (Resilienz beim Daemon-Neustart)log-opts.max-size=50m (Log-Rotation)LimitRange definiert (Standard-Requests + Limits)ResourceQuota definiert (Namespace-Erschöpfung verhindern)pod-security.kubernetes.io/enforce=restrictedNetworkPolicy default-deny + explizites allowrequests.cpu und requests.memory immer gesetztlimits.cpu und limits.memory gesetzt (oder Grund dokumentiert)securityContext.runAsNonRootallowPrivilegeEscalation=falsereadOnlyRootFilesystem=true mit tmpfs-MountsseccompProfile.type=RuntimeDefaultpids-limit gesetzt (auf Daemon oder Container)volumeMounts für /tmp, /var/run, /var/cachekernel.shm* via Host-sysctls optimiertmaxmemory + maxmemory-policy gesetztworker_connections, worker_rlimit_nofile abgestimmt-Xms/-Xmx = 25-50 % des Pod-SpeichersruntimeClassName: nvidia, nvidia.com/gpu requestsrequests setzen: BestEffort wird zuerst evicted und ist unvorhersehbarcap-drop ALL, no-new-privileges, seccomp=RuntimeDefault reduzieren die Angriffsfläche ohne Performance-Einbußendmesg, sysctl und Container-Logs, um Engpässe zu identifizierenmaxmemory, die JVM benötigt Puffer über den Heap hinaus--file=/etc/sysctl.d/*.conf) und kubelet-Konfigurationen erneut anwendenEine gut abgestimmte Umgebung bewältigt das 10-fache an Traffic, erholt sich reibungslos von Ausfällen und bleibt sicher gegenüber gängigen Container-Escape-Vektoren.
man sysctl, man cgroupsbcc-tools, bpftrace für fortgeschrittenes Container-Debugging