Docker-Images und ihre Layer

Docker-Images und ihre Layer

Docker-Images und ihre Layer

Diese Zusammenfassung ist für alle Docker-Nerds interessant, die den Aufbau und die interne Funktionsweise von Docker-Images verstehen wollen. Für normaler Benutzer von Docker spielt dies eine untergeordnete Rolle. Hier zeige ich auf, wie sich Images aus verschiedenen Schichten, nachfolgend Layer genannt, zusammensetzen.

Die Struktur eines Images

Jedes Docker-Image besteht aus mehreren Read-Only-Layern. Beim Build eines Images anhand eines Dockerfiles wird für jede Dockerfile-Anweisung, die das Dateisystem des Basis-Images modifiziert, ein neuer Layer erstellt. Dieser neue Layer bildet die Modifikation am Dateisystem ab, er repräsentiert also eine Diff zum vorherigen Zustand.

Bei den Dockerfile-Anweisungen, die das Dateisystem modifizieren können, handelt es sich namentlich um ADD, COPY und RUN.

image-layer.png


Um Verwirrung zu vermeiden: Natürlich besteht auch das Basis-Image selbst aus verschiedenen Layern.

Jedes Docker-Image ist intern lediglich ein Konfigurations-Objekt, welches im JSON-Format gespeichert wird. Dieses JSON-Objekt enthält neben Metadaten zum Image, wie beispielsweise seine CMD-Anweisung, eine geordnete Liste verschiedener Layer. Dass in dieser Liste lediglich die Layer-IDs hinterlegt sind, lässt sich mit docker image inspect leicht herausfinden:

JSON:
"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:bcf2f368fe234217249e00ad9d762d8f1a3156d60c442ed92079fa5b120634a1",
        "sha256:aabe8fddede54277f929724919213cc5df2ab4e4175a5ce45ff4e00909a4b757",
        "sha256:fbe16fc07f0d81390525c348fbd720725dcae6498bd5e902ce5d37f2b7eed743",
    ]
}

Jeder layer wird mit einer ID in der Form <Algorithmus>:<Hashwert des Layers> identifiziert, was später noch eine Rolle spielen wird. Diese mit Docker 1.10 eingeführten IDs werden auch als Content Addressable IDs bezeichnet, weil der Hashwert dem Inhalt des Layers entspricht.

Die Separierung von Images und Layern ist wichtig, weil dadurch beliebig viele Images einen bestimmten Layer referenzieren können. Damit muss auch ein mehrfach verwendetes Basis-Image lediglich einmal auf der Festplatte gespeichert werden.

Erstellen eines Beispiel-Images

Das folgende Dockerfile lädt ein Python-Image von Docker Hub herunter, kopiert alle Dateien nach /code und startet die Anwendung.

Dockerfile:
FROM python:3.4-alpine
COPY . /code
WORKDIR /code
CMD ["python", "app.py"]

Baut man das entsprechende Image mit docker build --no-cache -t python-app ., erhält man folgende Ausgabe:

Code:
Status: Downloaded newer image for python:3.4-alpine
---> c06adcf62f6e

Step 2/4 : COPY . /code
---> 35706ada4e09

Step 3/4 : WORKDIR /code
---> Running in 9ef5d63297f7
Removing intermediate container 9ef5d63297f7
---> c80bbfbdad34

Step 4/4 : CMD ["python", "app.py"]
---> Running in bcd75eac5de0
Removing intermediate container bcd75eac5de0
---> 35cfdf68d3a6

Diese Ausgabe gibt Auskunft über die Struktur des gebauten Images. Beim lokalen Build eines Images wird für jeden Layer, der in das zu bauende Image committed wird, ein temporäres Image erstellt.

Solche temporären Images sind in dieser Ausgabe an ihrer ID erkennbar, z. B. ---> c06adcf62f6e. Auch für Dockerfile-Anweisungen, die keine Modifikation am Dateisystem vornehmen, wird ein temporäres Image erstellt. Das ist beispielsweise bei unserer WORKDIR-Anweisung der Fall. Temporäre Images sind eine Besonderheit bei lokalen Builds und erlauben die Nutzung des Build-Caches für bessere Performance.

Die verschiedenen Layer anzeigen

Die verschiedenen Layer, aus denen das Image aus dem obigen Beispiel besteht, können mit docker image history python-app angezeigt werden. Auch die Größe der einzelnen Layer ist in dieser Ausgabe enthalten.

Code:
IMAGE               CREATED             CREATED BY                                      SIZE
f07355217003        9 seconds ago       /bin/sh -c #(nop)  CMD ["python" "app.py"]      0B
ed9ac8b8ab0d        10 seconds ago      /bin/sh -c #(nop) WORKDIR /code                 0B
0e81b30e3c9f        11 seconds ago      /bin/sh -c #(nop) COPY dir:9a64c4777f86fbfd1…   38MB
c06adcf62f6e        11 months ago       /bin/sh -c #(nop)  CMD ["python3"]              0B
<missing>           11 months ago       /bin/sh -c set -ex;   wget -O get-pip.py 'ht…   6.04MB
<missing>           11 months ago       /bin/sh -c #(nop)  ENV PYTHON_PIP_VERSION=19…   0B
<missing>           11 months ago       /bin/sh -c cd /usr/local/bin  && ln -s idle3…   32B
<missing>           11 months ago       /bin/sh -c set -ex  && apk add --no-cache --…   60.8MB
<missing>           11 months ago       /bin/sh -c #(nop)  ENV PYTHON_VERSION=3.4.10    0B
<missing>           11 months ago       /bin/sh -c #(nop)  ENV GPG_KEY=97FC712E4C024…   0B
<missing>           11 months ago       /bin/sh -c apk add --no-cache ca-certificates   551kB
<missing>           11 months ago       /bin/sh -c #(nop)  ENV LANG=C.UTF-8             0B
<missing>           11 months ago       /bin/sh -c #(nop)  ENV PATH=/usr/local/bin:/…   0B
<missing>           11 months ago       /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>           11 months ago       /bin/sh -c #(nop) ADD file:88875982b0512a9d0…   5.53MB

Zwei Dinge fallen dabei auf.

Erstens lautet die Spaltenüberschrit "Image" und nicht "Layer". Das hat einerseits historische Gründe, weil vor Docker 1.10 jeder Layer tatsächlich auch einem echten Image zugeordnet war. Andererseits hat es auch mit der oben erwähnten Besonderheit bei lokalen Builds zu tun: Es handelt sich nämlich tatsächlich um (temporäre) Images, die auf dem Host vorhanden sind und für den lokalen Build genutzt wurden. Alle Einträge mit <missing> als Image stammen vom Basis-Image und sind keine temporären Images eines lokalen Builds, sondern lediglich Layer.

Zweitens fällt auf, dass einige Layer offenbar 0 Byte groß sind, während andere Layer teilweise mehrere Megabyte groß sind. Lediglich die Layer, die mit einer Änderung im Dateisystem verbunden sind - also solche, die durch RUN, COPY und ADD enstanden sind -, besitzen eine physikalische Größe. In diesem Fall sind das fünf Layer. Alle anderen Anweisungen werden in einem einzigen, leeren Layer zusammengefasst.

Verifizieren lässt sich dies mit docker image inspect python-app:

JSON:
"RootFS": {
    "Type": "layers",
    "Layers": [
        "sha256:bcf2f368fe234217249e00ad9d762d8f1a3156d60c442ed92079fa5b120634a1",
        "sha256:aabe8fddede54277f929724919213cc5df2ab4e4175a5ce45ff4e00909a4b757",
        "sha256:fbe16fc07f0d81390525c348fbd720725dcae6498bd5e902ce5d37f2b7eed743",
        "sha256:58026b9b6bf1a7dbc0872462e9ea675cad54a45bc7682bd3631dd4f3c16b1332",
        "sha256:62de8bcc470aef81ddbec19b7f5aeed24d7b7ec1bff09422f7e0da3a4842d346",
        "sha256:8605394513ec8103a4b386e62f5dcca888651e770d36d4a58bc0f1a723526e1d"
    ]
}

Hier werden insgesamt sechs Layer referenziert: Die fünf Layer mit einer richtigen Größe, die eine Diff im Dateisystem abbilden, sowie ein weiterer Layer für alle restlichen Dockerfile-Anweisungen.

Die ImageDB

Images sind physische Konfigurations-Objekte, die im JSON-Format gespeichert werden. Docker speichert diese Konfigurationsdateien unter /var/lib/docker/image/<Treiber>/imagedb, wobei <Treiber> der verwendete Storage-Treiber ist. In unserem Beispiel ist dies overlay2. Wechselt man nun in dieses Verzeichnis, können alle Dateien (sprich: Images) mit ls -l aufgelistet werden. Auch die oben erwähnten temporären Images tauchen hier auf.

Code:
6293 May 22 08:00 35706ada4e09a0a7f73e5c802b0cc1710203b8c7ca043396129b642e0d6831aa
6546 May 22 08:00 35cfdf68d3a69aecd9a2fcb5322280437c86e8fac01465357ba650fcb402cf03
6086 May 22 08:00 c06adcf62f6ef21ae5c586552532b04b693f9ab6df377d7ea066fd682c470864
6433 May 22 08:00 c80bbfbdad34999518ef3caf434c13cbe0ede4dc224da400ba318fe32dc77c03

Gibt man nun eine dieser Dateien mit cat aus, erscheint die selbe Ausgabe, die auch von docker image inspect erzeugt wird. Der Inhalt ist lediglich unformatiert. Das bedeutet auch, dass die IDs der unterschiedlichen Layer ebenfalls auftauchen:

Code:
"rootfs": {
    "type": "layers",
    "diff_ids": [
        "sha256:bcf2f368fe234217249e00ad9d762d8f1a3156d60c442ed92079fa5b120634a1",
        "sha256:aabe8fddede54277f929724919213cc5df2ab4e4175a5ce45ff4e00909a4b757",
        "sha256:fbe16fc07f0d81390525c348fbd720725dcae6498bd5e902ce5d37f2b7eed743",
        "sha256:58026b9b6bf1a7dbc0872462e9ea675cad54a45bc7682bd3631dd4f3c16b1332",
        "sha256:62de8bcc470aef81ddbec19b7f5aeed24d7b7ec1bff09422f7e0da3a4842d346",
        "sha256:b6ffd37affa5acc285f4fa06b2f93bef635ac774c6a49038a682b678f125e5dc"
    ]
}

Merken wir uns doch den ersten Layer in der Liste, dessen Hashwert mit bcf2... beginnt.

Die LayerDB

Layer werden intern ähnlich wie Images gespeichert. Allerdings wird ein Layer nicht in Form einer einzelnen Datei beschrieben, sondern erhält ein eigenes Verzeichnis unter /var/lib/docker/image/<Treiber>/layerdb/<Algorithmus>. Hier schließt sich der Kreis zu den Layer-IDs: <Algorithmus> ist in unserem Fall sha256 und der Name des jeweiligen Verzeichnisses ist der Hashwert des Layers.

Hier die Verzeichnisse in /var/lib/docker/image/overlay2/layerdb/sha256:

Code:
85 Feb 18 08:00 31af966d116c33f2120c0bbbb603026bc9aafd09f716e44feae9f8786e874da3
85 Feb 18 08:00 62b3e01ef883f4dc5459318cff905442c1ab3db4a09fe32c7020a9aa2ab819fa
71 Feb 18 08:00 bcf2f368fe234217249e00ad9d762d8f1a3156d60c442ed92079fa5b120634a1
85 Feb 18 08:00 cd22abdaccb6b4ce8dc28afbf4c03b9711c99990068dcbddc476bebd4395e899
85 Feb 18 08:00 cd296739505d41d98f1bcb846b11fa5655cc0a157851f8bb0e2a51451ea875a2
85 Feb 18 08:00 d95b0c9211330afe1efda7c47eb3f45116a20e981fbd38e9d1259d2994f29a59

Tatsächlich taucht hier an dritter Stelle ein Verzeichnis bcf2... auf - der Hashwert der ID, die wir uns gemerkt haben.

Jedes Layer-Verzeichnis enthält folgende Dateien für weitere Informationen:
  • diff: Enthält den Hashwert des Layers und ist demnach mit dem Verzeichnisnamen identisch
  • size: Enthält die physikalische Größe des Layers
  • cache-id: Enthält die ID des assozierten Caches für den Layer

Die unscheinbare Datei cache-id mag zunächst nebensächlich wirken, tatsächlich ist sie aber der Schlüssel zur Magie hinter Layern. Beispielweise enthält diese Datei für den bcf2...-Layer folgende Cache-ID:

Bash:
$ cat cache-id
  640a857fec521662acd5324b88340a1597bb53c56085176af729bb3021471c22

Das, was den Layer letztendlich ausmacht, verbirgt sich im Cache mit der besagten ID.

Die Caches

Jeder Cache wird von Docker als eigenes Verzeichnis unter /var/lib/docker/<Treiber> gespeichert, wobei <Treiber> hier wieder der Storage-Treiber overlay2 ist. Ähnlich wie bei Layer-Verzeichnissen entspricht auch hier der Verzeichnisname der jeweiligen Cache-ID:

Code:
72 May 22 08:00 1a75cb4ce44af189da03799c159344694ae7e7107479047d6f4e892e87b365ba
72 May 22 08:00 2dfc6b8be7d1773c4d2387a8841f19bb6d173e202e361be6aef81547e6c3fffb
47 May 22 08:00 640a857fec521662acd5324b88340a1597bb53c56085176af729bb3021471c22
72 May 22 08:00 c168758d87097d29d5e8b005a0d1cf856434a392aa021e3dcc031d878bcfd45b
72 May 22 08:00 c92c65c7461f85d4ad87e7ffc78bc72c6278db039457c3022395cd829733e1d1
72 May 22 08:00 d05f04050b81cc53223cb87e2deb1fb7634e9fc2c045677731cb4ee953e2f2db

Da für jeden Layer genau ein Cache existiert, liegen hier insgesamt sechs Verzeichnisse vor. Das dritte davon entspricht der gerade gezeigten Cache-ID des Layers bcf2....

Neben einigen Dateien mit Metadaten enthält jeder dieser Caches ein Verzeichnis diff, das die Modifikationen am Dateisystem abbildet. Kopiert ein COPY-Befehl im Dockerfile eine Datei ins Image-Dateisystem, findet sich diese Datei genau hier wieder. Der entsprechende Layer, genau genommen dessen Cache, enthält lediglich diese eine Datei als Diff zum vorherigen Zustand des Dateisystems.

All diese Caches mit ihren Dateien addieren sich mit dem Build eines Images nach und nach zum finalen Dateisystem. Man könnte auch sagen, die einzelnen Layer vereinen sich zu einem Dateisystem, weshalb es sich bei dieser Art von System um ein Union Filesystem handelt. Und dementsprechend sieht das diff-Verzeichnis für einen Layer, der Linux zum Image hinzufügt, folgendermaßen aus:

Bash:
$ ls -l diff
  4096 Mar  4  2019 bin
     6 Mar  4  2019 dev
  4096 Mar  4  2019 etc
     6 Mar  4  2019 home
   185 Mar  4  2019 lib
    44 Mar  4  2019 media
     6 Mar  4  2019 mnt
     6 Mar  4  2019 opt
                    ...

Dieses hochgradig flexible System erlaubt es Docker, möglichst platzsparend zu arbeiten und einzelne Layer effizient wiederzuverwenden.

Der beschreibbare Layer

All diese Layer sind Read-Only-Layer. Ein Container, der auf einem bestimmten Image basiert, kann also nicht schreibend auf das Dateisystem dieses Images zugreifen. Diese Restriktion bietet den Vorteil, dass beliebig viele Container auf dem selben Image basieren können und der Zustand eines neu erstellten Containers vorhersehbar ist.

Um aber Containern zumindest eine Art Schreibzugriff auf das Dateisystem zu gewähren, nutzt Docker das Copy-On-Write-Prinzip. Beim Starten eines Containers wird über alle Read-Only-Layer des Images ein sehr schlanker beschreibbarer Layer gelegt, der sogenannte Container-Layer. Möchte der Container zur Laufzeit eine Datei aus dem Image-Dateisystem modifizieren, wird die Datei in diesen beschreibbaren Layer kopiert und dort modifiziert. Aus Sicht des Containers handelt es sich dabei um die Originaldatei, weil die kopierte Datei im übergeordneten Container-Layer die ursprüngliche Datei überdeckt.

Dass lediglich die modifizierten Dateien in einem schlanken, Container-eigenen Layer landen, ermöglicht kurze Startup-Zeiten für Container. Wird der Container gelöscht, verschwindet auch der beschreibbare Container-Layer und das eigentliche Image verbleibt unverändert.
Autor
dominik
Aufrufe
1.109
Erstellt am
Letzte Bearbeitung
Bewertung
0,00 Stern(e) 0 Bewertung(en)

Weitere Ressourcen von dominik

Oben Unten