★ Leichtgewichtige virtuelle Maschinen mit Docker oder wie man 100 VMs laufen lässt
— 9 minutes read
Virtuelle Maschinen haben viele Vorteile. Sie nutzen die Hardware besser aus, können leicht gesichert sowie ausgetauscht werden und isolieren einzelne Dienste voneinander. Sie haben jedoch auch Nachteile. Images sind sperrig und virtuelle Maschinen verbrauchen recht viele Ressourcen, da sie Hardware emulieren und ein gesamtes Betriebssystem laufen lassen. Mit Linux Containern gibt es eine leichtgewichtige Alternative. In diesem Artikel stelle ich Docker vor, ein Tool, das die praktische Anwendung von Linux Containern erleichtert. Dazu zeige ich anhand eines Beispiels, wie man 100 virtuelle Maschinen auf seinem System laufen lassen kann. Das Beispiel liegt dazu bei Github, so dass man es selbst ausprobieren kann.
Was ist Docker?
Linux Container (LXC) sind seit Version 2.6.24 Bestandteil von Linux and erlauben sogenannte System-Level Virtualisierungen. Sie setzen auf Linux cgroup und name spaces, die Prozesse voneinander isolieren. Sie laufen daher scheinbar auf ihrem eigenen System. Docker baut auf Linux Containern auf und besteht aus drei Teilen: Docker Daemon, Docker Images, the Docker Repositorys.
Docker Daemon läuft als root und verwaltet alle laufenden Container. So wie Virtuelle Maschinen auf Images basieren, basieren Docker Container auf Docker Images. Diese Images sind winzig verglichen mit virtuellen Maschinen Images und sind dank AUFS ”stapelbar”, so dass nur Änderungen gespeichert werden — siehe unten. Mit Hilfe von privaten und öffentlichen Docker Repositorys können Docker Images mit anderen geteilt und wie Source Code versioniert werden.
Ein Beispiel: 100 virtuelle Maschinen auf einem System
Um in die Nutzung von Docker einzuführen, zeige ich im Folgenden anhand eines Beispiels, wie Docker funktioniert. In diesem Beispiel werden 100 Docker Container jeweils mit eigener IP-Adresse gestartet und beantworten Webanfragen auf Port 8080. Als Webserver dient ein kleines Python Script. Das vollständige Beispiel ist bei Github verfügbar, so dass man es selbst ausprobieren kann.
Vom Dockerfile zum Docker Image
Ein Dockerfile beschreibt wie ein Docker Image erstellt werden soll. Für das Beispiel nutze ich zwei Dockerfiles, eins für die Python-Laufzeitumgebung und ein zweites für den eigentlichen Python Webserver. Listings 1 und 2 zeigen die beiden Dockerfiles.
FROM ubuntu:quantal
MAINTAINER Lukas Pustina <lukas.pustina@centerdevice.com>
RUN apt-get install -y python
Listing 1
Das FROM Kommando gibt das Ausgangsimage für das neue Image an. Hier wird ein öffentliches Ubuntu Image benutzt. Images werden zuerst lokal und dann im öffentlichen Docker Repository nachgeschlagen. Das RUN Kommando spezifiziert Shell-Kommandos, die während des Builds ausgeführt werden sollen. Hier wird Python installiert.
FROM docker-demo/python
MAINTAINER Lukas Pustina <lukas.pustina@centerdevice.com>
ADD webserver.py /opt/webserver/webserver.py
ADD run.sh /opt/webserver/run.sh
EXPOSE 8080
VOLUME ["/logs”]
Listing 2
Das zweite Docker Image leitet vom ersten Image mit der Python Laufzeitumgebung ab und fügt die beiden Dateien webserver.py and run.sh hinzu. Im Allgemeinen dürfen Docker Container nicht mit der Außenwelt kommunizieren — eine Deny All Regel. Falls Kommunikation möglich sein soll, muss diese explizit mit dem EXPOSE Kommando erlaubt werden. Erst dann wird der Port 8080 nach außen weitergeleitet. Das VOLUME Kommando gibt einen Mount Point an, an den Dateisysteme aus dem Host gebunden werden können. Auf diese Weise können global wiederverwendbare und teilbare Mount Points eingehängt werden.
Images bauen: docker build
Docker Images werden aus Dockerfiles erzeugt. Dabei erzeugt jedes Kommando eines Dockerfiles ein neues Image, das wie ein git Commit über eine eindeutige ID referenziert werden kann. Zum Beispiel erzeugt folgender Befehl das Image für die Python-Laufzeitumgebung
> docker build -rm -t docker-demo/python python
im Verzeichnis python, löscht dann alle Teil-Images (-rm) und taggt das Ergebnis-Image mit ”docker-demo/python”.
> docker images
zeigt all zur Zeit existierenden Images an. In unserem Beispiel (Die Größe bezeichnet die virtuelle Größe — siehe unten):
docker-demo/python latest ef489f0186e1 Less than a second ago 210.7 MB
docker-demo/webserver latest b898a85622e4 Less than a second ago 210.7 MB
ubuntu quantal b750fe79269d 9 months ago 175.3 MB
> docker images -tree
zeigt die Vererbungshierarchie der Images sowie ihre physikalische und virtuelle Größe an:
└─27cf78414709 Size: 175.3 MB (virtual 175.3 MB)
└─b750fe79269d Size: 77 B (virtual 175.3 MB) Tags: ubuntu:quantal
└─7c1926658b21 Size: 0 B (virtual 175.3 MB)
└─ef489f0186e1 Size: 35.43 MB (virtual 210.7 MB) Tags: docker-demo/python:latest
└─90807ce05b64 Size: 0 B (virtual 210.7 MB)
└─d3822b811f4c Size: 0 B (virtual 210.7 MB)
└─380871b22bde Size: 1.454 kB (virtual 210.7 MB)
└─8f5a9cde5ae2 Size: 161 B (virtual 210.7 MB)
└─b898a85622e4 Size: 0 B (virtual 210.7 MB) Tags: docker-demo/webserver:latest
Die physikalische Größe ändert sich also nur, wenn Dateien hinzugefügt werden. Das Tagging ändert die Größe nicht. Beachtenswert ist, dass ein Docker Image mit einem Python Webserver nur ca. 180 MB groß ist.
Änderungen machen
Wenn man eine Änderung an einem Image durchführt, so werden nur die entsprechenden Container geändert. In diesem Beispiel wird der Webserver Container ein zweites Mal gebaut und docker images -tree erneut aufgerufen:
└─27cf78414709 Size: 175.3 MB (virtual 175.3 MB)
└─b750fe79269d Size: 77 B (virtual 175.3 MB) Tags: ubuntu:quantal
└─7c1926658b21 Size: 0 B (virtual 175.3 MB)
└─ef489f0186e1 Size: 35.43 MB (virtual 210.7 MB) Tags: docker-demo/python:latest
└─90807ce05b64 Size: 0 B (virtual 210.7 MB)
|─0e153dd3c547 Size: 0 B (virtual 210.7 MB)
| └─922d6e34d2d5 Size: 1.454 kB (virtual 210.7 MB)
| └─4bfc3d0fb5b2 Size: 161 B (virtual 210.7 MB)
| └─abac38e20f27 Size: 0 B (virtual 210.7 MB) Tags: docker-demo/webserver:latest
└─d3822b811f4c Size: 0 B (virtual 210.7 MB)
└─380871b22bde Size: 1.454 kB (virtual 210.7 MB)
└─8f5a9cde5ae2 Size: 161 B (virtual 210.7 MB)
└─b898a85622e4 Size: 0 B (virtual 210.7 MB)
Die Basis-Images bleiben gleich und nur die Kommandos des zweiten Dockerfiles werden erneut ausgeführt. Sie ändern nur wenige Bytes; nicht schlecht für ein Image einer virtuellen Maschine.
Container starten: docker run
Um Docker Container eines bestimmten Images zu starten ruft man docker run auf:
> docker run -d -cidfile=webserver.cid -name webserver -v `pwd`/logs:/logs docker-demo/webserver:latest /opt/webserver/run.sh
In unserem Beispiel wird das Docker Image das zuletzt mit ”docker-demo/webserver” getagt worden ist gestartet. Das heißt, dass Image abac38e20f27 an Stelle von b898a85622e4 genutzt wird. Die Container-ID wird in die Datei webserver.cid (-cidfile) geschrieben, der Container “webserver” benannt (-name), und das lokale Verzeichnis logs wird nach /logs im Container gemappt. (-v; siehe Dockerfile oben). Der Prozess, der im Container ausgeführt werden soll, ist /opt/webserver/run.sh, welcher im zweiten Dockerfile in das Image kopiert worden ist. Man kann diesen Befehl auch mehrmals ausführen:
> for i in `seq 1 10`; do docker run -d -cidfile=webserver-$i.cid -name webserver-$i -v `pwd`/logs:/logs docker-demo/webserver:latest /opt/webserver/run.sh /logs; done
Das vollständige Beispiel auf Github startet ohne Probleme 100 Maschinen in einer Vagrant Box.
Anzeigen der laufenden Container: docker ps
Ähnlich wie ps zeigt docker ps die laufenden Container:
CONTAINER ID IMAGE COMMAND PORTS NAMES
63120c069662 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-10
5af293d56a0b docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-9
a4a44f159658 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-8
40b35ff10aa2 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-7
6e9127f8a113 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-6
4623905191d2 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-5
e57def12fe25 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-4
c352eccb21de docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-3
f65e43115864 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-2
111c936c2763 docker-demo/webserver:latest /opt/webserver/run.s 8080/tcp webserver-1
Wie man sehen laufen 10 Container mit den Namen *webserver-** und lauschen auf Port 8080.
Informationen anzeigen lassen: docker inspect
docker inspect gibt zusätzliche Informationen über Container als JSON Dokument zurück. Dies beinhaltet u.a. den hostname, das Verzeichnis des Images oder auch die IP-Adresse, nach der im Folgenden gegrept wird:
> docker ps -q | xargs docker inspect | grep IPAddress | cut -d ’”’ -f 4
172.17.0.12
172.17.0.11
172.17.0.10
172.17.0.9
172.17.0.8
172.17.0.7
172.17.0.6
172.17.0.5
172.17.0.4
172.17.0.3
Jeder Container hat also ein eigenes Netzwerkinterface mit eigener IP-Adresse. Zusammen bilden sie ein eigenes Netzwerk von isolierten, System-Level virtuellen Maschinen.
Ressourcenverbrauch
”ps aux | grep ‘lxc-start\|python’” zeigt die verbrauchten Ressourcen der 10 Linux Container an. Man kann sehen, dass pro Container ca. 1,3 MB und pro Python Instanz ca. 7 MB RAM verbraucht werden — ganz schön beeindruckend im Vergleich zu klassischen virtuellen Maschinen.
PID %CPU %MEM RSS COMMAND
12197 0.0 0.2 1316 lxc-start
12249 0.0 0.2 1320 lxc-start
12291 0.0 1.3 6968 /usr/bin/python /opt/webserver/webserver.py
12296 0.0 0.2 1320 lxc-start
12346 0.0 1.3 6960 /usr/bin/python /opt/webserver/webserver.py
12348 0.0 0.2 1316 lxc-start
12399 0.0 1.3 6964 /usr/bin/python /opt/webserver/webserver.py
12400 0.0 0.2 1316 lxc-start
12450 0.0 0.2 1316 lxc-start
12496 0.0 1.3 6964 /usr/bin/python /opt/webserver/webserver.py
12497 0.0 0.2 1316 lxc-start
12546 0.0 0.2 1316 lxc-start
12594 0.0 0.2 1320 lxc-start
12648 0.0 1.3 6968 /usr/bin/python /opt/webserver/webserver.py
12649 0.0 0.2 1316 lxc-start
12700 0.0 1.3 6968 /usr/bin/python /opt/webserver/webserver.py
12701 0.0 1.3 6968 /usr/bin/python /opt/webserver/webserver.py
12707 0.0 1.3 6960 /usr/bin/python /opt/webserver/webserver.py
12711 0.0 1.3 6968 /usr/bin/python /opt/webserver/webserver.py
12712 0.0 1.3 6964 /usr/bin/python /opt/webserver/webserver.py
docker stop, kill, remove
Um Container zu stoppen, genügt es, dem ausgeführten Prozess ein SIGTEM oder SIGKILL zu schicken. docker stop versucht zunächst ein SIGTERM zu schicken. Bleibt dies bis zu einem Timeout ohne Reaktion, wird ein SIGKILL geschickt. docker kill sendet sofort ein SIGKILL. Wie bei klassischen virtuellen Maschinen bleiben die Images nach Beenden der VM bestehen. docker rm löscht Container.
Es gibt mehr
Linux Container bieten durch Docker noch viele weitere Möglichkeiten, als ich hier beschrieben habe. So ist es möglich, Volumes zwischen Containern zu teilen. Container können auch multi-homed sein und mehrere Netzwerkinterfaces konfigurieren. Private Repositorys können leicht aufgesetzt werden und ermöglichen die Verwaltung eigener Images. Zusätzlich können weitere der Kommandozeilen-Argumente von Docker in Dockerfiles eingetragen werden.
Docker bei CenterDevice
Bei CenterDevice haben wir unsere Entwicklungsumgebung auf eine Vagrant-basierte Virtual Box migriert, die Docker Images für all unsere Backenddienste wie MongoDB, RabbitMQ und Tomcat nutzen. Auf diese Weise können Entwickler ihre Entwicklungsumgebung leicht aufsetzen sowie die Dienste isoliert und wie auf Produktion laufen lassen, ohne die Ressourcen ihres Entwicklungssystem zu überlasten.
In folgenden Artikeln werde ich zeigen, wie das Erzeugen von Docker Images in die Continuous Integration Pipeline eingebaut werden kann. So können erfolgreiche Builds automatisch in Docker Images überführt werden, die dann in der Entwicklung, zum Testen und sogar in der Produktion eingesetzt werden können.
Ich werde über dieses Thema auch in Zukunft bloggen. Also bleibt dran und meldet Euch bei mir, falls Ihr Fragen oder Anregungen habt.
First published at codecentric Blog.