...

Memory Fragmentation i webhosting: Ydelsesfælde for PHP & MySQL

Hukommelsesfragmentering I webhosting bremser PHP‑FPM og MySQL, selvom der tilsyneladende er nok RAM til rådighed, fordi hukommelsen opdeles i mange små blokke, og større allokeringer mislykkes. Jeg viser i praksis, hvordan fragmentering gør forespørgsler dyrere, udløser swap, og hvorfor målrettet tuning af PHP og MySQL øger indlæsningstider, pålidelighed og skalering markant.

Centrale punkter

  • PHP-FPM Genbrug: Genstart processer regelmæssigt via pm.max_requests
  • Buffer dosering: Hold MySQL-Per-Connection-Buffer konservativ
  • Bytte Undgå: Sænk swappiness, vær opmærksom på NUMA
  • Tabeller Vedligeholdelse: Kontroller Data_free, optimer målrettet
  • Overvågning Udnyt: Se tendenser tidligt og handle

Hvad betyder hukommelsesfragmentering i den daglige hosting?

I hosting mødes Fragmentering langvarige processer, der konstant anmoder om og frigiver hukommelse, hvilket skaber huller i adresserummet. Selvom den samlede mængde ledig RAM virker stor, mangler der sammenhængende blokke til større allokeringer, hvilket forsinker allokeringsforsøg. Jeg ser dette i PHP‑FPM-arbejdere og i mysqld, som efter flere timer altid virker mere „oppustede“. Effekten gør hver anmodning minimalt dyrere og øger svarstiderne mærkbart under belastning. Dette gør, at spidsbelastninger som salgsaktioner eller sikkerhedskopieringer bliver en bremseklods, selvom CPU og netværk forbliver upåvirkede.

Hvorfor PHP-FPM skaber fragmentering

Hver PHP‑FPM‑Worker indlæser kode, plugins og data i sin egen Adresserum, behandler forskellige anmodninger og efterlader spredte huller, når de frigives. Over tid vokser processer og frigiver intern hukommelse, men ikke nødvendigvis til operativsystemet, hvilket øger fragmenteringen. Forskellige scripts, importjob og billedbehandling forstærker denne blanding og fører til skiftende allokeringsmønstre. Jeg observerer dette som en snigende stigning i RAM, selvom belastningen og trafikken virker konstant. Uden genbrug forsinker denne interne fragmentering allokeringen og gør det vanskeligt at planlægge ved høje besøgsantal.

Mærkbare konsekvenser for opladningstider og pålidelighed

Fragmenterede processer skaber mere Overhead i hukommelsesadministrationen, hvilket udmønter sig i langsommere admin-backends og tøvende checkouts. Især WordPress-butikker eller store CMS-instanser reagerer trægt, når mange samtidige anmodninger møder fragmenterede arbejdere. Dette resulterer i timeouts, 502/504-fejl og øgede gentagelser på NGINX- eller Apache-siden. Jeg aflæser sådanne situationer i målinger som spidsbelastninger i responstiden, stigende RAM-baselinje og pludselig stigende swap-brug. Hvis man ignorerer dette, går man glip af ydeevne, forringer brugeroplevelsen og øger afbrydelsesraten i kritiske funnels.

Indstil PHP-FPM korrekt: Begrænsninger, puljer, genbrug

Jeg satser på realistiske Grænser, separate puljer og konsekvent genbrug for at begrænse fragmentering. pm.max_requests afslutter jeg, så arbejdere regelmæssigt starter forfra uden at forstyrre besøgende. For trafikprofiler med belastningsspidser passer pm = dynamic ofte bedre, mens pm = ondemand sparer RAM på rolige websteder. Jeg holder memory_limit pr. websted bevidst moderat og tilpasser det med henblik på reelle scripts; emnet giver en indgang til dette. PHP-hukommelsesgrænse. Derudover opdeler jeg projekter med stor belastning i separate puljer, så en hukommelsessluger ikke påvirker alle websteder.

OPcache, preloading og PHP-allocator i fokus

For at mindske fragmentering i PHP-processen satser jeg på en korrekt dimensioneret OPcache. En generøs, men ikke overdreven opcache.memory_consumption og tilstrækkelige interne strenge reducerer gentagne allokeringer pr. anmodning. Jeg overvåger hitrate, spild og restkapacitet; hvis spildet stiger over tid, er en planlagt genindlæsning bedre end at lade arbejdere vokse ukontrolleret. Preloading kan holde hot-code stabilt i hukommelsen og dermed udjævne allokeringsmønstre, forudsat at kodebasen er forberedt i overensstemmelse hermed. Derudover holder jeg øje med Allocator-valg: Afhængigt af distributionen fungerer PHP‑FPM og udvidelser med forskellige Malloc‑implementeringer. Alternative Allocator som jemalloc reducerer i nogle opsætninger fragmenteringen mærkbart. Jeg implementerer dog kun sådanne ændringer efter at have testet dem, da debugging, DTrace/eBPF‑profilering og hukommelsesdumps reagerer forskelligt afhængigt af Allocator.

Til hukommelseskrævende opgaver som billedbehandling eller eksport foretrækker jeg separate puljer med snævrere grænser. På den måde vokser hovedpuljen ikke ukontrolleret, og fragmentering forbliver isoleret. Desuden begrænser jeg hukommelseskrævende udvidelser (f.eks. via miljøvariabler) og bruger backpressure: Anmodninger, der kræver store buffere, begrænses eller flyttes til asynkrone køer i stedet for at overbelaste alle arbejdere på samme tid.

Forstå MySQL-hukommelse: Buffer, forbindelser, tabeller

I MySQL skelner jeg mellem globale Buffer som InnoDB-bufferpoolen, bufferen pr. forbindelse og midlertidige strukturer, der kan vokse for hver operation. For store værdier fører til eksplosiv RAM-behov og mere fragmentering på OS-niveau ved høj forbindelsesbelastning. Derudover bliver tabeller ødelagt af opdateringer/sletninger og efterlader Data_free-andele, som gør det sværere at udnytte bufferpoolen. Derfor kontrollerer jeg regelmæssigt størrelse, hit-ratios og antallet af midlertidige disktabeller. Følgende oversigt hjælper mig med at identificere typiske symptomer og afveje mulige foranstaltninger.

Symptom Sandsynlig årsag Mål
RAM stiger konstant, swap begynder For stor bufferpool eller mange buffere pr. forbindelse Begræns pool til passende størrelse, sænk via forbindelsesbuffer
Mange langsomme sorteringer/sammenføjninger Manglende indekser, overdrevne sort/join-buffere Kontroller indekser, hold sort/join konservativt
Stor Data_free i tabeller Store opdateringer/sletninger, sider splittet Målrettet OPTIMIZE, arkivering, strømlinjeformning af skemaer
Spidsbelastninger ved midlertidige disktabeller For lille tmp_table_size eller uegnede forespørgsler Hæv værdierne moderat, omstrukturér forespørgsler

MySQL Memory Tuning: Vælg størrelser i stedet for at overdrive

Jeg vælger InnoDB-bufferpuljen, så Operativsystem har tilstrækkelig kapacitet til filsystemcache og tjenester, især på kombiservere med web og DB. Per-forbindelsesbuffer som sort_buffer_size, join_buffer_size og read-buffer skalerer jeg konservativt, så mange samtidige forbindelser ikke fører til RAM-storm. tmp_table_size og max_heap_table_size indstiller jeg, så uvigtige operationer ikke kræver enorme in-memory-tabeller. For yderligere justeringsmuligheder finder jeg under MySQL-ydeevne Hjælpsomme tanker. Det afgørende er stadig: Jeg foretrækker at indstille lidt mere sparsomt og måle, frem for at skrue op i blinde og risikere fragmentering og swap.

InnoDB i detaljer: Genopbygningsstrategier og poolinstanser

For at holde MySQL internt „mere kompakt“ planlægger jeg regelmæssige Genopbygninger til tabeller med stor skriveandel. En målrettet OPTIMIZE TABLE (eller en online-genopbygning via ALTER) samler data og indekser og reducerer Data_free. Jeg vælger tidsvinduer med lav belastning, da genopbygninger er I/O-intensive. Indstillingen innodb_file_per_table Jeg holder den aktiv, fordi den tillader tabelstyrede genopbygninger og mindsker risikoen for, at enkelte „problembørn“ fragmenterer hele tablespace-filen.

Jeg bruger flere Bufferpool-instanser (innodb_buffer_pool_instances) i forhold til poolstørrelse og CPU-kerner for at aflaste interne latches og fordele adgangen. Dette forbedrer ikke kun paralleliteten, men udjævner også allokeringsmønstrene i poolen. Derudover kontrollerer jeg størrelsen af redo-logfilerne og aktiviteten af purge-trådene, da opbygget historik kan binde hukommelse og I/O, hvilket øger fragmenteringen på OS-niveau. Det er vigtigt at ændre indstillingerne gradvist, måle dem og kun beholde dem, hvis latenstider og fejlrater faktisk falder.

Undgå swap: Kernel-indstillinger og NUMA

Så snart Linux begynder at swappe aktivt, stiger responstiderne med Størrelsesordener, fordi RAM-adgang bliver til langsom I/O. Jeg sænker vm.swappiness betydeligt, så kernen bruger fysisk RAM længere. På multi-CPU-værter kontrollerer jeg NUMA-topologi og aktiverer interleaving efter behov for at reducere ujævn hukommelsesudnyttelse. For baggrundsinformation og hardwarepåvirkning hjælper perspektivet på NUMA-arkitektur. Derudover planlægger jeg sikkerhedsreserver til pagecache, da en udtømt cache fremskynder fragmenteringen af hele maskinen.

Transparente store sider, overforpligtelse og valg af allokator

Gennemsigtige store sider (THP) kan føre til latenstoppe i databaser, fordi sammenlægning/opdeling af store sider sker på et uheldigt tidspunkt. Jeg indstiller THP til „madvise“ eller deaktiverer det, hvis MySQL reagerer for langsomt under belastning. Samtidig bemærker jeg Overforpligtelse: Med en for generøs vm.overcommit_memory-konfiguration risikerer man OOM-kills netop når fragmentering gør store sammenhængende blokke sjældne. Jeg foretrækker konservative overcommit-indstillinger og tjekker regelmæssigt kernel-logs for tegn på memory pressure.

Den Allocator-valg på systemniveau er værd at kigge på. glibc‑malloc, jemalloc eller tcmalloc opfører sig forskelligt med hensyn til fragmentering. Jeg tester altid alternativer isoleret, måler RSS‑forløb og latenstider og implementerer kun ændringer, hvis målingerne forbliver stabile under reel trafik. Fordelen varierer meget afhængigt af arbejdsbyrde, udvidelsesmix og OS‑version.

Genkende fragmentering: Metrikker og tip

Jeg holder øje med langsomt stigende basislinjer ved RAM, flere 5xx-svar under belastning og forsinkelser ved administratorhandlinger. Et kig på PM-statistikker fra PHP-FPM viser, om børn støder på begrænsninger eller lever for længe. I MySQL tjekker jeg hit-ratios, midlertidige tabeller på disken og Data_free pr. tabel. Parallelt hjælper OS-metrikker som Page Faults, Swap-In/Out og Memory-Fragmentation-indikatorer afhængigt af kernelversionen. Hvis man samler disse signaler, kan man tidligt genkende mønstre og planlægge foranstaltninger.

Uddyb overvågningen: Hvordan jeg samler signaler

Jeg korrelerer Applikationsmålinger (p95/p99-latenser, fejlprocenter) med procesmetrikker (RSS pr. FPM-arbejder, mysqld-hukommelse) og OS-værdier (Pagecache, Slab, Major Faults). I PHP‑FPM bruger jeg statusgrænsefladen til kø-længder, aktive/spawned Children og arbejdstagernes levetid. I MySQL observerer jeg Created_tmp_disk_tables, Handler_write/Handler_tmp_write samt Buffer‑Pool‑Misses. Derudover ser jeg på proceskort (pmap/smaps) for at finde ud af, om der er opstået mange små arenaer. Det er vigtigt for mig at trendorientering: Det er ikke den enkelte spidsbelastning, men den langsomme forskydning over timer/dage, der afgør, om fragmentering bliver en reel fare.

Praktisk rutine: Vedligeholdelse og datapleje

Jeg rydder regelmæssigt op Data på: udløbne sessioner, gamle logfiler, unødvendige revisioner og forældede caches. For tabeller, der ændrer sig meget, planlægger jeg målrettede OPTIMIZE-vinduer for at samle fragmenterede sider. Store importopgaver eller cron-bølger fordeler jeg tidsmæssigt, så ikke alle processer kræver maksimal buffer på samme tid. Ved voksende projekter adskiller jeg web og DB tidligt for at isolere mønstre, der kræver meget hukommelse. Denne disciplin holder arbejdshukommelsen mere sammenhængende og reducerer risikoen for burst-latenser.

Beregn størrelser korrekt: Dimensioner grænser og puljer

Jeg bestemmer pm.max_children ud fra den faktisk tilgængelige RAM til PHP. Til det måler jeg gennemsnitlig RSS af en worker under reel belastning (inklusive udvidelser og OPcache) og tilføjer sikkerhedsmargener for spidsbelastninger. Eksempel: På en 16 GB-host reserverer jeg 4-6 GB til OS, pagecache og MySQL. Der er 10 GB tilbage til PHP; med 150 MB pr. worker giver det teoretisk 66 børn. I praksis sætter jeg pm.max_children til ~80-90% af denne værdi for at give plads til spidsbelastninger, altså ca. 52-58. pm.max_anmodninger Jeg vælger, at arbejdere genbruges, før der opstår mærkbar fragmentering (ofte i området 500-2.000, afhængigt af kodeblandingen).

For MySQL beregner jeg Bufferpulje fra den aktive datastørrelse, ikke fra den samlede DB-størrelse. Vigtige tabeller og indekser skal kunne være der, men OS-cachen har brug for plads til binlogs, sockets og statiske aktiver. Per-connection-buffer beregner jeg med den maksimale realistiske parallelitet. Hvis 200 forbindelser er mulige, dimensionerer jeg ikke, så der i alt eksploderer flere gigabyte pr. forbindelse, men sætter hårde grænser, der heller ikke under peak er farlige for swap.

Afkobling af køer, billedbehandling og sideopgaver

Mange fragmenteringsproblemer opstår, når bijob de samme puljer som frontend-anmodninger. Til eksport, crawling, billedkonvertering eller opdatering af søgeindeks bruger jeg separate FPM-puljer eller CLI-jobs med klare ulimit-grænser. Jeg begrænser billedoperationer med GD/Imagick yderligere ved hjælp af passende ressourcebegrænsninger, så enkelte store konverteringer ikke „hakker“ hele adresserummet. Jeg planlægger job med tidsforskydning og giver dem deres egne samtidighedsbegrænsninger, så de ikke overbelaster frontend-stien.

Containere og virtualisering: Cgroups, OOM og balloneffekter

I containere observerer jeg Hukommelsesgrænser og OOM-killeren særligt nøje. Fragmentering får processer til at køre tidligere ved Cgroup-grænser, selvom der stadig er ledig RAM på værten. Jeg tilpasser pm.max_children strengt til containergrænsen og holder nok reserve til at afbøde spidsbelastninger. Jeg undgår swapping inden for containere (eller på værten), fordi den ekstra indirektion forstærker latenstoppe. I VM'er tjekker jeg balloning og KSM/UKSM; aggressiv deduplikering sparer RAM, men kan forårsage ekstra latenstid og forvrænge fragmenteringsbilledet.

Kort tjekliste uden punktopstilling

Jeg sætter først et realistisk memory_limit pr. websted og observerer, hvordan spidsbelastningen udvikler sig over flere dage. Derefter tilpasser jeg PHP-FPM med passende pm-værdier og en fornuftig pm.max_requests, så fragmenterede arbejdere kører som planlagt. For MySQL fokuserer jeg på en passende bufferpoolstørrelse og konservative per-connection-buffere i stedet for generelle forøgelser. På kernelsiden sænker jeg swappiness, tjekker NUMA-indstillinger og holder reserver til pagecache fri. Til sidst evaluerer jeg tabeller med Data_free-afvigelser og planlægger optimeringer uden for den daglige drift.

Kort sagt: Hvad tæller i virksomheden

Jeg opnår den største effekt mod hukommelsesfragmentering ved konsekvent at genanvendelse PHP-FPM-arbejderen, moderate grænser og rene puljer. MySQL drager fordel af fornuftige størrelser for bufferpuljen og per-forbindelsesbufferen samt ryddelige tabeller. Jeg undgår proaktivt swap ved at være opmærksom på swappiness og NUMA og reservere ledig RAM til filcachen. Overvågning afslører snigende mønstre, før brugerne bemærker det, og muliggør rolige, planlagte indgreb. Hvis man bruger disse håndtag disciplineret, kan man holde PHP og MySQL hurtigere, mere pålidelige og omkostningseffektive uden øjeblikkelige hardwareopgraderinger.

Aktuelle artikler