Am 10. August startete in Sljorm , in dem wir es komplett behandeln – von den grundlegenden Abstraktionen bis hin zu den Netzwerkeinstellungen.
In diesem Artikel sprechen wir über die Entstehungsgeschichte von Docker und seine grundlegenden Abstraktionen: Image, CLI, Dockerfile. Die Vorlesung richtet sich an Anfänger, daher wird sie erfahrene Benutzer wahrscheinlich nicht interessieren. Hier wird es keine blutigen Details, keinen Appendix und keine tiefgehende Analyse geben. Nur die Grundlagen.

Was ist Docker
Lassen Sie uns die Definition von Docker aus Wikipedia betrachten.
Docker ist eine Software zur Automatisierung der Bereitstellung und Verwaltung von Anwendungen in containerisierungsfähigen Umgebungen.
Aus dieser Definition wird nichts klar. Besonders unverständlich ist, was "in containerisierungsfähigen Umgebungen" bedeutet. Um das zu klären, gehen wir in die Vergangenheit zurück. Beginnen wir mit einer Ära, die ich als "Monolithische Ära" bezeichne.
Monolithische Ära
Die monolithische Ära ist der Anfang der 2000er Jahre, als alle Anwendungen monolithisch waren, mit vielen Abhängigkeiten. Die Entwicklung dauerte lange. Zu diesem Zeitpunkt gab es nicht so viele Server, wir kannten sie alle beim Namen und überwachten sie. Es gibt einen interessanten Vergleich:

Haustiere — das sind unsere treuen Begleiter. In der Ära der Monolithen behandelten wir unsere Server wie Haustiere, kümmerten uns um sie und entfernten jeden Staub. Um die Ressourcen besser zu verwalten, setzten wir Virtualisierung ein: Wir nahmen einen Server und teilten ihn in mehrere virtuelle Maschinen auf, um so eine isolierte Umgebung zu schaffen.
Hypervisor-basierte Virtualisierungssysteme
Über Virtualisierungssysteme hat sicher jeder schon gehört: VMware, VirtualBox, Hyper-V, Qemu KVM usw. Diese bieten Anwendungsisolierung und Ressourcenmanagement, haben jedoch auch Nachteile. Um Virtualisierung durchzuführen, benötigt man einen Hypervisor, was einen Ressourcenüberhead bedeutet. Zudem ist eine virtuelle Maschine oft ziemlich umfangreich — ein schweres Abbild, auf dem ein Betriebssystem, Nginx, Apache und möglicherweise auch MySQL laufen. Dieses große Abbild macht die Verwaltung virtueller Maschinen unhandlich. In der Folge kann die Arbeit mit virtuellen Maschinen langsam sein. Um dieses Problem zu lösen, wurden Kernvirtualisierungssysteme entwickelt.
Kernvirtualisierungssysteme
Kernvirtualisierung wird von Systemen wie OpenVZ, Systemd-nspawn und LXC unterstützt. Ein herausragendes Beispiel dafür ist LXC (Linux-Container).
LXC — ein Betriebssystem-virtualisierungssystem zur Ausführung mehrerer isolierter Instanzen des Linux-Betriebssystems auf einem Knoten. LXC verwendet keine virtuellen Maschinen, sondern schafft eine virtuelle Umgebung mit eigenen Prozessräumen und Netzwerkstacks.
Im Grunde genommen erstellt LXC Container. Was ist der Unterschied zwischen virtuellen Maschinen und Containern?

Container sind nicht geeignet, um Prozesse zu isolieren: In Kernvirtualisierungssystemen gibt es Schwachstellen, die es ermöglichen, aus dem Container auf den Host zu gelangen. Daher ist es besser, eine virtuelle Maschine zu verwenden, wenn Sie etwas isolieren müssen.
Die Unterschiede zwischen Virtualisierung und Containerisierung können in der Abbildung gesehen werden.
Es gibt Hardware-Hypervisoren, Hypervisoren über dem Betriebssystem und Container.

Hardware-Hypervisoren sind eine großartige Sache, wenn Sie wirklich etwas isolieren möchten. Denn hier besteht die Möglichkeit, auf Seitenebene und Prozessoren zu isolieren.
Es gibt Hypervisoren als Programme und Container, über die wir im Folgenden sprechen werden. In Containerisierungssystemen gibt es keinen Hypervisor, sondern eine Container-Engine, die Container erstellt und verwaltet. Diese Technologie ist leichter und hat aufgrund der Arbeit mit dem Kernel weniger Overhead oder keinen überhaupt.
Was auf Kernel-Ebene für die Containerisierung verwendet wird
Die wichtigsten Technologien, die es ermöglichen, einen von anderen Prozessen isolierten Container zu erstellen, sind Namespaces und Control Groups.
Namespaces: PID, Networking, Mount und User. Es gibt noch weitere, aber zur Vereinfachung konzentrieren wir uns auf diese.
Der PID Namespace beschränkt Prozesse. Wenn wir zum Beispiel einen PID Namespace erstellen und dort einen Prozess platzieren, erhält dieser die PID 1. Normalerweise ist in Systemen PID 1 systemd oder init. Wenn wir also einen Prozess in einen neuen Namespace verschieben, erhält auch dieser die PID 1.
Der Networking Namespace ermöglicht es, das Netzwerk zu beschränken/zu isolieren und darin eigene Schnittstellen zu platzieren. Mount ist eine Einschränkung des Dateisystems. User ist eine Einschränkung der Nutzer.
Control Groups: Memory, CPU, IOPS, Network – insgesamt etwa 12 Einstellungen. Sie werden auch als Cgroups ("C-Gruppe") bezeichnet.
Control Groups verwalten die Ressourcen für Container. Mit Control Groups können wir festlegen, dass ein Container nicht mehr Ressourcen verbrauchen darf als eine bestimmte Menge.
Um die Containerisierung vollständig zu nutzen, werden zusätzliche Technologien verwendet: Capabilities, Copy-on-Write und andere.
Capabilities legen fest, was ein Prozess tun darf und was nicht. Auf Kernel-Ebene handelt es sich dabei einfach um Bitmaps mit vielen Parametern. Zum Beispiel hat der Root-Benutzer vollständige Berechtigungen und kann alles tun. Der Zeitserver kann die Systemzeit ändern: Er hat die entsprechenden Capabilities für Time Capsule – und das war's. Mit Berechtigungen lassen sich die Einschränkungen für Prozesse flexibel festlegen und so die eigene Sicherheit erhöhen.
Das Copy-on-Write-System ermöglicht es uns, mit Docker-Images effizienter zu arbeiten.
Derzeit hat Docker Probleme mit der Kompatibilität zu Cgroups v2, daher werden in diesem Artikel speziell Cgroups v1 behandelt.
Kehren wir zur Geschichte zurück.
Als die Virtualisierungssysteme auf Kernel-Ebene eingeführt wurden, wurden sie schnell eingesetzt. Der Overhead des Hypervisors fiel weg, aber einige Probleme blieben bestehen:
- Große Images: In dasselbe OpenVZ werden das Betriebssystem, Bibliotheken und eine Menge verschiedener Software gepackt, und letztendlich wird das Image dennoch ziemlich groß.
- Es gibt keinen angemessenen Standard für Verpackung und Lieferung, weshalb das Abhängigkeitsproblem bestehen bleibt. Es gibt Situationen, in denen zwei Teile des Codes dieselbe Bibliothek verwenden, jedoch mit unterschiedlichen Versionen. Dadurch können Konflikte auftreten.
Um all diese Probleme zu lösen, ist die nächste Ära angebrochen.
Die Ära der Container
Als die Ära der Container begann, änderte sich die Philosophie im Umgang mit ihnen:
- Ein Prozess — ein Container.
- Alle notwendigen Abhängigkeiten für den Prozess werden in seinen Container geliefert. Das erfordert, Monolithen in Mikroservices zu zerteilen.
- Je kleiner das Image, desto besser – weniger potenzielle Schwachstellen, schnelleres Deployment und so weiter.
- Instanzen werden ephemeral.
Erinnern Sie sich, ich habe von Haustieren und Vieh gesprochen? Früher waren Instanzen wie Haustiere, jetzt sind sie wie Vieh - cattle. Früher gab es ein Monolith - eine Anwendung. Jetzt sind es 100 Microservices, 100 Container. Einige Container können 2-3 Replikate haben. Es ist weniger wichtig, jeden Container zu kontrollieren. Wichtiger ist die Verfügbarkeit des Dienstes selbst: das, was dieses Set von Containern bereitstellt. Das verändert die Ansätze im Monitoring.
In den Jahren 2014-2015 erlebte Docker seinen Aufschwung - die Technologie, über die wir jetzt sprechen werden.
Docker hat die Philosophie verändert und die Verpackung von Anwendungen standardisiert. Mit Docker können wir eine Anwendung verpacken, sie in ein Repository schicken, von dort herunterladen und bereitstellen.
In einem Docker-Container integrieren wir alles Notwendige, was das Problem der Abhängigkeiten löst. Docker gewährleistet Reproduzierbarkeit. Viele von uns haben wahrscheinlich schon die Herausforderung der Nicht-Reproduzierbarkeit erlebt: alles funktioniert lokal, aber im Produktionsumfeld klappt es nicht mehr. Mit Docker gehört dieses Problem der Vergangenheit an. Wenn dein Docker-Container startet und das tut, was erforderlich ist, besteht eine hohe Wahrscheinlichkeit, dass er auch in der Produktion funktioniert und das Gleiche leistet.
Ein kurzer Exkurs zum Overhead
Es gibt ständig Diskussionen über den Overhead. Einige sind der Meinung, dass Docker keinen zusätzlichen Aufwand verursacht, da es den Linux-Kernel und alle notwendigen Prozesse für die Containerisierung nutzt. Man könnte sagen: 'Wenn Sie behaupten, Docker sei Overhead, dann ist das Linux-Kernel auch Overhead.'
Auf der anderen Seite, wenn man genauer hinsieht, hat Docker tatsächlich einige Aspekte, die man mit etwas Recht als Overhead bezeichnen könnte.
Das erste ist der PID-Namespace. Wenn wir einen Prozess in einen Namespace verschieben, wird ihm die PID 1 zugewiesen. Gleichzeitig hat dieser Prozess eine weitere PID, die sich im Host-Namespace, außerhalb des Containers, befindet. Zum Beispiel, wenn wir Nginx in einem Container starten, wird es zu PID 1 (Master-Prozess). Auf dem Host hat es jedoch die PID 12623. Es ist schwierig zu sagen, wie hoch dieser Overhead ist.
Das zweite Element sind Cgroups. Nehmen wir Cgroups für den Arbeitsspeicher, das heißt die Möglichkeit, den Speicher eines Containers zu begrenzen. Wenn sie aktiviert sind, wird das Speicher-Accounting aktiviert: Der Kern muss verstehen, wie viele Seiten zugewiesen wurden und wie viele noch frei sind für diesen Container. Das könnte einen Overhead verursachen, aber ich habe keine genauen Studien dazu gefunden, wie sich dies auf die Leistung auswirkt. Und ich habe auch nicht bemerkt, dass eine Anwendung, die in Docker läuft, plötzlich stark an Leistung verloren hat.
Und noch eine Anmerkung zur Leistung. Einige Kernelparameter werden vom Host in den Container durchgereicht. Insbesondere betrifft dies bestimmte Netzwerkeinstellungen. Wenn Sie also etwas Hochleistungsfähiges in Docker betreiben möchten, wie beispielsweise Anwendungen, die intensiv das Netzwerk nutzen, sollten Sie diese Parameter anpassen. Zum Beispiel etwas wie nf_conntrack.
Über das Konzept von Docker
Docker besteht aus mehreren Komponenten:
- Docker Daemon — das ist die Container-Engine; sie startet die Container.
- Docker CLI — das Tool zur Verwaltung von Docker.
- Dockerfile — eine Anleitung zum Erstellen eines Images.
- Image — das Image, aus dem der Container erstellt wird.
- Container.
- Docker-Registry — ein Speicher für Images.
Schematically looks like this:

Auf dem Docker-Host läuft der Docker-Daemon, der die Container startet. Es gibt einen Client, der Befehle überträgt: Image erstellen, Image herunterladen, Container starten. Der Docker-Daemon greift auf die Registry zu und führt diese aus. Der Docker-Client kann sowohl lokal (über den Unix-Socket) als auch über TCP von einem Remote-Host aus kommunizieren.
Lassen Sie uns jede Komponente durchgehen.
Docker-Daemon — das ist der Server-Teil, der auf der Hostmaschine läuft: er lädt Images herunter und startet Container daraus, erstellt ein Netzwerk zwischen den Containern und sammelt Protokolle. Wenn wir sagen "Erstelle ein Image", kümmert sich auch der Daemon darum.
Docker CLI — der Client-Teil von Docker, ein Konsolenwerkzeug zur Interaktion mit dem Daemon. Ich möchte betonen, dass es nicht nur lokal, sondern auch über das Netzwerk arbeiten kann.
Grundlegende Befehle:
docker ps — zeigt die derzeit auf dem Docker-Host laufenden Container an.
docker images — zeigt die lokal heruntergeladenen Images an.
docker search — sucht ein Image im Registry.
docker pull — lädt ein Image aus der Registry auf die Maschine herunter.
docker build <> — erstellt ein Image.
docker run — startet einen Container.
docker rm — entfernt einen Container.
docker logs — zeigt die Protokolle des Containers an.
docker start/stop/restart — verwaltet den Container.
Wenn Sie diese Befehle beherrschen und sie sicher anwenden können, dann betrachten Sie sich als zu 70% im Docker auf Anwender-Ebene vertraut.
Dockerfile — Anleitung zum Erstellen eines Images. Fast jeder Befehl in der Anleitung ist eine neue Schicht. Lassen Sie uns das anhand eines Beispiels betrachten.

So sieht ein Dockerfile aus: links die Befehle, rechts die Argumente. Jedes Kommando, das hier steht (und überhaupt in einem Dockerfile geschrieben wird), erzeugt eine neue Schicht im Image.
Selbst wenn man sich die linke Seite anschaut, kann man ungefähr nachvollziehen, was passiert. Wir sagen: 'Erstelle uns einen Ordner' – das ist eine Schicht. 'Mache den Ordner aktiv' – das ist eine weitere Schicht, und so weiter. Die Schichtenstruktur erleichtert das Leben. Wenn ich ein weiteres Dockerfile erstelle und in der letzten Zeile etwas ändere – anstelle von 'python main.py' etwas anderes starte oder Abhängigkeiten aus einer anderen Datei installiere – werden die vorherigen Schichten wie ein Cache wiederverwendet.
Bild – das ist die Verpackung des Containers, aus dem Images Container gestartet werden. Wenn man Docker aus der Perspektive eines Paketmanagers betrachtet (als würden wir mit deb- oder rpm-Paketen arbeiten), dann ist das Image im Grunde genommen ein rpm-Paket. Über yum install können wir Anwendungen installieren, sie deinstallieren, im Repository suchen und herunterladen. Hier ist es ähnlich: Aus dem Image werden Container gestartet, sie werden im Docker-Registry gespeichert (analog zu yum im Repository), und jedes Image hat einen SHA-256-Hash, einen Namen und ein Tag.
Images werden gemäß der Anleitung aus dem Dockerfile erstellt. Jede Anweisung im Dockerfile erzeugt eine neue Schicht. Schichten können wiederverwendet werden.
Docker-Registry – ist ein Repository für Docker-Images. Ähnlich wie bei Betriebssystemen hat Docker ein öffentliches Standard-Repository – dockerhub. Es ist jedoch auch möglich, ein eigenes Repository, eine eigene Docker-Registry, zu erstellen.
Container – ist das, was aus einem Image gestartet wird. Nach der Anleitung im Dockerfile haben wir das Image erstellt, und dann starten wir einen Container aus diesem Image. Dieser Container ist von anderen Containern isoliert und sollte alles Notwendige für den Betrieb der Anwendung enthalten. Hierbei gilt: Ein Container – ein Prozess. Es kommt vor, dass zwei Prozesse notwendig sind, aber das widerspricht ein wenig der Docker-Philosophie.
Die Anforderung »ein Container – ein Prozess« steht im Zusammenhang mit dem PID-Namespace. Wenn ein Prozess mit PID 1 im Namespace gestartet wird und er plötzlich stirbt, stirbt auch der gesamte Container. Wenn jedoch zwei Prozesse dort laufen: einer bleibt am Leben, der andere stirbt, bleibt der Container trotzdem am Leben. Dies gehört zum Thema Best Practices, über die wir in anderen Materialien sprechen werden.
Um die Besonderheiten und das vollständige Programm des Kurses näher zu erkunden, klicken Sie bitte auf den Link:».
Autor: Marcel Ibryaev, zertifizierter Kubernetes-Administrator, praktizierender Ingenieur bei Southbridge, Redner und Kursentwickler bei Slürm.
Quelle: habr.com
