Ottimizza le prestazioni dei container Docker con limiti di CPU e memoria

Impara a ottimizzare le prestazioni dei container Docker impostando limiti di CPU e memoria. Questa guida copre opzioni di configurazione essenziali come quote CPU, quote, limiti di memoria e swap. Scopri come monitorare l'utilizzo delle risorse dei container con `docker stats` e implementare le migliori pratiche per prevenire la fame di risorse, migliorare la stabilità delle applicazioni e aumentare l'efficienza complessiva del sistema.

Ottimizza le prestazioni dei container Docker con limiti di CPU e memoria

I container Docker non si comportano automaticamente come piccole macchine virtuali con risorse fisse. A meno che non lo si dica a Docker, un container può competere per CPU e memoria dell'host come qualsiasi altro processo. Questo è comodo su un laptop e rischioso su un server condiviso.

I limiti di CPU e memoria non sono magici potenziatori delle prestazioni. Un limite troppo basso rende un'applicazione più lenta o instabile. Un limite troppo alto non protegge l'host. L'obiettivo è dare a ogni container spazio sufficiente per il lavoro normale e di picco, impedendo a un processo difettoso di compromettere tutto il resto sulla macchina.

Perché i limiti sono importanti

La modalità di guasto comune è familiare: un container avvia un job fuori controllo, la memoria sale, l'host inizia a fare swapping e i servizi non correlati rallentano. Oppure un task batch pesante per la CPU utilizza tutti i core e il container API accanto inizia a mancare gli obiettivi di latenza.

I limiti aiutano con tre problemi pratici:

  • Proteggono l'host da un singolo container che utilizza tutta la memoria.
  • Rendono i test delle prestazioni più onesti perché il container viene eseguito con vincoli simili alla produzione.
  • Ti costringono a notare quando un'applicazione necessita di scaling o ottimizzazione invece di prendere silenziosamente in prestito risorse dai vicini.

Non iniziare con numeri casuali. Esegui il servizio sotto carico, osserva l'utilizzo effettivo e imposta i limiti con un margine. Un piccolo worker in background e un servizio JVM necessitano di trattamenti molto diversi.

Controlli della CPU

Docker espone alcuni controlli della CPU. Il più utile giorno per giorno è --cpus.

Limita un container a circa una CPU e mezza:

docker run -d --name api --cpus="1.5" nginx

Questo è più facile da leggere rispetto all'impostazione manuale di quota e periodo CFS. Sotto il cofano, Docker utilizza i controlli di scheduling della CPU di Linux.

--cpu-shares è diverso. È un peso relativo durante la contesa della CPU, non un limite rigido:

docker run -d --name important-api --cpu-shares 2048 nginx
docker run -d --name batch-worker --cpu-shares 512 worker-image

Quando l'host ha CPU inattiva, entrambi i container possono usarne di più. Quando competono, il primo container ha più peso di scheduling. Questo è utile per la prioritizzazione, ma non impedisce a un container di utilizzare CPU in eccesso.

Se hai bisogno di impostazioni CFS esatte, --cpu-period e --cpu-quota sono ancora disponibili:

docker run -d --name limited-api \
  --cpu-period 100000 \
  --cpu-quota 50000 \
  nginx

Questo dà al container 50.000 microsecondi di tempo CPU in ogni periodo di 100.000 microsecondi, circa metà di una CPU. Per la maggior parte dei team, --cpus="0.5" comunica la stessa intenzione in modo più chiaro.

Controlli della memoria

I limiti di memoria sono più pericolosi dei limiti della CPU perché superarli può uccidere il processo. Impostali deliberatamente e testa il comportamento di picco.

L'opzione di base è --memory:

docker run -d --name web --memory 512m nginx

Se il container supera il limite, il kernel potrebbe uccidere un processo all'interno del container. Nell'output di Docker vedrai spesso questo come un kill OOM:

docker inspect web --format '{{.State.OOMKilled}}'

Il comportamento dello swap è facile da fraintendere. Quando --memory è impostato, --memory-swap è la memoria totale più l'indennità di swap, non l'indennità di swap da sola.

Questo consente 256 MB di RAM e fino a 256 MB di swap, per un totale di 512 MB:

docker run -d --name worker --memory 256m --memory-swap 512m alpine

Impostare --memory-swap uguale a --memory disabilita lo swap aggiuntivo per quel container sui sistemi in cui è disponibile la contabilità dello swap:

docker run -d --name no-extra-swap --memory 256m --memory-swap 256m alpine

Impostare --memory-swap -1 consente uno swap illimitato fino allo swap disponibile dell'host. Questo può mantenere vivo un processo, ma può rendere la latenza terribile.

Monitora prima e dopo

Usa docker stats mentre il servizio è sotto carico realistico:

docker stats
docker stats web worker

Osserva CPU %, MEM USAGE / LIMIT, BLOCK I/O e PIDS. Un servizio che si trova al 100 percento del suo limite di memoria non è "efficiente"; è a un picco di traffico dall'essere ucciso. Un servizio legato alla CPU che viene costantemente limitato può mostrare una CPU media accettabile mentre gli utenti vedono richieste lente.

Per una rapida visualizzazione una tantum:

docker stats --no-stream

Per indizi OOM e riavvio:

docker ps -a
docker inspect <container> --format 'OOM={{.State.OOMKilled}} Exit={{.State.ExitCode}}'
docker logs --tail 100 <container>

Per un monitoraggio continuo, esporta le metriche dei container in Prometheus, Grafana, un servizio di monitoraggio cloud o la piattaforma che già usi. docker stats è triage, non un sistema di allarme a lungo termine.

Un flusso di lavoro di ottimizzazione sensato

Inizia senza limiti rigidi in un ambiente di test e cattura l'utilizzo inattivo, normale e di picco. Quindi imposta la memoria al di sopra del picco osservato con un margine sufficiente per garbage collection, crescita della cache, handshake TLS e brevi raffiche. Per la CPU, decidi se hai bisogno di un limite rigido o solo di una priorità relativa.

Esempio per una piccola API:

docker run -d --name api \
  --cpus="2" \
  --memory 1g \
  --memory-swap 1g \
  -p 8080:80 \
  my-api:latest

Questo dice che l'API ottiene fino a due CPU, 1 GB di RAM e nessuna indennità di swap aggiuntiva. Non è universalmente corretto. È un punto di partenza chiaro per un servizio che è stato testato vicino a quel range.

Per Docker Compose, il motore Docker locale supporta opzioni di risorse come:

services:
  api:
    image: my-api:latest
    mem_limit: 1g
    cpus: 2.0

Le impostazioni delle risorse di Compose e Swarm/Kubernetes non sono identiche, quindi controlla la destinazione del deployment prima di presumere che un campo abbia lo stesso comportamento ovunque.

Errori comuni

Impostare la memoria troppo bassa è l'errore più comune. L'applicazione si avvia, supera un test di fumo, poi muore sotto un pattern di richieste che alloca più memoria di quanto coperto dal test.

Usare i limiti della CPU per nascondere codice inefficiente è un altro. Se un servizio è lento perché sta facendo lavoro costoso per richiesta, un limite CPU più basso rende i sintomi più evidenti. Non risolve il percorso del codice.

Ignorare i runtime dei linguaggi fa anche male. Le app JVM, Node.js, Go, Python e Ruby rispondono diversamente alla pressione della memoria. Alcuni runtime necessitano di impostazioni esplicite dell'heap in modo che le loro ipotesi di memoria interna corrispondano al limite del container.

Infine, non ottimizzare solo un container. L'host ha ancora bisogno di CPU e memoria per il kernel, Docker, logging, agenti di monitoraggio e qualsiasi processo sidecar. Lascia spazio.

Buoni limiti rendono i guasti più piccoli e le prestazioni più prevedibili. Dovrebbero venire dalla misurazione, non da supposizioni.