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

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 cachen

Performance-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

ModusVerhaltenEinsatz
staticFeste Anzahl WorkerMaximale Performance, stabiler Traffic
dynamicSkaliert zwischen min/maxGuter Kompromiss, variabler Traffic
ondemandWorker nur bei BedarfMinimaler 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 = 0

High-Performance Static Configuration

Für dedizierte Server mit stabilem Traffic:

pm = static
pm.max_children = 100                     ; Alle Worker sofort verfügbar
pm.max_requests = 1000

Vorteil: 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=1

Kritisch: validate_timestamps=0 bedeutet, dass Codeänderungen erst nach PHP-FPM Neustart wirksam werden:

# Nach Deployment:
sudo systemctl restart php8.3-fpm

JIT 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öße

Modi:

  • 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 Tabelle

Sizing-Richtlinien:

Server-TypBuffer Pool
Dedizierter DB-Server80% RAM
Shared (Web + DB)50-70% RAM
Minimum pro Instance1 GB

Buffer Pool Effizienz prüfen

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';

Wichtige Metriken:

  • Innodb_buffer_pool_read_requests: Leseanfragen aus Buffer
  • Innodb_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;
EOF

Alternativ: 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 loggen

Analyse:

# Slow Queries analysieren
mysqldumpslow -s t /var/log/mysql/slow.log | head -20
 
# Oder mit Percona Toolkit
pt-query-digest /var/log/mysql/slow.log

Connection 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 Imports

6. 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 Descriptors

Anwenden:

sudo sysctl -p

BBR 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 = bbr

Voraussetzung: 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 65536

Methode 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-fpm

Prüfen:

# Aktuelle Limits
ulimit -n
 
# Service-spezifisch
systemctl show nginx.service | grep LimitNOFILE

7. 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 4567890

PHP-FPM Status

# Bereits in Pool-Config aktiviert: pm.status_path = /fpm-status
curl http://localhost/fpm-status?full

Wichtige Metriken:

  • active processes: Gerade arbeitende Worker
  • idle processes: Wartende Worker
  • listen 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

ToolZweck
htopCPU, RAM, Prozesse
iostatDisk I/O
MySQLTunerMySQL-Konfigurationsprüfung
NetdataReal-Time Monitoring Dashboard
Prometheus + GrafanaEnterprise Monitoring
# MySQLTuner
wget https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl
perl mysqltuner.pl

Vollstä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 = OFF

Fazit: Priorisierte Checkliste

1. Kritisch (Sofort umsetzen)

  • Nginx worker_processes auto setzen
  • PHP-FPM pm.max_children berechnen und konfigurieren
  • OPcache aktivieren mit validate_timestamps=0
  • MySQL innodb_buffer_pool_size auf 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.