SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-27

Limitare memoria e CPU di un processo con systemd

Configura MemoryHigh, MemoryMax, CPUQuota e TasksMax in systemd. Scopri perché una VPS può bloccarsi anche con limiti e come leggere un evento OOM.

Limitare memoria e CPU dei processi con un drop-in di systemd

Per limitare la memoria e la CPU di un processo su un VPS Linux, aggiungete alcune righe all'unità che esegue il processo. MemoryMax= è il limite massimo della memoria. CPUQuota= è il limite massimo del tempo di CPU. Entrambi i limiti vengono applicati tramite cgroup v2 (control groups, versione 2), la funzionalità del kernel che systemd usa già per contabilizzare ogni servizio presente sul server.

sudo systemctl edit myapp.service

Si apre un file drop-in contenente istruzioni nei commenti. Aggiungete queste righe prima dei commenti:

[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 deve riportare gli stessi valori usando le unità del kernel: MemoryMax=805306368 e CPUQuotaPerSecUSec=800ms. Se stampa MemoryMax=infinity, il drop-in non è stato caricato. Verificate che il file sia stato salvato in /etc/systemd/system/myapp.service.d/override.conf e che inizi con l'intestazione [Service], perché una riga di configurazione senza una sezione precedente fa registrare a systemd Assignment outside of section. Ignoring. e avvia il servizio senza alcun limite.

Il resto della guida spiega come scegliere questi valori e quali problemi possono verificarsi anche dopo averli impostati.

Perché un processo fuori controllo blocca una VPS senza esaurirla

Un processo che raggiunge un limite rigido di memoria termina in circa un secondo e il servizio viene riavviato. Questo è il caso positivo. Il caso negativo è quello in cui non termina nulla: il server risponde al ping, SSH accetta la connessione, ma il prompt della shell non compare mai. La macchina è attiva e occupata, ma nessuna di queste attività è utile.

Il meccanismo non è intuitivo. Quando la memoria libera si riduce, il kernel recupera pagine invece di assegnarne di nuove. Le pagine più economiche da recuperare sono quelle supportate da file, mentre la page cache contiene il codice eseguibile di tutto ciò che è in esecuzione. Il kernel espelle quindi le pagine di testo di sshd e la prossima istruzione sshd eseguita genera un page fault che deve rileggere quei byte dallo storage. Alla fine, ogni processo resta in attesa del disco invece di essere eseguito. Le stesse pagine vengono espulse e ricaricate in un ciclo continuo: questo fenomeno si chiama thrashing.

Due fattori peggiorano la situazione su una VPS rispetto a un laptop. Lo storage è spesso collegato tramite rete o condiviso, quindi ogni page fault richiede più millisecondi rispetto a un dispositivo NVMe locale. Inoltre, il kernel non misura il tempo: misura i fallimenti. Finché il reclaim continua a restituire una pagina, anche molto lentamente, il kernel ritiene di stare avanzando e non attiva l'out of memory (OOM) killer. Un server può restare in questo stato per molti minuti prima che venga terminato un processo.

È possibile osservare il fenomeno. Il kernel espone le informazioni pressure stall (PSI) su Linux 4.20 e versioni successive:

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

La riga full è quella importante. full avg10=48.15 indica che, negli ultimi dieci secondi, per il 48% del tempo tutti i task eseguibili sul server sono rimasti bloccati in attesa delle operazioni sulla memoria, quindi non è stato eseguito nulla. Su un server sano, full è vicino a zero. Oltre 10 il rallentamento è percepibile da una persona; con un valore pari o superiore a 40 si raggiunge lo stato che viene descritto come blocco completo.

Questo spiega anche perché un limite, da solo, non costituisce una garanzia. Un'unità soggetta a MemoryHigh= viene sottoposta a throttling invece di essere terminata: resta attiva e lenta, e systemd non la riavvia perché, dal suo punto di vista, non è mai fallita. Un'unità con un limite che può ancora usare lo swap genera letture e scritture addebitate a quell'unità, ma servite da un unico dispositivo condiviso. Di conseguenza può aumentare /proc/pressure/io per tutti gli altri servizi del server. I limiti stabiliscono chi sostiene il costo di una carenza, ma non possono creare capacità.

Verifica che il tuo VPS utilizzi cgroup v2

stat -fc %T /sys/fs/cgroup

cgroup2fs è la gerarchia unificata necessaria per tutte le impostazioni riportate di seguito. tmpfs indica che il server è stato avviato con la struttura v1 precedente, nella quale MemoryHigh= e MemorySwapMax= non esistono e il comportamento OOM per unità è diverso. Ubuntu 22.04 e versioni successive, così come Debian 11 e versioni successive, utilizzano v2 per impostazione predefinita. Un'immagine obsoleta o un kernel avviato con systemd.unified_cgroup_hierarchy=0 non la utilizza.

Con cgroup v2, systemd abilita per impostazione predefinita la contabilizzazione della memoria per ogni unità. I valori sono quindi già disponibili:

systemd-cgtop -m

Il comando elenca i cgroup ordinati in base all'utilizzo della memoria. È il modo più rapido per capire "che cosa sta consumando le risorse del server" mentre il server è ancora operativo. Se il server è nuovo, le attività relative all'account e al firewall descritte in i primi dieci minuti su un nuovo VPS vengono prima di questa verifica.

MemoryHigh rallenta. MemoryMax termina il processo.

La differenza tra le due impostazioni della memoria determina l'aspetto del guasto.

  • MemoryHigh= è un limite flessibile. Quando viene superato, il kernel recupera memoria in modo aggressivo da quel cgroup e rallenta intenzionalmente le nuove allocazioni. L'utilizzo può comunque superare il valore e nessun processo viene terminato.
  • MemoryMax= è un limite rigido. Quando un'allocazione non può essere soddisfatta entro questo limite, il killer OOM viene eseguito all'interno di quel cgroup e termina uno dei processi appartenenti a quella unità.

Questa seconda caratteristica è il vero motivo per impostare MemoryMax= su tutto ciò di cui non ti fidi completamente. Senza un limite, l'esaurimento della memoria diventa un problema dell'intero sistema e il killer OOM globale sceglie la vittima in base a oom_score, che nella maggior parte dei casi significa il processo più grande. Il processo più grande è in genere il database, non lo script che ha causato la perdita di memoria. Con un limite, il processo viene terminato all'interno dell'unità che ha causato il problema.

Impostali entrambi, con MemoryHigh= circa dal 20 al 30 percento al di sotto di MemoryMax=. La differenza è una zona di avviso: una perdita lenta supera High e si manifesta come un servizio diventato lento, mentre un picco improvviso supera direttamente Max e causa l'arresto del processo.

I valori percentuali vengono calcolati in base alla memoria fisica installata. Di conseguenza, MemoryMax=25% su un piano da 4 GB equivale a 1 GB e rimane pari a un quarto della memoria disponibile anche dopo il ridimensionamento del piano. MemorySwapMax=0 impedisce completamente a quell'unità di usare lo swap e trasforma un rallentamento prolungato in un arresto rapido e immediatamente riconoscibile.

Alcuni servizi consentono di definire in anticipo il consumo di memoria invece di misurarlo. Un'unità Ollama dimensiona la propria cache KV in base alla finestra di contesto configurata. Leggi quindi quanto costa in RAM aumentare num_ctx prima di scegliere il limite da applicare.

Un limite deve essere accompagnato da una policy di riavvio. In caso contrario, l'arresto del processo lascia semplicemente il servizio fermo.

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

StartLimit* deve essere inserito in [Unit] e Restart= in [Service]. Se inserisci uno dei due nella sezione sbagliata, systemd lo ignora. Cinque riavvii in cinque minuti indicano una perdita di memoria, non un problema temporaneo. Dopo questo numero systemd rinuncia e lascia l'unità nello stato failed. È lo stato che vuoi trovare in seguito, invece di un crash loop che nasconde il problema.

Limitare la CPU con CPUQuota oppure condividerla con CPUWeight

CPUQuota= usa una percentuale del tempo disponibile su una CPU. CPUQuota=50% equivale a metà di un core. CPUQuota=200% equivale a due core, che l’unità può distribuire tra tutti i thread necessari. Su un piano con 2 vCPU, CPUQuota=200% corrisponde all’intera macchina.

CPUWeight= è l’impostazione predefinita più adatta per la maggior parte dei servizi. È una quota relativa compresa tra 1 e 10000; il valore predefinito del kernel è 100. Ha effetto solo quando si verifica una contesa: sotto carico, un processo di backup con CPUWeight=20 cede risorse a un web server con valore 100, mentre continua a usare l’intera macchina quando questa è inattiva. Una quota rigida elimina la capacità inutilizzata.

È importante valutare correttamente i vantaggi di un limite CPU. Un processo che usa intensivamente la CPU raramente blocca Linux, perché lo scheduler continua ad assegnare tempo di CPU a tutti i processi. È la memoria a causare l’arresto della macchina. Usa CPUQuota= quando vuoi un limite massimo prevedibile, ad esempio per una compilazione o per un agent che altrimenti userebbe tutta la CPU per un’ora. Il dimensionamento di questo tipo di carico è un problema distinto, trattato in quanta RAM e CPU richiede un coding agent su un VPS.

Se la CPU risulta occupata mentre nessuno dei tuoi processi sta usando risorse in modo significativo, la causa potrebbe trovarsi dall’altra parte dell’hypervisor. Si tratta di CPU steal time causato da un vicino rumoroso e nessuna quota impostata da te può modificarlo.

TasksMax interrompe un ciclo di fork

TasksMax= è il numero massimo di processi e thread che un'unità può contenere. I thread vengono conteggiati, quindi un servizio Java o Go richiede un margine maggiore di quello che suggerisce l'elenco dei processi. È la protezione meno costosa contro uno script che esegue fork in un ciclo, perché il fork fallisce all'interno dell'unità invece di esaurire gli ID di processo del server.

TasksMax=128

Quando un'unità raggiunge il limite, il kernel registra una riga che identifica il cgroup:

cgroup: fork rejected by pids controller in /system.slice/myapp.service

Il programma stesso segnala in genere fork: retry: Resource temporarily unavailable. Verificare il valore applicato per impostazione predefinita dal manager con systemctl show -p DefaultTasksMax.

Limitare un job una tantum con systemd-run

Per usare queste funzionalità non è necessario un file di unità. systemd-run crea un'unità temporanea attorno a un singolo comando.

sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh

--scope esegue il comando nel terminale, dopo aver visualizzato Running scope as unit: run-r7c1a....scope. L'output rimane sullo schermo e i limiti scompaiono quando il comando termina. Qualsiasi proprietà di systemd.resource-control funziona dopo -p.

Per un job di lunga durata, rimuovere --scope e assegnargli un nome. Il job viene quindi eseguito in background come servizio temporaneo e registra i messaggi nel journal:

sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -f

Le stesse opzioni funzionano con --user anche quando non si dispone dei privilegi di root. Tuttavia, il manager dell'utente dispone soltanto dei controller delegati al relativo utente, quindi una proprietà può essere rifiutata. In tal caso, eseguirlo con sudo. Quando un job richiede una configurazione permanente, trasferire le impostazioni senza modificarle in una vera unità: vedere eseguire uno script come servizio e timer systemd.

La questione dello swap, con una risposta onesta

Lo swap modifica il modo in cui si manifesta il guasto, ma non lo previene.

Senza swap, una perdita di memoria raggiunge il limite massimo e nel giro di pochi secondi termina un processo. L'interruzione è evidente, breve e facile da analizzare successivamente nel journal. Con lo swap, il kernel scrive su disco le pagine anonime non utilizzate e guadagna tempo. Se il processo stava per stabilizzarsi, lo swap può salvarvi. Se invece la crescita è incontrollata, lo swap trasforma un'interruzione di cinque secondi in un rallentamento di venti minuti. Il rallentamento è peggiore, perché un processo terminato lascia comunque disponibile una shell funzionante, mentre un sistema in thrashing no.

swapon --show
free -h

Su una VPS di piccole dimensioni, una soluzione pratica è mantenere un file di swap moderato per le pagine allocate una sola volta e mai più utilizzate, quindi impostare MemorySwapMax=0 sulle unità che si è disposti a perdere. I servizi importanti mantengono l'accesso allo swap. Quelli imprevedibili raggiungono rapidamente il limite e vengono riavviati.

Ridurre vm.swappiness è un intervento poco efficace, ed è utile capire il motivo. Modifica soltanto l'equilibrio tra l'espulsione della page cache e lo spostamento nello swap delle pagine anonime; in entrambi i casi, in seguito sarà necessaria una lettura dal disco. Cambia quali pagine entrano in thrashing, non se il sistema entra o meno in thrashing.

Un demone OOM preventivo termina i processi prima del blocco

Il kernel attende che il reclaim fallisca completamente. Su una VPS di piccole dimensioni, questa attesa corrisponde esattamente all'intervallo in cui si perde l'accesso alla macchina. Due demoni in userspace riducono questo intervallo monitorando direttamente la memoria e terminando i processi prima.

earlyoom monitora la memoria disponibile e lo swap libero. Termina il processo con il punteggio più alto quando uno dei due valori scende sotto una soglia.

sudo apt install earlyoom
systemctl status earlyoom

Il pacchetto Debian e Ubuntu avvia il servizio durante l'installazione. Le opzioni si trovano in /etc/default/earlyoom:

EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"

-m PERCENT imposta il minimo di memoria disponibile e -s PERCENT il minimo di swap libero, entrambi al 10 percento per impostazione predefinita. Il secondo numero di ogni coppia indica la soglia per SIGKILL: earlyoom invia SIGTERM quando si scende sotto il primo valore, quindi SIGKILL quando si scende sotto il secondo. Per impostazione predefinita, quest'ultimo è la metà del primo. Applica una modifica con sudo systemctl restart earlyoom e leggi journalctl -u earlyoom per vedere quale processo è stato terminato e quanta memoria stava utilizzando.

systemd-oomd è l'altra opzione. La relativa pagina del manuale la descrive come "un servizio di sistema che usa cgroups-v2 e le informazioni PSI (pressure stall information) per monitorare il sistema e adottare misure correttive prima che si verifichi un OOM nello spazio del kernel". Agisce su interi cgroup anziché su singoli processi. Termina quindi una unità, non un processo figlio isolato. Le unità possono abilitarla con ManagedOOMMemoryPressure=kill o ManagedOOMSwap=kill. Le soglie si trovano in /etc/systemd/oomd.conf.

systemctl status systemd-oomd
oomctl

oomctl mostra cosa sta monitorando in quel momento. Su un'immagine server spesso non mostra nulla, perché l'impostazione deve essere abilitata singolarmente per ogni unità. Scegli un solo demone e fermati lì. Eseguire entrambi significa lasciare che due componenti competano per scegliere la vittima. Diventa quindi più difficile ricostruire il motivo di ogni terminazione.

Quale unità era responsabile?

Inizia dal kernel, perché registra ogni processo che termina.

journalctl -k --grep "Killed process" --since "2 hours ago"

Un kill eseguito dall'OOM killer globale ha questo aspetto:

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 è la memoria che il processo occupava in RAM quando è terminato, circa 1.8 GB in questo caso. Interpreta con cautela il nome tra parentesi. È la vittima scelta dal kernel, che seleziona il processo più grande. Non è sempre il processo che ha causato l'esaurimento della memoria.

Un kill causato dal limite di un cgroup ha un prefisso diverso. Il report stampato sopra indica il cgroup che ha raggiunto il proprio limite:

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

Quel prefisso contiene gran parte della diagnosi. Memory cgroup out of memory indica che una unità ha raggiunto il MemoryMax= che le hai assegnato, mentre il resto del sistema funzionava normalmente. Un semplice Out of memory indica che la memoria dell'intera macchina era esaurita. In questo caso i limiti mancavano oppure, sommati, erano troppo elevati.

Poi chiedi a systemd che cosa ha rilevato:

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 indica la stessa cosa su una sola riga, come Active: failed (Result: oom-kill).

I contatori del cgroup sono la terza fonte di informazioni e l'unica che registra il throttling, che non genera mai una riga di log:

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 conta quante volte l'unità ha superato MemoryHigh= ed è stata sottoposta a throttling. max conta quante volte ha raggiunto il limite rigido, mentre oom_kill conta i processi effettivamente terminati. Un valore elevato di high insieme a oom_kill 0 indica il caso silenzioso descritto in precedenza: il servizio è in esecuzione, è diventato estremamente lento e non ha segnalato alcun errore. memory.peak (Linux 5.19 e versioni successive) contiene il valore massimo raggiunto dal consumo del cgroup. Questo è il numero da usare per dimensionare MemoryMax=. Entrambi i file vengono azzerati quando l'unità viene riavviata, perché systemd ricrea il cgroup.

Alla base di tutto questo c'è un prerequisito. Se /var/log/journal non esiste, il journal risiede in RAM e tutte le righe vengono perse dopo il riavvio necessario per recuperare il sistema.

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-boots

Se journalctl --list-boots mostra un valore superiore a quello del boot corrente, la cronologia viene conservata. In questo modo journalctl -k -b -1 può mostrare i messaggi del kernel relativi al boot terminato.

Un punto di partenza per un piccolo VPS

Con un piano da 2 GB, lascia da 300 a 400 MB al kernel e alla page cache. Non impostare i limiti in modo che la somma raggiunga tutti i 2 GB, perché ogni unità può raggiungere il picco nello stesso momento. Assegna la quota maggiore al servizio più importante, quindi limita tutto ciò che è secondario attorno a esso.

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

Mantenere una modalità di accesso al server richiede un'impostazione aggiuntiva. OOMScoreAdjust=-500 in un drop-in per ssh.service riduce notevolmente la probabilità che l'OOM killer globale scelga il demone SSH come vittima. Questo può fare la differenza tra risolvere il problema sul server e doverlo riavviare dal pannello di controllo. Modifica soltanto la scelta del processo da terminare da parte del kernel. Non riduce la durata del blocco.

I container vengono eseguiti in cgroup propri, creati dal container runtime e non dai file delle unità. Per questo, un limite su docker.service non diventa un limite per un singolo container. Gli equivalenti per container di MemoryMax= e CPUQuota= sono descritti in impostazione dei limiti di memoria e CPU in Docker Compose.

FAQ

Perché il mio VPS si è bloccato invece di terminare il processo fuori controllo?

Perché il kernel valuta l'avanzamento in base al fatto che il reclaim restituisca pagine, non in base al tempo necessario. Quando la memoria è insufficiente, espelle la page cache, comprese le pagine eseguibili dei programmi in esecuzione, quindi le rilegge alla successiva istruzione. Tutto resta in attesa dello storage e nessuna allocazione fallisce tecnicamente, quindi l'OOM killer non viene mai chiamato. Controlla /proc/pressure/memory mentre il problema è in corso: un valore full avg10 superiore a 40 indica che quasi nessun task è riuscito a essere eseguito negli ultimi dieci secondi. Un demone userspace come earlyoom termina il processo prima che il sistema raggiunga questo stato.

Qual è la differenza tra MemoryHigh e MemoryMax?

MemoryHigh= è un limite morbido che applica il throttling. Il kernel esegue il reclaim in modo aggressivo sull'unità e rallenta le sue allocazioni, ma l'utilizzo può superare il valore e nessun processo viene terminato. MemoryMax= è un limite rigido: un'allocazione che non può essere soddisfatta entro tale limite invoca l'OOM killer all'interno del cgroup dell'unità, quindi muore il processo che ha causato il problema invece del processo più grande presente sul sistema. Imposta MemoryHigh= al di sotto di MemoryMax= e considera la differenza tra i due valori come una zona di avviso.

Come posso individuare il servizio colpito dall'OOM killer?

Esegui journalctl -k --grep "Killed process" --since "2 hours ago". Una riga che inizia con Memory cgroup out of memory indica che un'unità ha raggiunto il proprio MemoryMax=, mentre un semplice Out of memory indica che la memoria dell'intero sistema è esaurita. Poi esegui journalctl -u <unit> -n 50 e cerca Failed with result 'oom-kill'. Se /var/log/journal non esiste sul server, il journal era conservato in RAM e le informazioni sono andate perse con il riavvio; crea quindi quella directory prima del prossimo incidente.

Devo aggiungere lo swap a un VPS di piccole dimensioni?

Un file di swap piccolo è utile per le pagine fredde, allocate una sola volta e mai più utilizzate. Non risolve il problema di un processo fuori controllo: ritarda la terminazione e trasforma una breve interruzione in un blocco prolungato, durante il quale non è possibile accedere al sistema per risolvere il problema. Mantieni lo swap su dimensioni moderate e imposta MemorySwapMax=0 sulle unità che sei disposto a perdere, così raggiungono il limite e si riavviano rapidamente, mentre i servizi importanti continuano a usare il proprio swap.

Posso limitare un comando senza scrivere un file unit?

Sì. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh esegue il comando nel terminale all'interno di uno scope transitorio con quei limiti, che scompaiono quando il comando termina. Dopo -p è disponibile ogni proprietà di systemd.resource-control, quindi funzionano anche MemorySwapMax=, TasksMax= e CPUWeight=. Rimuovi --scope e aggiungi --unit=name per eseguire il job in background con l'output nel journal.