LEMP Stack Tuning: Nginx, PHP-FPM und MySQL optimal konfigurieren

Foto von Steve Johnson auf Unsplash
Ein falsch konfigurierter LEMP-Stack verschenkt 50-70% seiner möglichen Performance. In diesem Artikel zeige ich Ihnen die wichtigsten Stellschrauben für Nginx, PHP-FPM und MySQL – mit konkreten Konfigurationsbeispielen für Produktionsumgebungen.
Warum LEMP statt LAMP?
Der LEMP-Stack (Linux, Nginx, MySQL/MariaDB, PHP) ist dem klassischen LAMP-Stack (mit Apache) bei hoher Parallelität überlegen:
- Nginx ist event-basiert und asynchron
- Apache erstellt einen Prozess pro Verbindung
Bei tausenden gleichzeitigen Verbindungen macht das einen enormen Unterschied im Speicherverbrauch und der Antwortzeit.
Was Sie nach diesem Artikel erreichen können:
- 2-4x mehr gleichzeitige Verbindungen bei gleichem RAM
- 30-50% schnellere Response-Zeiten durch FastCGI Cache
- 50% CPU-Reduktion durch OPcache und JIT
1. Nginx Optimierung
Worker Processes und Connections
Die wichtigsten Nginx-Einstellungen befinden sich in /etc/nginx/nginx.conf:
user www-data;
worker_processes auto; # Automatisch: 1 Worker pro CPU-Kern
worker_rlimit_nofile 65536; # Max offene Dateien pro Worker
pid /run/nginx.pid;
events {
use epoll; # Effizientes Event-Polling (Linux)
worker_connections 4096; # Verbindungen pro Worker
multi_accept on; # Alle neuen Verbindungen auf einmal akzeptieren
}Berechnung der Kapazität:
Maximale Verbindungen = worker_processes × worker_connections
Beispiel: 4 CPU-Kerne × 4096 = 16.384 gleichzeitige Verbindungen
Buffer-Einstellungen
Zu kleine Buffer zwingen Nginx, temporäre Dateien auf die Festplatte zu schreiben:
http {
# Client Buffer
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 8m; # Max Upload-Größe
large_client_header_buffers 4 4k;
# Timeouts
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;
# TCP-Optimierungen
sendfile on; # Zero-Copy für statische Dateien
tcp_nopush on; # Header in einem Paket senden
tcp_nodelay on; # Keine Verzögerung bei kleinen Paketen
}Gzip Kompression
Gzip reduziert die Bandbreite um 30-50% für Text-basierte Inhalte:
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6; # Kompression 1-9 (6 = guter Kompromiss)
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript
application/xml+rss application/rss+xml
font/truetype font/opentype
image/svg+xml;
gzip_disable "msie6";Wichtig: Komprimieren Sie keine bereits komprimierten Formate (JPEG, PNG, Videos).
HTTP/2 und HTTP/3 aktivieren
HTTP/2 ist Standard, HTTP/3 (QUIC) bringt Vorteile bei mobilen Verbindungen:
server {
listen 443 ssl;
listen [::]:443 ssl;
# HTTP/3 (Nginx 1.25+, erfordert OpenSSL 3.5.1+)
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
http2 on;
http3 on;
quic_retry on;
ssl_early_data on; # 0-RTT für schnellere Verbindungen
# HTTP/3 Support signalisieren
add_header Alt-Svc 'h3=":443"; ma=86400';
# TLS 1.3 ist Voraussetzung für QUIC
ssl_protocols TLSv1.2 TLSv1.3;
ssl_certificate /etc/ssl/certs/domain.crt;
ssl_certificate_key /etc/ssl/private/domain.key;
}Voraussetzungen für HTTP/3:
- Nginx 1.25.0 oder neuer
- OpenSSL 3.5.1+ (oder BoringSSL/QuicTLS)
- Linux Kernel 5.7+ für optimale Performance
- Firewall: UDP Port 443 muss offen sein!
Security Headers
Sicherheit und Performance schließen sich nicht aus:
# HSTS (HTTP Strict Transport Security)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Clickjacking-Schutz
add_header X-Frame-Options "SAMEORIGIN" always;
# MIME-Sniffing verhindern
add_header X-Content-Type-Options "nosniff" always;
# Referrer-Policy
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Content Security Policy (erst testen, dann enforcing!)
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;2. FastCGI Cache konfigurieren
Der FastCGI Cache ist der größte Performance-Hebel für PHP-Anwendungen. Er speichert fertig gerenderte Seiten und umgeht PHP komplett bei Cache-Hits.
Cache-Pfad einrichten
In /etc/nginx/nginx.conf im http-Block:
http {
# Cache-Verzeichnis definieren
fastcgi_cache_path /var/cache/nginx
levels=1:2
keys_zone=PHPCACHE:100m # 100 MB für Cache-Keys
max_size=10g # Max 10 GB Cache-Daten
inactive=60m # Ungenutzte Einträge nach 60 Min löschen
use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
}Cache im Server-Block aktivieren
server {
# Cache-Bypass Variablen
set $skip_cache 0;
# POST-Requests nicht cachen
if ($request_method = POST) {
set $skip_cache 1;
}
# Query-Strings nicht cachen
if ($query_string != "") {
set $skip_cache 1;
}
# Admin-Bereiche und Logins ausschließen
if ($request_uri ~* "/wp-admin/|/wp-login.php|/admin/|/login") {
set $skip_cache 1;
}
# Eingeloggte Benutzer nicht cachen
if ($http_cookie ~* "wordpress_logged_in|PHPSESSID|laravel_session") {
set $skip_cache 1;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# Cache-Konfiguration
fastcgi_cache PHPCACHE;
fastcgi_cache_valid 200 301 302 60m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_lock on; # Verhindert "Cache Stampede"
fastcgi_cache_use_stale error timeout updating;
# Debug-Header (in Produktion entfernen)
add_header X-Cache-Status $upstream_cache_status;
}
}Microcaching für High-Traffic
Für Seiten mit hohem Traffic können Sie selbst dynamische Inhalte für 1 Sekunde cachen:
fastcgi_cache_valid 200 1s; # Nur 1 Sekunde cachenPerformance-Gewinn: Bis zu 400x schneller bei Traffic-Spitzen, da identische Anfragen innerhalb einer Sekunde aus dem Cache bedient werden.
3. PHP-FPM Tuning
PHP-FPM ist der "Fast Process Manager" für PHP und entscheidend für die Performance.
Process Manager Modi verstehen
| Modus | Verhalten | Einsatz |
|---|---|---|
| static | Feste Anzahl Worker | Maximale Performance, stabiler Traffic |
| dynamic | Skaliert zwischen min/max | Guter Kompromiss, variabler Traffic |
| ondemand | Worker nur bei Bedarf | Minimaler RAM, höhere Latenz |
Empfehlung: dynamic für die meisten Produktionsumgebungen.
pm.max_children berechnen
Die wichtigste Einstellung ist pm.max_children. Zu viele Worker führen zu Speicherengpässen, zu wenige zu Warteschlangen.
Formel:
pm.max_children = (Verfügbarer RAM - OS-Overhead) / RAM pro PHP-Worker
Beispiel (8 GB RAM Server):
- OS und Services: ~2 GB
- Verfügbar für PHP: 6 GB
- PHP-Worker (WordPress): ~128 MB
- max_children = 6000 MB / 128 MB ≈ 46
Vollständige Pool-Konfiguration
In /etc/php/8.3/fpm/pool.d/www.conf:
[www]
user = www-data
group = www-data
; Socket statt TCP (schneller)
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
listen.backlog = 65535
; Process Manager
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500 ; Worker recyceln (Memory Leaks verhindern)
pm.process_idle_timeout = 10s
; Monitoring (Status-Seite)
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong
; Slow Log (Requests > 5 Sekunden)
slowlog = /var/log/php8.3-fpm-slow.log
request_slowlog_timeout = 5s
; PHP-Einstellungen pro Pool
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 30
php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 64M
; Resource Limits
rlimit_files = 8192
rlimit_core = 0High-Performance Static Configuration
Für dedizierte Server mit stabilem Traffic:
pm = static
pm.max_children = 100 ; Alle Worker sofort verfügbar
pm.max_requests = 1000Vorteil: Kein Overhead für Prozessverwaltung. Nachteil: Konstant hoher RAM-Verbrauch.
4. OPcache und JIT (PHP 8.3)
OPcache speichert kompilierten PHP-Bytecode im RAM und eliminiert wiederholtes Parsen.
OPcache Konfiguration
In /etc/php/8.3/fpm/conf.d/10-opcache.ini:
[opcache]
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256 ; 256-384 MB für Produktionsserver
opcache.interned_strings_buffer=64 ; String-Interning (wichtig für Frameworks)
opcache.max_accelerated_files=50000 ; Max gecachte Dateien
opcache.validate_timestamps=0 ; WICHTIG: In Produktion deaktivieren!
opcache.revalidate_freq=0
opcache.save_comments=1 ; Für Doctrine/Annotations nötig
opcache.huge_code_pages=1 ; Huge Pages nutzen (Linux)
opcache.enable_file_override=1Kritisch: validate_timestamps=0 bedeutet, dass Codeänderungen erst nach PHP-FPM Neustart wirksam werden:
# Nach Deployment:
sudo systemctl restart php8.3-fpmJIT aktivieren (PHP 8.0+)
Der JIT-Compiler bringt zusätzliche Performance für CPU-intensive Operationen:
opcache.jit=tracing ; Empfohlen: tracing (oder function)
opcache.jit_buffer_size=100M ; JIT-Buffer GrößeModi:
tracing: Analysiert den gesamten Aufrufstapel (empfohlen)function: Optimiert nur innerhalb einzelner Funktionen
Einschränkung: JIT funktioniert nicht mit aktiviertem Xdebug.
OPcache-Status überwachen
<?php
// opcache-status.php
$status = opcache_get_status();
$config = opcache_get_configuration();
$hitRate = $status['opcache_statistics']['hits'] /
($status['opcache_statistics']['hits'] + $status['opcache_statistics']['misses']) * 100;
echo "Hit Rate: " . round($hitRate, 2) . "%\n";
echo "Memory Used: " . round($status['memory_usage']['used_memory'] / 1024 / 1024, 2) . " MB\n";
echo "Cached Scripts: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";Ziel: Hit Rate > 98%, keine OOM-Restarts.
5. MySQL/MariaDB Performance
InnoDB Buffer Pool dimensionieren
Der Buffer Pool ist der wichtigste MySQL-Parameter. Er cached Daten und Indizes im RAM.
[mysqld]
# Buffer Pool: 60-80% des RAM auf dedizierten DB-Servern
innodb_buffer_pool_size = 8G
innodb_buffer_pool_instances = 8 ; Bei > 1 GB Buffer Pool
innodb_buffer_pool_chunk_size = 128M
# Log-Dateien
innodb_log_file_size = 2G ; ~25% des Buffer Pools
innodb_log_buffer_size = 64M
# I/O Optimierung
innodb_flush_method = O_DIRECT ; Kein Double-Buffering (Linux)
innodb_flush_log_at_trx_commit = 2 ; 1=ACID, 2=Performance
innodb_flush_neighbors = 0 ; Für SSDs
# Datei-Organisation
innodb_file_per_table = 1 ; Separate Datei pro TabelleSizing-Richtlinien:
| Server-Typ | Buffer Pool |
|---|---|
| Dedizierter DB-Server | 80% RAM |
| Shared (Web + DB) | 50-70% RAM |
| Minimum pro Instance | 1 GB |
Buffer Pool Effizienz prüfen
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';Wichtige Metriken:
Innodb_buffer_pool_read_requests: Leseanfragen aus BufferInnodb_buffer_pool_reads: Leseanfragen von Disk
Hit Rate berechnen:
Hit Rate = (1 - reads / read_requests) × 100%
Ziel: > 99%
Query Cache Alternative: ProxySQL
Der Query Cache wurde in MySQL 8.0 entfernt. ProxySQL bietet einen modernen Ersatz:
# ProxySQL installieren
sudo apt install proxysql
# Query Cache aktivieren
mysql -h 127.0.0.1 -P 6032 -u admin -padmin <<EOF
UPDATE global_variables SET variable_value='256' WHERE variable_name='mysql-query_cache_size_MB';
UPDATE global_variables SET variable_value='true' WHERE variable_name='mysql-query_cache_stores_empty_result';
LOAD MYSQL VARIABLES TO RUNTIME;
SAVE MYSQL VARIABLES TO DISK;
EOFAlternativ: Redis oder Memcached auf Applikationsebene nutzen.
Slow Query Log
Langsame Queries sind der häufigste Performance-Killer:
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 ; Queries > 1 Sekunde loggen
log_queries_not_using_indexes = 1 ; Auch ohne Index loggenAnalyse:
# Slow Queries analysieren
mysqldumpslow -s t /var/log/mysql/slow.log | head -20
# Oder mit Percona Toolkit
pt-query-digest /var/log/mysql/slow.logConnection Management
max_connections = 500 ; An Traffic anpassen
wait_timeout = 600 ; Idle-Timeout (10 Min)
interactive_timeout = 600
max_allowed_packet = 64M ; Für große Imports6. Linux Kernel Tuning
Das Betriebssystem ist oft der übersehene Flaschenhals.
TCP/IP Optimierungen
In /etc/sysctl.conf:
# TCP Performance
net.ipv4.tcp_syncookies = 1 # SYN-Flood Schutz
net.ipv4.tcp_max_syn_backlog = 65535 # Backlog-Queue
net.core.somaxconn = 65535 # Max wartende Verbindungen
net.ipv4.tcp_fin_timeout = 30 # TIME_WAIT Timeout reduzieren
# TCP Buffer (für Hochgeschwindigkeitsnetze)
net.core.rmem_max = 16777216 # Max Empfangsbuffer (16 MB)
net.core.wmem_max = 16777216 # Max Sendebuffer (16 MB)
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 87380 16777216
# BBR Congestion Control (Linux 4.9+)
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# Memory Management
vm.swappiness = 1 # Swapping minimieren (0-100)
vm.vfs_cache_pressure = 50 # Filesystem Cache behalten
vm.overcommit_memory = 1 # Memory Overcommit erlauben
vm.dirty_ratio = 10 # Dirty Page Threshold
vm.dirty_background_ratio = 5
# File System
fs.file-max = 400000 # System-weite File DescriptorsAnwenden:
sudo sysctl -pBBR Congestion Control
BBR (Bottleneck Bandwidth and Round-trip propagation time) von Google verbessert den Durchsatz:
# BBR aktivieren
echo "net.core.default_qdisc = fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control = bbr" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
# Prüfen
sysctl net.ipv4.tcp_congestion_control
# Ausgabe: net.ipv4.tcp_congestion_control = bbrVoraussetzung: Linux Kernel 4.9 oder neuer.
File Descriptor Limits
Standard-Limits sind oft zu niedrig für Webserver:
Methode 1: /etc/security/limits.conf
www-data soft nofile 65536
www-data hard nofile 65536
nginx soft nofile 65536
nginx hard nofile 65536
mysql soft nofile 65536
mysql hard nofile 65536Methode 2: systemd Override
# Für Nginx
sudo mkdir -p /etc/systemd/system/nginx.service.d/
sudo cat > /etc/systemd/system/nginx.service.d/limits.conf <<EOF
[Service]
LimitNOFILE=65536
EOF
# Für PHP-FPM
sudo mkdir -p /etc/systemd/system/php8.3-fpm.service.d/
sudo cat > /etc/systemd/system/php8.3-fpm.service.d/limits.conf <<EOF
[Service]
LimitNOFILE=65536
EOF
# Reload
sudo systemctl daemon-reload
sudo systemctl restart nginx php8.3-fpmPrüfen:
# Aktuelle Limits
ulimit -n
# Service-spezifisch
systemctl show nginx.service | grep LimitNOFILE7. Monitoring und Validierung
Nginx Status
# In server-Block hinzufügen
location /nginx_status {
stub_status on;
allow 127.0.0.1;
deny all;
}curl http://localhost/nginx_status
# Active connections: 42
# server accepts handled requests
# 1234567 1234567 4567890PHP-FPM Status
# Bereits in Pool-Config aktiviert: pm.status_path = /fpm-status
curl http://localhost/fpm-status?fullWichtige Metriken:
active processes: Gerade arbeitende Workeridle processes: Wartende Workerlisten queue: Warteschlange (sollte 0 sein!)max children reached: Limit erreicht (problematisch!)
MySQL Performance
-- InnoDB Status
SHOW ENGINE INNODB STATUS\G
-- Key Performance Metrics
SHOW GLOBAL STATUS WHERE Variable_name LIKE 'Innodb_buffer_pool%'
OR Variable_name LIKE 'Threads_%'
OR Variable_name LIKE 'Slow_queries';Empfohlene Tools
| Tool | Zweck |
|---|---|
| htop | CPU, RAM, Prozesse |
| iostat | Disk I/O |
| MySQLTuner | MySQL-Konfigurationsprüfung |
| Netdata | Real-Time Monitoring Dashboard |
| Prometheus + Grafana | Enterprise Monitoring |
# MySQLTuner
wget https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl
perl mysqltuner.plVollständige Beispielkonfigurationen
nginx.conf
user www-data;
worker_processes auto;
worker_rlimit_nofile 65536;
pid /run/nginx.pid;
events {
use epoll;
worker_connections 4096;
multi_accept on;
}
http {
# Basics
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
keepalive_requests 100;
types_hash_max_size 2048;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Buffer
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 64m;
large_client_header_buffers 4 4k;
# Timeouts
client_body_timeout 12;
client_header_timeout 12;
send_timeout 10;
# Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss;
# FastCGI Cache
fastcgi_cache_path /var/cache/nginx levels=1:2
keys_zone=PHPCACHE:100m max_size=10g inactive=60m use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
# File Cache
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}my.cnf (MySQL/MariaDB)
[mysqld]
user = mysql
pid-file = /var/run/mysqld/mysqld.pid
socket = /var/run/mysqld/mysqld.sock
port = 3306
datadir = /var/lib/mysql
# Character Set
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
# InnoDB
innodb_buffer_pool_size = 4G
innodb_buffer_pool_instances = 4
innodb_log_file_size = 1G
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_file_per_table = 1
innodb_flush_neighbors = 0
# Connections
max_connections = 300
wait_timeout = 600
interactive_timeout = 600
max_allowed_packet = 64M
# Logging
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
# Performance Schema (deaktivieren für weniger Overhead)
performance_schema = OFFFazit: Priorisierte Checkliste
1. Kritisch (Sofort umsetzen)
- Nginx
worker_processes autosetzen - PHP-FPM
pm.max_childrenberechnen und konfigurieren - OPcache aktivieren mit
validate_timestamps=0 - MySQL
innodb_buffer_pool_sizeauf 50-80% RAM setzen - File Descriptor Limits auf 65536 erhöhen
2. Wichtig (Hoher Impact)
- FastCGI Cache aktivieren
- Gzip Kompression einschalten
- HTTP/2 aktivieren
- TCP BBR Congestion Control aktivieren
- Security Headers setzen
3. Empfohlen (Zusätzliche Gewinne)
- HTTP/3 aktivieren (falls unterstützt)
- PHP JIT aktivieren
- ProxySQL für Query-Caching evaluieren
- Monitoring einrichten (Nginx Status, FPM Status, MySQLTuner)
Erwartete Verbesserungen:
- Response-Zeit: 30-50% schneller
- Gleichzeitige Verbindungen: 2-4x mehr
- CPU-Auslastung: 30-50% Reduktion
Haben Sie Fragen zur Konfiguration? Kontaktieren Sie mich – ich unterstütze Sie gerne bei der Optimierung Ihres LEMP-Stacks.