Docker: ottimizzare i container attraverso la comprensione dei layer

Oggigiorno si è abituati a spazio disco stratosferico e banda illimitata. Però, non tutti i Cloud/Hosting provider garantiscono non è sempre ed ovunque così.

Anche su server aziendali, lo spazio è spesso e volentieri centellinato, per motivi di gestione ottimale dei backup e per l’ottimizzazione dei costi.

Ecco perchè, comprendere come funzionano i layer di docker è fondamentale per creare immagini ottimizzate, riusabili ed essenziali.

Cos’è un layer

Mi piace ragionare per analogia, aiuta a semplificare i concetti e permette a chi non conosce l’argomento di comprendere per associazione di concetto.

Immagina di avere 3 di fogli trasparenti. Sul primo foglio si disegna un cerchio. Si sovrappone ad esso un altro foglio trasparente e si disegnano due puntini all’interno del cerchio, uno in alto a destra, l’alto in alto a sinistra. Infine applichiamo un terzo foglio trasparente al di sopra degli altri e disegniamo un arco rivolto verso il basso, all’interno del cerchio nella parte inferiore.

Il risultato sarà uno smile. Ecco spiegato come funziona un layer ad un bambino di 5 anni.

Spieghiamolo adesso per un tecnico.

Ciascuna direttiva riportata nel Dockerfile corrisponde ad uno dei fogli trasparenti sui quali è stata riportata una piccola parte del disegno.

La direttiva successiva rappresenta un nuovo livello che è comunque e sempre dipendente dalle direttive precedenti, così come i due puntini che isolati dal contesto non hanno alcun significato ma contestualizzati rappresentano i due occhi dello Smile.

Pertanto il seguente Dockerfile:

FROM php:latest
RUN mkdir /my-workspace
WORKDIR /my-workspace
COPY . .
CMD ["php", "/my-workspace/hello.php"]

sarà costituito da 4 layer espliciti e un certo numero di layer impliciti (definiti dalla stessa immagine php:latest).

Eseguendo il comando docker build -t php_hello_world . dalla directory di progetto, così come descritto anche nel precedente articolo sarà creato il container, ovvero l’insieme dei layer che danno forma all’ambiente virtuale.

Leggere l’elenco dei layer

Per chi utilizza Docker Desktop potrà consultare il dettaglio dei layer attraverso la sezione images e accedendone ai dettagli. Segue uno screenshot dei layer prodotti dal Dockefile sopra:

Ciasucno dei layer prodotti avrà un ingombro su disco che non è la somma dei layer precedenti ma un differenziale. Per chi ha conoscenza di git un layer è come un commit di git, non viene registrato l’intero contenuto del file ma solo le differenze che intercorrono tra la versione precedente e l’attuale.

Per analizzare i layer da CLI utilizzare il comando:

docker history php_hello_world

La differenza tra le due modalità, è che attraverso Docker Desktop l’elenco è mostrato secondo l’ordine di sovrapposizione dei layer: la numerazione descrive l’ordine con cui sono sovrapposti, più è alto il numero più in cima alla pila si trova.

Al contrario da CLI l’ordine è invertito: come per una torre di mattoncini, le righe più in alto rappresentano rappresentano i parapetti, mentra la riga rappresentata in fondo all’elenco rappresenta lo zoccolo.

Utilità dei layer

I layer hanno una grandissima utilità in diverse situazioni:

  1. Se due container derivano da Dockerfile molto simili (dove la similitudine è data dal numero di layer identici a partire dal primo) il tempo di build si riduce drasticamente grazie ad un uso molto efficiente della cache.
  2. Se due container derivano dalla stessa immagine i tempi di download dell’immagine si annullano. Buona pratica è non usare mai il tag latest poichè ci esponiamo ad eventuali bug su rilasci di nuove versioni dell’immgine ma rendere esplicito il numero di versione dell’immagine di partenza da utilizzare.
  3. Il rebuild del medesimo container sfrutterà al massimo la cache, rielaborando solo i layer che effettivamente sono cambiati.

Ottimizzare il numero di layer

Assumiamo che il Dockerfile di riferimento sia questo:

FROM php:latest
RUN mkdir /my-workspace
RUN mkdir /tmp/test
RUN touch /tmp/test/dummy.txt
RUN echo "I am a dummy file" > /tmp/test/dummy.txt
WORKDIR /my-workspace
COPY . .
CMD ["php", "/my-workspace/hello.php"]

Come si nota ci sono tre direttive RUN in sequenza che possono essere raggruppati in un unico comando.

In questo articolo ne anticipavo i benefici, adesso vediamo come farlo.

Aggregare quando possibile le direttive RUN

La direttiva RUN esegue il comando che lo segue direttamente nella shell del container in fase di build. Ciò vuol dire che tutto quello che segue la direttiva RUN è soggetto alle regole della CLI di Linux.

Quindi è possibile concatenare più istruzioni in linux attraverso l’uso del && tra due comandi. Di conseguenza la sequenza di direttive RUN nel Dockerfile di sopra può essere scritto come:

FROM php:latest
RUN mkdir /my-workspace && \
    mkdir /tmp/test && \
    touch /tmp/test/dummy.txt && \
    echo "I am a dummy file" >> /tmp/test/dummy.txt
WORKDIR /my-workspace
COPY . .
CMD ["php", "/my-workspace/hello.php"]

Significato di && nella shell di Linux

L’ottimizzazione di un container richiede una certa padronanza del sistema operativo alla base dell’immagine che si sta utilizzando. Tale conoscenza, in questo caso, si trasforma nello svolgere dei comandi in modo sequenziale, solo se il comando precedente si completa con esito positivo.

In generale in linux ogni comando eseguito da shell quando si conclude termina con un exit code 0, ovvero il software ha svolto il suo compito con esito positivo. Ogni exit code diverso da zero è interpretato dal sistema come un errore.

L’utilizzo della sequenza && tra due comandi indica al sistema di shell che il secondo comando deve essere eseguito solo se il primo comando terminerà con esito positivo (exit code 0).

Il beneficio di utilizzare più comandi in sequenza separati da && in un Dockerfile, permette di ridurre il numero dei layer poichè, come già anticipato ogni istruzione del Dockerfile, genera un livello.

Cercare di ridurre al minimo il numero di layer generati è una buona pratica.

Ma attenzione, questa operazione deve essere svolta con attenzione e consapevolezza.

Per un cliente di Axio Studio che aveva bisogno di mettere in piedi un’architettura a microservizi indipendenti, distribuiti e intercambiabili, abbiamo creato un Dockerfile simile tra i vari microservizi, ottimizzando i tempi di compilazione e l’ingombro su disco.

Significato dello \ nella shell di linux

In molti linguaggi di programmazione, ed introdotto dal linguaggio B, il carattere “\“ è denominato anche come ESCAPE CHAR, ovvero un carattere speciale che permette di interpretare il carattere che lo segue con uno scopo speciale. Per chi programma in un linguaggio più moderno quale Javascript, in PHP, Python o in C# è abituato a questo carattere.

Il più delle volte è seguito dal carattere n (\n) per rappresentare un ritorno a capo o da un carattere t (\t) per rappresentare una tabulazione. Nella forma più complessa per identificare il ritorno a capo standard dei sistemi di Microsoft Windows definito dalla sequenza complessa carriage return + line feed è rappresentato dalla sequenza \r\n.

Nel caso della shell di Linux, lo \ a fine riga, seguito da un ritorno a capo, è identificato come un soft new line, ovvero, non interrompe la sequenza di istruzioni e verrà interpretato dalla shell come unica riga.

Quindi è possibile affermare che questo carattere semplifica la leggibilità delle istruzioni, permettendo di mandare a capo la moltitudine di istruzioni inviate alla direttiva RUN.

Raggruppare le direttive per livello autorizzativo

Nella creazione di un Dockerfile, può succedere di aver bisogno di un livello di accesso superiore all’utente comune, denominato anche super user per svolgere dei compiti particolari.

La direttiva che permette di fare questo in un Dockerfile è user, seguito dal nome dell’utente che nel container dovrà essere l’effettivo esecutore delle istruzioni successive.

USER root

Ogni volta che si esegue tale direttiva, viene creato un nuovo livello, pertanto, avere numerosi cambi di utente, creerà altrettanti livelli extra, talvolta inutili.

Pertanto organizzare il Dockerfile con istruzioni raggruppate per ambito autorizzativo aiuta a migliorare le performance di build dell’immagine finale.

Ridurre al minimo il cambio di workidir

Anche il workdir, come user crea un nuovo livello di build, quindi ulteriore ingombro su disco, fare tanti switch di workdir, causerà la creazione di livelli inutili. Ridurre al minimo indispensabile, bilanciando la leggibilità del Dockerfile, con le operazioni che effettivamente devono essere svolte è una buona pratica da seguire.

Conclusioni

Creare dei Dockerfile ben ottimizzati, aiuta a produrre dei container di qualità.

Il processo di ottimizzazione di un Dockerfile può essere lungo e potrebbe richiedere numerose iterazioni.

Creare dei Dockerfile ottimizzati aiuta a velocizzare il processo di sviluppo e di deploy più in generale, in caso di rebuild delle immagini, sfruttando la cache di Docker.

Ci sono tanti altri aspetti legati all’ottimizzazione delle immagini Docker da poter mettere in pratica. Il primo punto da cui partire è approfondire le proprie conoscenze sugli strumenti utilizzati, non soffermandosi ad un semplice Copia & Incolla.

Se hai altri suggerimenti, che ritieni fondamentali per ottimizzare un Container, lascia un commento per aiutare qualcuno con meno esperienza di te a crescere professionalmente. Ricorda che la diffusione della conoscenza viene ricompensata con altra conoscenza!