SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-13

CPU en geheugen van systemd service beperken

Voorkom dat een proces uw VPS bevriest door limieten in te stellen met MemoryMax en CPUQuota. Leer hoe u cgroup v2 correct configureert en de foutmelding Unknown lvalue vermijdt.

Geheugen- en CPU-gebruik van processen beperken 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 serviceproces op de server te beheren.

sudo systemctl edit myapp.service

Hiermee 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=128
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMax

systemctl show moet uw getallen bevestigen in de eigen eenheden van de kernel: MemoryMax=805306368 en CPUQuotaPerSecUSec=800ms. Als de uitvoer MemoryMax=infinity bevat, is de drop-in niet geladen. Controleer of het bestand op de locatie /etc/systemd/system/myapp.service.d/override.conf is opgeslagen en of het begint met de [Service]-header. Een instelling zonder sectie erboven zorgt er namelijk voor dat systemd de melding 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 niet nuttig.

Dit is het mechanisme, aangezien het niet direct voor de hand ligt. Wanneer er weinig vrij geheugen 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 vanaf 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, wat thrashing wordt genoemd.

Twee factoren maken dit erger op een VPS 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/io
some avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233

De regel full is de relevante regel. full avg10=48.15 betekent dat gedurende de laatste tien seconden 48% van de tijd elke uitvoerbare taak op de server stilstond in afwachting van geheugenwerk, 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 meer 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 gethrottled in plaats van beëindigd, waardoor deze in leven blijft en traag blijft, en niets start de unit opnieuw op omdat deze vanuit het perspectief van systemd nooit heeft 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 deze /proc/pressure/io kan opdrijven voor elke andere service op de server. Limieten bepalen wie betaalt voor een tekort, en ze kunnen geen capaciteit creëren.

Controleer of uw VPS cgroup v2 gebruikt

stat -fc %T /sys/fs/cgroup

cat /sys/fs/cgroup/cgroup.controllers

cgroup2fs is de uniforme hiërarchie, wat vereist is voor alle onderstaande instellingen. tmpfs betekent dat de server is opgestart met de oudere v1-indeling, 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 -m

systemd-cgtop

Dit geeft cgroups weer, gesorteerd op geheugengebruik. Dit is de snelste manier om te achterhalen wat de server belast terwijl deze nog reageert. Als de server nieuw is, moet het werk aan het account en de firewall in de eerste tien minuten op een nieuwe VPS voorafgaan aan deze stap.

MemoryHigh vertraagt. MemoryMax beëindigt.

Het verschil tussen deze twee geheugeninstellingen bepaalt hoe een fout zich manifesteert.

  • MemoryHigh= is een zachte limiet. Boven deze waarde claimt de kernel agressief geheugen terug van die cgroup en vertraagt het proces bij het toewijzen van geheugen. Het verbruik kan de limiet overschrijden en er wordt niets beëindigd.
  • MemoryMax= is een harde limiet. Wanneer een toewijzing niet binnen deze limiet kan worden voldaan, voert de OOM killer binnen die cgroup een actie uit en beëindigt een van de processen van die unit.

Dat laatste is de voornaamste reden om MemoryMax= in te stellen voor alles wat u niet volledig vertrouwt. Zonder limiet is een tekort een probleem voor de gehele server, waarbij de globale OOM killer zijn slachtoffer kiest op basis van oom_score; dit is meestal 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. Het gat fungeert als een waarschuwingszone: een langzaam lek overschrijdt High en uit zich in een trage service, terwijl een plotselinge piek direct door Max gaat en de service beëindigt.

Percentagewaarden worden berekend op basis van het geïnstalleerde fysieke geheugen. MemoryMax=25% op een 4 GB-plan is dus 1 GB en blijft een kwart van de server, ook nadat u het plan schaalt. MemorySwapMax=0 houdt die unit volledig uit de swap, wat een langdurige vertraging verandert in een snelle, duidelijke beëindiging.

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=5s

StartLimit* hoort thuis in [Unit] en Restart= in [Service]. Plaats ze niet in de verkeerde sectie, want dan negeert systemd ze. Vijf herstarts in vijf minuten wijst op een lek in plaats van een incidentele storing; na vijf pogingen 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. Het is een relatief aandeel van 1 tot 10000, waarbij de kernel-standaard 100 is. Dit is alleen van invloed 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 onbenutte capaciteit.

Wees realistisch over wat een CPU-limiet u oplevert. Een CPU-intensief proces laat Linux zelden vastlopen, omdat de scheduler tijd blijft toewijzen aan alle processen. Geheugengebruik is wat een server daadwerkelijk platlegt. 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 bepalen van de omvang van dergelijke workloads is een apart vraagstuk, dat wordt behandeld in hoeveel RAM en CPU een coding agent VPS 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 buurman, 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. Dit is de goedkoopste bescherming tegen een script dat in een lus forks uitvoert, omdat de fork binnen de unit faalt in plaats van dat het systeem zonder proces-ID's komt te zitten.

TasksMax=128

Wanneer 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.service

Het 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 uit systemd.resource-control werkt na -p.

Voor een langdurige taak laat u --scope weg en geeft u de taak een naam. Deze draait vervolgens 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 -f

Dezelfde opties werken met --user wanneer u geen root bent, hoewel uw user manager alleen beschikt over de controllers die eraan zijn gedelegeerd, waardoor een eigenschap daar geweigerd kan worden. 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 een fout in plaats van deze te voorkomen.

Zonder swap bereikt een geheugenlek het plafond en sterft een proces binnen enkele seconden. De uitval is luidruchtig, kort en achteraf eenvoudig terug te lezen in de journal. Met swap schrijft de kernel inactieve anonieme pagina's naar de schijf en koopt u tijd. Als het geheugengebruik van het proces stabiliseert, redt swap u. Als er sprake is van een runaway-proces, verandert swap een uitval van vijf seconden in een vertraging van twintig minuten. Die vertraging is erger, omdat een gestopt proces u nog een werkende shell laat, terwijl een systeem dat constant naar schijf schrijft (thrashing) onbruikbaar is.

swapon --show
free -h

Een werkbaar compromis op een kleine VPS: houd een bescheiden swap-bestand aan voor pagina's die eenmalig worden toegewezen en daarna nooit meer worden aangeraakt, en stel MemorySwapMax=0 in op de units die u kunt missen. 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 middel, en het is belangrijk om te weten waarom. Het verschuift enkel de balans tussen het verwijderen van de page cache en het swappen van anonieme pagina's; beide acties resulteren later in een schijf-read. Het verandert alleen 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. Op een kleine VPS is die wachttijd precies het moment waarop u de controle over de machine verliest. Twee userspace-daemons sluiten dit gat door zelf het geheugengebruik te monitoren en eerder in te grijpen.

earlyoom houdt het beschikbare geheugen en de vrije swap in de gaten. Het beëindigt het proces met de hoogste score zodra een van beide onder een drempelwaarde zakt.

sudo apt install earlyoom
systemctl status earlyoom

Het Debian- en Ubuntu-pakket start de service direct na installatie. De opties bevinden zich 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 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 man-page 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 optreedt in de kernel-space". Het handelt op basis van volledige cgroups in plaats van individuele processen; het beëindigt dus een unit en niet slechts een losstaand kindproces. Units melden zich aan via ManagedOOMMemoryPressure=kill of ManagedOOMSwap=kill, en de drempelwaarden staan in /etc/systemd/oomd.conf.

systemctl status systemd-oomd
oomctl

oomctl 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 beperk u daartoe. Het gelijktijdig 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 elk proces dat hij beëindigt, 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:0

anon-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 daarboven 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:0

Dat 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; uw limieten ontbraken dus of waren te ruim ingesteld.

Vraag vervolgens aan systemd wat het heeft waargenomen:

systemctl status myapp.service
journalctl -u myapp.service -n 50
myapp.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.peak
low 0
high 4213
max 118
oom 12
oom_kill 12

high telt hoe vaak de unit over de MemoryHigh= werd geduwd en werd vertraagd. max telt hoe vaak de harde limiet werd bereikt, en oom_kill telt het aantal daadwerkelijk beëindigde processen. Een hoge high in combinatie met oom_kill 0 is het stille scenario van eerder: de service draait, is extreem traag geworden 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 de 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-boots

journalctl --list-boots die meer toont dan de huidige opstart betekent dat de geschiedenis nu behouden blijft, waardoor journalctl -k -b -1 u de kernelberichten kan tonen van de opstart die is vastgelopen.

Een startpunt voor een kleine VPS

Houd op een abonnement van 2 GB ongeveer 300 tot 400 MB vrij voor de kernel en de page cache. Zorg dat de limieten samen niet de volledige 2 GB bereiken, omdat alle units op hetzelfde moment een piek kunnen vertonen. Geef de belangrijkste service het grootste aandeel en beperk alle overige, minder kritieke processen daaromheen.

[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5s

Het 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 herstellen van de server en het moeten herstarten via het configuratiescherm. Het verandert alleen de keuze van de kernel voor het 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. Daarom geldt een limiet op docker.service niet als limiet voor een individuele 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 feit of het vrijmaken van geheugen pagina's oplevert, en niet op basis van hoe lang dat duurt. Bij een tekort aan geheugen verwijdert de kernel de page cache, inclusief de uitvoerbare pagina's van draaiende programma's, om deze vervolgens bij de volgende instructie weer in te lezen. Alles wacht op opslag en er is technisch gezien 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 de afgelopen tien seconden heeft kunnen draaien. Een userspace-daemon zoals earlyoom beëindigt processen voordat de server die toestand bereikt.

Wat is het verschil tussen MemoryHigh en MemoryMax?

MemoryHigh= is een zachte limiet die het proces vertraagt. De kernel maakt agressief geheugen vrij binnen de unit en vertraagt de toewijzingen, maar het gebruik kan het getal 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 sterft het proces dat het probleem veroorzaakte, in plaats van het grootste proces op de server. Stel MemoryHigh= in onder MemoryMax= en beschouw het verschil 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 bereikt, terwijl een gewone 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-geheugen bewaard en is het bewijs verloren gegaan bij de herstart; maak die map daarom aan vóór het volgende incident.

Moet ik swap toevoegen aan een kleine VPS?

Een klein swap-bestand helpt bij 'koude' pagina's die eenmalig worden toegewezen en daarna niet meer worden gebruikt. Het helpt niet bij een op hol geslagen proces: het vertraagt het beëindigen en vervangt een korte uitval door een lange blokkade waardoor u niet meer kunt inloggen om het probleem te herstellen. Houd swap bescheiden en stel MemorySwapMax=0 in op de units die u kunt missen, zodat deze hun limiet bereiken en snel herstarten terwijl de 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 die limieten; de limieten vervallen zodra het commando eindigt. Elke eigenschap van 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.