Cos'è Docker: una breve escursione nella storia e nelle astrazioni di base

Iniziato il 10 agosto a Slurm Videocorso Docker, in cui lo analizziamo completamente, dalle astrazioni di base ai parametri di rete.

In questo articolo parleremo della storia di Docker e delle sue principali astrazioni: Immagine, Cli, Dockerfile. La lezione è destinata ai principianti, quindi è improbabile che possa interessare gli utenti esperti. Non ci sarà sangue, appendice o immersione profonda. Le basi.

Cos'è Docker: una breve escursione nella storia e nelle astrazioni di base

Cos'è Docker

Diamo un'occhiata alla definizione di Docker da Wikipedia.

Docker è un software per automatizzare la distribuzione e la gestione di applicazioni in ambienti containerizzati.

Niente è chiaro da questa definizione. Soprattutto non è chiaro cosa significhi “in ambienti che supportano la containerizzazione”. Per scoprirlo, torniamo indietro nel tempo. Cominciamo con l’era che convenzionalmente chiamo “Era Monolitica”.

Era monolitica

L’era monolitica risale ai primi anni 2000, quando tutte le applicazioni erano monolitiche, con un sacco di dipendenze. Lo sviluppo ha richiesto molto tempo. Allo stesso tempo non c’erano molti server, tutti li conoscevamo per nome e li monitoravamo. C'è un paragone così divertente:

Gli animali domestici sono animali domestici. Nell’era monolitica, trattavamo i nostri server come animali domestici, curati e amati, spazzando via i granelli di polvere. E per una migliore gestione delle risorse, abbiamo utilizzato la virtualizzazione: abbiamo preso un server e lo abbiamo suddiviso in più macchine virtuali, garantendo così l'isolamento dell'ambiente.

Sistemi di virtualizzazione basati su hypervisor

Probabilmente tutti hanno sentito parlare dei sistemi di virtualizzazione: VMware, VirtualBox, Hyper-V, Qemu KVM, ecc. Forniscono l'isolamento delle applicazioni e la gestione delle risorse, ma presentano anche degli svantaggi. Per eseguire la virtualizzazione, è necessario un hypervisor. E l'hypervisor è una risorsa in eccesso. E la macchina virtuale stessa è solitamente un intero colosso: un'immagine pesante contenente un sistema operativo, Nginx, Apache e forse MySQL. L'immagine è grande e la macchina virtuale è scomoda da utilizzare. Di conseguenza, lavorare con le macchine virtuali può essere lento. Per risolvere questo problema sono stati creati sistemi di virtualizzazione a livello del kernel.

Sistemi di virtualizzazione a livello di kernel

La virtualizzazione a livello di kernel è supportata dai sistemi OpenVZ, Systemd-nspawn e LXC. Un esempio lampante di tale virtualizzazione è LXC (Linux Containers).

LXC è un sistema di virtualizzazione a livello di sistema operativo per l'esecuzione di 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.

Essenzialmente LXC crea contenitori. Qual è la differenza tra macchine virtuali e contenitori?

Cos'è Docker: una breve escursione nella storia e nelle astrazioni di base

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

Le differenze tra virtualizzazione e containerizzazione possono essere visualizzate nel diagramma.
Esistono hypervisor hardware, hypervisor sul sistema operativo e contenitori.

Cos'è Docker: una breve escursione nella storia e nelle astrazioni di base

Gli hypervisor hardware sono utili se vuoi davvero isolare qualcosa. Perché è possibile isolare a livello di pagine di memoria e processori.

Esistono hypervisor come programma e ci sono contenitori e ne parleremo ulteriormente. I sistemi di containerizzazione non hanno un hypervisor, ma esiste un Container Engine che crea e gestisce i container. Questa cosa è più leggera, quindi grazie al lavoro con il nucleo c'è meno sovraccarico o nessuno.

Cosa viene utilizzato per la containerizzazione a livello di kernel

Le principali tecnologie che consentono di creare un contenitore isolato da altri processi sono Namespace e Control Group.

Spazi dei nomi: PID, Rete, Montaggio e Utente. Ce ne sono altri, ma per comodità di comprensione ci concentreremo su questi.

Lo spazio dei nomi PID limita i processi. Quando, ad esempio, creiamo uno spazio dei nomi PID e inseriamo un processo lì, diventa con PID 1. Di solito nei sistemi PID 1 è systemd o init. Di conseguenza, quando inseriamo un processo in un nuovo spazio dei nomi, riceve anche il PID 1.

Networking Namespace ti consente di limitare/isolare la rete e inserire le tue interfacce al suo interno. Il montaggio è una limitazione del file system. Utente: limitazione sugli utenti.

Gruppi di controllo: memoria, CPU, IOPS, rete: circa 12 impostazioni in totale. Altrimenti vengono anche chiamati Cgroup (“Gruppi C”).

I gruppi di controllo gestiscono le risorse per un contenitore. Attraverso i Gruppi di Controllo possiamo dire che il contenitore non dovrebbe consumare più di una certa quantità di risorse.

Affinché la containerizzazione funzioni pienamente, vengono utilizzate tecnologie aggiuntive: funzionalità, copia su scrittura e altre.

Le capacità sono quando diciamo a un processo cosa può e cosa non può fare. A livello del kernel, queste sono semplicemente bitmap con molti parametri. Ad esempio, l'utente root ha tutti i privilegi e può fare tutto. Il time server può modificare l'ora del sistema: ha funzionalità su Time Capsule e questo è tutto. Utilizzando i privilegi è possibile configurare in modo flessibile le restrizioni per i processi e quindi proteggersi.

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

Docker attualmente presenta problemi di compatibilità con Cgroups v2, quindi questo articolo si concentra specificamente su Cgroups v1.

Ma torniamo alla storia.

Quando i sistemi di virtualizzazione apparvero a livello del kernel, iniziarono ad essere utilizzati attivamente. Il sovraccarico sull'hypervisor è scomparso, ma sono rimasti alcuni problemi:

  • immagini di grandi dimensioni: inseriscono un sistema operativo, librerie, un mucchio di software diversi nello stesso OpenVZ e alla fine l'immagine risulta comunque piuttosto grande;
  • Non esiste uno standard normale per l’imballaggio e la consegna, quindi rimane il problema delle dipendenze. Ci sono situazioni in cui due pezzi di codice utilizzano la stessa libreria, ma con versioni diverse. Potrebbe esserci un conflitto tra loro.

Per risolvere tutti questi problemi, è arrivata la prossima era.

Era dei contenitori

Quando arrivò l’era dei container, la filosofia di lavorare con loro cambiò:

  • Un processo: un contenitore.
  • Forniamo tutte le dipendenze di cui il processo ha bisogno nel suo contenitore. Ciò richiede il taglio dei monoliti in microservizi.
  • Più piccola è l'immagine, meglio è: ci sono meno possibili vulnerabilità, viene distribuita più velocemente e così via.
  • Le istanze diventano effimere.

Ricordi cosa ho detto sugli animali domestici contro i bovini? In precedenza, gli esemplari erano come animali domestici, ma ora sono diventati come il bestiame. In precedenza, esisteva un monolite: un'applicazione. Ora ci sono 100 microservizi, 100 contenitori. Alcuni contenitori possono avere 2-3 repliche. Diventa meno importante per noi controllare ogni contenitore. Ciò che è più importante per noi è la disponibilità del servizio stesso: cosa fa questo insieme di contenitori. Ciò cambia l’approccio al monitoraggio.

Nel 2014-2015 è fiorita Docker, la tecnologia di cui parleremo ora.

Docker ha cambiato la filosofia e ha standardizzato il pacchetto delle applicazioni. Utilizzando Docker possiamo creare un pacchetto di un'applicazione, inviarla a un repository, scaricarla da lì e distribuirla.

Inseriamo tutto ciò di cui abbiamo bisogno nel contenitore Docker, quindi il problema delle dipendenze è risolto. Docker garantisce la riproducibilità. Penso che molte persone si siano scontrate con l'irriproducibilità: tutto funziona per te, lo metti in produzione e lì smette di funzionare. Con Docker questo problema scompare. Se il tuo contenitore Docker si avvia e fa ciò che deve fare, con un alto grado di probabilità inizierà la produzione e farà lo stesso lì.

Digressione sulle spese generali

Ci sono sempre controversie sulle spese generali. Alcune persone credono che Docker non comporti un carico aggiuntivo, poiché utilizza il kernel Linux e tutti i suoi processi necessari per la containerizzazione. Ad esempio, "se dici che Docker è in sovraccarico, allora il kernel Linux è in sovraccarico".

D'altra parte, se si va più in profondità, ci sono effettivamente diverse cose in Docker che, con un tratto, si può dire che siano in alto.

Il primo è lo spazio dei nomi PID. Quando inseriamo un processo in uno spazio dei nomi, gli viene assegnato il PID 1. Allo stesso tempo, questo processo ha un altro PID, che si trova nello spazio dei nomi host, all'esterno del contenitore. Ad esempio, abbiamo lanciato Nginx in un contenitore, è diventato PID 1 (processo principale). E sull'host ha PID 12623. Ed è difficile dire quanto sia un sovraccarico.

La seconda cosa è Cgroups. Prendiamo Cgroup per memoria, ovvero la capacità di limitare la memoria di un contenitore. Quando è abilitato vengono attivati ​​i contatori e l'accounting della memoria: il kernel deve capire quante pagine sono state allocate e quante sono ancora libere per questo contenitore. Questo è probabilmente un sovraccarico, ma non ho visto studi precisi su come influisce sulle prestazioni. E io stesso non ho notato che l'applicazione in esecuzione in Docker ha subito improvvisamente una forte perdita di prestazioni.

E un'altra nota sulle prestazioni. Alcuni parametri del kernel vengono passati dall'host al contenitore. In particolare, alcuni parametri di rete. Pertanto, se desideri eseguire qualcosa ad alte prestazioni in Docker, ad esempio qualcosa che utilizzerà attivamente la rete, devi almeno regolare questi parametri. Alcuni nf_conntrack, per esempio.

Informazioni sul concetto di Docker

Docker è costituito da diversi componenti:

  1. Docker Daemon è lo stesso Container Engine; lancia contenitori.
  2. Docker CII è un'utilità di gestione Docker.
  3. Dockerfile: istruzioni su come creare un'immagine.
  4. Immagine: l'immagine da cui viene lanciato il contenitore.
  5. Contenitore.
  6. Il registro Docker è un repository di immagini.

Schematicamente assomiglia a questo:

Cos'è Docker: una breve escursione nella storia e nelle astrazioni di base

Il demone Docker viene eseguito su Docker_host e avvia i contenitori. C'è un Client che invia comandi: crea l'immagine, scarica l'immagine, avvia il contenitore. Il demone Docker va al registro e li esegue. Il client Docker può accedere sia localmente (a un socket Unix) che tramite TCP da un host remoto.

Esaminiamo ogni componente.

Demone Docker - questa è la parte server, funziona sulla macchina host: scarica immagini e avvia contenitori da esse, crea una rete tra contenitori, raccoglie log. Quando diciamo “crea un’immagine”, anche il demone sta facendo lo stesso.

interfaccia a riga di comando Docker — Parte client Docker, utilità console per lavorare con il demone. Ripeto, può funzionare non solo localmente, ma anche in rete.

Comandi di base:

docker ps: mostra i contenitori attualmente in esecuzione sull'host Docker.
immagini docker: mostra le immagini scaricate localmente.
docker search <>: cerca un'immagine nel registro.
docker pull <>: scarica un'immagine dal registro sulla macchina.
build della finestra mobile < > - raccogli l'immagine.
docker run <>: avvia il contenitore.
docker rm <> - rimuove il contenitore.
log della finestra mobile <> - log del contenitore
docker start/stop/restart <> - lavora con il contenitore

Se padroneggi questi comandi e sei sicuro di usarli, considerati esperto al 70% in Docker a livello utente.

Dockerfile - istruzioni per creare un'immagine. Quasi ogni comando di istruzione è un nuovo livello. Diamo un'occhiata a un esempio.

Cos'è Docker: una breve escursione nella storia e nelle astrazioni di base

Ecco come appare il Dockerfile: comandi a sinistra, argomenti a destra. Ogni comando presente qui (e generalmente scritto nel Dockerfile) crea un nuovo livello in Image.

Anche guardando il lato sinistro, puoi capire approssimativamente cosa sta succedendo. Diciamo: "crea una cartella per noi" - questo è un livello. "Rendi funzionante la cartella" è un altro livello e così via. La torta a strati semplifica la vita. Se creo un altro Dockerfile e cambio qualcosa nell'ultima riga (eseguo qualcosa di diverso da "python" "main.py" o installo dipendenze da un altro file), i livelli precedenti verranno riutilizzati come cache.

Immagine - questo è l'imballaggio del contenitore; i contenitori vengono lanciati dall'immagine. Se consideriamo Docker dal punto di vista di un gestore di pacchetti (come se stessimo lavorando con pacchetti deb o RPM), allora image è essenzialmente un pacchetto RPM. Tramite yum install possiamo installare l'applicazione, cancellarla, trovarla nel repository e scaricarla. Qui è più o meno lo stesso: i contenitori vengono lanciati dall'immagine, sono archiviati nel registro Docker (simile 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 del Dockerfile. Ogni istruzione del Dockerfile crea un nuovo livello. I livelli possono essere riutilizzati.

Registro Docker è un repository di immagini Docker. Similmente al sistema operativo, Docker dispone di un registro standard pubblico: dockerhub. Ma puoi creare il tuo repository, il tuo registro Docker.

Contenitore - cosa viene lanciato dall'immagine. Abbiamo creato un'immagine secondo le istruzioni del Dockerfile, quindi la lanciamo da questa immagine. Questo contenitore è isolato dagli altri contenitori e deve contenere tutto il necessario per il funzionamento dell'applicazione. In questo caso, un contenitore, un processo. Succede che devi eseguire due processi, ma questo è in qualche modo contrario all'ideologia Docker.

Il requisito "un contenitore, un processo" è correlato allo spazio dei nomi PID. Quando un processo con PID 1 inizia nel Namespace, se muore improvvisamente, muore anche l'intero contenitore. Se lì sono in esecuzione due processi: uno è vivo e l'altro è morto, il contenitore continuerà comunque a vivere. Ma questa è una questione di Best Practices, ne parleremo in altri materiali.

Per approfondire le caratteristiche e il programma completo del corso vi invitiamo a seguire il link: “Videocorso Docker'.

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

Fonte: habr.com

Aggiungi un commento