+39 3662317539/+34 620899163
info@simonecosci.com

Docker: limitare CPU e memoria di un container, e cosa succede se non lo fai

Sviluppo Siti Web a Tenerife

Docker: limitare CPU e memoria di un container, e cosa succede se non lo fai

Su un VPS con cinque container ne basta uno con un memory leak per far cadere anche gli altri quattro. Non è un caso raro: è il comportamento predefinito. Un container senza limiti è un processo che può prendersi tutta la macchina, e il giorno in cui lo fa non sceglie lui chi paga il conto.

Il limite costa una riga. Qui c’è quale riga, cosa fa davvero, e il dettaglio sullo swap che quasi tutti danno per scontato in modo sbagliato.

Il default: nessun limite

La documentazione di Docker, alla pagina Resource constraints, lo dice nella prima frase:

“By default, a container has no resource constraints and can use as much of a given resource as the host’s kernel scheduler allows.”

Cioè: di default un container non ha vincoli e usa tutta la risorsa che lo scheduler del kernel gli concede. Per la CPU vuol dire contendersela con tutti gli altri. Per la memoria è peggio, e la stessa pagina spiega perché:

“On Linux hosts, if the kernel detects that there isn’t enough memory to perform important system functions, it throws an OOME, or Out Of Memory Exception, and starts killing processes to free up memory. Any process is subject to killing, including Docker and other important applications. This can effectively bring the entire system down if the wrong process is killed.”

Quando la RAM finisce, il kernel inizia a uccidere processi, e qualsiasi processo può essere scelto: il database dell’altro container, il demone Docker, sshd. Docker abbassa la probabilità che tocchi al demone, ma la documentazione precisa che “The OOM priority on containers isn’t adjusted”: per i container non fa niente.

Cosa cambia con un limite

Con --memory il container ha un tetto suo. La descrizione di --oom-kill-disable nella stessa pagina mostra le due situazioni una accanto all’altra: “By default, if an out-of-memory (OOM) error occurs, the kernel kills processes in a container”, mentre “If the -m flag isn’t set, the host can run out of memory and the kernel may need to kill the host system’s processes to free memory”.

È tutta qui la differenza. Con il limite, quando il container arriva al tetto, a pagare sono i processi di quel container. Senza, il problema diventa del server.

docker run -d --name app --memory=512m --cpus=1.5 --restart=on-failure mia-immagine
  • --memory=512m (o -m): “The maximum amount of memory the container can use”. Le unità sono b, k, m, g, e il minimo accettato è 6m.
  • --cpus=1.5: su una macchina con due CPU, “the container is guaranteed at most one and a half of the CPUs”. È un tetto, non una prenotazione.
  • --restart=on-failure: se il container viene ucciso, riparte. Il default di --restart è no.

--memory-swap: il default che nessuno si aspetta

Chi scrive --memory=300m di solito pensa di aver dato al container 300 MB. Non è così, se il server ha lo swap attivo:

“If --memory-swap is unset, and --memory is set, the container can use as much swap as the --memory setting, if the host has swap memory configured. For instance, if --memory="300m" and --memory-swap is not set, the container can use 600m in total of memory and swap.”

Quindi --memory=300m da solo vuol dire 300 MB di RAM più 300 MB di swap. Il container non muore a 300 MB: rallenta, scrive su disco, e per un po’ sembra solo un server lento.

--memory-swap è il totale di RAM più swap, non lo swap da solo. Le combinazioni utili sono tre:

  • non impostato: swap pari a --memory, come sopra;
  • uguale a --memory: “the container doesn’t have access to swap”. Il limite è netto;
  • -1: “the container is allowed to use unlimited swap, up to the amount available on the host system”.
docker run -d --memory=300m --memory-swap=300m mia-immagine

Per un servizio web preferisco il secondo caso: un container che muore e riparte si nota e si sistema, uno che va in swap per ore degrada tutto il resto. C’è un dettaglio in più che confonde il debug: “Inside the container, tools like free report the host’s available swap, not what’s available inside the container”. Dentro il container free non vi dice il vostro limite.

La CPU: tetto rigido o peso relativo

Per la CPU le opzioni sono due, e fanno cose diverse.

--cpus è un tetto rigido. La documentazione spiega che --cpus="1.5" “is the equivalent of setting --cpu-period="100000" and --cpu-quota="150000"“: nel periodo di 100 ms il container può usare 150 ms di tempo CPU, poi viene rallentato anche se il resto della macchina è ferma.

--cpu-shares invece è un peso. Il default è 1024, e conta solo quando la CPU scarseggia:

“This is only enforced when CPU cycles are constrained. When plenty of CPU cycles are available, all containers use as much CPU as they need. In that way, this is a soft limit.”

Se volete che il job notturno di backup non rallenti il sito, --cpu-shares=512 sul backup basta: di notte, con la CPU libera, il backup va a piena velocità, e quando il sito ha traffico il backup cede il passo. Se invece volete che un container non superi mai una certa quota, serve --cpus.

Lo stesso in Docker Compose

In un file Compose ci sono due modi. Il primo è nella sezione deploy, l’esempio è quello della documentazione:

services:
  frontend:
    image: example/webapp
    deploy:
      resources:
        limits:
          cpus: '0.50'
          memory: 50M
          pids: 1
        reservations:
          cpus: '0.25'
          memory: 20M

limits è il tetto (“The platform must prevent the container from allocating more resources”), reservations il minimo garantito. pids limita il numero di processi: utile contro un processo che continua a fare fork.

Il secondo modo sono gli attributi a livello di servizio: mem_limit, memswap_limit, cpus, pids_limit. La documentazione dice che, se li usate, “must be consistent” con i valori corrispondenti in deploy. Scegliete un modo e restate su quello, soprattutto se il file lo modifica più di una persona.

Come controllare che il limite ci sia

Non fidatevi del file, guardate il container in esecuzione:

docker stats --no-stream

La colonna MEM USAGE / LIMIT mostra “the total memory the container is using, and the total amount of memory it is allowed to use”. Se lì non c’è il valore che avete scritto, il limite non è applicato, qualunque cosa dica il file Compose.

Per sapere se un container è stato ucciso per memoria, Docker emette un evento apposito: tra gli eventi dei container c’è oom.

docker events --filter 'event=oom' --since 24h

E se il limite va cambiato su un container già in esecuzione, docker update “dynamically updates container configuration” senza ricrearlo:

docker update --memory=1g --memory-swap=1g app

Il cambio vale per quel container. Se non riportate il valore anche nel file Compose o nello script di avvio, alla prossima ricreazione il limite torna quello vecchio.

Errori da evitare

  • Mettere --memory e pensare di aver finito. Senza --memory-swap, sul server con swap il container ha il doppio della memoria che credete.
  • Usare --oom-kill-disable per “proteggere” un container. La documentazione lo sconsiglia esplicitamente: “You shouldn’t try to circumvent these safeguards by manually setting --oom-score-adj to an extreme negative number on the daemon or a container, or by setting --oom-kill-disable on a container”. Senza -m, poi, è l’host a rimetterci.
  • Fissare il limite a occhio. Prima guardate docker stats per qualche giorno di traffico normale, poi mettete il tetto con un margine. Un limite troppo basso vi fa uccidere il container a ogni picco.
  • Limitare la memoria e non mettere una restart policy. Il container ucciso resta fermo, e ve ne accorgete quando un cliente vi scrive.
  • Dimenticare il rootless. Con Docker in modalità rootless, --cpus, --memory e --pids-limit funzionano solo con cgroup v2 e systemd. Se docker info mostra none come Cgroup Driver, “rootless mode ignores the cgroup-related docker run flags”: il comando non dà errore, il limite semplicemente non c’è. Anche qui, docker stats è la prova.

In sintesi

Un limite di memoria non rende l’applicazione più efficiente. Fa sì che il problema resti dove nasce: nel container che ha il leak, non nel database accanto o nella sessione SSH con cui dovete entrare a sistemarlo. Tre opzioni e una restart policy, e un container che impazzisce smette di poter fermare tutto il server.

Se avete un VPS con più servizi in Docker e volete capire cosa succede quando uno di loro va fuori controllo, scriveteci: è il tipo di verifica che facciamo prima che serva.