SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-13

Limitare memoria e CPU di un processo con systemd

Una VPS può bloccarsi anche con memoria libera. Configura MemoryHigh, MemoryMax, CPUQuota e TasksMax nel drop-in systemd e verifica l'evento OOM.

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

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

sudo systemctl edit myapp.service

Questo apre un file drop-in con istruzioni nei commenti. Aggiungi queste righe prima delle istruzioni:

[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 restituire gli stessi valori, convertiti nelle unità usate dal kernel: MemoryMax=805306368 e CPUQuotaPerSecUSec=800ms. Se stampa MemoryMax=infinity, il drop-in non è stato caricato. Verifica che il file sia stato creato in /etc/systemd/system/myapp.service.d/override.conf e che inizi con l'header [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 esaurire la memoria

Un processo che raggiunge un limite rigido di memoria termina dopo 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.

Ecco il meccanismo, che non è intuitivo. Quando la memoria libera si riduce, il kernel recupera pagine invece di assegnarne di nuove. Le pagine più semplici da recuperare sono quelle associate ai file, e 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 causa un page fault che deve rileggere quei byte dallo storage. Ogni processo finisce per attendere il disco invece di eseguire. Le stesse pagine vengono espulse e ricaricate in un ciclo, un fenomeno chiamato thrashing.

Due fattori peggiorano il problema 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, ma gli errori: finché il recupero continua a restituire una pagina, anche molto lentamente, il kernel considera di stare avanzando e non attiva l'out of memory (OOM) killer. Un server può rimanere in questo stato per molti minuti prima che venga terminato un processo.

È possibile osservare il fenomeno. Su Linux 4.20 e versioni successive, il kernel esporta le pressure stall information (PSI):

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 tutte le attività eseguibili sul server sono rimaste bloccate in attesa di operazioni sulla memoria, quindi non è stata eseguita alcuna attività. Su un server in buone condizioni, full è vicino a zero. Oltre 10 il rallentamento è percepibile; con valori pari o superiori a 40 il sistema è nello stato che viene descritto come bloccato.

Questo spiega anche perché un limite, da solo, non costituisce una garanzia. Un'unità soggetta a MemoryHigh= viene limitata invece di essere terminata, quindi rimane attiva ma lenta, e nulla la riavvia perché, dal punto di vista di systemd, non ha mai avuto esito negativo. 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 il layout v1 precedente, nel 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 precedente o un kernel avviato con systemd.unified_cgroup_hierarchy=0 non lo utilizza.

Con cgroup v2, systemd abilita per impostazione predefinita il conteggio della memoria per ogni unità, quindi i valori sono già disponibili:

systemd-cgtop -m

Il comando elenca i cgroup ordinati in base all'utilizzo della memoria. È il modo più rapido per capire quale processo sta consumando le risorse del server mentre il server è ancora operativo. Se il server è nuovo, prima di questo passaggio occorre completare la configurazione dell'account e del firewall descritta in i primi dieci minuti su un nuovo VPS.

MemoryHigh limita. MemoryMax termina il processo.

La differenza tra le due impostazioni della memoria determina il comportamento in caso di errore.

  • MemoryHigh= è un limite elastico. 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, entra in funzione l'OOM killer all'interno di quel cgroup e termina uno dei processi appartenenti a quella unità.

Questa seconda caratteristica è il motivo principale per impostare MemoryMax= su tutto ciò di cui non si ha piena fiducia. Senza un limite, un esaurimento della memoria diventa un problema dell'intero server e l'OOM killer globale sceglie la vittima in base a oom_score, che nella maggior parte dei casi significa il processo più grande. Il processo più grande è solitamente 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.

Imposta entrambi i limiti, con MemoryHigh= circa dal 20 al 30 percento al di sotto di MemoryMax=. La differenza crea una zona di avviso: una perdita lenta supera High e si manifesta con un servizio che rallenta, mentre un picco improvviso supera direttamente Max e causa la terminazione.

I valori percentuali vengono calcolati sulla memoria fisica installata. Pertanto, MemoryMax=25% su un piano da 4 GB equivale a 1 GB e resta pari a un quarto del server anche dopo il ridimensionamento del piano. MemorySwapMax=0 impedisce completamente a quell'unità di usare lo swap, trasformando un rallentamento prolungato in una terminazione rapida e immediatamente visibile.

Un limite deve essere affiancato da una politica di riavvio. In caso contrario, la terminazione lascia semplicemente il servizio arrestato.

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

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

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

Limita la CPU con CPUQuota oppure condividila con CPUWeight

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

CPUWeight= è l'impostazione predefinita migliore per la maggior parte dei servizi. Rappresenta una quota relativa da 1 a 10000; il valore predefinito del kernel è 100. Ha effetto solo quando più processi competono per la CPU: sotto carico, un job di backup con CPUWeight=20 cede risorse a un web server con valore 100, ma quando la macchina è inattiva continua a usare l'intera capacità disponibile. Una quota rigida spreca questa capacità inutilizzata.

Valuta con attenzione i vantaggi effettivi di un limite CPU. Un processo limitato dalla CPU raramente blocca Linux, perché lo scheduler continua ad assegnare tempo di esecuzione a tutti i processi. È la memoria a causare il blocco della macchina. Usa CPUQuota= quando ti serve un limite massimo prevedibile, ad esempio per una compilazione o per un agent che altrimenti utilizzerebbe tutta la CPU per un'ora. Il dimensionamento di questo tipo di carico è un problema distinto, trattato in quanta RAM e CPU servono a un VPS per un coding agent.

Se la CPU risulta occupata mentre nessuno dei tuoi processi sta facendo un uso significativo delle risorse, la causa potrebbe trovarsi dall'altra parte dell'hypervisor. Si tratta del CPU steal time causato da un vicino rumoroso; nessuna quota che imposti può modificarlo.

TasksMax interrompe un ciclo di fork

TasksMax= indica il numero massimo di processi e thread che una unit può contenere. I thread vengono conteggiati, quindi un servizio Java o Go richiede un margine maggiore di quanto suggerisca l’elenco dei processi. È la protezione meno costosa contro uno script che esegue fork in un ciclo, perché il fork non riesce all’interno della unit invece di esaurire gli identificativi dei processi sull’intero sistema.

TasksMax=128

Quando una unit raggiunge il limite, il kernel registra una riga che indica il cgroup:

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

Il programma stesso generalmente segnala fork: retry: Resource temporarily unavailable. Verifica il limite applicato per impostazione predefinita dal manager con systemctl show -p DefaultTasksMax.

Limita un job una tantum con systemd-run

Non serve un file di unità per usare queste funzioni. systemd-run crea un'unità transitoria 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 resta sullo schermo e i limiti scompaiono al termine del comando. Qualsiasi proprietà di systemd.resource-control funziona dopo -p.

Per un job di lunga durata, rimuovi --scope e assegna un nome. Il job viene quindi eseguito in background come servizio transitorio e scrive i log 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 quando non hai privilegi root. Tuttavia, il tuo user manager dispone soltanto dei controller che gli sono stati delegati, quindi una proprietà può essere rifiutata. In tal caso, eseguilo con sudo. Quando un job richiede una configurazione permanente, trasferisci le impostazioni senza modificarle in una vera unità: consulta eseguire uno script come servizio e timer systemd.

La questione dello swap, con una risposta onesta

Lo swap modifica la dinamica del guasto, ma non lo previene.

Senza swap, una perdita di memoria raggiunge il limite e un processo termina nel giro di pochi secondi. L’interruzione del servizio è evidente, breve e facile da analizzare in seguito nel journal. Con lo swap, il kernel scrive su disco le pagine anonime fredde e guadagna tempo. Se il processo stava per stabilizzarsi, lo swap permette di superare il picco. Se invece il processo è fuori controllo, lo swap trasforma un’interruzione di cinque secondi in un blocco di venti minuti. Il blocco è peggiore, perché un processo terminato lascia comunque disponibile una shell funzionante, mentre un sistema in thrashing no.

swapon --show
free -h

Una soluzione equilibrata per un VPS di piccole dimensioni consiste nel mantenere un file di swap di dimensioni moderate per le pagine allocate una volta e mai più utilizzate, e impostare MemorySwapMax=0 sulle unità che si è disposti a perdere. I servizi importanti continuano a usare lo swap. Quelli imprevedibili raggiungono rapidamente il limite e vengono riavviati.

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

Un demone OOM anticipato interviene prima del blocco

Il kernel attende che il reclaim fallisca completamente. Su una VPS di piccole dimensioni, questa attesa coincide con l'intervallo in cui la macchina diventa irraggiungibile. Due demoni userspace riducono questo intervallo monitorando direttamente la memoria e terminando prima i processi.

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

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 hanno come valore predefinito il 10 percento. Il secondo numero di ciascuna coppia indica la soglia per SIGKILL: earlyoom invia SIGTERM quando il valore scende sotto il primo numero, quindi SIGKILL quando scende sotto il secondo. Per impostazione predefinita, il secondo valore è la metà del primo. Applica una modifica con sudo systemctl restart earlyoom. Consulta journalctl -u earlyoom per verificare 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 pressure stall information (PSI) 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à abilitano esplicitamente questa funzione con ManagedOOMMemoryPressure=kill o ManagedOOMSwap=kill. Le soglie si trovano in /etc/systemd/oomd.conf.

systemctl status systemd-oomd
oomctl

oomctl mostra ciò che sta monitorando in quel momento. Su un'immagine server spesso non mostra nulla, perché l'impostazione richiede l'abilitazione esplicita per ogni unità. Scegli un solo demone. Esegui entrambi significa lasciare che due componenti competano nella scelta della vittima, rendendo più difficile ricostruire il motivo di ogni terminazione.

Quale unità era responsabile?

Inizia dal kernel, perché registra ogni terminazione che esegue.

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

Una terminazione eseguita 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 indica la memoria che il processo occupava nella RAM quando è stato terminato, circa 1.8 GB in questo caso. Leggi con cautela il nome tra parentesi. È il processo scelto dal kernel come vittima, ma il kernel sceglie il processo più grande, che non è sempre quello che ha causato l’esaurimento della memoria.

Una terminazione causata dal limite di un cgroup ha un prefisso diverso, e il report visualizzato 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 significa che un’unità ha raggiunto il MemoryMax= che le hai assegnato e che il resto del sistema funzionava normalmente. Un semplice Out of memory significa che la memoria si è esaurita sull’intera macchina, quindi i limiti erano assenti oppure troppo elevati nel loro insieme.

Chiedi quindi 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 produce 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 e 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 di utilizzo raggiunto dal cgroup. È il numero da usare per dimensionare MemoryMax=. Entrambi i file vengono azzerati quando l’unità viene riavviata, perché systemd ricrea il cgroup.

Tutto questo dipende da un prerequisito. Se /var/log/journal non esiste, il journal risiede nella RAM e ogni riga viene persa dopo il riavvio necessario per ripristinare 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 più dati rispetto al boot corrente, la cronologia viene conservata. In questo caso journalctl -k -b -1 può mostrare i messaggi del kernel relativi al boot che si è interrotto.

Un punto di partenza per un piccolo VPS

Su un piano da 2 GB, lascia da 300 a 400 MB al kernel e alla page cache. Non impostare limiti la cui 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 tutti gli altri servizi, la cui necessità è solo ipotizzata, intorno a questo servizio.

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

Mantenere una modalità di accesso è importante e giustifica 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 correggere il server e doverlo riavviare dal pannello di controllo. Modifica soltanto la scelta della vittima da parte del kernel. Non riduce la durata del blocco.

I container vengono eseguiti in cgroup propri, creati dal runtime dei container 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 è tecnicamente fallita, quindi l'OOM killer non viene mai chiamato. Controllare /proc/pressure/memory mentre il problema è in corso: un valore di full avg10 superiore a 40 significa che quasi nessun task ha potuto essere eseguito negli ultimi dieci secondi. Un demone userspace come earlyoom termina i processi 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 questo limite invoca l'OOM killer all'interno del cgroup dell'unità, quindi viene terminato il processo che ha causato il problema, non il processo più grande dell'intero sistema. Impostare MemoryHigh= a un valore inferiore a MemoryMax= e considerare la differenza tra i due valori come una zona di avviso.

Come posso individuare il servizio colpito dall'OOM killer?

Eseguire 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 è terminata la memoria dell'intero sistema. Eseguire quindi journalctl -u <unit> -n 50 e cercare 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; creare quindi quella directory prima del prossimo incidente.

Devo aggiungere swap a un VPS di piccole dimensioni?

Un file di swap di dimensioni contenute è utile per le pagine fredde, allocate una sola volta e mai più utilizzate. Non è utile per 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. Mantenere lo swap su dimensioni moderate e impostare MemorySwapMax=0 sulle unità che si è disposti a perdere, in modo che raggiungano il limite massimo e vengano riavviate rapidamente, mentre i servizi importanti continuano a usare il proprio swap.

Posso limitare un comando senza scrivere un file di unità?

Sì. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh esegue il comando nel terminale all'interno di uno scope temporaneo con quei limiti; i limiti scompaiono quando il comando termina. Tutte le proprietà di systemd.resource-control sono disponibili dopo -p, quindi anche MemorySwapMax=, TasksMax= e CPUWeight= funzionano in questo contesto. Rimuovere --scope e aggiungere --unit=name per eseguire il job in background con l'output nel journal.