Guida Completa ai Cgroup di Systemd per Limitazione e Isolamento delle Risorse

Utilizza i cgroup, le slice e le proprietà delle unità di systemd per limitare CPU, memoria e I/O senza modificare direttamente i file dei cgroup.

Guida Completa ai Cgroup di Systemd per Limitazione e Isolamento delle Risorse

Systemd inserisce già i servizi nei control group di Linux. Non è necessario creare manualmente directory cgroup per impedire a un worker batch di consumare l'intera macchina. In molti casi, puoi aggiungere alcune proprietà a un servizio o a una slice, ricaricare systemd e ottenere controlli su CPU, memoria, task e I/O che sopravvivono al riavvio e sono visibili nei normali strumenti systemctl.

Il trucco sta nello scegliere il tipo di limite giusto. Un limite rigido di memoria può proteggere l'host ma uccidere il servizio se impostato troppo basso. I pesi della CPU sono delicati finché il sistema non è occupato. Le quote CPU sono rigide ma possono aggiungere latenza. I limiti I/O dipendono dallo stack di archiviazione e dalla versione dei cgroup. Il controllo delle risorse non è una casella da spuntare; è un compromesso operativo.

Comprendere i Control Group (cgroup)

Prima di addentrarci nell'implementazione di systemd, è essenziale comprendere i concetti fondamentali dei cgroup. I cgroup sono un meccanismo gerarchico nel kernel Linux che permette di raggruppare processi e assegnare politiche di gestione delle risorse a questi gruppi. Queste politiche possono includere:

  • CPU: Limitare il tempo CPU, dare priorità all'accesso alla CPU.
  • Memoria: Impostare limiti di utilizzo della memoria, prevenire condizioni di out-of-memory (OOM).
  • I/O: Limitare le operazioni di lettura/scrittura su disco.
  • Rete: Il controllo di rete è possibile tramite il traffic control di Linux e strumenti correlati, ma le proprietà integrate delle unità systemd si concentrano principalmente su CPU, memoria, numero di processi, accesso ai dispositivi e I/O a blocchi.
  • Accesso ai Dispositivi: Controllare l'accesso a dispositivi specifici.

Il kernel espone le configurazioni dei cgroup attraverso un file system virtuale, tipicamente montato su /sys/fs/cgroup. Ogni controller (es. cpu, memory) ha la propria directory e, al loro interno, gerarchie di directory rappresentano gruppi e i loro limiti di risorse associati.

Architettura di Gestione dei Cgroup di Systemd

Systemd astrae la complessità della manipolazione diretta dei cgroup fornendo un sistema strutturato di gestione delle unità. Organizza i processi in una gerarchia di unità, che vengono poi mappate alle gerarchie dei cgroup. I tipi di unità principali rilevanti per la gestione delle risorse sono:

  • Slice: Sono contenitori astratti per unità di servizio. Le slice formano una gerarchia, consentendo la delega delle risorse. Ad esempio, una slice per sessioni utente potrebbe contenere slice per singole applicazioni. Systemd crea automaticamente slice per servizi di sistema, sessioni utente e macchine virtuali/contenitori.
  • Scope: Sono tipicamente utilizzati per gruppi temporanei o creati dinamicamente di processi, spesso associati a sessioni utente o servizi di sistema che non sono gestiti come unità di servizio complete. Sono transitori ed esistono finché i processi al loro interno sono in esecuzione.
  • Servizi: Sono le unità fondamentali per gestire demoni e applicazioni. Quando un'unità di servizio viene avviata, systemd inserisce i suoi processi in una gerarchia cgroup, solitamente all'interno di una slice. I limiti di risorse possono essere applicati direttamente alle unità di servizio.

La gerarchia predefinita di systemd spesso appare così:

-.slice (Slice radice)
  |- system.slice
  |  |- <nome_servizio>.service
  |  |- altro-servizio.service
  |  ... 
  |- user.slice
  |  |- user-1000.slice
  |  |  |- session-c1.scope
  |  |  |  |- <applicazione>.service (se avviato dall'utente)
  |  |  |  ...
  |  |  ...
  |  ... 
  |- machine.slice (per VM/contenitori)
  ... 

Applicare Limiti di Risorse con i File delle Unità Systemd

Systemd permette di specificare i limiti di risorse dei cgroup direttamente nei file .service, .slice o .scope. Queste direttive vengono inserite rispettivamente nelle sezioni [Service], [Slice] o [Scope].

Limiti CPU

Le direttive principali per il controllo delle risorse CPU sono:

  • CPUQuota=: Limita il tempo CPU totale che l'unità può utilizzare. Viene specificato come percentuale (es. 50% per mezzo core CPU) o frazione di un core CPU (es. 0.5). È anche possibile specificare un valore in microsecondi per periodo. Il periodo predefinito è 100ms.
  • CPUWeight=: Imposta un peso relativo per il tempo CPU sui sistemi cgroup v2. Un'unità con un peso maggiore ottiene una quota maggiore quando c'è contesa, ma non riserva CPU quando la macchina è inattiva.
  • CPUShares=: Peso più vecchio dell'era cgroup v1. Preferisci CPUWeight= sulle distribuzioni moderne a meno che non sia necessaria la compatibilità con v1.
  • CPUQuotaPeriodSec=: Imposta il periodo per CPUQuota. Il valore predefinito è 100ms.

Esempio: Limitare un server web al 75% di un core CPU:

Crea o modifica un file di servizio, ad esempio /etc/systemd/system/mywebapp.service:

[Unit]
Description=La Mia Applicazione Web

[Service]
ExecStart=/usr/bin/mywebapp
User=webappuser
Group=webappgroup

# Limita al 75% di un core CPU
CPUQuota=75%

[Install]
WantedBy=multi-user.target

Dopo aver creato o modificato il file di servizio, ricarica il demone systemd e riavvia il servizio:

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service

Limiti di Memoria

I limiti di memoria sono controllati da direttive come:

  • MemoryMax=: Imposta un limite rigido sulla quantità di memoria che i processi dell'unità possono consumare. Può essere specificato in byte o con suffissi come K, M, G, T (es. 512M).
  • MemoryLimit=: Ortografia più vecchia mantenuta su alcuni sistemi per compatibilità. Preferisci MemoryMax= sulle versioni moderne di systemd.
  • MemoryHigh=: Imposta un limite morbido. Quando questo limite viene avvicinato, il recupero della memoria (swapping) viene attivato in modo più aggressivo, ma il limite rigido non è ancora applicato.
  • MemorySwapMax=: Limita la quantità di spazio di swap che l'unità può utilizzare.

Esempio: Limitare un database a 2GB di RAM:

Crea o modifica un file di servizio, ad esempio /etc/systemd/system/mydb.service:

[Unit]
Description=Servizio Database

[Service]
ExecStart=/usr/bin/mydb
User=dbuser
Group=dbgroup

# Limita la memoria a 2 Gigabyte
MemoryMax=2G

[Install]
WantedBy=multi-user.target

Ricaricare e riavviare:

sudo systemctl daemon-reload
sudo systemctl restart mydb.service

Limiti I/O

La limitazione dell'I/O può essere controllata utilizzando direttive come:

  • IOWeight=: Imposta un peso relativo per le operazioni I/O. Valori più alti danno maggiore priorità I/O. L'intervallo è da 1 a 1000 (predefinito 500).
  • IOReadBandwidthMax=: Limita la larghezza di banda di lettura I/O. Specificato come [<dispositivo>] <byte_al_secondo>. Ad esempio, IOReadBandwidthMax=/dev/sda 100M limita le operazioni di lettura su /dev/sda a 100MB/s.
  • IOWriteBandwidthMax=: Limita la larghezza di banda di scrittura I/O. Formato simile a IOReadBandwidthMax.

Esempio: Limitare un servizio di elaborazione in background a 50MB/s su un disco specifico:

Crea o modifica un file di servizio, ad esempio /etc/systemd/system/batchproc.service:

[Unit]
Description=Servizio di Elaborazione Batch

[Service]
ExecStart=/usr/bin/batchproc
User=batchuser
Group=batchgroup

# Limita le operazioni di scrittura a 50MB/s su /dev/sdb
IOWriteBandwidthMax=/dev/sdb 50M

# Dai una priorità di lettura moderata
IOWeight=200

[Install]
WantedBy=multi-user.target

Ricaricare e riavviare:

sudo systemctl daemon-reload
sudo systemctl restart batchproc.service

Gestire e Monitorare i Cgroup

Systemd fornisce strumenti per ispezionare e gestire i cgroup associati alle tue unità.

Ispezionare lo Stato dei Cgroup

Il comando systemctl status fornisce informazioni sull'appartenenza ai cgroup di un'unità e sull'utilizzo delle risorse.

systemctl status mywebapp.service

Cerca righe che indicano il percorso del cgroup. Ad esempio:

● mywebapp.service - La Mia Applicazione Web
     Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-27 10:00:00 UTC; 1 day ago
       Docs: man:mywebapp(8)
   Main PID: 12345 (mywebapp)
      Tasks: 5 (limit: 4915)
     Memory: 15.5M
        CPU: 2h 30m 15s
      CGroup: /system.slice/mywebapp.service
              └─12345 /usr/bin/mywebapp

Puoi anche ispezionare direttamente il file system dei cgroup:

systemd-cgls # Mostra la gerarchia cgroup gestita da systemd
systemd-cgtop # Simile a top, ma per i cgroup

Per vedere i limiti specifici applicati al cgroup di un servizio:

# Per i limiti di memoria su un host cgroup v2 tipico
cat /sys/fs/cgroup/system.slice/mywebapp.service/memory.max

# Per i limiti CPU
cat /sys/fs/cgroup/system.slice/mywebapp.service/cpu.max

I percorsi e i nomi dei file esatti variano in base alla versione dei cgroup e alla distribuzione. Sui sistemi cgroup v1, potrebbero ancora esistere percorsi specifici del controller come /sys/fs/cgroup/memory/.... Sui sistemi cgroup v2, la gerarchia unificata sotto /sys/fs/cgroup/... è la vista normale.

Modificare i Limiti dei Cgroup al Volo

Sebbene sia buona pratica impostare i limiti nei file delle unità, puoi modificarli temporaneamente usando systemctl set-property:

sudo systemctl set-property mywebapp.service CPUQuota=50%

A seconda della versione di systemd e dei flag, set-property può scrivere un drop-in sotto /etc/systemd/system.control/ per proprietà persistenti. Usa systemctl cat mywebapp.service e systemctl show mywebapp.service -p CPUQuota -p MemoryMax per confermare cosa è successo. Per infrastruttura come codice e revisione tra pari, un drop-in esplicito dell'unità è solitamente più chiaro.

Slice per la Delega delle Risorse

Le slice sono potenti per gestire gruppi di servizi o applicazioni. Puoi definire limiti di risorse su una slice, e tutti i servizi o scope all'interno di quella slice erediteranno o saranno vincolati da tali limiti.

Esempio: Creare una slice dedicata per job batch intensivi di risorse:

Crea un file slice, ad esempio /etc/systemd/system/batch.slice:

[Unit]
Description=Slice di Elaborazione Batch

[Slice]
# Limita la CPU totale per tutti i job in questa slice a 1 core
CPUQuota=100%
# Limita la memoria totale a 4GB
MemoryMax=4G

Ora puoi configurare i servizi per essere eseguiti all'interno di questa slice usando la direttiva Slice= nei loro file .service:

[Unit]
Description=Job Batch Specifico

[Service]
ExecStart=/usr/bin/mybatchjob

# Posiziona questo servizio nella slice batch
Slice=batch.slice

[Install]
WantedBy=multi-user.target

Ricaricare systemd, abilitare/avviare la slice se necessario (anche se spesso viene attivata implicitamente) e avviare il servizio.

sudo systemctl daemon-reload
sudo systemctl start mybatchjob.service

Questo approccio ti permette di raggruppare processi correlati e gestire il loro consumo collettivo di risorse.

Migliori Pratiche e Considerazioni

  • Inizia con Limiti Incrementali: Quando imposti i limiti, inizia con valori conservativi e aumentali gradualmente secondo necessità. Limiti aggressivi possono destabilizzare le applicazioni.
  • Monitora: Monitora regolarmente l'utilizzo delle risorse del tuo sistema e l'impatto delle impostazioni dei cgroup. Strumenti come systemd-cgtop, htop, top e iotop sono preziosi.
  • Comprendi Cgroup v1 vs. v2: Systemd supporta sia cgroup v1 che v2. Mentre molte direttive sono simili, v2 offre una gerarchia unificata e alcune differenze comportamentali. Assicurati di sapere quale versione sta usando il tuo sistema se incontri problemi complessi.
  • Prioritizzazione vs. Limiti Rigidi: Usa CPUWeight per la prioritizzazione quando le risorse sono scarse e CPUQuota per limiti rigidi. Allo stesso modo, MemoryHigh è per la pressione prima del limite rigido, e MemoryMax è il limite rigido.
  • Servizio vs. Slice: Usa le unità di servizio per singole applicazioni e le slice per gestire gruppi di applicazioni correlate o pool di risorse.
  • Documentazione: Documenta chiaramente i limiti di risorse applicati ai servizi critici, specialmente in ambienti di produzione.
  • OOM Killer: Tieni presente che se un processo supera il suo limite MemoryMax, il killer Out-Of-Memory (OOM) del kernel potrebbe terminarlo, anche se è all'interno di un cgroup. Systemd può gestire come si comporta il killer OOM per cgroup specifici usando direttive come OOMPolicy=.

Un Modo Più Sicuro per Implementare i Limiti

Inizia con l'osservazione. Prima di aggiungere limiti, guarda come si comporta il servizio durante il carico normale e durante il peggior carico previsto:

systemctl status mywebapp.service
systemd-cgtop
systemctl show mywebapp.service -p MemoryCurrent -p CPUUsageNSec -p TasksCurrent

Per la memoria, una buona prima mossa è spesso MemoryHigh= piuttosto che MemoryMax=:

[Service]
MemoryHigh=1G
MemoryMax=1536M

MemoryHigh= dice al kernel di applicare pressione prima che il servizio raggiunga il limite massimo. MemoryMax= è il muro. Se il processo lo supera e la memoria non può essere recuperata, il kernel potrebbe uccidere un processo nel cgroup. Questo può essere esattamente ciò che vuoi per un worker fuori controllo, ma è una brutta sorpresa per un database a meno che tu non lo abbia pianificato.

Per la CPU, decidi se vuoi equità o un limite rigido:

[Service]
CPUWeight=50

Questo abbassa la priorità in caso di contesa ma permette comunque al servizio di utilizzare CPU inattiva. Per job in background, spesso è meglio di una quota.

[Service]
CPUQuota=200%

Questo limita il servizio a circa due core CPU di tempo. È utile per un elaboratore batch rumoroso, ma può danneggiare applicazioni sensibili alla latenza se i thread di lavoro vengono limitati durante i picchi di traffico.

Per esplosioni di processi, aggiungi un limite di task:

[Service]
TasksMax=200

Questo protegge l'host da tempeste di fork accidentali. Impostalo abbastanza alto per i normali conteggi di thread. Carichi di lavoro come Java, database e browser possono utilizzare più task di quanto ti aspetti.

Drop-In Invece di Modificare le Unità del Venditore

Evita di modificare i file delle unità forniti dai pacchetti sotto /usr/lib/systemd/system/ o /lib/systemd/system/. Usa un drop-in:

sudo systemctl edit mywebapp.service

Poi aggiungi:

[Service]
MemoryHigh=1G
MemoryMax=1536M
CPUWeight=80

Dopo aver salvato:

sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service
systemctl cat mywebapp.service

systemctl cat mostra l'unità del venditore e la tua sovrascrittura insieme. Questo rende il debug futuro molto più semplice perché la configurazione attiva è visibile in un unico comando.

Slice per Team, Tenant e Classi di Carico di Lavoro

Le slice diventano utili quando smetti di pensare a un servizio alla volta. Supponiamo che un host esegua l'API, un generatore di report e diversi worker di importazione. Potrebbe non interessarti quale worker di importazione usa la CPU, ma ti interessa che tutto il lavoro di importazione insieme non possa affamare l'API.

Crea una slice:

# /etc/systemd/system/import.slice
[Unit]
Description=Carichi di lavoro di importazione e backfill

[Slice]
CPUWeight=30
MemoryHigh=4G
MemoryMax=5G

Metti i servizi di importazione al suo interno:

[Service]
Slice=import.slice
ExecStart=/usr/local/bin/import-worker

Ora il gruppo ha una pressione condivisa. Questo è più pulito che mettere limiti rigidi separati su ogni worker e sperare che i calcoli funzionino ancora dopo che qualcuno ne aggiunge uno nuovo.

C'è un dettaglio di denominazione che coglie di sorpresa: i nomi delle slice codificano la gerarchia. customer-a.slice è una slice di primo livello. customer-a-batch.slice non è un figlio di customer-a.slice; è solo un altro nome di primo livello. Le slice gerarchiche usano i trattini come separatori in un modo specifico, quindi leggi systemd.slice(5) prima di progettare un grande albero di slice.

Cosa i Limiti di Risorsa Non Possono Risolvere

I cgroup possono impedire a un carico di lavoro di sopraffare l'host, ma non possono rendere veloce una macchina sottodimensionata. Se un database ha bisogno di più memoria per il suo working set di quanto tu permetta, potrebbe impiegare più tempo a recuperare memoria o fallire sotto carico. Se un'API ha bisogno di tempi di risposta brevi, una quota CPU rigorosa può creare ritardi di limitazione che sembrano latenza casuale. Se un dispositivo di archiviazione è già saturo, i pesi I/O possono migliorare l'equità ma non creare throughput.

Tratta i limiti come guardrail. Abbinali a impostazioni a livello di applicazione: dimensioni del buffer del database, numero di worker, concorrenza della coda, limiti heap JVM, GOMEMLIMIT di Go, flag di memoria di Node, o qualunque cosa fornisca il tuo runtime. La configurazione migliore è solitamente entrambe: l'applicazione conosce il proprio modello di memoria e concorrenza, e systemd protegge il resto della macchina se quel modello si rompe.

Il Modello Mentale da Tenere a Mente

Usa i limiti a livello di servizio per un singolo demone. Usa i limiti a livello di slice per un gruppo di carichi di lavoro correlati. Usa i pesi quando vuoi priorità in caso di contesa. Usa quote e limiti rigidi di memoria quando hai bisogno di un confine fermo e sei preparato alle conseguenze. Verifica le proprietà effettive con systemctl show, osserva il comportamento con systemd-cgtop e mantieni la configurazione in drop-in o file di unità che il tuo team possa revisionare.