Che cos'è Docker: una breve storia e le principali astrazioni

Il 10 agosto è iniziato su Sljorm un videocorso su Docker, in cui lo esploriamo completamente: dalle astrazioni di base alle configurazioni di rete.

In questo articolo parleremo della storia di Docker e delle sue principali astrazioni: Image, Cli, Dockerfile. La lezione è rivolta ai principianti, quindi difficilmente sarà interessante per gli utenti esperti. Qui non ci saranno elementi scioccanti, appendici o immersioni profonde. Solo le basi.

Che cos'è Docker: una breve storia e le principali astrazioni

Che cos'è Docker

Diamo un'occhiata alla definizione di Docker da Wikipedia.

Docker è un software per automatizzare la distribuzione e la gestione delle applicazioni in ambienti che supportano la containerizzazione.

Da questa definizione non si capisce molto. In particolare, non è chiaro cosa significhi 'in ambienti che supportano la containerizzazione'. Per comprenderlo, torniamo indietro nel tempo. Iniziamo dall'epoca che chiamerei 'Era Monolitica'.

Era Monolitica

L'era monolitica è l'inizio degli anni 2000, quando tutte le applicazioni erano monolitiche, con molte dipendenze. Lo sviluppo richiedeva molto tempo. All'epoca, i server non erano così tanti, li conoscevamo tutti per nome e li monitoravamo. C'è un divertente confronto:

Riproduci video

I pets — sono animali domestici. Nell'era monolitica, trattavamo i nostri server come animali domestici, coccolandoli e curandoli, spolverando via la polvere. Per una migliore gestione delle risorse, utilizzavamo la virtualizzazione: prendevamo un server e lo suddividevamo in più macchine virtuali, garantendo così l'isolamento dell'ambiente.

Sistemi di virtualizzazione basati su hypervisor

Di sicuro tutti hanno sentito parlare dei sistemi di virtualizzazione: VMware, VirtualBox, Hyper-V, Qemu KVM e così via. Essi garantiscono isolamento delle applicazioni e gestione delle risorse, ma presentano anche degli svantaggi. Per implementare la virtualizzazione, è necessario un hypervisor. E l'hypervisor comporta un overhead di risorse. Inoltre, la macchina virtuale stessa è generalmente un processo complesso: un'immagine pesante, su cui è installato un sistema operativo, Nginx, Apache e forse anche MySQL. L'immagine è grande e operare con una macchina virtuale può risultare scomodo. Di conseguenza, lavorare con le virtual machine può essere lento. Per risolvere questo problema, sono stati creati sistemi di virtualizzazione a livello di kernel.

Sistemi di virtualizzazione a livello di kernel

La virtualizzazione a livello di kernel è supportata da sistemi come OpenVZ, Systemd-nspawn, LXC. Un esempio lampante di questa virtualizzazione è LXC (Linux Containers).

LXC — un sistema di virtualizzazione a livello di sistema operativo per eseguire più istanze isolate del sistema operativo Linux su un singolo nodo. LXC non utilizza macchine virtuali, ma crea un ambiente virtuale con il proprio spazio di processo e stack di rete.

In sostanza, LXC crea contenitori. Qual è la differenza tra macchine virtuali e contenitori?

Che cos'è Docker: una breve storia e le principali astrazioni

Il contenitore non è adatto per isolare i processi: nei sistemi di virtualizzazione a livello di kernel si trovano vulnerabilità che consentono di uscire dal contenitore verso l'host. Pertanto, se hai bisogno di isolare qualcosa, è meglio utilizzare una macchina virtuale.

Le differenze tra virtualizzazione e containerizzazione possono essere viste nello schema.
Ci sono hypervisor hardware, hypervisor sopra il sistema operativo e contenitori.

Che cos'è Docker: una breve storia e le principali astrazioni

Gli hypervisor 'bare-metal' sono una grande cosa se desideri davvero isolare qualcosa. Perché lì hai la possibilità di isolare a livello di pagine di memoria e processori.

Esistono hypervisor sia come programma, sia come contenitori; di quest'ultimi parleremo in seguito. Nei sistemi di containerizzazione non ci sono hypervisor, ma esiste un Container Engine che crea e gestisce i contenitori. Questa soluzione è più leggera, quindi, grazie al lavoro con il kernel, l'overhead è minore o addirittura assente.

Cosa viene utilizzato per la containerizzazione a livello di kernel

Le principali tecnologie che consentono di creare un contenitore isolato dagli altri processi sono Namespaces e Control Groups.

Namespaces: PID, Networking, Mount e User. Ce ne sono altri, ma per semplificare il discorso ci concentreremo su questi.

Il PID Namespace limita i processi. Quando, ad esempio, creiamo un PID Namespace e ci inseriamo un processo, questo riceve PID 1. Di solito nei sistemi, PID 1 è systemd o init. Pertanto, quando inseriamo un processo in un nuovo namespace, anche questo riceve PID 1.

Il Networking Namespace consente di limitare/isolare la rete e già al suo interno di collocare le proprie interfacce. Mount è una restrizione a livello di filesystem. User è una limitazione sugli utenti.

Control Groups: Memoria, CPU, IOPS, Rete — circa 12 impostazioni in totale. Altrimenti sono chiamati Cgroups ("C gruppi").

I gruppi di controllo gestiscono le risorse per il container. Attraverso i gruppi di controllo possiamo specificare che il container non deve consumare più di una certa quantità di risorse.

Per una corretta funzionamento della containerizzazione, vengono utilizzate tecnologie aggiuntive: Capabilities, Copy-on-write e altre.

Le Capabilities definiscono ciò che un processo può e non può fare. A livello del kernel, si tratta di semplici bitmap con molti parametri. Ad esempio, l'utente root ha privilegi completi e può fare qualsiasi cosa. Un server del tempo può modificare l'ora di sistema: ha capabilities su Time Capsule, e così via. Grazie ai privilegi, è possibile impostare flessibilmente le limitazioni per i processi, aumentando la sicurezza.

Il sistema Copy-on-write ci permette di lavorare con le immagini Docker in modo più efficiente.

Attualmente, Docker ha problemi di compatibilità con Cgroups v2, quindi in questo articolo si discutono specificamente i Cgroups v1.

Ma torniamo alla storia.

Quando sono emersi i sistemi di virtualizzazione a livello di kernel, sono stati adottati attivamente. Il sovraccarico sull'hypervisor è scomparso, ma alcuni problemi sono rimasti:

  • immagini grandi: nello stesso OpenVZ si trasferiscono il sistema operativo, le librerie, una miriade di software, e alla fine l'immagine risulta comunque piuttosto grande;
  • non esiste uno standard adeguato per l'imballaggio e la consegna, quindi persiste il problema delle dipendenze. Ci sono situazioni in cui due pezzi di codice utilizzano la stessa libreria, ma con versioni diverse. Tra di loro può verificarsi un conflitto.

Per risolvere tutti questi problemi, è iniziata una nuova era.

L'era dei container

Con l'arrivo dell'era dei container, è cambiata la filosofia di lavoro con essi:

  • Un processo — un container.
  • Tutte le dipendenze necessarie per il processo vengono fornite nel suo container. Questo richiede di suddividere i monoliti in microservizi.
  • Più piccolo è l'immagine, meglio è — meno vulnerabilità potenziali, si distribuisce più rapidamente e così via.
  • Gli istanze diventano effimere.

Ricordi quando parlavo di pets vs cattle? Prima gli istanze erano simili a animali domestici, ora sono diventati come il bestiame. Prima c'era un monolite - un'unica applicazione. Ora ci sono 100 microservizi, 100 container. Alcuni di questi container possono avere 2-3 repliche. Non è più così importante controllare ogni container. È più importante garantire la disponibilità del servizio stesso: ciò che fa questo insieme di container. Questo cambia gli approcci nel monitoraggio.

Nel 2014-2015 c'è stata l'ascesa di Docker - quella tecnologia di cui parleremo ora.

Docker ha cambiato la filosofia e standardizzato il packaging delle applicazioni. Con Docker possiamo imballare un'applicazione, inviarla a un repository, scaricarla da lì e distribuirla.

Nel container Docker includiamo tutto il necessario, quindi si risolve il problema delle dipendenze. Docker garantisce la riproducibilità. Penso che molti abbiano affrontato problemi di riproducibilità: tutto funziona, lo si pubblica in produzione e lì smette di funzionare. Con Docker questo problema scompare. Se il tuo container Docker si avvia e fa quello che deve fare, è molto probabile che si avvii anche in produzione e faccia la stessa cosa.

Riflessione sull'overhead

Ci sono costanti dibattiti sull'overhead. Alcuni sostengono che Docker non comporti un carico aggiuntivo, poiché utilizza il kernel Linux e tutti i suoi processi necessari per la containerizzazione. Si potrebbe dire: "se affermate che Docker è un overhead, allora anche il kernel Linux è un overhead".

D'altra parte, se si approfondisce, ci sono effettivamente alcune cose in Docker che si possono giustificare come overhead, anche se con un certo sforzo.

La prima è il namespace PID. Quando poniamo un processo all'interno del namespace, gli viene assegnato il PID 1. Allo stesso tempo, questo processo ha un altro PID che si trova nel namespace host, al di fuori del contenitore. Ad esempio, abbiamo avviato Nginx nel contenitore, che è diventato PID 1 (processo master). Ma sul host ha il PID 12623. E non è facile dire quanto questo rappresenti un overhead.

La seconda cosa sono i Cgroups. Prendiamo i Cgroups per la memoria, ovvero la possibilità di limitare la memoria a un contenitore. Quando è attivata, si attivano i contatori, il memory accounting: il kernel deve capire quante pagine sono state allocate e quante ne rimangono disponibili per quel contenitore. Questo può comportare un overhead, ma non ho trovato studi precisi su come influisca sulle prestazioni, e non ho notato che un'applicazione avviata in Docker improvvisamente perdesse drasticamente prestazioni.

E un'ulteriore nota sulle prestazioni. Alcuni parametri del kernel vengono passati dall'host al container. In particolare, alcuni parametri di rete. Quindi, se desideri eseguire qualcosa di ad alte prestazioni in Docker, ad esempio qualcosa che utilizza intensamente la rete, dovrai almeno modificare questi parametri. Qualcosa come nf_conntrack, per esempio.

Sull'idea di Docker

Docker è composto da diversi componenti:

  1. Docker Daemon — il motore dei container; avvia i container.
  2. Docker CLI — l'utility per gestire Docker.
  3. Dockerfile — le istruzioni su come creare un'immagine.
  4. Image — l'immagine da cui viene creato il container.
  5. Container.
  6. Docker registry — il repository delle immagini.

In modo schematico, appare più o meno così:

Che cos'è Docker: una breve storia e le principali astrazioni

Su Docker_host gira il daemon Docker, che avvia i container. C'è un Client che invia comandi: crea un'immagine, scarica un'immagine, avvia un container. Il daemon Docker accede al registry e esegue le operazioni. Il client Docker può interagire sia localmente (con il socket Unix) sia tramite TCP da un host remoto.

Passiamo in rassegna ciascun componente.

Docker daemon (daemon) — è la parte server, funziona sulla macchina host: scarica le immagini e avvia i container, crea una rete tra i container e raccoglie i log. Quando diciamo «crea immagine», anche questo è compito del demone.

Docker CLI — è la parte client di Docker, un'utilità da console per lavorare con il demone. Ripeto, può funzionare non solo localmente, ma anche in rete.

Comandi di base:

docker ps — mostra i container attualmente in esecuzione sull'host Docker.
docker images — mostra le immagini scaricate localmente.
docker search <> — ricerca un'immagine nel registry.
docker pull <> — scarica un'immagine dal registry sulla macchina.
docker build <</path/to/dir>> — costruisce un'immagine.
docker run <> — avvia un container.
docker rm <> — elimina un container.
docker logs <> — log del container
docker start/stop/restart <> — gestione del container

Se padroneggi questi comandi e li utilizzi con sicurezza, puoi considerarti aver acquisito il 70% delle competenze di Docker a livello utente.

Dockerfile — istruzioni per creare un'immagine. Quasi ogni comando dell'istruzione rappresenta un nuovo layer. Guardiamo un esempio.

Che cos'è Docker: una breve storia e le principali astrazioni

Ecco come appare un Dockerfile: a sinistra ci sono i comandi, a destra gli argomenti. Ogni comando presente qui (e in generale in un Dockerfile) crea un nuovo strato nell'immagine.

Anche guardando la parte sinistra, si può capire approssimativamente cosa sta succedendo. Diciamo: "crea una cartella" — questo è uno strato. "Imposta la cartella come active" — questo è un altro strato, e così via. La stratificazione semplifica la vita. Se creo un altro Dockerfile e modifico qualcosa nell'ultima riga — non eseguirò "python" "main.py", ma qualcos'altro, o installerò dipendenze da un altro file — gli strati precedenti verranno riutilizzati come cache.

Image — è l'imballaggio del contenitore, dai quali vengono avviati i contenitori. Se guardiamo a Docker come a un gestore di pacchetti (come se stessimo lavorando con pacchetti deb o rpm), l'immagine è essenzialmente un pacchetto rpm. Tramite yum install possiamo installare un'applicazione, rimuoverla, trovarla nel repository, scaricarla. Qui è più o meno la stessa cosa: dai l'immagine vengono avviati i contenitori, che sono conservati nel Docker registry (analogamente a yum, in un repository), e ogni immagine ha un hash SHA-256, un nome e un tag.

L'immagine viene creata secondo le istruzioni nel Dockerfile. Ogni istruzione del Dockerfile genera un nuovo strato. Gli strati possono essere riutilizzati.

Docker registry è un repository di immagini Docker. Analogamente ai sistemi operativi, Docker ha un registro pubblico standard — dockerhub. Tuttavia, è possibile creare il proprio repository, il proprio Docker registry.

Contenitore è ciò che viene eseguito dall'immagine. Secondo le istruzioni del Dockerfile, abbiamo costruito un'immagine, quindi la eseguiamo da quest'immagine. Questo contenitore è isolato dagli altri contenitori e deve contenere tutto il necessario per il funzionamento dell'applicazione. In un contenitore c'è un solo processo. A volte è necessario avere due processi, ma questo è un po' in contraddizione con la filosofia di Docker.

Il requisito "un contenitore — un processo" è legato al PID Namespace. Quando un processo con PID 1 viene avviato nel Namespace, se per caso muore, anche il contenitore termina. Se invece ci sono due processi attivi: uno vive e l'altro muore, il contenitore continuerà a vivere. Ma questo riguarda le Best Practices, di cui parleremo in altri materiali.

Per approfondire le caratteristiche e il programma completo del corso, puoi seguire il link: «Corso video su Docker».

Autore: Marcel Ibraev, amministratore Kubernetes certificato, ingegnere praticante presso Southbridge, relatore e sviluppatore di corsi su Slurm.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster