...

Memory Fragmentation im Webhosting: Leistungsfalle für PHP & MySQL

Memory Fragmentation im Webhosting verlangsamt PHP‑FPM und MySQL, obwohl scheinbar genug RAM vorhanden ist, weil der Speicher in viele kleine Blöcke zerfällt und größere Allokationen scheitern. Ich zeige praxisnah, wie Fragmentierung Anfragen verteuert, Swap triggert und warum gezieltes Tuning bei PHP und MySQL Ladezeiten, Zuverlässigkeit und Skalierung sichtbar hebt.

Zentrale Punkte

  • PHP-FPM recyceln: Prozesse regelmäßig über pm.max_requests neu starten
  • Buffer dosieren: MySQL-Per-Connection-Buffer konservativ halten
  • Swap vermeiden: Swappiness senken, NUMA beachten
  • Tabellen pflegen: Data_free prüfen, gezielt optimieren
  • Monitoring nutzen: Trends früh sehen und handeln

Was bedeutet Memory Fragmentation im Hosting-Alltag?

Im Hosting trifft Fragmentierung auf langlaufende Prozesse, die ständig Speicher anfordern und freigeben, wodurch Lücken im Adressraum entstehen. Obwohl die Summe des freien RAMs groß wirkt, fehlen zusammenhängende Blöcke für größere Allokationen, was Allokationsversuche verlangsamt. Ich sehe das in PHP‑FPM-Workern und in mysqld, die nach Stunden immer „aufgeblähter“ erscheinen. Der Effekt macht jeden Request minimal teurer und schiebt Antwortzeiten unter Last spürbar nach oben. Dadurch geraten Spitzen wie Sales‑Aktionen oder Backups zum Bremsklotz, obwohl CPU und Netzwerk unauffällig bleiben.

Warum PHP-FPM Fragmentierung erzeugt

Jeder PHP‑FPM‑Worker lädt Code, Plugins und Daten in einen eigenen Adressraum, bedient verschiedenste Requests und hinterlässt beim Freigeben verstreute Lücken. Über Zeit wachsen Prozesse und geben Speicher intern frei, aber nicht zwingend ans Betriebssystem, wodurch die Fragmentierung zunimmt. Unterschiedliche Skripte, Importjobs und Bildverarbeitung verstärken diesen Mix und führen zu wechselnden Allokationsmustern. Ich beobachte das als schleichenden RAM‑Anstieg, obwohl Last und Traffic konstant wirken. Ohne Recycling verlangsamt diese innere Fragmentierung die Allokation und erschwert Planbarkeit bei hohen Besucherzahlen.

Spürbare Folgen für Ladezeiten und Zuverlässigkeit

Fragmentierte Prozesse erzeugen mehr Overhead in der Speicherverwaltung, was sich in langsameren Admin‑Backends und zögerlichen Checkouts äußert. Besonders WordPress‑Shops oder große CMS‑Instanzen reagieren träge, wenn viele gleichzeitige Requests auf fragmentierte Worker treffen. Daraus resultieren Timeouts, 502/504‑Fehler und vermehrte Retries auf NGINX oder Apache‑Seite. Ich lese solche Situationen in Metriken wie Response‑Zeit‑Spitzen, steigender RAM‑Basislinie und plötzlich wachsender Swap‑Nutzung. Wer dies ignoriert, verschenkt Performance, verschlechtert das Nutzererlebnis und erhöht die Abbruchquote in kritischen Funnels.

PHP-FPM richtig einstellen: Limits, Pools, Recycling

Ich setze auf realistische Limits, getrennte Pools und konsequentes Recycling, um Fragmentierung einzudämmen. pm.max_requests beende ich so, dass Worker regelmäßig frisch starten, ohne laufende Besucher zu stören. Für Traffic‑Profile mit Lastspitzen passt pm = dynamic oft besser, während pm = ondemand bei ruhigen Sites RAM spart. Das memory_limit pro Site halte ich bewusst moderat und passe es mit Blick auf reale Skripte an; einen Einstieg liefert das Thema PHP-Memory-Limit. Zusätzlich trenne ich stark belastete Projekte in eigene Pools, damit ein Speicherfresser nicht alle Sites beeinträchtigt.

OPcache, Preloading und PHP-Allocator im Blick

Um Fragmentierung im PHP‑Prozess zu dämpfen, setze ich auf einen sauber dimensionierten OPcache. Ein großzügiger, aber nicht überzogener opcache.memory_consumption und ausreichend interned strings reduzieren wiederholte Allokationen je Request. Ich beobachte Hit‑Rate, Waste und Restkapazität; steigt der Waste über Zeit, ist ein geplanter Reload besser, als Worker unkontrolliert anwachsen zu lassen. Preloading kann Hot‑Code stabil im Speicher halten und so Allokationsmuster glätten, vorausgesetzt, die Codebasis ist entsprechend vorbereitet. Zusätzlich achte ich auf die Allocator‑Wahl: Je nach Distribution arbeiten PHP‑FPM und Erweiterungen mit verschiedenen Malloc‑Implementierungen. Alternative Allocator wie jemalloc verringern in manchen Setups die Fragmentierung spürbar. Ich rolle solche Änderungen jedoch nur getestet aus, da Debugging, DTrace/eBPF‑Profiling und Speicherdumps je nach Allocator unterschiedlich reagieren.

Für speicherintensive Aufgaben wie Bildverarbeitung oder Exporte ziehe ich separate Pools mit engeren Limits vor. So wächst der Hauptpool nicht unkontrolliert und Fragmentierung bleibt isoliert. Außerdem limitiere ich speicherhungrige Erweiterungen (z. B. über Umgebungsvariablen) und nutze Backpressure: Requests, die große Buffers benötigen, werden gedrosselt oder in asynchrone Queues ausgelagert, statt alle Worker gleichzeitig aufzublasen.

MySQL-Speicher verstehen: Buffer, Verbindungen, Tabellen

Bei MySQL unterscheide ich globale Buffer wie den InnoDB Buffer Pool, pro‑Verbindungs‑Buffer und temporäre Strukturen, die je Operation wachsen können. Zu große Werte führen bei hoher Verbindungslast zu explodierendem RAM‑Bedarf und mehr Fragmentierung auf OS‑Ebene. Zusätzlich zerfleddern Tabellen durch Updates/Deletes und hinterlassen Data_free‑Anteile, die den Buffer Pool schlechter ausnutzen lassen. Ich prüfe daher regelmäßig Größe, Hit‑Ratios und die Zahl temporärer Disk‑Tabellen. Die folgende Übersicht hilft mir, typische Symptome treffsicher zuzuordnen und gegen Maßnahmen abzuwägen.

Symptom Wahrscheinliche Ursache Maßnahme
RAM steigt stetig, Swap beginnt Zu großer Buffer Pool oder viele per‑Connection‑Buffer Pool auf passende Größe begrenzen, per‑Connection‑Buffer senken
Viele langsame Sorts/Joins Fehlende Indexe, überzogene sort/join‑Buffer Indexe prüfen, sort/join konservativ halten
Großes Data_free in Tabellen Starke Updates/Deletes, Seiten zersplittert Gezieltes OPTIMIZE, Archivierung, Schema straffen
Spitzen bei temporären Disk‑Tabellen Zu kleine tmp_table_size oder ungeeignete Queries Werte maßvoll anheben, Queries umbauen

MySQL Memory Tuning: Größen wählen statt überziehen

Ich wähle den InnoDB Buffer Pool so, dass das Betriebssystem genug Atem für Filesystem‑Cache und Dienste behält, besonders bei Kombi‑Servern mit Web und DB. Per‑Connection‑Buffer wie sort_buffer_size, join_buffer_size und read‑Buffer skaliere ich konservativ, damit viele gleichzeitige Verbindungen nicht zu RAM‑Stürmen führen. tmp_table_size und max_heap_table_size setze ich so, dass unwichtige Operationen nicht riesige In‑Memory‑Tabellen anfordern. Für weitere Stellschrauben finde ich unter MySQL-Performance hilfreiche Denkanstöße. Entscheidend bleibt: Ich stelle lieber etwas knapper ein und messe, als blind aufzudrehen und Fragmentierung plus Swap zu riskieren.

InnoDB im Detail: Rebuild-Strategien und Pool-Instanzen

Um MySQL intern „kompakter“ zu halten, plane ich regelmäßige Rebuilds für Tabellen mit starkem Write‑Anteil. Ein gezieltes OPTIMIZE TABLE (oder ein Online‑Rebuild per ALTER) führt Daten und Indexe zusammen und reduziert Data_free. Dabei wähle ich Zeitfenster mit niedriger Last, da Rebuilds I/O‑intensiv sind. Die Option innodb_file_per_table halte ich aktiv, weil sie per‑Tabelle kontrollierte Rebuilds erlaubt und das Risiko verringert, dass einzelne „Problemkinder“ die gesamte Tablespace‑Datei fragmentieren.

Ich nutze mehrere Buffer‑Pool‑Instanzen (innodb_buffer_pool_instances) in Relation zur Poolgröße und CPU‑Kernen, um interne Latches zu entlasten und Zugriffe zu verteilen. Das verbessert nicht nur Parallelität, sondern glättet auch die Allokationsmuster im Pool. Zusätzlich prüfe ich die Größe der Redo‑Logs und die Aktivität der Purge‑Threads, denn aufgebaute History kann Speicher und I/O binden, was Fragmentierung auf OS‑Ebene verstärkt. Wichtig bleibt: Einstellungen schrittweise ändern, messen, und nur beibehalten, wenn Latenzen und Fehlerraten tatsächlich sinken.

Swap vermeiden: Kernel-Settings und NUMA

Sobald Linux aktiv swappt, steigen Antwortzeiten um Größenordnungen, weil RAM‑Zugriffe zu langsamen I/O werden. Ich senke vm.swappiness deutlich, damit der Kernel physischen RAM länger nutzt. Auf Multi‑CPU‑Hosts prüfe ich NUMA‑Topologie und aktiviere bei Bedarf Interleaving, um ungleichmäßige Speicherauslastung zu mindern. Für Hintergründe und Hardware‑Einfluss hilft mir die Perspektive zur NUMA-Architektur. Zusätzlich plane ich Sicherheitsreserven für Pagecache, denn ein ausgehungerter Cache beschleunigt die Fragmentierung der gesamten Maschine.

Transparent Huge Pages, Overcommit und Allocator-Wahl

Transparent Huge Pages (THP) können bei Datenbanken zu Latenzspitzen führen, weil das Zusammenführen/Spalten großer Seiten zur Unzeit passiert. Ich stelle THP auf „madvise“ oder deaktiviere es, wenn MySQL unter Last zu zögerlich reagiert. Gleichzeitig beachte ich Overcommit: Mit einer zu großzügigen vm.overcommit_memory‑Konfiguration riskiert man OOM‑Kills genau dann, wenn Fragmentierung große zusammenhängende Blöcke rar macht. Ich bevorzuge konservative Overcommit‑Einstellungen und prüfe regelmäßig Kernel‑Logs auf Anzeichen von Memory Pressure.

Auch die Allocator‑Wahl auf Systemebene lohnt einen Blick. glibc‑malloc, jemalloc oder tcmalloc verhalten sich hinsichtlich Fragmentierung unterschiedlich. Ich teste Alternativen stets isoliert, messe RSS‑Verlauf und Latenzen und rolle Änderungen nur aus, wenn die Metriken unter Real‑Traffic stabil bleiben. Der Nutzen variiert stark je nach Workload, Extension‑Mix und OS‑Version.

Fragmentierung erkennen: Metriken und Hinweise

Ich achte auf langsam steigende Basislinien bei RAM, mehr 5xx‑Antworten unter Last und Verzögerungen bei Admin‑Aktionen. Ein Blick auf PM‑Statistiken von PHP‑FPM zeigt, ob Children an Limits stoßen oder zu lange leben. In MySQL prüfe ich Hit‑Ratios, temporäre Tabellen auf Disk und Data_free pro Tabelle. Parallel helfen OS‑Metriken wie Page Faults, Swap‑In/Out und Memory‑Fragmentation‑Indikatoren je nach Kernel‑Version. Wer diese Signale zusammenführt, erkennt Muster früh und kann Maßnahmen geplant einsteuern.

Monitoring vertiefen: Wie ich Signale zusammenführe

Ich korreliere Applikationsmetriken (p95/p99‑Latenzen, Fehlerquoten) mit Prozessmetriken (RSS je FPM‑Worker, mysqld‑Speicher) und OS‑Werten (Pagecache, Slab, Major Faults). In PHP‑FPM nutze ich das Status‑Interface für Queue‑Längen, aktive/spawned Children und die Lebenszeit der Worker. In MySQL beobachte ich Created_tmp_disk_tables, Handler_write/Handler_tmp_write, sowie Buffer‑Pool‑Misses. Ergänzend schaue ich auf Prozess‑Maps (pmap/smaps), um herauszufinden, ob viele kleine Arenen entstanden sind. Wichtig ist für mich die Trendorientierung: nicht der einzelne Peak, sondern die schleichende Verschiebung über Stunden/Tage entscheidet, ob Fragmentierung zur echten Gefahr wird.

Praktische Routine: Wartung und Datenpflege

Ich räume regelmäßig Daten auf: abgelaufene Sessions, alte Logs, unnötige Revisionen und verwaiste Caches. Für stark veränderliche Tabellen plane ich gezielte OPTIMIZE‑Fenster, um fragmentierte Seiten zusammenzuführen. Große Importjobs oder Cron‑Wellen verteile ich zeitlich, damit nicht alle Prozesse gleichzeitig maximale Buffer anfordern. Bei wachsenden Projekten trenne ich Web und DB frühzeitig, um speicherhungrige Muster zu isolieren. Diese Disziplin hält den Arbeitsspeicher zusammenhängender und reduziert das Risiko von Burst‑Latenzen.

Größen sauber berechnen: Limits und Pools dimensionieren

Ich bestimme pm.max_children ausgehend vom tatsächlich verfügbaren RAM für PHP. Dafür messe ich den durchschnittlichen RSS eines Workers unter Real‑Last (inklusive Erweiterungen und OPcache) und addiere Sicherheitsaufschläge für Peaks. Beispiel: Auf einem 16‑GB‑Host reserviere ich 4‑6 GB für OS, Pagecache und MySQL. Bleiben 10 GB für PHP; bei 150 MB je Worker ergibt das theoretisch 66 Children. Ich setze pm.max_children in der Praxis auf ~80‑90% dieses Werts, um Headroom für Spikes zu lassen, also etwa 52–58. pm.max_requests wähle ich so, dass Worker vor spürbarer Fragmentierung recycelt werden (oft im Bereich 500–2.000, abhängig vom Code‑Mix).

Für MySQL rechne ich den Buffer Pool aus der aktiven Datengröße, nicht aus der gesamten DB‑Größe. Wichtige Tabellen und Indexe sollten hinein passen, aber der OS‑Cache braucht Luft für Binlogs, Sockets und statische Assets. Per‑Connection‑Buffer kalkuliere ich mit der maximalen realistischen Parallelität. Wenn 200 Verbindungen möglich sind, dimensioniere ich nicht so, dass in Summe mehrere Gigabyte pro Connection explodieren, sondern setze harte Grenzen, die auch unter Peak nicht swap‑gefährlich sind.

Queues, Bildverarbeitung und Nebenjobs entkoppeln

Viele Fragmentierungsprobleme entstehen, wenn Nebenjobs dieselben Pools wie Frontend‑Requests beanspruchen. Für Exporte, Crawls, Bildkonvertierungen oder Suchindex‑Updates nutze ich getrennte FPM‑Pools oder CLI‑Jobs mit klaren ulimit‑Grenzen. Bildoperationen mit GD/Imagick begrenze ich zusätzlich durch passende Resource‑Limits, damit einzelne riesige Konvertierungen nicht den gesamten Adressraum „zerhacken“. Jobs plane ich zeitlich versetzt und gebe ihnen eigene Concurrency‑Limits, damit sie den Frontend‑Pfad nicht zerdrücken.

Container und Virtualisierung: Cgroups, OOM und Ballon-Effekte

In Containern beobachte ich Memory Limits und den OOM‑Killer besonders genau. Fragmentierung lässt Prozesse früher an Cgroup‑Grenzen laufen, obwohl im Host noch RAM frei wäre. Ich richte pm.max_children strikt am Container‑Limit aus und halte genug Reserve, um Peaks abzufedern. Swapping innerhalb von Containern (oder auf dem Host) vermeide ich, weil die zusätzliche Indirektion Latenzspitzen verstärkt. In VMs prüfe ich Ballooning und KSM/UKSM; aggressives Deduplizieren spart zwar RAM, kann aber zusätzliche Latenz verursachen und das Fragmentierungsbild verfälschen.

Kurze Checkliste ohne Bulletpoints

Ich setze zuerst ein realistisches memory_limit pro Site und beobachte, wie sich die Peak‑Nutzung über Tage verhält. Danach tune ich PHP‑FPM mit passenden pm‑Werten und einem sinnvollen pm.max_requests, damit fragmentierte Worker planmäßig gehen. Für MySQL fokussiere ich auf eine angemessene Buffer‑Pool‑Größe und konservative per‑Connection‑Buffer statt pauschaler Vergrößerungen. Kernel‑seitig senke ich Swappiness, prüfe NUMA‑Einstellungen und halte Reserven für den Pagecache frei. Abschließend evaluiere ich Tabellen mit Data_free‑Auffälligkeiten und plane Optimierungen außerhalb des Tagesgeschäfts.

Kurz zusammengefasst: Was zählt im Betrieb

Die größte Wirkung gegen Memory‑Fragmentierung erreiche ich mit konsequentem Recycling der PHP‑FPM‑Worker, maßvollen Limits und sauberen Pools. MySQL profitiert von vernünftigen Größen für Buffer Pool und per‑Connection‑Buffer sowie von aufgeräumten Tabellen. Swap vermeide ich proaktiv, indem ich Swappiness und NUMA beachte und freien RAM für den Dateicache reserviere. Monitoring deckt schleichende Muster auf, bevor Nutzer es merken, und erlaubt ruhige, geplante Eingriffe. Wer diese Stellhebel diszipliniert bedient, hält PHP und MySQL schneller, verlässlicher und kosteneffizient ohne sofortige Hardware‑Upgrades.

Aktuelle Artikel