Grundlagen

Virtualisierung und Container

Ein Computer, der so tut, als wäre er viele: Was eine virtuelle Maschine von einem Container unterscheidet, warum ein Container eigentlich nur ein Prozess mit Scheuklappen ist und was die JVM davon wissen muss.

Lesestand

Virtualisierung heißt: Ein Rechner tut so, als wäre er mehrere. Eine virtuelle Maschine (VM) ist ein komplett nachgebildeter Computer mit eigenem Betriebssystem. Ein Hypervisor teilt die echte Hardware unter den VMs auf.

Ein Container geht einen leichteren Weg: Er ist ein ganz normaler Prozess, dem der Linux-Kernel eine eigene Sicht auf die Welt vorgaukelt – eigene Prozessliste, eigenes Netzwerk, eigenes Dateisystem – und den er in Rechenzeit und Speicher begrenzt. Alle Container teilen sich den Kernel des Hosts. Das macht sie klein und schnell, aber weniger stark voneinander getrennt als VMs.

Welches Problem wird gelöst?

Drei alte Ärgernisse aus dem Serverbetrieb:

1

„Bei mir läuft's“

Auf dem Entwicklerrechner ist Java 21 installiert, auf dem Server Java 17, und eine Bibliothek fehlt. Man möchte die Anwendung samt Umgebung ausliefern.

2

Leere Server

Früher bekam jede Anwendung ihren eigenen Server, aus Angst vor gegenseitiger Störung. Die meisten Server langweilten sich die meiste Zeit.

3

Gegenseitige Störung

Teilen sich Anwendungen einen Server, frisst eine fehlerhafte den ganzen Speicher – und reißt die anderen mit.

Virtualisierung und Container lösen alle drei: Jede Anwendung bekommt eine abgeschottete, reproduzierbare Umgebung, und viele davon teilen sich einen Rechner.

Virtuelle Maschinen

Eine VM ist ein Computer aus Software. Das Betriebssystem darin – der Gast – glaubt, es habe Prozessor, Speicher, Festplatte und Netzwerkkarte für sich allein. Tatsächlich verteilt der Hypervisor die echte Hardware des Hosts. Moderne Prozessoren helfen dabei mit eigenen Befehlen (Intel VT-x, AMD-V, bei ARM eigene Virtualisierungsmodi), sodass Gast-Code fast mit voller Geschwindigkeit direkt auf der CPU läuft.

1

Typ 1: direkt auf der Hardware

Der Hypervisor ist selbst ein schlankes Betriebssystem. Beispiele: KVM (im Linux-Kernel), Microsoft Hyper-V, VMware ESXi, Xen. So laufen Rechenzentren und die Cloud.

2

Typ 2: als Programm

Der Hypervisor läuft als Anwendung auf einem normalen Betriebssystem. Beispiele: VirtualBox, VMware Workstation, Parallels. Praktisch für den Schreibtisch.

★

Was VMs können

Schnappschüsse anlegen, eine laufende VM auf einen anderen Server umziehen, ohne dass sie es merkt, und jedes beliebige Betriebssystem ausführen – auch Windows auf Linux.

Container: ein Prozess mit Scheuklappen

Ein Container enthält kein eigenes Betriebssystem im engeren Sinn – keinen eigenen Kernel. Er enthält nur die Dateien, die eine Anwendung braucht: die Anwendung selbst, ihre Bibliotheken, vielleicht eine JVM und ein paar Systemprogramme. Gestartet wird daraus ein ganz normaler Prozess auf dem Host-Kernel. Zwei Kernel-Funktionen machen ihn zum Container:

N

Namespaces: was er sieht

Eigene Prozessliste, eigenes Netzwerk, eigenes Dateisystem, eigener Rechnername, eigene Benutzer. Der Prozess sieht nur seinen Ausschnitt der Welt.

C

cgroups: was er darf

Control Groups begrenzen Rechenzeit, Arbeitsspeicher und Ein-/Ausgabe. Überschreitet ein Container sein Speicherlimit, beendet ihn der Kernel.

L

Image: was er mitbringt

Ein Paket aus Dateischichten, das alles enthält, was die Anwendung braucht. Unveränderlich – jede Änderung im laufenden Container landet in einer eigenen, wegwerfbaren Schicht.

Drei Arten, Anwendungen zu betreiben

Ein Server, alles direkt

App A
App B
App C
gemeinsame Bibliotheken – Versionskonflikte
Betriebssystem
Hardware

billig, aber keine Trennung

Virtuelle Maschinen

App A
App B
App C
Bibl.
Bibl.
Bibl.
Gast-OS mit Kernel
Gast-OS mit Kernel
Gast-OS mit Kernel
Hypervisor
Hardware

starke Trennung, aber dreimal ein ganzes Betriebssystem

Container

App A
App B
App C
Bibl.
Bibl.
Bibl.
Container-Laufzeit (Namespaces, cgroups)
ein Betriebssystem mit einem Kernel
Hardware

leicht und schnell, Trennung durch den Kernel

Was der Unterschied praktisch bedeutet, zeigt eine Rechnung: Wie viele Anwendungen passen auf einen Server?

Wie viele passen auf einen Server?
Arbeitsspeicher des Servers
Speicher pro Anwendung

Als virtuelle Maschinen

pro Stück zusätzlich
≈ 1 GB Gast-Betriebssystem
Start
Sekunden bis Minuten
Image
oft mehrere GB

Als Container

pro Stück zusätzlich
wenige MB
Start
meist unter 1 Sekunde
Image
oft unter 200 MB

Grobe Modellrechnung: 2 GB bleiben für den Host, jede VM braucht zusätzlich etwa 1 GB für ihr eigenes Betriebssystem. In Wirklichkeit kommen Rechenzeit, Platten und Netzwerk als Grenzen hinzu.

Dass ein Container nur ein Prozess ist, kann man auf jedem Linux-Host sehen: ps aux zeigt ihn in der normalen Prozessliste. Von innen sieht die Welt ganz anders aus. Schalte die Namespaces einzeln ein:

Was sieht der Prozess?

Images und Schichten

Ein Container-Image wird meist aus einem Dockerfile gebaut. Jede Anweisung erzeugt eine Schicht (Layer), und unveränderte Schichten werden aus dem Cache genommen – beim Bauen wie beim Hochladen. Deshalb ist die Reihenfolge wichtig: Was sich selten ändert, gehört nach unten.

Was muss neu gebaut werden?

Schichten von unten nach oben. Größen beispielhaft.

Kurze Geschichte

Die Idee ist älter, als man denkt: IBM virtualisierte Großrechner schon in den 1960er-Jahren. Mit chroot bekam Unix 1979 den Vorläufer der Container – einen Prozess mit eigenem Wurzelverzeichnis. FreeBSD Jails (2000) und Linux-Container (LXC, 2008) bauten das aus. 2013 machte Docker Container mit einem einfachen Werkzeug und dem Image-Format populär. Heute ist das Format als OCI-Standard festgeschrieben, und es gibt viele Werkzeuge dafür – Docker, Podman, containerd.

Viele Container: Orchestrierung

Ein Container ist leicht zu starten. Hunderte auf Dutzenden Servern zu betreiben – mit Neustarts bei Absturz, Lastverteilung, Updates ohne Ausfall –, ist eine eigene Aufgabe. Das übernimmt ein Orchestrierer, heute fast immer Kubernetes. Man beschreibt den gewünschten Zustand („drei Instanzen dieser Anwendung, je 1 GB Speicher, erreichbar unter diesem Namen“), und Kubernetes sorgt ständig dafür, dass er hergestellt wird. Stirbt ein Container, startet es einen neuen; fällt ein Server aus, verteilt es die Container auf andere.

Wie sicher ist die Trennung?

Hier liegt der eigentliche Preis der Container: Alle teilen sich einen Kernel. Findet ein Angreifer in einem Container eine Lücke im Kernel, kann er unter Umständen ausbrechen und den Host übernehmen. Bei einer VM müsste er zusätzlich durch den Hypervisor, der viel kleiner und besser abgeschottet ist.

Deshalb gibt es Mischformen. Firecracker, entwickelt von Amazon für AWS Lambda, startet winzige VMs in Sekundenbruchteilen – so schnell wie Container, so getrennt wie VMs. Projekte wie gVisor und Kata Containers gehen ähnliche Wege. Und auf dem Mac und unter Windows laufen Linux-Container ohnehin in einer verborgenen Linux-VM, denn sie brauchen einen Linux-Kernel.

Für Entwickler übersetzt: Java im Container

Erst einmal ein Namenskonflikt: Auch die JVM ist eine virtuelle Maschine – aber eine ganz andere Art. Eine VM wie bei VirtualBox ist eine System-VM: Sie bildet einen ganzen Computer nach. Die JVM ist eine Prozess-VM: Sie bildet einen gedachten Prozessor für ein einzelnes Programm nach, damit derselbe Bytecode überall läuft. Im Container läuft also eine virtuelle Maschine in einem Prozess mit Scheuklappen – möglicherweise in einer virtuellen Maschine in der Cloud.

# Dockerfile für eine Java-Anwendung – selten Geändertes nach unten
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/lib/ lib/                 # Abhängigkeiten: ändern sich selten
COPY target/app.jar app.jar           # eigener Code: ändert sich ständig
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]
ThemaWas man wissen sollte
SpeicherlimitDie JVM erkennt seit Java 10 die cgroup-Grenzen des Containers. Standardmäßig nimmt sie aber nur ein Viertel davon als maximalen Heap. -XX:MaxRAMPercentage=75 gibt ihr mehr – Rest bleibt für Metaspace, Thread-Stacks und nativen Speicher.
ProzessorkerneRuntime.availableProcessors() richtet sich nach dem CPU-Limit des Containers. Davon hängen Größe von GC-Threads und Common Pool ab.
Exit-Code 137Der Kernel hat den Container wegen Speicherüberschreitung beendet (OOMKilled) – kein OutOfMemoryError, keine Shutdown-Hooks. Meist ist der Heap zu groß im Verhältnis zum Limit, oder nativer Speicher wächst.
StartzeitContainer sollen schnell starten, die JVM wärmt sich langsam auf. Hilfen: Class Data Sharing, der AOT-Cache aus Projekt Leyden (seit Java 24), CRaC oder ein GraalVM Native Image.
Images ohne Docker bauenWerkzeuge wie Jib oder Spring Boot Buildpacks erzeugen aus Maven oder Gradle direkt gut geschichtete Images.

Häufige Irrtümer

„Ein Container ist eine kleine VM.“ – Nein: kein eigener Kernel, keine nachgebildete Hardware. Ein Container ist ein Prozess, dem der Kernel eine eingeschränkte Sicht gibt.
„Docker ist Container.“ – Docker hat Container populär gemacht. Die Technik steckt im Linux-Kernel, das Format ist ein offener Standard, und es gibt viele Werkzeuge dafür.
„Container sind genauso sicher getrennt wie VMs.“ – Sie teilen sich einen Kernel. Für Code, dem man nicht vertraut, nimmt man VMs oder Mischformen wie Firecracker.
„Im Container ist ein ganzes Ubuntu.“ – Nur dessen Dateien, also Programme und Bibliotheken. Der Kernel ist immer der des Hosts.
„Was ich im laufenden Container ändere, bleibt.“ – Nur bis der Container gelöscht wird. Dauerhafte Daten gehören in ein Volume oder eine Datenbank.

Verwandte Begriffe