Il 10 agosto è iniziato , in cui esaminiamo ogni aspetto — dalle astrazioni di base ai parametri di rete.
In questo articolo parleremo della storia di Docker e delle sue principali astrazioni: Image, Cli, Dockerfile. La lezione è destinata ai principianti, quindi difficilmente sarà interessante per gli utenti esperti. Non ci saranno elementi estremi, appendici o immersioni profonde. Solo le basi.

Cos'è Docker
Diamo un'occhiata alla definizione di Docker tratta da Wikipedia.
Docker è un software per l'automazione della distribuzione e gestione delle applicazioni in ambienti che supportano la containerizzazione.
Da questa definizione non si capisce molto. Non è chiaro, in particolare, cosa significhi 'in ambienti che supportano la containerizzazione'. Per chiarire, torniamo indietro nel tempo. Iniziamo con l'epoca che definisco 'Era Monolitica'.
Era Monolitica
L'era monolitica è l'inizio degli anni 2000, quando tutte le applicazioni erano monolitiche, con molte dipendenze. Lo sviluppo era lungo. All'epoca non c'erano molti server, li conoscevamo tutti per nome e li monitoravamo. C'è un confronto divertente:

Gli animali domestici sono come animali da compagnia. Nell'era monolitica trattavamo i nostri server come animali domestici, li curavamo e li coccolavamo, spolverando ogni particella. 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
Tutti hanno certamente sentito parlare dei sistemi di virtualizzazione: VMware, VirtualBox, Hyper-V, Qemu KVM, ecc. Questi forniscono isolamento delle applicazioni e gestione delle risorse, ma hanno anche degli svantaggi. Per implementare la virtualizzazione è necessario un hypervisor. E un hypervisor comporta un overhead delle risorse. Inoltre, la macchina virtuale stessa è generalmente piuttosto pesante — un'immagine grande, con un sistema operativo, Nginx, Apache, e magari anche MySQL. L'immagine è grande, e la macchina virtuale è scomoda da gestire. 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 chiaro esempio di tale 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 unico nodo. LXC non utilizza macchine virtuali, ma crea un ambiente virtuale con uno spazio di processi e uno stack di rete propri.
In sostanza, LXC crea contenitori. Qual è la differenza tra macchine virtuali e contenitori?

Il contenitore non è adatto per isolare i processi: nei sistemi di virtualizzazione a livello di kernel sono state trovate vulnerabilità che consentono di uscire dal contenitore verso l'host. Pertanto, se hai bisogno di isolare qualcosa, è meglio utilizzare una virtual machine.
Le differenze tra virtualizzazione e containerizzazione possono essere viste in uno schema.
Ci sono hypervisor hardware, hypervisor sopra i sistemi operativi e contenitori.

Gli hypervisor 'bare-metal' sono ottimi se desideri davvero isolare qualcosa, perché offrono la possibilità di isolare a livello di pagine di memoria e CPU.
Ci sono hypervisor come programmi e ci sono contenitori, di cui parleremo di seguito. Nei sistemi di containerizzazione non c'è hypervisor, ma c'è il Container Engine che crea e gestisce i contenitori. Questa è una 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 ci concentreremo su questi.
Il PID Namespace limita i processi. Quando, ad esempio, creiamo un PID Namespace e ci mettiamo un processo, esso ottiene il PID 1. Di solito, nel sistema, il PID 1 è systemd o init. Pertanto, quando mettiamo un processo in un nuovo namespace, riceve anch'esso il PID 1.
Il Networking Namespace consente di limitare/isolare la rete e di gestire le proprie interfacce al suo interno. Mount è una limitazione sulla filesystem. User è una limitazione sugli utenti.
Control Groups: Memoria, CPU, IOPS, Rete — ci sono circa 12 impostazioni. Sono anche chiamati Cgroups ('C-gruppi').
I Control Groups gestiscono le risorse per il contenitore. Attraverso i Control Groups possiamo specificare che un contenitore non deve consumare più di una certa quantità di risorse.
Affinché la containerizzazione funzioni a pieno, vengono utilizzate tecnologie aggiuntive: Capabilities, Copy-on-write e altre.
Capabilities sono quando diciamo a un processo cosa può fare e cosa non può fare. A livello di kernel, si tratta semplicemente di bitmap con una grande quantità di parametri. Per esempio, l'utente root ha pieni privilegi, può fare tutto. Il server del tempo può modificare l'ora di sistema: ha capabilities su Time Capsule, e basta. Con i privilegi, è possibile configurare in modo flessibile le restrizioni per i processi e così proteggere se stessi.
Il sistema Copy-on-write ci consente di lavorare con le immagini Docker, utilizzandole in modo più efficiente.
Attualmente, Docker ha problemi di compatibilità con Cgroups v2, quindi in questo articolo si considerano proprio i Cgroups v1.
Ma torniamo alla storia.
Quando sono emersi i sistemi di virtualizzazione a livello di kernel, hanno iniziato a essere adottati attivamente. L'overhead del hypervisor è scomparso, ma alcuni problemi sono rimasti:
- immagini grandi: nella stessa OpenVZ vengono spinti il sistema operativo, le librerie e una moltitudine di software, e alla fine l'immagine risulta comunque abbastanza grande;
- non c'è uno standard adeguato per l'imballaggio e la consegna, quindi rimane il problema delle dipendenze. Ci sono situazioni in cui due pezzi di codice utilizzano una stessa libreria, ma con versioni diverse. Tra di esse può sorgere un conflitto.
Per risolvere tutti questi problemi, è giunta la successiva era.
L'era dei contenitori
Quando è arrivata l'Era dei contenitori, la filosofia di lavoro con essi è cambiata:
- Un processo — un contenitore.
- Tutte le dipendenze necessarie per il processo vengono fornite nel suo contenitore. Questo richiede di suddividere i monoliti in microservizi.
- Minore è l'immagine, meglio è: meno potenziali vulnerabilità, più veloce il rilascio e così via.
- Gli istanze diventano effimere.
Ricordate, parlavo di pets vs cattle? Prima, le istanze erano come animali domestici, mentre ora sono come cattle — bestiame. Prima c'era un monolite — un'applicazione. Ora ci sono 100 microservizi, 100 contenitori. Alcuni di questi contenitori possono avere 2-3 repliche. Non ci importa tanto controllare ogni contenitore. Ciò che è più importante è la disponibilità del servizio stesso: quello che fa questo insieme di contenitori. Questo cambia gli approcci nel monitoraggio.
Tra il 2014 e il 2015 c'è stata l'esplosione di Docker — quella tecnologia di cui parleremo ora.
Docker ha cambiato la filosofia e ha standardizzato l'imballaggio delle applicazioni. Con Docker possiamo imballare un'applicazione, inviarla in un repository, scaricarla e distribuirla.
Nel contenitore Docker includiamo tutto il necessario, quindi il problema delle dipendenze viene risolto. Docker garantisce la riproducibilità. Credo che molti abbiano affrontato il problema della non riproducibilità: tutto funziona, lo carichi su produzione e lì smette di funzionare. Con Docker questo problema scompare. Se il tuo contenitore Docker si avvia e svolge ciò che deve fare, è molto probabile che si avvii in produzione e faccia lo stesso.
Un accenno sull'overhead
Ci sono continui dibattiti sull'overhead. Alcuni ritengono che Docker non comporti un carico aggiuntivo, poiché utilizza il kernel Linux e tutti i suoi processi necessari per la containerizzazione. Dicono che "se dici che Docker è overhead, allora anche il kernel Linux è overhead".
D'altra parte, se approfondiamo, in Docker ci sono effettivamente alcune cose che si possono considerare, con un po' di elasticità, come overhead.
Il primo è il namespace PID. Quando collochiamo un processo in un 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, è diventato PID 1 (processo master). E sull'host ha PID 12623. Ed è difficile dire quanto sia overhead.
La seconda cosa è i Cgroups. Prendiamo i Cgroups per la memoria, cioè la possibilità di limitare la memoria di un contenitore. Quando è attivata, vengono attivati i contatori, il memory accounting: il kernel deve capire quante pagine sono allocate e quante sono ancora libere per questo contenitore. Questo potrebbe essere overhead, ma non ho trovato studi precisi su come influisca sulle prestazioni, e non ho notato che un'applicazione avviata in Docker perda all'improvviso prestazioni.
E un'altra osservazione sulle prestazioni. Alcuni parametri del kernel vengono passati dall'host al contenitore. In particolare, alcuni parametri di rete. Pertanto, se desideri eseguire in Docker qualcosa di ad alte prestazioni, ad esempio qualcosa che utilizzi attivamente la rete, dovrai almeno modificare questi parametri. Qualcosa come nf_conntrack, per esempio.
Sulla concezione di Docker
Docker è composto da diversi componenti:
- Docker Daemon — il motore del contenitore; avvia i contenitori.
- Docker CII — strumento per la gestione di Docker.
- Dockerfile — istruzioni su come costruire un'immagine.
- Image — immagine da cui viene distribuito il contenitore.
- Contenitore.
- Docker registry — deposito di immagini.
Schema, questo appare più o meno così:

Sul Docker_host gira il daemon Docker, che avvia i contenitori. C'è un Client che invia comandi: costruisci un'immagine, scarica un'immagine, avvia un contenitore. Il daemon Docker va nel registry e li esegue. Il client Docker può comunicare sia localmente (con un socket unix) che tramite TCP da un host remoto.
Diamo un'occhiata a ciascun componente.
Docker daemon (daemon) — è la parte server, gira sulla macchina host: scarica le immagini e avvia i contenitori, crea una rete tra i contenitori, raccoglie i registri. Quando diciamo 'crea un'immagine', anche questo è gestito dal daemon.
Docker CLI — la parte client di Docker, strumento da riga di comando per lavorare con il daemon. Ripeto, può operare non solo localmente, ma anche in rete.
Comandi di base:
docker ps — mostra i contenitori attualmente in esecuzione sul Docker-host.
docker images — mostra le immagini scaricate localmente.
docker search <> — ricerca di un'immagine nel registry.
docker pull <> — scarica un'immagine dal registry sul computer.
docker build <<\/path\/to\/dir>> — costruisce un'immagine.
docker run <> — avvia il contenitore.
docker rm <> — rimuove il contenitore.
docker logs <> — registri del contenitore
docker start\/stop\/restart <> — gestione del contenitore
Se padroneggi questi comandi e li usi con sicurezza, considera di aver padroneggiato Docker al 70% a livello utente.
Dockerfile — istruzioni per la creazione di un'immagine. Quasi ogni comando delle istruzioni è un nuovo strato. Guardiamo con un esempio.

Ecco come appare un Dockerfile: a sinistra i comandi, a destra — argomenti. Ogni comando qui presente (e in generale scritto in 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 quella di lavoro' — questo è un altro strato, e così via. La torta a strati semplifica la vita. Se creo un altro Dockerfile e nell'ultima riga modifichiamo qualcosa — avviando non "python" "main.py", ma qualcos'altro, oppure installo dipendenze da un altro file — gli strati precedenti verranno riutilizzati come cache.
Immagine — è il pacchetto del container, da cui vengono avviati i container. Se consideriamo Docker dal punto di vista di 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 lo stesso: dai container vengono avviati, sono conservati nel Docker registry (analogamente a yum, nel 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.
Docker registry — è un repository di immagini Docker. Analogamente ai sistemi operativi, Docker ha un registro pubblico standard: dockerhub. Ma è possibile costruire il proprio repository, il proprio Docker registry.
Contenitore — è ciò che viene eseguito dall'immagine. Abbiamo creato l'immagine secondo le istruzioni del Dockerfile, poi la avviamo da questa immagine. Questo container è isolato dagli altri container, deve contenere tutto il necessario per il funzionamento dell'applicazione. Un container equivale a un processo. A volte è necessario eseguire due processi, ma ciò è un po' contro la filosofia di Docker.
Il requisito "un container - un processo" è legato al PID Namespace. Quando in un Namespace viene avviato un processo con PID 1, se per caso muore, anche l'intero container muore. Se ci sono due processi in esecuzione: uno vive, mentre l'altro è morto, il container continua a vivere. Ma questo riguarda le Best Practices, di cui parleremo in altri materiali.
Per studiare più dettagliatamente le caratteristiche e il programma completo del corso, puoi seguire il link: "».
Autore: Marcell Ibraev, amministratore certificato Kubernetes, ingegnere pratico presso l'azienda Southbridge, relatore e sviluppatore di corsi Slyorm.
Fonte: habr.com
