10 sierpnia w Słurmie rozpoczął się , w którym omawiamy go w całości — od podstawowych abstrakcji po parametry sieciowe.
W tym artykule omówimy historię pojawienia się Dockera i jego podstawowe abstrakcje: Image, Cli, Dockerfile. Wykład jest przeznaczony dla nowicjuszy, więc raczej nie będzie interesujący dla doświadczonych użytkowników. Nie będzie tu krwi, wyrostka i głębokiego zagłębienia. Same podstawy.

Czym jest Docker
Spójrzmy na definicję Dockera z Wikipedii.
Docker to oprogramowanie do automatyzacji wdrażania i zarządzania aplikacjami w środowiskach wspierających konteneryzację.
Z tej definicji nic nie wynika. Szczególnie niejasne jest, co oznacza „w środowiskach wspierających konteneryzację”. Aby to zrozumieć, wróćmy do przeszłości. Zacznijmy od epoki, którą nazywam „Epoką monolitów”.
Epoka monolitów
Epoka monolitów to początek lat 2000, kiedy wszystkie aplikacje były monolityczne, z mnóstwem zależności. Proces rozwoju trwał długo. Serwerów nie było zbyt dużo, wszyscy znaliśmy je po imieniu i monitorowaliśmy. Istnieje zabawne porównanie:

Pets — to zwierzęta domowe. W epoce monolitów odnosiliśmy się do naszych serwerów jak do zwierząt domowych, pielęgnowaliśmy je, dbaliśmy o nie, zdmuchiwaliśmy kurz. A aby lepiej zarządzać zasobami, używaliśmy wirtualizacji: braliśmy serwer i dzieliliśmy go na kilka maszyn wirtualnych, zapewniając tym samym izolację środowiska.
Systemy wirtualizacji oparte na hipernadzorze
O systemach wirtualizacji zapewne wszyscy słyszeli: VMware, VirtualBox, Hyper-V, Qemu KVM itd. Zapewniają one izolację aplikacji i zarządzanie zasobami, ale mają też swoje wady. Aby uzyskać wirtualizację, potrzebny jest hipernadzorca. A hipernadzorca to nadmiar zasobów. Poza tym sama maszyna wirtualna zazwyczaj jest dużym bytem — ciężki obraz, na którym znajduje się system operacyjny, Nginx, Apache, a być może i MySQL. Obraz jest duży, trudno operować maszyną wirtualną. W konsekwencji praca z wirtualkami może być wolna. Aby rozwiązać ten problem, stworzono systemy wirtualizacji na poziomie jądra.
Systemy wirtualizacji na poziomie jądra
Wirtualizację na poziomie jądra wspierają systemy OpenVZ, Systemd-nspawn, LXC. Jaskrawym przykładem takiej wirtualizacji są LXC (Linux Containers).
LXC — system wirtualizacji na poziomie systemu operacyjnego, który umożliwia uruchamianie wielu izolowanych instancji systemu operacyjnego Linux na jednym węźle. LXC nie korzysta z maszyn wirtualnych, lecz tworzy środowisko wirtualne z własną przestrzenią procesów i stosu sieciowego.
W zasadzie LXC tworzy kontenery. Jaka jest różnica między maszynami wirtualnymi a kontenerami?

Kontener nie nadaje się do izolowania procesów: w systemach wirtualizacji na poziomie jądra odkrywane są luki, które pozwalają na wydostanie się z kontenera na hoście. Dlatego jeśli chcesz coś izolować, lepiej użyć maszyny wirtualnej.
Różnice między wirtualizacją a konteneryzacją można zobaczyć na schemacie.
Są hyperwizory sprzętowe, hyperwizory nad systemem operacyjnym i kontenery.

«Sprzętowe» hyperwizory to świetne narzędzie, jeśli naprawdę chcesz coś izolować. Ponieważ istnieje możliwość izolowania na poziomie stron pamięci i procesorów.
Są hyperwizory jako programy, a są kontenery, o których będziemy mówić dalej. W systemach konteneryzacji nie ma hyperwizora, ale jest Container Engine, który tworzy kontenery i zarządza nimi. Jest to lżejsze rozwiązanie, dlatego dzięki pracy z jądrem overhead jest mniejszy lub wręcz nie występuje.
Co jest używane do konteneryzacji na poziomie jądra
Podstawowe technologie, które umożliwiają tworzenie kontenerów izolowanych od innych procesów, to Namespaces i Control Groups.
Namespaces: PID, Networking, Mount i User. Jest ich więcej, ale dla uproszczenia zatrzymamy się na tych.
Namespace PID ogranicza procesy. Gdy na przykład tworzymy namespace PID i umieszczamy w nim proces, staje się on PID 1. Zwykle w systemach PID 1 to systemd lub init. Odpowiednio, kiedy umieszczamy proces w nowym namespace, również otrzymuje PID 1.
Networking Namespace pozwala na ograniczenie/izolację sieci i wewnątrz już umieszczanie własnych interfejsów. Mount to ograniczenie w systemie plików. User to ograniczenie dotyczące użytkowników.
Control Groups: Pamięć, CPU, IOPS, Sieć — łącznie około 12 ustawień. Inaczej nazywane Cgroups (C grupy).
Control Groups zarządzają zasobami dla kontenera. Dzięki Control Groups możemy powiedzieć, że kontener nie powinien zużywać więcej niż określoną ilość zasobów.
Aby konteneryzacja mogła działać w pełni, wykorzystuje się dodatkowe technologie: Capabilities, Copy-on-write i inne.
Capabilities to sytuacja, w której mówimy procesowi, co może robić, a czego nie. Na poziomie jądra to po prostu bitmapy z wieloma parametrami. Na przykład użytkownik root ma pełne uprawnienia i może robić wszystko. Serwer czasu może zmieniać czas systemowy: ma capabilities na Time Capsule i już. Dzięki uprawnieniom można elastycznie dostosować ograniczenia dla procesów, co pozwala zwiększyć bezpieczeństwo.
System Copy-on-write pozwala nam skuteczniej pracować z obrazami Docker.
Obecnie Docker ma problemy z kompatybilnością Cgroups v2, dlatego w artykule skupiamy się na Cgroups v1.
Ale wróćmy do historii.
Kiedy pojawiły się systemy wirtualizacji na poziomie jądra, zaczęto je intensywnie wykorzystywać. Oprócz obciążenia hipernadzorcą, pojawiły się inne problemy:
- duże obrazy: do OpenVZ wrzuca się system operacyjny, biblioteki, masę różnych programów, a w efekcie obraz i tak okazuje się być sporych rozmiarów;
- brak normalnego standardu pakowania i dostarczania, co prowadzi do problemu z zależnościami. Czasami dwa kawałki kodu używają tej samej biblioteki, ale w różnych wersjach. Między nimi może wystąpić konflikt.
Aby rozwiązać te wszystkie problemy, nastała kolejna era.
Era kontenerów
Kiedy nastała Era kontenerów, zmieniła się filozofia pracy z nimi:
- Jeden proces — jeden kontener.
- Wszystkie niezbędne zależności dla procesu dostarczamy do jego kontenera. Wymaga to rozbicia monolitów na mikrousługi.
- Im mniejszy obraz, tym lepiej — mniej potencjalnych luk w zabezpieczeniach, szybsze wdrażanie i tak dalej.
- Instancje stają się efemeryczne.
Pamiętacie, mówiłem o zwierzętach domowych i bydle? Kiedyś instancje były jak zwierzęta domowe, a teraz stały się jak bydło. Kiedyś był monolit — jedna aplikacja. Teraz to 100 mikrousług, 100 kontenerów. Niektóre kontenery mogą mieć po 2-3 repliki. Nie jest już dla nas tak ważne kontrolowanie każdego kontenera. Bardziej liczy się dostępność samej usługi: tego, co robi ten zbiór kontenerów. To zmienia podejście do monitorowania.
W latach 2014-2015 nastąpił rozkwit Dockera — tej technologii, o której będziemy teraz mówić.
Docker zmienił filozofię i znormalizował pakowanie aplikacji. Dzięki Docker możemy zapakować aplikację, wysłać ją do repozytorium, pobrać stamtąd i wdrożyć.
W kontenerze Docker pakujemy wszystko, co niezbędne, więc rozwiązujemy problem zależności. Docker gwarantuje reprodukowalność. Myślę, że wielu spotkało się z brakiem reprodukowalności: wszystko działa u Ciebie, wrzucasz to na produkcję, a tam przestaje działać. Z Dockerem ten problem znika. Jeśli Twój kontener Docker uruchamia się i wykonuje to, co ma robić, to z dużym prawdopodobieństwem uruchomi się na produkcji i tam także zrobi to samo.
Dygresja o overheadzie
Wokół overheadu trwają nieustanne dyskusje. Niektórzy twierdzą, że Docker nie niesie dodatkowego obciążenia, ponieważ korzysta z jądra Linuxa i wszystkich jego procesów potrzebnych do konteneryzacji. Mówią, że "jeśli mówisz, że Docker to overhead, to w takim razie jądro Linuxa też jest overheadem".
Z drugiej strony, jeżeli zagłębimy się w temat, to w Dockerze rzeczywiście istnieje kilka aspektów, które można by z trudem określić jako overhead.
Pierwszy — to przestrzeń nazw PID. Kiedy umieszczamy jakiś proces w przestrzeni nazw, przypisywany jest mu PID 1. Jednocześnie ten proces ma jeszcze jeden PID, który znajduje się w przestrzeni nazw hosta, poza kontenerem. Na przykład uruchamiamy Nginx w kontenerze, staje się on PID 1 (proces główny). A na hoście ma PID 12623. Trudno powiedzieć, na ile to jest overhead.
Drugą kwestią są Cgroups. Weźmy Cgroups dla pamięci, czyli możliwość ograniczania pamięci kontenerowi. Po jej włączeniu aktywują się liczniki, accounting pamięci: jądro musi rozumieć, ile stron zostało przydzielonych, a ile jeszcze jest wolnych dla tego kontenera. Może to być overhead, ale nie spotkałem się z dokładnymi badaniami na temat tego, jak wpływa to na wydajność. I sam nie zauważyłem, żeby aplikacja uruchomiona w Dockerze nagle traciła na wydajności.
I jeszcze jedna uwaga dotycząca wydajności. Niektóre parametry jądra są przekazywane z hosta do kontenera. W szczególności niektóre parametry sieciowe. Dlatego jeśli chcesz uruchomić w Dockerze coś o wysokiej wydajności, na przykład coś, co będzie aktywnie wykorzystywać sieć, to musisz przynajmniej te parametry dostosować. Na przykład nf_conntrack.
O koncepcji Dockera
Docker składa się z kilku komponentów:
- Docker Daemon — to właśnie jest Container Engine; uruchamia kontenery.
- Docker CII — narzędzie do zarządzania Dockerem.
- Dockerfile — instrukcja dotycząca tego, jak zbudować obraz.
- Image — obraz, z którego budowany jest kontener.
- Kontener.
- Docker registry — magazyn obrazów.
Schematycznie wygląda to mniej więcej tak:

Na Docker_host działa Docker daemon, uruchamiając kontenery. Jest Client, który przesyła polecenia: zbuduj obraz, pobierz obraz, uruchom kontener. Docker daemon komunikuje się z registry i wykonuje te polecenia. Klient Docker może łączyć się zarówno lokalnie (z gniazda unixowego), jak i przez TCP z zdalnego hosta.
Przejdźmy do każdego komponentu.
Docker daemon (demon) — to część serwerowa, działająca na maszynie hosta: pobiera obrazy i uruchamia z nich kontenery, tworzy sieć między kontenerami, zbiera logi. Kiedy mówimy „stwórz obraz”, również zajmuje się tym demon.
Docker CLI — to część klienta Docker, konsolowe narzędzie do pracy z demonem. Podkreślam, że może działać nie tylko lokalnie, ale również w sieci.
Podstawowe komendy:
docker ps — pokazuje kontenery, które są aktualnie uruchomione na Docker-host.
docker images — pokazuje lokalnie pobrane obrazy.
docker search — wyszukiwanie obrazu w registry.
docker pull — pobierz obraz z registry na maszynę.
docker build <> — zbuduj obraz.
docker run — uruchom kontener.
docker rm — usuń kontener.
docker logs — logi kontenera
docker start/stop/restart — praca z kontenerem.
Jeśli opanujesz te komendy i będziesz się nimi pewnie posługiwał, to możesz uznać, że w 70% opanowałeś Docker na poziomie użytkownika.
Dockerfile — instrukcja do tworzenia obrazu. Prawie każda komenda w instrukcji to nowa warstwa. Zobaczmy na przykładzie.

Tak wygląda Dockerfile: po lewej stronie komendy, po prawej — argumenty. Każda komenda, która tutaj jest (i w ogóle pisze się w Dockerfile), tworzy nową warstwę w obrazie.
Patrząc tylko na lewą część, można mniej więcej zrozumieć, co się dzieje. Mówimy: „stwórz nam folder” — to jedna warstwa. „Ustaw folder jako roboczy” — to kolejna warstwa, i tak dalej. Warstwowy tort ułatwia życie. Jeśli stworzę jeszcze jeden Dockerfile i w ostatniej linii coś zmienię — uruchomię nie "python" "main.py", lecz coś innego, lub zainstaluję zależności z innego pliku — to poprzednie warstwy będą ponownie wykorzystywane jako cache.
Obraz — to jest pakiet kontenera, z którego uruchamiane są kontenery. Patrząc na Dockera z perspektywy menedżera pakietów (jakbyśmy pracowali z pakietami deb lub rpm), obraz to zasadniczo pakiet rpm. Poprzez yum install możemy zainstalować aplikację, usunąć ją, znaleźć w repozytorium, pobrać. Tutaj jest podobnie: kontenery są uruchamiane z obrazu, przechowywane w rejestrze Dockera (analogicznie do yum w repozytorium), a każdy obraz ma hash SHA-256, nazwę i tag.
Obraz jest tworzony według instrukcji z Dockerfile. Każda instrukcja z Dockerfile tworzy nową warstwę. Warstwy mogą być używane ponownie.
Rejestr Dockera — to repozytorium obrazów Dockera. Analogicznie do systemu operacyjnego, Docker ma publiczny standardowy rejestr — dockerhub. Można jednak zbudować własne repozytorium, własny rejestr Dockera.
Kontener — to, co jest uruchamiane z obrazu. Zgodnie z instrukcją z Dockerfile zbudowaliśmy obraz, następnie uruchamiamy go z tego obrazu. Ten kontener jest izolowany od innych kontenerów, musi zawierać wszystko, co potrzebne do działania aplikacji. Przy tym jeden kontener — jeden proces. Zdarza się, że trzeba uruchomić dwa procesy, ale to nieco sprzeczne z ideologią Dockera.
Wymaganie „jeden kontener — jeden proces” wiąże się z PID Namespace. Kiedy w Namespace uruchamiany jest proces z PID 1, jeśli nagle umrze, to cały kontener też umiera. Jeśli jednak są uruchomione dwa procesy: jeden żyje, a drugi umarł, to kontener i tak będzie żył. Ale to temat Best Practices, o których porozmawiamy w innych materiałach.
Więcej szczegółów na temat cech i pełnego programu kursu można znaleźć pod linkiem: „».
Autor: Marsel Ibraev, certyfikowany administrator Kubernetes, inżynier praktykujący w firmie Southbridge, mówca i twórca kursów z Słérm.
Źródło: habr.com
