CPU en geheugen beperken met systemd op Linux
Beperk resourcegebruik van processen met MemoryMax en CPUQuota in systemd. Voorkom dat een op hol geslagen proces uw VPS bevriest door cgroup v2 limieten correct in te stellen.
Beperk procesgeheugen en CPU met een systemd drop-in
U beperkt het geheugen- en CPU-gebruik van een proces op een Linux VPS door enkele regels toe te voegen aan de unit die het proces uitvoert. MemoryMax= is de harde limiet voor geheugen. CPUQuota= is de limiet voor processortijd. Beide worden afgedwongen door cgroup v2 (control groups, versie 2), de kernel-functionaliteit die systemd al gebruikt om elk proces op de server bij te houden.
sudo systemctl edit myapp.serviceHiermee opent u een drop-in-bestand met instructies in de vorm van commentaar. Voeg het volgende toe boven deze regels:
[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMaxsystemctl show moet uw getallen herhalen in de eigen eenheden van de kernel: MemoryMax=805306368 en CPUQuotaPerSecUSec=800ms. Als de output MemoryMax=infinity is, is de drop-in niet geladen. Controleer of het bestand is opgeslagen op /etc/systemd/system/myapp.service.d/override.conf en of het begint met de [Service]-header. Een instellingsregel zonder sectie erboven zorgt er namelijk voor dat systemd Assignment outside of section. Ignoring. logt en de service start zonder enige beperkingen.
De rest van deze handleiding beschrijft hoe u deze getallen kiest en wat er nog mis kan gaan zodra ze zijn ingesteld.
Waarom een op hol geslagen proces een VPS bevriest die niet vol zit
Een proces dat een harde geheugenlimiet bereikt, sterft binnen ongeveer een seconde en de service start opnieuw op. Dat is het gunstige scenario. Het ongunstige scenario is dat er niets sterft: de server reageert op ping, SSH accepteert de verbinding, maar de shell-prompt verschijnt nooit. De machine is actief en bezet, maar het werk is nutteloos.
Dit is het mechanisme, aangezien het niet direct voor de hand ligt. Wanneer het vrije geheugen laag is, claimt de kernel pagina's terug in plaats van nieuwe toe te wijzen. De goedkoopste pagina's om terug te claimen zijn bestand-gebaseerd, en de page cache bevat de uitvoerbare code van alles wat draait. De kernel verwijdert dus de tekstpagina's van sshd, en de volgende instructie die sshd uitvoert, is een page fault die die bytes terug moet lezen van de opslag. Elk proces wacht uiteindelijk op de schijf in plaats van te draaien. Dezelfde pagina's verlaten het geheugen en komen in een lus terug; dit wordt thrashing genoemd.
Twee factoren maken dit op een VPS erger dan op een laptop. Opslag is vaak netwerkgebonden of gedeeld, waardoor elke fout meer milliseconden kost dan bij een lokaal NVMe-apparaat. Bovendien meet de kernel geen tijd, maar falen: zolang het terugclaimen een pagina oplevert, hoe langzaam ook, gelooft de kernel dat er vooruitgang wordt geboekt en roept deze de out of memory (OOM) killer niet aan. Een server kan vele minuten in die staat verkeren voordat er iets wordt beëindigd.
U kunt dit proces observeren. De kernel exporteert pressure stall information (PSI) op Linux 4.20 en nieuwer:
cat /proc/pressure/memory
cat /proc/pressure/iosome avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233De regel full is de relevante regel. full avg10=48.15 betekent dat in de afgelopen tien seconden 48% van de tijd elke uitvoerbare taak op de server stilstond in afwachting van geheugenbewerkingen, waardoor er niets werd uitgevoerd. Een gezonde server leest bijna nul op full. Boven de 10 voelt het voor een mens traag aan, en 40 of hoger is de staat die mensen omschrijven als bevroren.
Dit is ook de reden waarom een limiet op zichzelf geen garantie is. Een unit die onder MemoryHigh= wordt gehouden, wordt vertraagd in plaats van beëindigd, dus blijft deze in leven en traag, en niets start de unit opnieuw op omdat deze vanuit het perspectief van systemd nooit is gefaald. Een gelimiteerde unit die nog steeds mag swappen, genereert lees- en schrijfacties die aan die unit worden toegerekend, maar worden afgehandeld door één gedeeld apparaat, waardoor /proc/pressure/io voor elke andere service op de server kan stijgen. Limieten bepalen wie betaalt voor een tekort, maar ze kunnen geen capaciteit creëren.
Controleer of uw VPS cgroup v2 gebruikt
stat -fc %T /sys/fs/cgroupcgroup2fs is de uniforme hiërarchie die vereist is voor alle onderstaande instellingen. tmpfs betekent dat de server is opgestart met de oudere v1-layout, waarbij MemoryHigh= en MemorySwapMax= niet bestaan en het OOM-gedrag per unit anders is. Ubuntu 22.04 en nieuwer, evenals Debian 11 en nieuwer, gebruiken standaard v2. Een oude image, of een kernel die is opgestart met systemd.unified_cgroup_hierarchy=0, doet dit niet.
Op cgroup v2 schakelt systemd standaard geheugenadministratie in voor elke unit, waardoor de cijfers al beschikbaar zijn:
systemd-cgtop -mDit geeft een overzicht van cgroups gesorteerd op geheugengebruik. Dit is de snelste manier om te achterhalen "wat deze server verbruikt" terwijl deze nog reageert. Als de server nieuw is, dient het account- en firewallbeheer uit de eerste tien minuten op een nieuwe VPS vooraf te gaan aan deze stappen.
MemoryHigh throttles. MemoryMax kills.
Het verschil tussen deze twee geheugeninstellingen bepaalt hoe een foutmelding eruitziet.
MemoryHigh=is een zachte limiet. Boven deze waarde haalt de kernel agressief geheugen terug uit die cgroup en vertraagt het de toewijzingen bewust. Het gebruik kan nog steeds boven dit getal uitkomen en er wordt niets beëindigd.MemoryMax=is een harde limiet. Wanneer een toewijzing hierbinnen niet kan worden voldaan, voert de OOM killer een actie uit binnen die cgroup en beëindigt een van de processen van die unit zelf.
Dat tweede punt is de werkelijke reden om MemoryMax= in te stellen voor alles wat u niet volledig vertrouwt. Zonder limiet is een tekort een probleem voor de gehele server, en de globale OOM killer kiest zijn slachtoffer op basis van oom_score, wat meestal neerkomt op het grootste proces. Het grootste proces is doorgaans uw database, niet het script dat geheugen lekt. Met een limiet vindt de beëindiging plaats binnen de unit die het probleem veroorzaakte.
Stel beide in, waarbij MemoryHigh= ongeveer 20 tot 30 procent onder MemoryMax= ligt. De ruimte daartussen is een waarschuwingszone: een langzame lek overschrijdt High en uit zich als een service die traag wordt, terwijl een plotselinge piek direct door Max gaat en de service beëindigt.
Percentage-waarden worden berekend op basis van het geïnstalleerde fysieke geheugen, dus MemoryMax=25% op een 4 GB-plan is 1 GB en blijft een kwart van de server nadat u het plan heeft aangepast. MemorySwapMax=0 houdt die unit volledig buiten de swap, wat een langdurige vertraging verandert in een snelle, duidelijke beëindiging.
Enkele services laten u hun behoefte vooraf bepalen in plaats van deze te meten: een Ollama-unit bepaalt de grootte van zijn KV-cache op basis van het contextvenster dat u opgeeft, dus lees wat het verhogen van num_ctx kost aan RAM voordat u een plafond voor een dergelijke unit kiest.
Een limiet vereist een herstartbeleid, anders blijft u na een beëindiging achter met een gestopte service.
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sStartLimit* hoort in [Unit] en Restart= in [Service]. Plaats ze in de verkeerde sectie en systemd negeert ze. Vijf herstarts in vijf minuten duidt op een lek in plaats van een tijdelijke storing; na die vijf keer geeft systemd het op en laat de unit in de status 'failed'. Dit is de status die u later wilt aantreffen in plaats van een crash-loop die het probleem maskeert.
CPU beperken met CPUQuota of delen met CPUWeight
CPUQuota= neemt een percentage van de beschikbare tijd op één CPU. CPUQuota=50% staat gelijk aan de helft van één core. CPUQuota=200% is het equivalent van twee cores, waarbij de unit deze capaciteit over zoveel threads mag verdelen als gewenst. Op een plan met 2 vCPU's is CPUQuota=200% de volledige machine.
CPUWeight= is voor de meeste services de betere standaardinstelling. Dit is een relatief aandeel van 1 tot 10000, waarbij de standaardwaarde van de kernel 100 is. Dit is alleen merkbaar wanneer er sprake is van concurrentie: een back-up-taak op CPUWeight=20 wijkt onder belasting voor een webserver op 100, maar gebruikt nog steeds de volledige machine wanneer deze inactief is. Een harde quota verspilt die inactieve capaciteit.
Wees realistisch over wat een CPU-limiet oplevert. Een CPU-intensief proces laat Linux zelden vastlopen, omdat de scheduler tijd blijft toewijzen aan alle processen. Geheugengebruik is wat een server onderuit haalt. Gebruik CPUQuota= wanneer u een voorspelbaar plafond wilt, bijvoorbeeld bij een build-proces of een agent die anders een uur lang op volle kracht zou draaien. Het dimensioneren van dergelijke workloads is een vraagstuk op zich, dat wordt behandeld in hoeveel RAM en CPU een VPS voor een coding agent nodig heeft.
Als de CPU als bezet wordt aangegeven terwijl geen van uw processen veel uitvoert, kan de oorzaak aan de andere kant van de hypervisor liggen. Dit is CPU steal time door een luidruchtige buur, en geen enkele quota die u instelt zal daar verandering in brengen.
TasksMax voorkomt een fork-loop
TasksMax= is het aantal processen en threads dat een unit mag bevatten. Threads tellen mee, dus een Java- of Go-service heeft meer ruimte nodig dan de proceslijst doet vermoeden. Het is de goedkoopste bescherming tegen een script dat in een lus fork-opdrachten uitvoert, omdat de fork binnen de unit faalt in plaats van dat de server zonder proces-ID's komt te zitten.
TasksMax=128Wanneer een unit de limiet bereikt, logt de kernel een regel met de naam van de cgroup:
cgroup: fork rejected by pids controller in /system.slice/myapp.serviceHet programma zelf rapporteert meestal fork: retry: Resource temporarily unavailable. Controleer wat de manager standaard toepast met systemctl show -p DefaultTasksMax.
Beperk een eenmalige taak met systemd-run
U heeft geen unit-bestand nodig om dit te gebruiken. systemd-run bouwt een tijdelijke unit rondom een enkel commando.
sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh--scope voert het commando uit in uw terminal, nadat Running scope as unit: run-r7c1a....scope is afgedrukt. De output blijft op uw scherm staan en de limieten vervallen zodra het commando is afgesloten. Elke eigenschap van systemd.resource-control werkt na -p.
Laat voor een langdurige taak --scope weg en geef het een naam. Het draait dan op de achtergrond als een tijdelijke service en logt naar de journal:
sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -fDezelfde opties werken met --user wanneer u niet als root bent ingelogd, hoewel uw user manager alleen beschikt over de controllers die eraan zijn gedelegeerd, waardoor een eigenschap kan worden geweigerd. Voer het uit met sudo als dat gebeurt. Wanneer een taak een permanente plek krijgt, verhuizen de instellingen ongewijzigd naar een echte unit: zie een script uitvoeren als een systemd service en timer.
De swap-vraag, eerlijk beantwoord
Swap verandert de aard van het falen in plaats van het te voorkomen.
Zonder swap bereikt een geheugenlek het plafond en sterft er binnen enkele seconden een proces. De uitval is luidruchtig, kort en achteraf eenvoudig terug te lezen in de journal. Met swap schrijft de kernel koude anonieme pagina's naar de schijf en koopt u tijd. Als het proces zou stabiliseren, redt swap u. Als het proces echter op hol slaat, verandert swap een uitval van vijf seconden in een blokkade van twintig minuten. Die blokkade is erger, omdat een gestopt proces u nog een werkende shell laat, terwijl een systeem dat constant naar schijf schrijft (thrashing) dat niet doet.
swapon --show
free -hEen werkbaar midden op een kleine VPS: behoud een bescheiden swap-bestand voor pagina's die eenmalig worden toegewezen en daarna nooit meer worden aangeraakt, en stel MemorySwapMax=0 in op de units die u bereid bent te verliezen. De belangrijke services behouden hun swap. De onvoorspelbare services lopen snel tegen de muur aan en herstarten.
Het verlagen van vm.swappiness is een zwak instrument, en het is nuttig om te weten waarom. Het verschuift enkel de balans tussen het verwijderen van de page cache en het swappen van anonieme pagina's, en beide acties kosten later een schijf-read. Het verandert enkel welke pagina's voor thrashing zorgen, niet of het systeem als geheel gaat thrash-en.
Een vroege OOM-daemon beëindigt processen vóór de blokkade
De kernel wacht tot het vrijmaken van geheugen volledig faalt, en op een kleine VPS is die wachttijd precies het moment waarop u de machine verliest. Twee userspace-daemons sluiten dit gat door zelf het geheugen te monitoren en eerder in te grijpen.
earlyoom houdt het beschikbare geheugen en de vrije swap in de gaten en beëindigt het proces met de hoogste score wanneer een van beide onder een drempelwaarde zakt.
sudo apt install earlyoom
systemctl status earlyoomHet Debian- en Ubuntu-pakket start de service direct na installatie. De opties staan in /etc/default/earlyoom:
EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"-m PERCENT stelt het minimum voor beschikbaar geheugen in en -s PERCENT het minimum voor vrije swap; beide staan standaard op 10 procent. Het tweede getal in elk paar is het SIGKILL-punt: earlyoom verstuurt een SIGTERM zodra u onder de eerste waarde zakt, en vervolgens een SIGKILL onder de tweede waarde, die standaard de helft van de eerste is. Pas wijzigingen toe met sudo systemctl restart earlyoom en lees journalctl -u earlyoom om te zien welk proces werd beëindigd en hoeveel geheugen dat proces in beslag nam.
systemd-oomd is de andere optie. De handleiding beschrijft het als "een systeemservice die cgroups-v2 en pressure stall information (PSI) gebruikt om te monitoren en corrigerende maatregelen te nemen voordat een OOM in de kernelruimte optreedt". Het handelt op basis van volledige cgroups in plaats van individuele processen, waardoor het een unit beëindigt in plaats van een losstaand subproces. Units melden zich aan met ManagedOOMMemoryPressure=kill of ManagedOOMSwap=kill, en de drempelwaarden staan in /etc/systemd/oomd.conf.
systemctl status systemd-oomd
oomctloomctl toont wat er momenteel wordt gemonitord; op een server-image is dit vaak niets, omdat de instelling per unit moet worden geactiveerd. Kies één daemon en houd het daarbij. Het draaien van beide betekent dat twee processen strijden om een slachtoffer te kiezen, waardoor de reden voor een beëindiging lastiger te achterhalen is.
Welke unit was verantwoordelijk?
Begin bij de kernel, aangezien deze elke beëindiging die hij uitvoert, registreert.
journalctl -k --grep "Killed process" --since "2 hours ago"Een beëindiging door de globale OOM-killer ziet er als volgt uit:
Out of memory: Killed process 4127 (node) total-vm:2731084kB, anon-rss:1874232kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:4212kB oom_score_adj:0anon-rss is het geheugen dat het proces in het RAM-geheugen vasthield op het moment van overlijden, in dit geval ongeveer 1,8 GB. Bekijk de naam tussen haakjes met argwaan. Dat is het slachtoffer dat de kernel heeft gekozen; de kernel kiest het grootste proces, wat niet altijd het proces is dat het tekort heeft veroorzaakt.
Een beëindiging door een cgroup-limiet heeft een ander voorvoegsel en het rapport dat erboven wordt afgedrukt, benoemt de cgroup die zijn eigen plafond heeft bereikt:
Memory cgroup out of memory: Killed process 8811 (python3) total-vm:1044320kB, anon-rss:769112kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:1720kB oom_score_adj:0Dat voorvoegsel vormt het grootste deel van de diagnose. Memory cgroup out of memory betekent dat één unit de MemoryMax= heeft bereikt die u eraan heeft toegewezen, terwijl de rest van de server in orde was. Een eenvoudige Out of memory betekent dat de machine als geheel geen geheugen meer had, wat inhoudt dat uw limieten ontbraken of te ruim waren ingesteld om de som te dekken.
Vraag vervolgens aan systemd wat het heeft waargenomen:
systemctl status myapp.service
journalctl -u myapp.service -n 50myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.systemctl status zegt hetzelfde in één regel, als Active: failed (Result: oom-kill).
De cgroup-tellers zijn de derde bron en de enige die throttling registreert, wat nooit een logregel genereert:
cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peaklow 0
high 4213
max 118
oom 12
oom_kill 12high telt hoe vaak de unit over de MemoryHigh= is geduwd en werd vertraagd. max telt hoe vaak de harde limiet werd bereikt en oom_kill telt het aantal daadwerkelijk beëindigde processen. Een grote high met oom_kill 0 is het stille geval van eerder: de service draait, is vertraagd tot een slakkengang en heeft aan niemand een fout gerapporteerd. memory.peak (Linux 5.19 en nieuwer) bevat het hoogste verbruik dat de cgroup heeft bereikt; dit is het getal waartegen u MemoryMax= moet afzetten. Beide bestanden worden gereset wanneer de unit opnieuw opstart, omdat systemd de cgroup opnieuw aanmaakt.
Eén vereiste ligt aan de basis van dit alles. Als /var/log/journal niet bestaat, bevindt het journal zich in het RAM-geheugen en is elke regel verdwenen na de herstart die u nodig had om de server te herstellen.
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-bootsjournalctl --list-boots die meer toont dan de huidige boot betekent dat de geschiedenis nu behouden blijft, waardoor journalctl -k -b -1 u de kernelberichten kan tonen van de boot die is vastgelopen.
Een startpunt voor een kleine VPS
Houd op een 2 GB-abonnement 300 tot 400 MB vrij voor de kernel en de page cache. Zorg dat de limieten samen niet de volledige 2 GB bereiken, omdat elke unit op hetzelfde moment een piek kan vertonen. Geef de belangrijkste service het grootste aandeel en beperk alle speculatieve processen daaromheen.
[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5sHet behouden van een toegangsweg is een extra instelling waard. OOMScoreAdjust=-500 in een drop-in voor ssh.service zorgt ervoor dat de globale OOM-killer uw SSH-daemon veel minder snel als slachtoffer kiest. Dit is het verschil tussen het repareren van de server en het herstarten ervan via het configuratiescherm. Het verandert alleen de keuze van de kernel voor een slachtoffer. Het verkort de vertraging niet.
Containers draaien in hun eigen cgroups, die door de container-runtime worden aangemaakt in plaats van door uw unit-bestanden. Een limiet op docker.service wordt daarom geen limiet voor één container. De equivalenten per container van MemoryMax= en CPUQuota= worden behandeld in geheugen- en CPU-limieten instellen in Docker Compose.
FAQ
Waarom bevriest mijn VPS in plaats van het op hol geslagen proces te beëindigen?
Omdat de kernel voortgang beoordeelt op basis van het vrijgeven van geheugenpagina's, niet op basis van de tijd die dit in beslag neemt. Bij een tekort aan geheugen verwijdert de kernel de page cache, inclusief de uitvoerbare pagina's van draaiende programma's, om deze bij de volgende instructie direct weer in te lezen. Alles wacht op de opslag en technisch gezien is er nog geen toewijzing mislukt, waardoor de OOM killer niet wordt geactiveerd. Controleer /proc/pressure/memory terwijl dit gebeurt: een full avg10 boven 40 betekent dat vrijwel geen enkele taak in de afgelopen tien seconden heeft kunnen draaien. Een userspace-daemon zoals earlyoom beëindigt processen voordat het systeem deze toestand bereikt.
Wat is het verschil tussen MemoryHigh en MemoryMax?
MemoryHigh= is een zachte limiet die het proces vertraagt. De kernel voert agressief geheugenbeheer uit op de unit en remt de toewijzingen af, maar het verbruik kan de limiet overschrijden zonder dat er processen worden beëindigd. MemoryMax= is een harde limiet: een toewijzing die hierbinnen niet kan worden voldaan, activeert de OOM killer binnen de cgroup van die specifieke unit. Hierdoor wordt het proces dat het probleem veroorzaakte beëindigd, in plaats van het grootste proces op de server. Stel MemoryHigh= in onder MemoryMax= en beschouw de ruimte daartussen als een waarschuwingszone.
Hoe vind ik welke service door de OOM killer is beëindigd?
Voer journalctl -k --grep "Killed process" --since "2 hours ago" uit. Een regel die begint met Memory cgroup out of memory betekent dat een unit zijn eigen MemoryMax= heeft overschreden, terwijl een algemene Out of memory betekent dat het geheugen van de gehele machine op was. Voer daarna journalctl -u <unit> -n 50 uit en zoek naar Failed with result 'oom-kill'. Als /var/log/journal niet bestaat op uw server, werd de journal in het RAM bewaard en is het bewijs verloren gegaan bij de reboot. Maak deze map daarom aan vóór het volgende incident.
Moet ik swap toevoegen aan een kleine VPS?
Een klein swapbestand helpt bij het verplaatsen van 'koude' pagina's die eenmalig worden toegewezen en daarna niet meer worden gebruikt. Het helpt niet bij een op hol geslagen proces: het stelt het beëindigen uit en vervangt een korte uitval door een lange blokkade waardoor u niet meer kunt inloggen om het probleem te herstellen. Houd de swap bescheiden en stel MemorySwapMax=0 in op de units die u kunt missen. Zo bereiken deze hun limiet en herstarten ze snel, terwijl belangrijke services hun swap behouden.
Kan ik een commando beperken zonder een unit-bestand te schrijven?
Ja. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh voert het commando in uw terminal uit binnen een tijdelijke scope met de opgegeven limieten; de limieten vervallen zodra het commando eindigt. Elke eigenschap uit systemd.resource-control is beschikbaar na -p, dus MemorySwapMax=, TasksMax= en CPUWeight= werken daar ook. Laat --scope weg en voeg --unit=name toe om de taak op de achtergrond uit te voeren met de uitvoer in de journal.